Compare commits
754 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 |
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
|
||||||
|
...
|
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`.
|
510
GNUmakefile
510
GNUmakefile
@ -3,9 +3,14 @@
|
|||||||
MAKEFLAGS += --warn-undefined-variables
|
MAKEFLAGS += --warn-undefined-variables
|
||||||
MAKEFLAGS += --no-builtin-rules
|
MAKEFLAGS += --no-builtin-rules
|
||||||
|
|
||||||
VERSION_CODE := 14
|
VERSION_CODE := 30
|
||||||
VERSION_NUMBER := 0.0.14
|
VERSION_NUMBER := 0.0.25-wip
|
||||||
VERSION_NAME := Served on apple cider dressed winter greens.
|
VERSION_NAME := This program kills fascists.
|
||||||
|
|
||||||
|
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470000.zip
|
||||||
|
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
|
||||||
|
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||||
|
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool
|
||||||
|
|
||||||
PROJECT = tildefriends
|
PROJECT = tildefriends
|
||||||
BUILD_DIR ?= out
|
BUILD_DIR ?= out
|
||||||
@ -13,6 +18,12 @@ UNAME_S := $(shell uname -s)
|
|||||||
UNAME_M := $(shell uname -m)
|
UNAME_M := $(shell uname -m)
|
||||||
|
|
||||||
ANDROID_SDK ?= ~/Android/Sdk
|
ANDROID_SDK ?= ~/Android/Sdk
|
||||||
|
BUNDLETOOL = out/bundletool.jar
|
||||||
|
|
||||||
|
HAVE_WIN := 0
|
||||||
|
|
||||||
|
export SOURCE_DATE_EPOCH=1
|
||||||
|
export TZ=UTC
|
||||||
|
|
||||||
ifeq ($(UNAME_S),Darwin)
|
ifeq ($(UNAME_S),Darwin)
|
||||||
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
|
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
|
||||||
@ -26,7 +37,8 @@ BUILD_TYPES := debug release
|
|||||||
CFLAGS += -Dstatic_assert=_Static_assert
|
CFLAGS += -Dstatic_assert=_Static_assert
|
||||||
LDFLAGS += \
|
LDFLAGS += \
|
||||||
-lbsd \
|
-lbsd \
|
||||||
-lnetwork
|
-lnetwork \
|
||||||
|
-Wno-stringop-overflow
|
||||||
else ifeq ($(UNAME_S),OpenBSD)
|
else ifeq ($(UNAME_S),OpenBSD)
|
||||||
BUILD_TYPES := debug release
|
BUILD_TYPES := debug release
|
||||||
CFLAGS += \
|
CFLAGS += \
|
||||||
@ -36,7 +48,6 @@ LDFLAGS += \
|
|||||||
-lc++abi
|
-lc++abi
|
||||||
HAVE_ANDROID := 0
|
HAVE_ANDROID := 0
|
||||||
HAVE_LINUX_IOS := 0
|
HAVE_LINUX_IOS := 0
|
||||||
HAVE_WIN := 0
|
|
||||||
else
|
else
|
||||||
$(error Unexpected host platform $(UNAME_S).)
|
$(error Unexpected host platform $(UNAME_S).)
|
||||||
endif
|
endif
|
||||||
@ -46,17 +57,23 @@ CFLAGS += \
|
|||||||
-Wall \
|
-Wall \
|
||||||
-Wextra \
|
-Wextra \
|
||||||
-Wno-unused-parameter \
|
-Wno-unused-parameter \
|
||||||
|
-Wno-unknown-warning-option \
|
||||||
-MMD \
|
-MMD \
|
||||||
|
-MP \
|
||||||
-ffunction-sections \
|
-ffunction-sections \
|
||||||
-fdata-sections \
|
-fdata-sections \
|
||||||
-fno-exceptions \
|
-fno-exceptions \
|
||||||
-g
|
-g
|
||||||
|
LDFLAGS += \
|
||||||
|
-Wno-attributes \
|
||||||
|
-Wno-aggressive-loop-optimizations \
|
||||||
|
-flto=auto
|
||||||
|
|
||||||
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
|
|
||||||
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-33
|
|
||||||
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.0.10792818
|
|
||||||
ANDROID_MIN_SDK_VERSION := 24
|
ANDROID_MIN_SDK_VERSION := 24
|
||||||
ANDROID_TARGET_SDK_VERSION := 34
|
ANDROID_TARGET_SDK_VERSION := 34
|
||||||
|
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
|
||||||
|
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-$(ANDROID_TARGET_SDK_VERSION)
|
||||||
|
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.3.11579264
|
||||||
|
|
||||||
ANDROID_ARMV7A_TARGETS := \
|
ANDROID_ARMV7A_TARGETS := \
|
||||||
out/androiddebug-armv7a/tildefriends \
|
out/androiddebug-armv7a/tildefriends \
|
||||||
@ -85,7 +102,7 @@ BUILD_TYPES += \
|
|||||||
androidrelease-x86 \
|
androidrelease-x86 \
|
||||||
androiddebug-x86_64 \
|
androiddebug-x86_64 \
|
||||||
androidrelease-x86_64
|
androidrelease-x86_64
|
||||||
all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk
|
all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk out/TildeFriends-release.fdroid.apk
|
||||||
endif
|
endif
|
||||||
|
|
||||||
WINDOWS_TARGETS := \
|
WINDOWS_TARGETS := \
|
||||||
@ -145,21 +162,28 @@ ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
|
|||||||
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
|
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
|
||||||
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(ALL_TARGETS))
|
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(ALL_TARGETS))
|
||||||
NONMACOS_TARGETS := $(filter-out $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS),$(ALL_TARGETS))
|
NONMACOS_TARGETS := $(filter-out $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS),$(ALL_TARGETS))
|
||||||
|
DEADSTRIP_TARGETS := $(filter-out $(ANDROID_TARGETS),$(NONMACOS_TARGETS))
|
||||||
|
ifneq ($(UNAME_S),OpenBSD)
|
||||||
|
$(NONMACOS_TARGETS): LDFLAGS += -static-libgcc
|
||||||
|
endif
|
||||||
|
|
||||||
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
|
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
|
||||||
$(filter-out $(ANDROID_TARGETS) $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
|
$(filter-out $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
|
||||||
$(ANDROID_TARGETS): CFLAGS += \
|
$(ANDROID_TARGETS): CFLAGS += \
|
||||||
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
|
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
|
||||||
-fPIC \
|
-fPIC \
|
||||||
-fdebug-compilation-dir . \
|
-fdebug-compilation-dir . \
|
||||||
-fomit-frame-pointer \
|
-fomit-frame-pointer \
|
||||||
-fno-asynchronous-unwind-tables \
|
-fno-asynchronous-unwind-tables \
|
||||||
-funwind-tables
|
-funwind-tables \
|
||||||
|
-Wno-unknown-warning-option
|
||||||
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
|
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
|
||||||
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
|
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
|
||||||
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
|
$(RELEASE_TARGETS): CFLAGS += \
|
||||||
|
-DNDEBUG \
|
||||||
|
-flto
|
||||||
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
|
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
|
||||||
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Os
|
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
|
||||||
$(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32
|
$(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32
|
||||||
$(WINDOWS_TARGETS): AS = $(CC)
|
$(WINDOWS_TARGETS): AS = $(CC)
|
||||||
$(WINDOWS_TARGETS): CFLAGS += \
|
$(WINDOWS_TARGETS): CFLAGS += \
|
||||||
@ -200,17 +224,25 @@ $(ANDROID_X86_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86/usr/local/lib
|
|||||||
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
|
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
|
||||||
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
|
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
|
||||||
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
|
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
|
||||||
$(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections
|
$(DEADSTRIP_TARGETS): LDFLAGS += -Wl,--gc-sections
|
||||||
$(IOS_TARGETS): CFLAGS += -mios-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
|
$(IOS_TARGETS): CFLAGS += -miphoneos-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
|
||||||
$(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
|
$(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=9.0 -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
|
||||||
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
|
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
|
||||||
$(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/lib
|
$(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/lib
|
||||||
|
|
||||||
ifeq ($(UNAME_M),x86_64)
|
ifeq ($(UNAME_M),x86_64)
|
||||||
ifneq ($(UNAME_S),Haiku)
|
ifeq ($(UNAME_S),Linux)
|
||||||
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
all: appimage
|
||||||
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
|
||||||
endif
|
endif
|
||||||
|
ifneq ($(UNAME_S),Haiku)
|
||||||
|
out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||||
|
out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifeq ($(UNAME_M),aarch64)
|
||||||
|
out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||||
|
out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||||
endif
|
endif
|
||||||
|
|
||||||
get_objs = \
|
get_objs = \
|
||||||
@ -228,6 +260,8 @@ APP_SOURCES_ios := $(wildcard src/*.m)
|
|||||||
APP_OBJS := $(call get_objs,APP_SOURCES)
|
APP_OBJS := $(call get_objs,APP_SOURCES)
|
||||||
$(APP_OBJS): CFLAGS += \
|
$(APP_OBJS): CFLAGS += \
|
||||||
-Ideps/base64c/include \
|
-Ideps/base64c/include \
|
||||||
|
-Ideps/c-ares/include \
|
||||||
|
-Ideps/c-ares_config \
|
||||||
-Ideps/crypt_blowfish \
|
-Ideps/crypt_blowfish \
|
||||||
-Ideps/libbacktrace \
|
-Ideps/libbacktrace \
|
||||||
-Ideps/libsodium \
|
-Ideps/libsodium \
|
||||||
@ -239,7 +273,6 @@ $(APP_OBJS): CFLAGS += \
|
|||||||
-Ideps/quickjs \
|
-Ideps/quickjs \
|
||||||
-Ideps/sqlite \
|
-Ideps/sqlite \
|
||||||
-Ideps/valgrind \
|
-Ideps/valgrind \
|
||||||
-Ideps/xopt \
|
|
||||||
-Wdouble-promotion \
|
-Wdouble-promotion \
|
||||||
-Werror
|
-Werror
|
||||||
ifeq ($(UNAME_M),x86_64)
|
ifeq ($(UNAME_M),x86_64)
|
||||||
@ -247,6 +280,108 @@ $(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/macos% $(BUILD_DIR)/ios%,$(APP_O
|
|||||||
-fanalyzer
|
-fanalyzer
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
ARES_SOURCES := \
|
||||||
|
deps/c-ares/src/lib/ares_addrinfo2hostent.c \
|
||||||
|
deps/c-ares/src/lib/ares_addrinfo_localhost.c \
|
||||||
|
deps/c-ares/src/lib/ares_android.c \
|
||||||
|
deps/c-ares/src/lib/ares_cancel.c \
|
||||||
|
deps/c-ares/src/lib/ares_close_sockets.c \
|
||||||
|
deps/c-ares/src/lib/ares_conn.c \
|
||||||
|
deps/c-ares/src/lib/ares_cookie.c \
|
||||||
|
deps/c-ares/src/lib/ares_data.c \
|
||||||
|
deps/c-ares/src/lib/ares_destroy.c \
|
||||||
|
deps/c-ares/src/lib/ares_free_hostent.c \
|
||||||
|
deps/c-ares/src/lib/ares_free_string.c \
|
||||||
|
deps/c-ares/src/lib/ares_freeaddrinfo.c \
|
||||||
|
deps/c-ares/src/lib/ares_getaddrinfo.c \
|
||||||
|
deps/c-ares/src/lib/ares_getenv.c \
|
||||||
|
deps/c-ares/src/lib/ares_gethostbyaddr.c \
|
||||||
|
deps/c-ares/src/lib/ares_gethostbyname.c \
|
||||||
|
deps/c-ares/src/lib/ares_getnameinfo.c \
|
||||||
|
deps/c-ares/src/lib/ares_hosts_file.c \
|
||||||
|
deps/c-ares/src/lib/ares_init.c \
|
||||||
|
deps/c-ares/src/lib/ares_library_init.c \
|
||||||
|
deps/c-ares/src/lib/ares_metrics.c \
|
||||||
|
deps/c-ares/src/lib/ares_options.c \
|
||||||
|
deps/c-ares/src/lib/ares_parse_into_addrinfo.c \
|
||||||
|
deps/c-ares/src/lib/ares_process.c \
|
||||||
|
deps/c-ares/src/lib/ares_qcache.c \
|
||||||
|
deps/c-ares/src/lib/ares_query.c \
|
||||||
|
deps/c-ares/src/lib/ares_search.c \
|
||||||
|
deps/c-ares/src/lib/ares_send.c \
|
||||||
|
deps/c-ares/src/lib/ares_set_socket_functions.c \
|
||||||
|
deps/c-ares/src/lib/ares_socket.c \
|
||||||
|
deps/c-ares/src/lib/ares_sortaddrinfo.c \
|
||||||
|
deps/c-ares/src/lib/ares_strerror.c \
|
||||||
|
deps/c-ares/src/lib/ares_sysconfig.c \
|
||||||
|
deps/c-ares/src/lib/ares_sysconfig_files.c \
|
||||||
|
deps/c-ares/src/lib/ares_sysconfig_mac.c \
|
||||||
|
deps/c-ares/src/lib/ares_sysconfig_win.c \
|
||||||
|
deps/c-ares/src/lib/ares_update_servers.c \
|
||||||
|
deps/c-ares/src/lib/ares_version.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares_array.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares_htable.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares_htable_asvp.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares_htable_dict.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares_htable_strvp.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares_htable_szvp.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares_htable_vpvp.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares_llist.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares_slist.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_configchg.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_epoll.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_kqueue.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_poll.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_select.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_thread.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_wake_pipe.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_win32.c \
|
||||||
|
deps/c-ares/src/lib/inet_net_pton.c \
|
||||||
|
deps/c-ares/src/lib/inet_ntop.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_create_query.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_expand_name.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_expand_string.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_fds.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_getsock.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_a_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_aaaa_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_caa_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_mx_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_naptr_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_ns_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_ptr_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_soa_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_srv_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_txt_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_uri_reply.c \
|
||||||
|
deps/c-ares/src/lib/record/ares_dns_mapping.c \
|
||||||
|
deps/c-ares/src/lib/record/ares_dns_multistring.c \
|
||||||
|
deps/c-ares/src/lib/record/ares_dns_name.c \
|
||||||
|
deps/c-ares/src/lib/record/ares_dns_parse.c \
|
||||||
|
deps/c-ares/src/lib/record/ares_dns_record.c \
|
||||||
|
deps/c-ares/src/lib/record/ares_dns_write.c \
|
||||||
|
deps/c-ares/src/lib/str/ares_buf.c \
|
||||||
|
deps/c-ares/src/lib/str/ares_str.c \
|
||||||
|
deps/c-ares/src/lib/str/ares_strsplit.c \
|
||||||
|
deps/c-ares/src/lib/util/ares_iface_ips.c \
|
||||||
|
deps/c-ares/src/lib/util/ares_math.c \
|
||||||
|
deps/c-ares/src/lib/util/ares_rand.c \
|
||||||
|
deps/c-ares/src/lib/util/ares_threads.c \
|
||||||
|
deps/c-ares/src/lib/util/ares_timeval.c \
|
||||||
|
deps/c-ares/src/lib/util/ares_uri.c \
|
||||||
|
deps/c-ares/src/lib/windows_port.c \
|
||||||
|
deps/c-ares/src/lib/ares_timeout.c
|
||||||
|
ARES_OBJS := $(call get_objs,ARES_SOURCES)
|
||||||
|
$(ARES_OBJS): CFLAGS += \
|
||||||
|
-Ideps/c-ares/include \
|
||||||
|
-Ideps/c-ares/src/lib \
|
||||||
|
-Ideps/c-ares/src/lib/include \
|
||||||
|
-Ideps/c-ares_config/ \
|
||||||
|
-D_GNU_SOURCE \
|
||||||
|
-Wno-unused-function \
|
||||||
|
-Wno-deprecated-declarations \
|
||||||
|
-Wno-unused-result
|
||||||
|
|
||||||
BLOWFISH_SOURCES := \
|
BLOWFISH_SOURCES := \
|
||||||
deps/crypt_blowfish/crypt_blowfish.c \
|
deps/crypt_blowfish/crypt_blowfish.c \
|
||||||
deps/crypt_blowfish/crypt_gensalt.c \
|
deps/crypt_blowfish/crypt_gensalt.c \
|
||||||
@ -374,10 +509,17 @@ $(UV_OBJS): CFLAGS += \
|
|||||||
-Wno-incompatible-pointer-types \
|
-Wno-incompatible-pointer-types \
|
||||||
-Wno-maybe-uninitialized \
|
-Wno-maybe-uninitialized \
|
||||||
-Wno-sign-compare \
|
-Wno-sign-compare \
|
||||||
|
-Wno-unknown-attributes \
|
||||||
-Wno-unused-but-set-parameter \
|
-Wno-unused-but-set-parameter \
|
||||||
-Wno-unused-but-set-variable \
|
-Wno-unused-but-set-variable \
|
||||||
-Wno-unused-result \
|
-Wno-unused-result \
|
||||||
-Wno-unused-variable
|
-Wno-unused-variable \
|
||||||
|
-Wno-nonnull
|
||||||
|
$(UV_OBJS): CFLAGS += -fno-lto
|
||||||
|
$(filter out/win%,$(UV_OBJS)): \
|
||||||
|
CFLAGS += \
|
||||||
|
-Wno-cast-function-type \
|
||||||
|
-Wno-missing-braces
|
||||||
ifeq ($(UNAME_S),Linux)
|
ifeq ($(UNAME_S),Linux)
|
||||||
$(UV_OBJS): CFLAGS += \
|
$(UV_OBJS): CFLAGS += \
|
||||||
-D_GNU_SOURCE
|
-D_GNU_SOURCE
|
||||||
@ -491,17 +633,6 @@ $(SQLITE_OBJS): CFLAGS += \
|
|||||||
-Wno-unused-function \
|
-Wno-unused-function \
|
||||||
-Wno-unused-variable
|
-Wno-unused-variable
|
||||||
|
|
||||||
XOPT_SOURCES := deps/xopt/xopt.c
|
|
||||||
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
|
|
||||||
$(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
|
|
||||||
-DHAVE_SNPRINTF \
|
|
||||||
-DHAVE_VSNPRINTF \
|
|
||||||
-DHAVE_VASNPRINTF \
|
|
||||||
-DHAVE_VASPRINTF \
|
|
||||||
-Dvsnprintf=rpl_vsnprintf
|
|
||||||
$(XOPT_OBJS): CFLAGS += \
|
|
||||||
-Wno-implicit-const-int-float-conversion
|
|
||||||
|
|
||||||
QUICKJS_SOURCES := \
|
QUICKJS_SOURCES := \
|
||||||
deps/quickjs/cutils.c \
|
deps/quickjs/cutils.c \
|
||||||
deps/quickjs/libbf.c \
|
deps/quickjs/libbf.c \
|
||||||
@ -521,6 +652,12 @@ $(QUICKJS_OBJS): CFLAGS += \
|
|||||||
-Wno-unused-variable
|
-Wno-unused-variable
|
||||||
$(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS
|
$(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS
|
||||||
|
|
||||||
|
ifeq ($(UNAME_S),Haiku)
|
||||||
|
$(QUICKJS_OBJS): CFLAGS += "-Dmalloc_usable_size(x)=0"
|
||||||
|
else ifeq ($(UNAME_S),OpenBSD)
|
||||||
|
$(QUICKJS_OBJS): CFLAGS += "-Dmalloc_usable_size(x)=0"
|
||||||
|
endif
|
||||||
|
|
||||||
LIBBACKTRACE_SOURCES := \
|
LIBBACKTRACE_SOURCES := \
|
||||||
deps/libbacktrace/atomic.c \
|
deps/libbacktrace/atomic.c \
|
||||||
deps/libbacktrace/backtrace.c \
|
deps/libbacktrace/backtrace.c \
|
||||||
@ -575,7 +712,7 @@ $(MINIUNZIP_OBJS): CFLAGS += \
|
|||||||
LDFLAGS += \
|
LDFLAGS += \
|
||||||
-pthread \
|
-pthread \
|
||||||
-lm
|
-lm
|
||||||
debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
$(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
||||||
-lssl \
|
-lssl \
|
||||||
-lcrypto
|
-lcrypto
|
||||||
ifneq ($(UNAME_S),Haiku)
|
ifneq ($(UNAME_S),Haiku)
|
||||||
@ -617,6 +754,7 @@ all: $(BUILD_TYPES)
|
|||||||
|
|
||||||
ALL_APP_OBJS := \
|
ALL_APP_OBJS := \
|
||||||
$(APP_OBJS) \
|
$(APP_OBJS) \
|
||||||
|
$(ARES_OBJS) \
|
||||||
$(BLOWFISH_OBJS) \
|
$(BLOWFISH_OBJS) \
|
||||||
$(LIBBACKTRACE_OBJS) \
|
$(LIBBACKTRACE_OBJS) \
|
||||||
$(MINIUNZIP_OBJS) \
|
$(MINIUNZIP_OBJS) \
|
||||||
@ -624,8 +762,7 @@ ALL_APP_OBJS := \
|
|||||||
$(QUICKJS_OBJS) \
|
$(QUICKJS_OBJS) \
|
||||||
$(SODIUM_OBJS) \
|
$(SODIUM_OBJS) \
|
||||||
$(SQLITE_OBJS) \
|
$(SQLITE_OBJS) \
|
||||||
$(UV_OBJS) \
|
$(UV_OBJS)
|
||||||
$(XOPT_OBJS)
|
|
||||||
|
|
||||||
DEPS = $(ALL_APP_OBJS:.o=.d)
|
DEPS = $(ALL_APP_OBJS:.o=.d)
|
||||||
-include $(DEPS)
|
-include $(DEPS)
|
||||||
@ -635,34 +772,34 @@ $(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
|
|||||||
.PHONY: $(1)
|
.PHONY: $(1)
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
|
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
|
||||||
@echo [link] $$@
|
@echo "[link] $$@"
|
||||||
@$$(CC) -o $$@ $$^ $$(LDFLAGS)
|
@$$(CC) -o $$@ $$^ $$(LDFLAGS)
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/%.o: %.c
|
$(BUILD_DIR)/$(1)/%.o: %.c
|
||||||
@mkdir -p $$(dir $$@)
|
@mkdir -p $$(dir $$@)
|
||||||
@echo [c] $$@
|
@echo "[c] $$@"
|
||||||
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/%.o: %.m
|
$(BUILD_DIR)/$(1)/%.o: %.m
|
||||||
@mkdir -p $$(dir $$@)
|
@mkdir -p $$(dir $$@)
|
||||||
@echo [m] $$@
|
@echo "[m] $$@"
|
||||||
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/%.o: %.S
|
$(BUILD_DIR)/$(1)/%.o: %.S
|
||||||
@mkdir -p $$(dir $$@)
|
@mkdir -p $$(dir $$@)
|
||||||
@echo [as] $$@
|
@echo "[as] $$@"
|
||||||
@$$(AS) -c $$< -o $$@
|
@$$(AS) -c $$< -o $$@
|
||||||
endef
|
endef
|
||||||
|
|
||||||
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
|
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
|
||||||
|
|
||||||
src/version.h : $(firstword $(MAKEFILE_LIST))
|
src/version.h : $(firstword $(MAKEFILE_LIST))
|
||||||
@echo [version] $@
|
@echo "[version] $@"
|
||||||
@echo "#define VERSION_NUMBER \"$(VERSION_NUMBER)\"" > $@
|
@echo "#define VERSION_NUMBER \"$(VERSION_NUMBER)\"" > $@
|
||||||
@echo "#define VERSION_NAME \"$(VERSION_NAME)\"" >> $@
|
@echo "#define VERSION_NAME \"$(VERSION_NAME)\"" >> $@
|
||||||
|
|
||||||
src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
|
src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
|
||||||
@echo [android_version] $@
|
@echo "[android_version] $@"
|
||||||
@sed -i \
|
@sed -i \
|
||||||
-e 's/versionCode=".*"/versionCode="$(VERSION_CODE)"/' \
|
-e 's/versionCode=".*"/versionCode="$(VERSION_CODE)"/' \
|
||||||
-e 's/versionName=".*"/versionName="$(VERSION_NUMBER)"/' \
|
-e 's/versionName=".*"/versionName="$(VERSION_NUMBER)"/' \
|
||||||
@ -673,82 +810,188 @@ src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
|
|||||||
# Android support.
|
# Android support.
|
||||||
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
|
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@echo [aapt2] $@
|
@echo "[aapt2] $@"
|
||||||
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
|
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
|
||||||
|
|
||||||
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
|
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@echo [aapt2] $@
|
@echo "[aapt2] $@"
|
||||||
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
|
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
|
||||||
|
|
||||||
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
|
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
|
||||||
@mkdir -p $(dir $@)
|
@echo [aapt2 link] res.apk
|
||||||
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
|
@mkdir -p out/apk/
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat \
|
||||||
|
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
|
||||||
|
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
|
||||||
|
--manifest src/android/AndroidManifest.xml \
|
||||||
|
-o out/apk/res.apk \
|
||||||
|
--java out/gen/
|
||||||
|
|
||||||
|
out/apk/res.fdroid.apk out/gen_fdroid/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
|
||||||
|
@echo [aapt2 link] res.fdroid.apk
|
||||||
|
@mkdir -p out/apk/
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat \
|
||||||
|
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
|
||||||
|
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
|
||||||
|
--rename-manifest-package com.unprompted.tildefriends.fdroid \
|
||||||
|
--manifest src/android/AndroidManifest.xml \
|
||||||
|
-o out/apk/res.fdroid.apk \
|
||||||
|
--java out/gen_fdroid/
|
||||||
|
|
||||||
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
|
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
|
||||||
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
|
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
|
||||||
|
|
||||||
$(CLASS_FILES) &: $(JAVA_FILES)
|
$(CLASS_FILES) &: $(JAVA_FILES)
|
||||||
@echo [javac] $(CLASS_FILES)
|
@echo "[javac] $(CLASS_FILES)"
|
||||||
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
|
@javac --release 8 -encoding UTF-8 -Xlint:deprecation -XDuseUnsharedTable=true -classpath $(ANDROID_PLATFORM)/android.jar:$(ANDROID_BUILD_TOOLS)/core-lambda-stubs.jar -d out/classes $(JAVA_FILES)
|
||||||
|
|
||||||
out/apk/classes.dex: $(CLASS_FILES)
|
out/apk/classes.dex: $(CLASS_FILES)
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@echo [d8] $@
|
@echo "[d8] $@"
|
||||||
@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
|
@$(ANDROID_BUILD_TOOLS)/d8 --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
|
||||||
|
|
||||||
PACKAGE_DIRS := \
|
PACKAGE_DIRS := \
|
||||||
apps/ \
|
apps/ \
|
||||||
core/ \
|
core/ \
|
||||||
deps/codemirror/ \
|
deps/codemirror/ \
|
||||||
|
deps/prettier/ \
|
||||||
deps/lit/
|
deps/lit/
|
||||||
|
|
||||||
RAW_FILES := $(filter-out apps/blog% apps/gg% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
|
RAW_FILES := $(sort $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f -not -name '.*')))
|
||||||
|
|
||||||
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
|
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
|
||||||
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
|
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
|
||||||
out/apk/TildeFriends-x86-debug.unsigned.apk: BUILD_TYPE := debug
|
out/apk/TildeFriends-x86-debug.unsigned.apk: BUILD_TYPE := debug
|
||||||
out/apk/TildeFriends-x86-release.unsigned.apk: BUILD_TYPE := release
|
out/apk/TildeFriends-x86-release.unsigned.apk: BUILD_TYPE := release
|
||||||
|
out/apk/TildeFriends-release.fdroid.unsigned.apk: BUILD_TYPE := release
|
||||||
|
|
||||||
out/apk/TildeFriends-arm-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
out/apk/TildeFriends-arm-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
out/apk/TildeFriends-arm-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
out/apk/TildeFriends-arm-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
out/apk/TildeFriends-x86-debug.unsigned.apk: out/apk/classes.dex out/androiddebug-x86_64/tildefriends out/androiddebug-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
out/apk/TildeFriends-x86-debug.unsigned.apk: out/apk/classes.dex out/androiddebug-x86_64/tildefriends out/androiddebug-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
out/apk/TildeFriends-x86-release.unsigned.apk: out/apk/classes.dex out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
out/apk/TildeFriends-x86-release.unsigned.apk: out/apk/classes.dex out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
out/apk/TildeFriends-release.fdroid.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.fdroid.apk
|
||||||
|
|
||||||
|
$(BUNDLETOOL):
|
||||||
|
@echo [curl] $(BUNDLETOOL_URL) TO $@
|
||||||
|
@curl -q -L --create-dirs -o $@ $(BUNDLETOOL_URL)
|
||||||
|
|
||||||
|
out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGETS)) $(RAW_FILES) out/apk/res.apk src/android/AndroidManifest.xml $(BUNDLETOOL)
|
||||||
|
@rm -rf out/aab/staging/
|
||||||
|
@mkdir -p out/aab/staging
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 link --proto-format -o out/aab/temporary.apk \
|
||||||
|
-I $(ANDROID_PLATFORM)/android.jar \
|
||||||
|
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
|
||||||
|
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
|
||||||
|
--manifest src/android/AndroidManifest.xml \
|
||||||
|
-R out/res/layout_activity_main.xml.flat \
|
||||||
|
-R out/res/drawable_icon.xml.flat \
|
||||||
|
--auto-add-overlay
|
||||||
|
@unzip out/aab/temporary.apk -d out/aab/staging/
|
||||||
|
@mkdir -p out/aab/staging/root/deps
|
||||||
|
@mkdir -p out/aab/staging/classes
|
||||||
|
@mkdir -p out/aab/staging/dex
|
||||||
|
@mkdir -p out/aab/staging/manifest
|
||||||
|
@mv out/aab/staging/AndroidManifest.xml out/aab/staging/manifest/AndroidManifest.xml
|
||||||
|
@cp out/apk/classes.dex out/aab/staging/dex/
|
||||||
|
@rm -fv out/base.zip
|
||||||
|
@mkdir -p out/aab/staging/lib/arm64-v8a out/aab/staging/lib/armeabi-v7a out/aab/staging/lib/x86_64 out/aab/staging/lib/x86
|
||||||
|
@cp out/androidrelease/tildefriends out/aab/staging/lib/arm64-v8a/libtildefriends.so
|
||||||
|
@cp out/androidrelease-armv7a/tildefriends out/aab/staging/lib/armeabi-v7a/libtildefriends.so
|
||||||
|
@cp out/androidrelease-x86_64/tildefriends out/aab/staging/lib/x86_64/libtildefriends.so
|
||||||
|
@cp out/androidrelease-x86/tildefriends out/aab/staging/lib/x86/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/arm64-v8a/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/armeabi-v7a/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86_64/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86/libtildefriends.so
|
||||||
|
@cp -r apps/ out/aab/staging/root/
|
||||||
|
@rm -rf out/aab/staging/root/apps/welcome*
|
||||||
|
@cp -r core/ out/aab/staging/root/
|
||||||
|
@cp -r deps/prettier/ out/aab/staging/root/deps/
|
||||||
|
@cp -r deps/lit/ out/aab/staging/root/deps/
|
||||||
|
@cp -r deps/codemirror/ out/aab/staging/root/deps/
|
||||||
|
@cd out/aab/staging/; zip -r ../base.zip *; cd ../../../
|
||||||
|
@java -jar $(BUNDLETOOL) build-bundle --overwrite --config=src/android/BundleConfig.json --modules=out/aab/base.zip --output=$@
|
||||||
|
@jarsigner -keystore .keys/android.jks $@ androidKey -storepass android
|
||||||
|
|
||||||
|
aab: out/TildeFriends.aab
|
||||||
|
.PHONY: aab
|
||||||
|
|
||||||
|
out/TildeFriends.apks: out/TildeFriends.aab $(BUNDLETOOL)
|
||||||
|
@java -jar $(BUNDLETOOL) build-apks --bundle out/TildeFriends.aab --overwrite --output $@ --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android
|
||||||
|
|
||||||
|
aabgo: out/TildeFriends.apks $(BUNDLETOOL)
|
||||||
|
@java -jar $(BUNDLETOOL) install-apks --apks out/TildeFriends.apks
|
||||||
|
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
|
||||||
|
|
||||||
out/apk/TildeFriends-arm-%.unsigned.apk:
|
out/apk/TildeFriends-arm-%.unsigned.apk:
|
||||||
@mkdir -p $(dir $@) out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/
|
@mkdir -p $(dir $@) out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/
|
||||||
@echo [aapt] $@
|
@echo "[aapt] $@"
|
||||||
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
|
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
|
||||||
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
|
||||||
@cp out/apk/res.apk $@
|
@cp out/apk/res.apk $@.zip
|
||||||
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
|
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
|
||||||
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
|
||||||
@zip -u $@ -q -9 $(RAW_FILES)
|
@zip -u $@.zip -q -9 $(RAW_FILES)
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
|
||||||
|
|
||||||
out/apk/TildeFriends-x86-%.unsigned.apk:
|
out/apk/TildeFriends-x86-%.unsigned.apk:
|
||||||
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
|
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
|
||||||
@echo [aapt] $@
|
@echo "[aapt] $@"
|
||||||
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
|
||||||
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/libtildefriends.so
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/libtildefriends.so
|
||||||
@cp out/apk/res.apk $@
|
@cp out/apk/res.apk $@.zip
|
||||||
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
|
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
|
||||||
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
|
||||||
@zip -u $@ -q -9 $(RAW_FILES)
|
@zip -u $@.zip -q -9 $(RAW_FILES)
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
|
||||||
|
|
||||||
|
out/apk/TildeFriends-%.fdroid.unsigned.apk:
|
||||||
|
@rm -rf out/apk-fdroid-$(BUILD_TYPE) out/apk-fdroid-$(BUILD_TYPE)-raw
|
||||||
|
@mkdir -p $(dir $@) out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/ out/apk-fdroid-$(BUILD_TYPE)/lib/x86/ out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/
|
||||||
|
@echo "[aapt] $@"
|
||||||
|
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
|
||||||
|
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/x86/libtildefriends.so
|
||||||
|
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
|
||||||
|
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/x86/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
|
||||||
|
@cp out/apk/res.fdroid.apk $@.zip
|
||||||
|
@cp out/apk/classes.dex out/apk-fdroid-$(BUILD_TYPE)/classes.dex
|
||||||
|
@touch -d @0 out/apk-fdroid-$(BUILD_TYPE)/classes.dex out/apk-fdroid-$(BUILD_TYPE)/lib/*/libtildefriends.so
|
||||||
|
@chmod 755 out/apk-fdroid-$(BUILD_TYPE)/classes.dex out/apk-fdroid-$(BUILD_TYPE)/lib/*/libtildefriends.so
|
||||||
|
@cd out/apk-fdroid-$(BUILD_TYPE) && zip -X -u ../../$@.zip -q classes.dex lib/*/libtildefriends.so && cd ../../
|
||||||
|
@mkdir out/apk-fdroid-$(BUILD_TYPE)-raw
|
||||||
|
@for i in $(RAW_FILES); do mkdir -p $$(dirname out/apk-fdroid-$(BUILD_TYPE)-raw/$$i) && cp $$i out/apk-fdroid-$(BUILD_TYPE)-raw/$$i && touch -d @0 out/apk-fdroid-$(BUILD_TYPE)-raw/$$i && chmod 644 out/apk-fdroid-$(BUILD_TYPE)-raw/$$i; done
|
||||||
|
@cd out/apk-fdroid-$(BUILD_TYPE)-raw && zip -X -u ../../$@.zip -q $(RAW_FILES) && cd ../../
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
|
||||||
|
|
||||||
out/%.apk: out/apk/%.unsigned.apk
|
out/%.apk: out/apk/%.unsigned.apk
|
||||||
@echo [apksigner] $(notdir $@)
|
@echo "[apksigner] $(notdir $@)"
|
||||||
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
|
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $<
|
||||||
|
|
||||||
release-apk: out/TildeFriends-arm-release.apk out/TildeFriends-x86-release.apk
|
out/%.zopfli.apk: out/%.apk
|
||||||
|
@echo "[zopfli] $(notdir $@)"
|
||||||
|
$(ANDROID_BUILD_TOOLS)/zipalign -f -z 4 $< $@.zopfli
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $@.zopfli
|
||||||
|
|
||||||
|
release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk
|
||||||
.PHONY: release-apk
|
.PHONY: release-apk
|
||||||
|
|
||||||
|
apkgo: out/TildeFriends-arm-debug.apk
|
||||||
|
@adb install -r $<
|
||||||
|
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
|
||||||
|
.PHONY: apkgo
|
||||||
|
|
||||||
releaseapkgo: out/TildeFriends-arm-release.apk
|
releaseapkgo: out/TildeFriends-arm-release.apk
|
||||||
@adb install -r $<
|
@adb install -r $<
|
||||||
@adb shell am start com.unprompted.tildefriends/.MainActivity
|
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
|
||||||
.PHONY: releaseapkgo
|
.PHONY: releaseapkgo
|
||||||
|
|
||||||
# iOS Support
|
# iOS Support
|
||||||
@ -759,24 +1002,35 @@ out/%.app/tildefriends.png: src/ios/tildefriends.png
|
|||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@cp -v $< $@
|
@cp -v $< $@
|
||||||
|
|
||||||
out/%/data.zip: $(RAW_FILES)
|
out/data.zip: $(RAW_FILES)
|
||||||
@zip -u $@ -q -9 $(RAW_FILES)
|
@zip -u $@ -q -9 $(RAW_FILES)
|
||||||
|
|
||||||
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/tildefriends-%.app/data.zip
|
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@cp -v $< $@
|
@cp -v $< $@
|
||||||
|
@cp -v out/data.zip $(@D)/
|
||||||
ifeq ($(HAVE_LINUX_IOS),1)
|
ifeq ($(HAVE_LINUX_IOS),1)
|
||||||
@zsign -q -k .keys/apple.p12 -f -m src/ios/embedded.mobileprovision $(realpath $(dir $@))
|
@zsign -q -k .keys/apple.p12 -f -m src/ios/embedded.mobileprovision $(realpath $(dir $@))
|
||||||
endif
|
endif
|
||||||
.SECONDARY:
|
.SECONDARY:
|
||||||
out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
|
out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
|
||||||
@echo [ipa] $@
|
@echo "[ipa] $@"
|
||||||
@rm -rf $@.tmp $@
|
@rm -rf $@.tmp $@
|
||||||
@mkdir -p $@.tmp/Payload/tildefriends.app/
|
@mkdir -p $@.tmp/Payload/tildefriends.app/
|
||||||
@cp -R $(dir $<)/* $@.tmp/Payload/tildefriends.app/
|
@cp -R $(dir $<)/* $@.tmp/Payload/tildefriends.app/
|
||||||
@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
|
@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
|
||||||
@rm -rf $@.tmp/
|
@rm -rf $@.tmp/
|
||||||
|
|
||||||
|
|
||||||
|
out/%/tildefriends.standalone: out/%/tildefriends out/data.zip
|
||||||
|
@echo "[standalone] $@"
|
||||||
|
@cat $< out/data.zip > $@
|
||||||
|
@chmod +x $@
|
||||||
|
out/%/tildefriends.standalone.exe: out/%/tildefriends.exe out/data.zip
|
||||||
|
@echo "[standalone] $@"
|
||||||
|
@cat $< out/data.zip > $@
|
||||||
|
@chmod +x $@
|
||||||
|
|
||||||
iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends
|
iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends
|
||||||
iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends
|
iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends
|
||||||
iosdebug-app: out/tildefriends-iosdebug.app/tildefriends
|
iosdebug-app: out/tildefriends-iosdebug.app/tildefriends
|
||||||
@ -798,18 +1052,74 @@ apklog:
|
|||||||
@adb logcat *:S tildefriends
|
@adb logcat *:S tildefriends
|
||||||
.PHONY: apklog
|
.PHONY: apklog
|
||||||
|
|
||||||
|
fetchdeps:
|
||||||
|
@echo "[fetch] sqlite"
|
||||||
|
@test -f out/deps/sqlite.zip && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p out/deps/ && curl -q $(SQLITE_URL) -o out/deps/sqlite.zip)
|
||||||
|
@test -d deps/sqlite/ && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p deps/sqlite/ && unzip -qDjo -d deps/sqlite/ out/deps/sqlite.zip)
|
||||||
|
@echo -n $(SQLITE_URL) > out/deps/sqlite.txt
|
||||||
|
@echo "[fetch] prettier"
|
||||||
|
@test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs
|
||||||
|
@test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs
|
||||||
|
@test -f deps/prettier/babel.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/babel.mjs
|
||||||
|
@test -f deps/prettier/estree.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/estree.mjs
|
||||||
|
.PHONY: fetchdeps
|
||||||
|
|
||||||
|
ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a
|
||||||
|
$(ANDROID_DEPS):
|
||||||
|
+@ANDROID_NDK_ROOT=$(ANDROID_NDK) tools/ssl-android
|
||||||
|
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
|
||||||
|
|
||||||
|
ifeq ($(HAVE_WIN),1)
|
||||||
|
WINDOWS_DEPS := deps/openssl/mingw64/usr/local/lib/libssl.a
|
||||||
|
$(WINDOWS_DEPS):
|
||||||
|
+@tools/ssl-mingw64
|
||||||
|
$(filter $(BUILD_DIR)/win%,$(APP_OBJS)): | $(WINDOWS_DEPS)
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifeq ($(UNAME_S),Darwin)
|
||||||
|
IOS_DEPS := deps/openssl/ios/ios64-xcrun/usr/local/lib/libssl.a
|
||||||
|
$(IOS_DEPS):
|
||||||
|
+@tools/ssl-ios
|
||||||
|
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
|
||||||
|
endif
|
||||||
|
|
||||||
|
out/tildefriends-x86_64.AppImage: out/release/tildefriends out/data.zip
|
||||||
|
@echo "[appimage] $$@"
|
||||||
|
@rm -rf out/tildefriends.AppDir
|
||||||
|
@mkdir -p out/tildefriends.AppDir/usr/bin
|
||||||
|
@mkdir -p out/tildefriends.AppDir/usr/share/applications
|
||||||
|
@mkdir -p out/tildefriends.AppDir/usr/share/icons/hicolor/scalable/apps
|
||||||
|
@mkdir -p out/tildefriends.AppDir/usr/share/tildefriends
|
||||||
|
@echo $(APPIMAGETOOL_MD5) > out/appimagetool.md5
|
||||||
|
@test -x out/appimagetool || curl -q -L -o out/appimagetool $(APPIMAGETOOL_URL) && md5sum -c out/appimagetool.md5 && chmod +x out/appimagetool
|
||||||
|
@echo "[Desktop Entry]\nName=tildefriends\nExec=/usr/bin/tildefriends\nIcon=/usr/share/icons/hicolor/scalable/apps/tildefriends\nType=Application\nCategories=Network" > out/tildefriends.AppDir/tildefriends.desktop
|
||||||
|
@cp src/ios/tildefriends.svg out/tildefriends.AppDir/usr/share/icons/hicolor/scalable/apps/
|
||||||
|
@cp src/ios/tildefriends.svg out/tildefriends.AppDir/
|
||||||
|
@cp out/release/tildefriends out/tildefriends.AppDir/usr/bin/
|
||||||
|
@cp out/data.zip out/tildefriends.AppDir/usr/share/tildefriends/data.zip
|
||||||
|
@echo "#!/bin/sh\n\$${APPDIR}/usr/bin/tildefriends run -z \$$APPDIR/usr/share/tildefriends/data.zip" > out/tildefriends.AppDir/AppRun
|
||||||
|
@chmod +x out/tildefriends.AppDir/AppRun
|
||||||
|
@cd out; ./appimagetool --appimage-extract; cd ..
|
||||||
|
@cd out; unset SOURCE_DATE_EPOCH; PATH=$$PATH:squashfs-root/usr/bin ARCH=x86_64 squashfs-root/usr/bin/appimagetool -u 'zsync|https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage.zsync' tildefriends.AppDir tildefriends-x86_64.AppImage; cd ..
|
||||||
|
|
||||||
|
appimage: out/tildefriends-x86_64.AppImage
|
||||||
|
.PHONY: appimage
|
||||||
|
|
||||||
|
flatpak: out/
|
||||||
|
flatpak-builder --force-clean --user --install-deps-from=flathub --install --repo=out/flatpak-repo out/flatpak src/com.unprompted.tildefriends.yml
|
||||||
|
flatpak build-bundle out/flatpak-repo out/tildefriends.flatpak com.unprompted.tildefriends
|
||||||
|
.PHONY: flatpak
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
|
|
||||||
dist: release-apk iosrelease-ipa
|
tarball:
|
||||||
@echo "[export] $$(svn info --show-item url)"
|
@echo [archive] out/tildefriends-$(VERSION_NUMBER).tar.xz
|
||||||
@rm -rf tildefriends-$(VERSION_NUMBER)
|
@rm -rf out/tildefriends-$(VERSION_NUMBER)
|
||||||
@svn export -q . tildefriends-$(VERSION_NUMBER)
|
@mkdir -p out/tildefriends-$(VERSION_NUMBER)
|
||||||
@echo "tildefriends-$(VERSION_NUMBER): $(VERSION_NAME)" > tildefriends-$(VERSION_NUMBER)/VERSION
|
@git ls-files --recurse-submodules | tar -c -T- | tar -x -C out/tildefriends-$(VERSION_NUMBER)
|
||||||
@echo "[tar] tildefriends-$(VERSION_NUMBER).tar.xz"
|
|
||||||
@tar \
|
@tar \
|
||||||
--exclude=apps/gg* \
|
|
||||||
--exclude=apps/welcome* \
|
--exclude=apps/welcome* \
|
||||||
--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
|
--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
|
||||||
--exclude=deps/libsodium/builds/msvc/vs* \
|
--exclude=deps/libsodium/builds/msvc/vs* \
|
||||||
@ -824,14 +1134,29 @@ dist: release-apk iosrelease-ipa
|
|||||||
--exclude=deps/sqlite/shell.c \
|
--exclude=deps/sqlite/shell.c \
|
||||||
--exclude=deps/zlib/contrib/vstudio \
|
--exclude=deps/zlib/contrib/vstudio \
|
||||||
--exclude=deps/zlib/doc \
|
--exclude=deps/zlib/doc \
|
||||||
-caf tildefriends-$(VERSION_NUMBER).tar.xz tildefriends-$(VERSION_NUMBER)
|
-caf out/tildefriends-$(VERSION_NUMBER).tar.xz \
|
||||||
@rm -rf tildefriends-$(VERSION_NUMBER)
|
-C out/ \
|
||||||
|
tildefriends-$(VERSION_NUMBER)
|
||||||
|
.PHONY: tarball
|
||||||
|
|
||||||
|
dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) out/TildeFriends-release.fdroid.apk appimage tarball
|
||||||
|
@mkdir -p dist/
|
||||||
|
@echo "[cp] tildefriends-$(VERSION_NUMBER).tar.xz"
|
||||||
|
@cp out/tildefriends-$(VERSION_NUMBER).tar.xz dist/tildefriends-$(VERSION_NUMBER).tar.xz
|
||||||
@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk"
|
@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk"
|
||||||
@cp out/TildeFriends-x86-release.apk TildeFriends-x86-$(VERSION_NUMBER).apk
|
@cp out/TildeFriends-x86-release.zopfli.apk dist/TildeFriends-x86-$(VERSION_NUMBER).apk
|
||||||
@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk"
|
@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk"
|
||||||
@cp out/TildeFriends-arm-release.apk TildeFriends-arm-$(VERSION_NUMBER).apk
|
@cp out/TildeFriends-arm-release.zopfli.apk dist/TildeFriends-arm-$(VERSION_NUMBER).apk
|
||||||
@echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
|
@echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
|
||||||
@cp out/tildefriends-release.ipa TildeFriends-$(VERSION_NUMBER).ipa
|
@cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
|
||||||
|
@test $(HAVE_WIN) && echo "[cp] tildefriends-$(VERSION_NUMBER).exe"
|
||||||
|
@test $(HAVE_WIN) && cp out/winrelease/tildefriends.standalone.exe dist/tildefriends-$(VERSION_NUMBER).exe
|
||||||
|
@echo "[cp] TildeFriends-$(VERSION_NUMBER).aab"
|
||||||
|
@cp out/TildeFriends.aab dist/TildeFriends-$(VERSION_NUMBER).aab
|
||||||
|
@echo "[cp] TildeFriends-$(VERSION_NUMBER).fdroid.apk"
|
||||||
|
@cp out/TildeFriends-release.fdroid.apk dist/TildeFriends-$(VERSION_NUMBER).fdroid.apk
|
||||||
|
@echo "[cp] TildeFriends-x86_64-$(VERSION_NUMBER).AppImage"
|
||||||
|
@cp out/tildefriends-x86_64.AppImage dist/TildeFriends-x86_64-$(VERSION_NUMBER).AppImage
|
||||||
.PHONY: dist
|
.PHONY: dist
|
||||||
|
|
||||||
dist-test: dist
|
dist-test: dist
|
||||||
@ -840,3 +1165,18 @@ dist-test: dist
|
|||||||
@docker build tildefriends-$(VERSION_NUMBER)/
|
@docker build tildefriends-$(VERSION_NUMBER)/
|
||||||
@rm -rf tildefriends-$(VERSION_NUMBER)
|
@rm -rf tildefriends-$(VERSION_NUMBER)
|
||||||
.PHONY: dist-test
|
.PHONY: dist-test
|
||||||
|
|
||||||
|
format:
|
||||||
|
@clang-format -i $(wildcard src/*.c src/*.h src/*.m)
|
||||||
|
.PHONY: format
|
||||||
|
|
||||||
|
prettier:
|
||||||
|
@npm run prettier
|
||||||
|
.PHONY: prettier
|
||||||
|
|
||||||
|
docs:
|
||||||
|
@doxygen
|
||||||
|
.PHONY: docs
|
||||||
|
|
||||||
|
fdroid: out/apk/TildeFriends-release.fdroid.unsigned.apk
|
||||||
|
.PHONY: fdroid
|
||||||
|
40
README.md
40
README.md
@ -1,37 +1,65 @@
|
|||||||
# Tilde Friends
|
# Tilde Friends
|
||||||
|
|
||||||
Tilde Friends is a tool for making and sharing.
|
Tilde Friends is a tool for making and sharing.
|
||||||
|
|
||||||
|
A public instance lives at https://www.tildefriends.net/.
|
||||||
|
|
||||||
It is both a peer-to-peer social network client, participating in Secure
|
It is both a peer-to-peer social network client, participating in Secure
|
||||||
Scuttlebutt, as well as a platform for writing and running web applications.
|
Scuttlebutt, as well as a platform for writing and running web applications.
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
1. Make it easy and fun to run all sorts of web applications.
|
1. Make it easy and fun to run all sorts of web applications.
|
||||||
2. Provide security that is easy to understand and protects your data.
|
2. Provide security that is easy to understand and protects your data.
|
||||||
3. Make creating and sharing web applications accessible to anyone with a
|
3. Make creating and sharing web applications accessible to anyone with a
|
||||||
browser.
|
browser.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
|
|
||||||
are kept up to date in the tree.
|
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
|
2. To build, run `make debug` or `make release`. An executable will be
|
||||||
generated in a subdirectory of `out/`.
|
generated in a subdirectory of `out/`.
|
||||||
3. It's possible to build for Android, iOS, and Windows on Linux, if you have
|
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
|
the right dependencies in the right places. `make windebug winrelease
|
||||||
iosdebug-ipa iosrelease-ipa release-apk`.
|
iosdebug-ipa iosrelease-ipa release-apk`.
|
||||||
4. To build in docker, `docker build .`.
|
4. To build in docker, `docker build .`.
|
||||||
|
5. `make format` will normalize formatting to the coding standard.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
By default, running the built `tildefriends` executable will start a web server
|
By default, running the built `tildefriends` executable will start a web server
|
||||||
at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
||||||
|
|
||||||
The first user to create an account and log in will be granted administrative
|
The first user to create an account and log in will be granted administrative
|
||||||
privileges. Further administration can be done at
|
privileges. Further administration can be done at
|
||||||
<http://localhost:12345/~core/admin/`>.
|
<http://localhost:12345/~core/admin/>.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
There are the very beginnings of developer documentation in `apps/docs/`
|
|
||||||
that can be read in-place or at <http://localhost:12345/~core/docs/>.
|
Docs are a work in progress:
|
||||||
|
<https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
All code unless otherwise noted in is provided under the
|
All code unless otherwise noted in is provided under the
|
||||||
[MIT](https://opensource.org/licenses/MIT) license.
|
[MIT](https://opensource.org/licenses/MIT) license.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🎛"
|
"emoji": "🎛",
|
||||||
|
"previous": "&R49FywYF8CXPhoSEydLbSCgvCddeyTiBwGuDU/gqY+M=.sha256"
|
||||||
}
|
}
|
@ -18,9 +18,13 @@ async function main() {
|
|||||||
for (let user of await core.users()) {
|
for (let user of await core.users()) {
|
||||||
data.users[user] = await core.permissionsForUser(user);
|
data.users[user] = await core.permissionsForUser(user);
|
||||||
}
|
}
|
||||||
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
|
await app.setDocument(
|
||||||
|
utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data))
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
await app.setDocument('<span style="color: #f00">Only an administrator can modify these settings.</span>');
|
await app.setDocument(
|
||||||
|
'<span style="color: #f00">Only an administrator can modify these settings.</span>'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
main();
|
main();
|
@ -1,10 +1,41 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="width: 100%">
|
<html style="width: 100%">
|
||||||
<head>
|
<head>
|
||||||
<script>const g_data = $data;</script>
|
<script>
|
||||||
|
const g_data = $data;
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="w3.css" />
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<style>
|
||||||
|
/* 2018 Valiant Poppy */
|
||||||
|
.w3-theme-l5 {color:#000 !important; background-color:#fbf3f3 !important}
|
||||||
|
.w3-theme-l4 {color:#000 !important; background-color:#f3d7d6 !important}
|
||||||
|
.w3-theme-l3 {color:#000 !important; background-color:#e6afae !important}
|
||||||
|
.w3-theme-l2 {color:#fff !important; background-color:#da8785 !important}
|
||||||
|
.w3-theme-l1 {color:#fff !important; background-color:#cd5f5d !important}
|
||||||
|
.w3-theme-d1 {color:#fff !important; background-color:#a93634 !important}
|
||||||
|
.w3-theme-d2 {color:#fff !important; background-color:#96302e !important}
|
||||||
|
.w3-theme-d3 {color:#fff !important; background-color:#832a28 !important}
|
||||||
|
.w3-theme-d4 {color:#fff !important; background-color:#702423 !important}
|
||||||
|
.w3-theme-d5 {color:#fff !important; background-color:#5e1e1d !important}
|
||||||
|
|
||||||
|
.w3-theme-light {color:#000 !important; background-color:#fbf3f3 !important}
|
||||||
|
.w3-theme-dark {color:#fff !important; background-color:#5e1e1d !important}
|
||||||
|
.w3-theme-action {color:#fff !important; background-color:#5e1e1d !important}
|
||||||
|
|
||||||
|
.w3-theme {color:#fff !important; background-color:#bd3d3a !important}
|
||||||
|
.w3-text-theme {color:#bd3d3a !important}
|
||||||
|
.w3-border-theme {border-color:#bd3d3a !important}
|
||||||
|
|
||||||
|
.w3-hover-theme:hover {color:#fff !important; background-color:#bd3d3a !important}
|
||||||
|
.w3-hover-text-theme:hover {color:#bd3d3a !important}
|
||||||
|
.w3-hover-border-theme:hover {border-color:#bd3d3a !important}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff; width: 100%">
|
<body class="w3-theme-l4">
|
||||||
|
<header class="w3-row w3-padding w3-header w3-theme-l1">
|
||||||
<h1>Tilde Friends Administration</h1>
|
<h1>Tilde Friends Administration</h1>
|
||||||
|
</header>
|
||||||
</body>
|
</body>
|
||||||
<script type="module" src="script.js"></script>
|
<script type="module" src="script.js"></script>
|
||||||
</html>
|
</html>
|
@ -3,85 +3,113 @@ import * as tfrpc from '/static/tfrpc.js';
|
|||||||
|
|
||||||
function delete_user(user) {
|
function delete_user(user) {
|
||||||
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
|
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
|
||||||
tfrpc.rpc.delete_user(user).then(function() {
|
tfrpc.rpc
|
||||||
|
.delete_user(user)
|
||||||
|
.then(function () {
|
||||||
alert(`User "${user}" deleted successfully.`);
|
alert(`User "${user}" deleted successfully.`);
|
||||||
}).catch(function(error) {
|
})
|
||||||
alert(`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`);
|
.catch(function (error) {
|
||||||
|
alert(
|
||||||
|
`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function global_settings_set(key, value) {
|
function global_settings_set(key, value) {
|
||||||
tfrpc.rpc.global_settings_set(key, value).then(function() {
|
tfrpc.rpc
|
||||||
|
.global_settings_set(key, value)
|
||||||
|
.then(function () {
|
||||||
alert(`Set "${key}" to "${value}".`);
|
alert(`Set "${key}" to "${value}".`);
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', function() {
|
function title_case(name) {
|
||||||
const permission_template = (permission) =>
|
return name
|
||||||
html` <code>${permission}</code>`;
|
.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) {
|
function input_template(key, description) {
|
||||||
if (description.type === 'boolean') {
|
if (description.type === 'boolean') {
|
||||||
return html`
|
return html`
|
||||||
<div style="margin-top: 1em">
|
<li class="w3-row">
|
||||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
|
||||||
<div>
|
<div class="w3-quarter w3-padding">${description.description}</div>
|
||||||
<input type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
|
<div class="w3-quarter w3-padding w3-center"><input class="w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input></div>
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
|
<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstChild.checked)}>Set</button>
|
||||||
<div>${description.description}</div>
|
</li>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
} else if (description.type === 'textarea') {
|
} else if (description.type === 'textarea') {
|
||||||
return html`
|
return html`
|
||||||
<div style="margin-top: 1em"">
|
<li class="w3-row">
|
||||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold"
|
||||||
<div style="width: 100%; padding: 0; margin: 0">
|
>${title_case(key)}</label
|
||||||
<div style="width: 90%; padding: 0 margin: 0">
|
>
|
||||||
<textarea style="vertical-align: top; width: 100%" rows=20 cols=80 id=${'gs_' + key}>${description.value}</textarea>
|
<div class="w3-rest w3-padding">${description.description}</div>
|
||||||
</div>
|
<textarea
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstElementChild.value)}>Set</button>
|
class="w3-input"
|
||||||
<div>${description.description}</div>
|
style="vertical-align: top; resize: vertical"
|
||||||
</div>
|
id=${'gs_' + key}
|
||||||
</div>
|
>
|
||||||
|
${description.value}</textarea
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-right w3-quarter w3-theme-action"
|
||||||
|
@click=${(e) =>
|
||||||
|
global_settings_set(
|
||||||
|
key,
|
||||||
|
e.srcElement.previousElementSibling.value
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Set
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<div style="margin-top: 1em">
|
<li class="w3-row">
|
||||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
|
||||||
<div>
|
<div class="w3-quarter w3-padding">${description.description}</div>
|
||||||
<input type="text" value="${description.value}" id=${'gs_' + key}></input>
|
<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input>
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
||||||
<div>${description.description}</div>
|
</li>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const user_template = (user, permissions) => html`
|
const user_template = (user, permissions) => html`
|
||||||
<li>
|
<li class="w3-card w3-margin">
|
||||||
<button @click=${(e) => delete_user(user)}>
|
<button
|
||||||
|
class="w3-button w3-theme-action"
|
||||||
|
@click=${(e) => delete_user(user)}
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
${user}:
|
${user}: ${permissions.map((x) => permission_template(x))}
|
||||||
${permissions.map(x => permission_template(x))}
|
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
const users_template = (users) =>
|
const users_template = (users) =>
|
||||||
html`<h2>Users</h2>
|
html` <header class="w3-container w3-theme-l2"><h2>Users</h2></header>
|
||||||
<ul>
|
<ul class="w3-ul">
|
||||||
${Object.entries(users).map(u => user_template(u[0], u[1]))}
|
${Object.entries(users).map((u) => user_template(u[0], u[1]))}
|
||||||
</ul>`;
|
</ul>`;
|
||||||
const page_template = (data) =>
|
const page_template = (data) =>
|
||||||
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
|
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
|
||||||
<h2>Global Settings</h2>
|
<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header>
|
||||||
<div>
|
<div class="w3-container">
|
||||||
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
|
<ul class="w3-ul">
|
||||||
|
${Object.keys(data.settings)
|
||||||
|
.sort()
|
||||||
|
.map((x) => html`${input_template(x, data.settings[x])}`)}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
${users_template(data.users)}
|
${users_template(data.users)}
|
||||||
</div>
|
</div> `;
|
||||||
`;
|
|
||||||
render(page_template(g_data), document.body);
|
render(page_template(g_data), document.body);
|
||||||
});
|
});
|
235
apps/admin/w3.css
Normal file
235
apps/admin/w3.css
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
||||||
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
|
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
|
||||||
|
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
|
||||||
|
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
|
||||||
|
a{background-color:transparent}a:active,a:hover{outline-width:0}
|
||||||
|
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
|
||||||
|
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
|
||||||
|
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||||
|
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
|
||||||
|
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
|
||||||
|
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
|
||||||
|
button,input{overflow:visible}button,select{text-transform:none}
|
||||||
|
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
|
||||||
|
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
|
||||||
|
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
|
||||||
|
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
|
||||||
|
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
|
||||||
|
[type=checkbox],[type=radio]{padding:0}
|
||||||
|
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
|
||||||
|
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
|
||||||
|
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
|
||||||
|
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
|
||||||
|
/* End extract */
|
||||||
|
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
|
||||||
|
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
|
||||||
|
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
|
||||||
|
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
|
||||||
|
hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
||||||
|
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
|
||||||
|
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
|
||||||
|
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
|
||||||
|
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
|
||||||
|
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
|
||||||
|
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
|
||||||
|
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
||||||
|
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
|
||||||
|
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
|
||||||
|
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
|
||||||
|
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
|
||||||
|
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
|
||||||
|
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
|
||||||
|
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
|
||||||
|
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
|
||||||
|
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
|
||||||
|
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
|
||||||
|
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
|
||||||
|
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
|
||||||
|
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
|
||||||
|
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
|
||||||
|
.w3-main,#main{transition:margin-left .4s}
|
||||||
|
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
|
||||||
|
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
|
||||||
|
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
|
||||||
|
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
|
||||||
|
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
|
||||||
|
.w3-bar .w3-button{white-space:normal}
|
||||||
|
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
|
||||||
|
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
|
||||||
|
.w3-responsive{display:block;overflow-x:auto}
|
||||||
|
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
|
||||||
|
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
|
||||||
|
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
|
||||||
|
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
|
||||||
|
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
|
||||||
|
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
|
||||||
|
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
|
||||||
|
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
|
||||||
|
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
|
||||||
|
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
|
||||||
|
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
|
||||||
|
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
|
||||||
|
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
|
||||||
|
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
|
||||||
|
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
|
||||||
|
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
|
||||||
|
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
|
||||||
|
@media (max-width:1205px){.w3-auto{max-width:95%}}
|
||||||
|
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
|
||||||
|
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
|
||||||
|
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
|
||||||
|
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
|
||||||
|
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
|
||||||
|
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
|
||||||
|
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
|
||||||
|
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
|
||||||
|
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
|
||||||
|
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
|
||||||
|
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
|
||||||
|
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
|
||||||
|
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
|
||||||
|
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
|
||||||
|
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
|
||||||
|
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
|
||||||
|
.w3-display-position{position:absolute}
|
||||||
|
.w3-circle{border-radius:50%}
|
||||||
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
|
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
|
||||||
|
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
|
||||||
|
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
|
||||||
|
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
|
||||||
|
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
|
||||||
|
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
|
||||||
|
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
|
||||||
|
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
|
||||||
|
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
|
||||||
|
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
|
||||||
|
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
|
||||||
|
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
|
||||||
|
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
|
||||||
|
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
|
||||||
|
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
|
||||||
|
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
|
||||||
|
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
|
||||||
|
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
|
||||||
|
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
|
||||||
|
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
|
||||||
|
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
|
||||||
|
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
|
||||||
|
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
|
||||||
|
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
|
||||||
|
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
|
||||||
|
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
|
||||||
|
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
|
||||||
|
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
|
||||||
|
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
|
||||||
|
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
|
||||||
|
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
|
||||||
|
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
|
||||||
|
.w3-left{float:left!important}.w3-right{float:right!important}
|
||||||
|
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||||
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
|
/* Colors */
|
||||||
|
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||||
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
|
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
|
||||||
|
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
|
||||||
|
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
|
||||||
|
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
|
||||||
|
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
|
||||||
|
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
|
||||||
|
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
|
||||||
|
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
|
||||||
|
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
|
||||||
|
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
|
||||||
|
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
|
||||||
|
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
|
||||||
|
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
|
||||||
|
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
|
||||||
|
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
|
||||||
|
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
|
||||||
|
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
|
||||||
|
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
|
||||||
|
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
|
||||||
|
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
|
||||||
|
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
|
||||||
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||||
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
|
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
|
||||||
|
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
|
||||||
|
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
|
||||||
|
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
|
||||||
|
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
|
||||||
|
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
|
||||||
|
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
|
||||||
|
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
|
||||||
|
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
|
||||||
|
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
|
||||||
|
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
|
||||||
|
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
|
||||||
|
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
|
||||||
|
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
|
||||||
|
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
|
||||||
|
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
|
||||||
|
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
|
||||||
|
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
|
||||||
|
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
|
||||||
|
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
|
||||||
|
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
|
||||||
|
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
|
||||||
|
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
|
||||||
|
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
|
||||||
|
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
|
||||||
|
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
|
||||||
|
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
|
||||||
|
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
|
||||||
|
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
|
||||||
|
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
|
||||||
|
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
|
||||||
|
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
|
||||||
|
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
|
||||||
|
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
|
||||||
|
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
|
||||||
|
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
|
||||||
|
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
|
||||||
|
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
|
||||||
|
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
|
||||||
|
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
|
||||||
|
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
|
||||||
|
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
|
||||||
|
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
|
||||||
|
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
|
||||||
|
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
|
||||||
|
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
|
||||||
|
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
|
||||||
|
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
|
||||||
|
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
|
||||||
|
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
|
||||||
|
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
|
||||||
|
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
|
||||||
|
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
|
||||||
|
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
|
||||||
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
@ -219,7 +219,7 @@ Parses an HTTP response.
|
|||||||
* *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse.
|
* *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
docs['sha1Digest()'] =`
|
docs['sha1Digest()'] = `
|
||||||
Calculates a SHA1 digest.
|
Calculates a SHA1 digest.
|
||||||
|
|
||||||
Completes synchronously.
|
Completes synchronously.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "💻"
|
"emoji": "💻",
|
||||||
|
"previous": "&icsPplXHgmpkbNWyo/WTvRdT/A8BXxW4lJixOtP4ueQ=.sha256"
|
||||||
}
|
}
|
147
apps/apps/app.js
147
apps/apps/app.js
@ -1,25 +1,87 @@
|
|||||||
|
/**
|
||||||
|
* Fetches information about the applications
|
||||||
|
* @param apps Record<appName, blobId>
|
||||||
|
* @returns an object including the apps' name, emoji, and blobs ids
|
||||||
|
*/
|
||||||
async function fetch_info(apps) {
|
async function fetch_info(apps) {
|
||||||
let result = {};
|
let result = {};
|
||||||
|
|
||||||
|
// For each app
|
||||||
for (let [key, value] of Object.entries(apps)) {
|
for (let [key, value] of Object.entries(apps)) {
|
||||||
|
// Get it's blob and parse it
|
||||||
let blob = await ssb.blobGet(value);
|
let blob = await ssb.blobGet(value);
|
||||||
blob = blob ? utf8Decode(blob) : '{}';
|
blob = blob ? utf8Decode(blob) : '{}';
|
||||||
|
|
||||||
|
// Add it to the result object
|
||||||
result[key] = JSON.parse(blob);
|
result[key] = JSON.parse(blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async function fetch_shared_apps() {
|
||||||
|
let messages = {};
|
||||||
|
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
|
FROM messages_fts('"application/tildefriends"')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
ORDER BY messages.timestamp
|
||||||
|
`,
|
||||||
|
[],
|
||||||
|
function (row) {
|
||||||
|
let content = JSON.parse(row.content);
|
||||||
|
for (let mention of content.mentions) {
|
||||||
|
if (mention?.type === 'application/tildefriends') {
|
||||||
|
messages[JSON.stringify([row.author, mention.name])] = {
|
||||||
|
message: row,
|
||||||
|
blob: mention.link,
|
||||||
|
name: mention.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = {};
|
||||||
|
for (let app of Object.values(messages).sort(
|
||||||
|
(x, y) => y.message.timestamp - x.message.timestamp
|
||||||
|
)) {
|
||||||
|
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app.blob)));
|
||||||
|
if (app_object) {
|
||||||
|
app_object.blob_id = app.blob;
|
||||||
|
result[app.name] = app_object;
|
||||||
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
var apps = await fetch_info(await core.apps());
|
const apps = await fetch_info(await core.apps());
|
||||||
var core_apps = await fetch_info(await core.apps('core'));
|
const core_apps = await fetch_info(await core.apps('core'));
|
||||||
var doc = `<!DOCTYPE html>
|
const shared_apps = await fetch_shared_apps();
|
||||||
<html>
|
|
||||||
<head>
|
const stylesheet = `
|
||||||
<style>
|
body {
|
||||||
|
color: whitesmoke;
|
||||||
|
font-family: sans-serif;
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
.container {
|
.container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, 64px);
|
grid-template-columns: repeat(auto-fill, 64px);
|
||||||
|
gap: 1em;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
|
background-color: #ffffff10;
|
||||||
|
border: 2px solid #073642;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
height: 96px;
|
height: 96px;
|
||||||
width: 64px;
|
width: 64px;
|
||||||
@ -34,44 +96,87 @@ async function main() {
|
|||||||
max-width: 64px;
|
max-width: 64px;
|
||||||
text-overflow: ellipsis ellipsis;
|
text-overflow: ellipsis ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
color: whitesmoke;
|
||||||
}
|
}
|
||||||
</style>
|
`;
|
||||||
</head>
|
|
||||||
<body style="background: #888">
|
const body = `
|
||||||
<h1 id="apps_title">Apps</h1>
|
<h1 style="text-shadow: #808080 0 0 10px;">Welcome to Tilde Friends.</h1>
|
||||||
<div id="apps" class="container"></div>
|
|
||||||
<h1>Core Apps</h1>
|
<h2>your apps</h2>
|
||||||
<div id="core_apps" class="container"></div>
|
<div id="apps" class="container"></div>
|
||||||
</body>
|
|
||||||
<script>
|
<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) {
|
function populate_apps(id, name, apps) {
|
||||||
|
// Our target
|
||||||
var list = document.getElementById(id);
|
var list = document.getElementById(id);
|
||||||
|
|
||||||
|
// For each app in the provided list
|
||||||
for (let app of Object.keys(apps).sort()) {
|
for (let app of Object.keys(apps).sort()) {
|
||||||
|
|
||||||
|
// Create the item
|
||||||
let div = list.appendChild(document.createElement('div'));
|
let div = list.appendChild(document.createElement('div'));
|
||||||
div.classList.add('app');
|
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_a = document.createElement('a');
|
||||||
let icon = document.createElement('div');
|
let icon = document.createElement('div');
|
||||||
icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
|
icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
|
||||||
icon.style.fontSize = 'xxx-large';
|
icon.style.fontSize = 'xxx-large';
|
||||||
icon_a.appendChild(icon);
|
icon_a.appendChild(icon);
|
||||||
icon_a.href = '/~' + name + '/' + app + '/';
|
icon_a.href = href;
|
||||||
icon_a.target = '_top';
|
icon_a.target = '_top';
|
||||||
div.appendChild(icon_a);
|
div.appendChild(icon_a);
|
||||||
|
|
||||||
|
// The app's name
|
||||||
let a = document.createElement('a');
|
let a = document.createElement('a');
|
||||||
a.appendChild(document.createTextNode(app));
|
a.appendChild(document.createTextNode(app));
|
||||||
a.href = '/~' + name + '/' + app + '/';
|
a.href = href;
|
||||||
a.target = '_top';
|
a.target = '_top';
|
||||||
div.appendChild(a);
|
div.appendChild(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.getElementById('apps_title').innerText = "~${escape(core.user.credentials?.session?.name || 'guest')}'s Apps";
|
|
||||||
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
||||||
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
||||||
</script>
|
populate_apps('shared_apps', undefined, ${JSON.stringify(shared_apps)});
|
||||||
</html>`;
|
`;
|
||||||
app.setDocument(doc);
|
|
||||||
|
// Build the document
|
||||||
|
const document = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
${stylesheet}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
${body}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
${script}
|
||||||
|
</script>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
// Send it to the browser
|
||||||
|
app.setDocument(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "tildefriends-app",
|
|
||||||
"emoji": "🛍"
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
async function get_apps() {
|
|
||||||
let results = {};
|
|
||||||
await ssb.sqlAsync(`
|
|
||||||
SELECT messages.*
|
|
||||||
FROM messages_fts('"application/tildefriends"')
|
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
|
||||||
ORDER BY timestamp
|
|
||||||
`,
|
|
||||||
[],
|
|
||||||
function(row) {
|
|
||||||
let content = JSON.parse(row.content);
|
|
||||||
for (let mention of content.mentions) {
|
|
||||||
if (mention?.type === 'application/tildefriends') {
|
|
||||||
results[JSON.stringify([row.author, mention.name])] = {
|
|
||||||
message: row,
|
|
||||||
blob: mention.link,
|
|
||||||
name: mention.name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return Object.values(results).sort((x, y) => y.message.timestamp - x.message.timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
function render_app(app) {
|
|
||||||
return `
|
|
||||||
<div style="border: 2px solid white; display: inline-block; margin: 8px; padding: 8px">
|
|
||||||
<a href="/~cory/ssb/#${app.message.author}">@</a>
|
|
||||||
<a href="/~cory/ssb/#${app.message.id}">%</a>
|
|
||||||
<a href="/${app.blob}/">${app.name}</a>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
let apps = await get_apps();
|
|
||||||
app.setDocument(`
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<base target="_top">
|
|
||||||
<style>
|
|
||||||
a:link { color: #bbf; }
|
|
||||||
a:visited { color: #ddd; }
|
|
||||||
a:hover { color: #ddf; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body style="color: #fff">
|
|
||||||
<h1>${apps.length} apps</h1>
|
|
||||||
${apps.map(render_app).join('\n')}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🪵",
|
"emoji": "🪵",
|
||||||
"previous": "&YHBylHM7DlDDiGfuNj+g95Bf7NFxTZs/IKG14TbWnhs=.sha256"
|
"previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256"
|
||||||
}
|
}
|
@ -1,14 +1,57 @@
|
|||||||
import * as commonmark from './commonmark.min.js';
|
import * as commonmark from './commonmark.min.js';
|
||||||
|
|
||||||
function escape(text) {
|
function escape(text) {
|
||||||
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
return (text ?? '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeAttribute(text) {
|
function escapeAttribute(text) {
|
||||||
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
return (text ?? '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
function markdown(md) {
|
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 reader = new commonmark.Parser({safe: true});
|
||||||
let writer = new commonmark.HtmlRenderer();
|
let writer = new commonmark.HtmlRenderer();
|
||||||
let parsed = reader.parse(md || '');
|
let parsed = reader.parse(md || '');
|
||||||
@ -17,10 +60,14 @@ function markdown(md) {
|
|||||||
while ((event = walker.next())) {
|
while ((event = walker.next())) {
|
||||||
node = event.node;
|
node = event.node;
|
||||||
if (event.entering) {
|
if (event.entering) {
|
||||||
if (node.type == 'image') {
|
if (node.destination?.startsWith('&')) {
|
||||||
if (node.destination.startsWith('&')) {
|
node.destination =
|
||||||
node.destination = '/' + node.destination + '/view';
|
'/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
||||||
}
|
} else if (
|
||||||
|
node.destination?.startsWith('@') ||
|
||||||
|
node.destination?.startsWith('%')
|
||||||
|
) {
|
||||||
|
node.destination = '/~core/ssb/#' + escape(node.destination);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -31,7 +78,12 @@ export async function render_blog_post_html(blog_post) {
|
|||||||
let blob = utf8Decode(await ssb.blobGet(blog_post.blog));
|
let blob = utf8Decode(await ssb.blobGet(blog_post.blog));
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>🪵Tilde Friends Blog - ${markdown(blog_post.title)}</title>
|
||||||
|
<base target="_top">
|
||||||
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<h1><a href="./">🪵Tilde Friends Blog</a></h1>
|
||||||
<div>
|
<div>
|
||||||
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</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>${markdown(blob)}</div>
|
||||||
@ -44,7 +96,7 @@ export async function render_blog_post_html(blog_post) {
|
|||||||
function render_blog_post(blog_post) {
|
function render_blog_post(blog_post) {
|
||||||
return `
|
return `
|
||||||
<div>
|
<div>
|
||||||
<h2><a href="../ssb/#${escapeAttribute(blog_post.id)}">${escape(blog_post.title)}</a></h2>
|
<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><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>${markdown(blog_post.summary)}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -55,21 +107,21 @@ export function render_html(blogs) {
|
|||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>🪵Tilde Blog</title>
|
<title>🪵Tilde Friends Blog</title>
|
||||||
<link href="./atom" type="application/atom+xml" rel="alternate" title="🪵Tilde Blog"/>
|
<link href="./atom" type="application/atom+xml" rel="alternate" title="🪵Tilde Blog"/>
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<base target="_blank">
|
<base target="_top">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div style="display: flex; flex-direction: row; align-items: center; gap: 1em">
|
<div style="display: flex; flex-direction: row; align-items: center; gap: 1em">
|
||||||
<h1>🪵Tilde Blog</h1>
|
<h1>🪵Tilde Friends Blog</h1>
|
||||||
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
|
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
|
||||||
</div>
|
</div>
|
||||||
${blogs.map(blog_post => render_blog_post(blog_post)).join('\n')}
|
${blogs.map((blog_post) => render_blog_post(blog_post)).join('\n')}
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
@ -77,7 +129,7 @@ export function render_html(blogs) {
|
|||||||
function render_blog_post_atom(blog_post) {
|
function render_blog_post_atom(blog_post) {
|
||||||
return `<entry>
|
return `<entry>
|
||||||
<title>${escape(blog_post.title)}</title>
|
<title>${escape(blog_post.title)}</title>
|
||||||
<link href="https://tildefriends.net/~cory/ssb/#${blog_post.id}" />
|
<link href="/~cory/ssb/#${blog_post.id}" />
|
||||||
<id>${blog_post.id}</id>
|
<id>${blog_post.id}</id>
|
||||||
<published>${escape(new Date(blog_post.timestamp).toString())}</published>
|
<published>${escape(new Date(blog_post.timestamp).toString())}</published>
|
||||||
<summary>${escape(blog_post.summary)}</summary>
|
<summary>${escape(blog_post.summary)}</summary>
|
||||||
@ -93,17 +145,19 @@ export function render_atom(blogs) {
|
|||||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
<title>🪵Tilde Blog</title>
|
<title>🪵Tilde Blog</title>
|
||||||
<subtitle>A subtitle.</subtitle>
|
<subtitle>A subtitle.</subtitle>
|
||||||
<link href="https://tildefriends.net/~cory/blog/atom" rel="self"/>
|
<link href="${core.url}/atom" rel="self"/>
|
||||||
<link href="https://tildefriends.net/~cory/blog/"/>
|
<link href="${core.url}"/>
|
||||||
<id>https://www.tildefriends.net/~cory/blog/</id>
|
<id>${core.url}</id>
|
||||||
<updated>${new Date().toString()}</updated>
|
<updated>${new Date().toString()}</updated>
|
||||||
${blogs.map(blog_post => render_blog_post_atom(blog_post)).join('\n')}
|
${blogs.map((blog_post) => render_blog_post_atom(blog_post)).join('\n')}
|
||||||
</feed>`;
|
</feed>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_posts() {
|
export async function get_posts() {
|
||||||
let blogs = [];
|
let blogs = [];
|
||||||
await ssb.sqlAsync(`
|
let ids = await ssb.getIdentities();
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
WITH
|
WITH
|
||||||
blogs AS (
|
blogs AS (
|
||||||
SELECT
|
SELECT
|
||||||
@ -139,11 +193,15 @@ export async function get_posts() {
|
|||||||
name IS NOT NULL)
|
name IS NOT NULL)
|
||||||
WHERE author_rank = 1)
|
WHERE author_rank = 1)
|
||||||
SELECT blogs.*, names.name FROM blogs
|
SELECT blogs.*, names.name FROM blogs
|
||||||
|
JOIN json_each(?) AS self ON self.value = blogs.author
|
||||||
JOIN public ON public.author = blogs.author
|
JOIN public ON public.author = blogs.author
|
||||||
LEFT OUTER JOIN names ON names.author = blogs.author
|
LEFT OUTER JOIN names ON names.author = blogs.author
|
||||||
ORDER BY blogs.timestamp DESC LIMIT 20
|
ORDER BY blogs.timestamp DESC LIMIT 20
|
||||||
`, [], function(row) {
|
`,
|
||||||
|
[JSON.stringify(ids)],
|
||||||
|
function (row) {
|
||||||
blogs.push(row);
|
blogs.push(row);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return blogs;
|
return blogs;
|
||||||
}
|
}
|
2
apps/blog/commonmark.min.js
vendored
2
apps/blog/commonmark.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,22 +1,51 @@
|
|||||||
import * as blog from './blog.js';
|
import * as blog from './blog.js';
|
||||||
|
|
||||||
async function main() {
|
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();
|
let blogs = await blog.get_posts();
|
||||||
for (let blog_post of blogs) {
|
for (let blog_post of blogs) {
|
||||||
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
|
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
|
||||||
if (request.path === title) {
|
if (request.path === title) {
|
||||||
respond({data: await blog.render_blog_post_html(blog_post), content_type: 'text/html; charset=utf-8'});
|
respond({
|
||||||
|
data: await blog.render_blog_post_html(blog_post),
|
||||||
|
content_type: 'text/html; charset=utf-8',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (request.path == 'atom') {
|
respond({
|
||||||
respond({data: blog.render_atom(blogs), content_type: 'application/atom+xml'});
|
data: blog.render_html(blogs),
|
||||||
} else {
|
content_type: 'text/html; charset=utf-8',
|
||||||
respond({data: blog.render_html(blogs), content_type: 'text/html; charset=utf-8'});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(function(error) {
|
main().catch(function (error) {
|
||||||
respond({data: `<!DOCTYPE html>
|
respond({
|
||||||
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`, content_type: 'text/html'});
|
data: `<!DOCTYPE html>
|
||||||
|
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`,
|
||||||
|
content_type: 'text/html',
|
||||||
|
});
|
||||||
});
|
});
|
50
apps/blog/lit-all.min.js
vendored
50
apps/blog/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "💽"
|
"emoji": "💽",
|
||||||
|
"previous": "&uQzkIe/Aj8yNhLKe3hEq+5fEJsGwIUx8NVBjJKwoV2U=.sha256"
|
||||||
}
|
}
|
@ -51,7 +51,20 @@ async function key_list(db) {
|
|||||||
app.setDocument(doc);
|
app.setDocument(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
core.register('message', async function(message) {
|
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') {
|
if (message.event == 'hashChange') {
|
||||||
let hash = message.hash.substring(1);
|
let hash = message.hash.substring(1);
|
||||||
if (hash.startsWith(':shared:')) {
|
if (hash.startsWith(':shared:')) {
|
||||||
@ -62,9 +75,9 @@ core.register('message', async function(message) {
|
|||||||
} else if (hash.length) {
|
} else if (hash.length) {
|
||||||
key_list(await database(hash.split(':').slice(1).join(':')));
|
key_list(await database(hash.split(':').slice(1).join(':')));
|
||||||
} else {
|
} else {
|
||||||
database_list();
|
load();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
database_list();
|
load();
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "tildefriends-app",
|
|
||||||
"emoji": "📚"
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
@ -1,16 +0,0 @@
|
|||||||
# Tilde Friends Developer's Guide
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
A Tilde Friends application runs on the server. To make an interesting
|
|
||||||
application that interacts with the client, it's necessary to understand
|
|
||||||
how the parts work together.
|
|
||||||
|
|
||||||
## Hello, world!
|
|
||||||
|
|
||||||
A simple starting point. Presents `Hello, world!` in the browser when
|
|
||||||
visited.
|
|
||||||
|
|
||||||
**app.js**:
|
|
||||||
```
|
|
||||||
app.setDocument('<h1>Hello, world!</h1>');
|
|
||||||
```
|
|
@ -1,12 +0,0 @@
|
|||||||
# Tilde Friends Documentation
|
|
||||||
|
|
||||||
Tilde Friends is a participating member of a greater social
|
|
||||||
network, [Secure Scuttlebutt](https://scuttlebutt.nz/),
|
|
||||||
adding a way to safely and securely write, share,
|
|
||||||
and run code in the form of server-side web applications.
|
|
||||||
|
|
||||||
- [Tilde Friends Vision](#vision)
|
|
||||||
- [Secure Scuttlebutt from Scratch](#ssb)
|
|
||||||
- [Structure](#structure)
|
|
||||||
- [Guide](#guide)
|
|
||||||
- [TODO](#todo)
|
|
@ -1,41 +0,0 @@
|
|||||||
# Secure Scuttlebutt from Scratch
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
This aims to be the missing reference for those who wish to create a Secure
|
|
||||||
Scuttlebutt client from scratch.
|
|
||||||
|
|
||||||
## Discovery
|
|
||||||
A good way to get started is to participate in local network discovery with a known working
|
|
||||||
client on the same network. The
|
|
||||||
[Scuttlebutt Programming Guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#local-network)
|
|
||||||
is a good start, here, with a few things to note:
|
|
||||||
|
|
||||||
1. Some clients advertise multiple addresses separated by semicolons (`;`).
|
|
||||||
2. Some clients advertise alternative protocols than `shs` and use hostnames instead of
|
|
||||||
IPv4 addresses.
|
|
||||||
|
|
||||||
So be prepared to accept variations.
|
|
||||||
|
|
||||||
There also an undocumented "new" style of discovery message.
|
|
||||||
|
|
||||||
## Secret Handshake, Box Stream, and RPC Protocol
|
|
||||||
Now that two clients are aware of eachother, they need to complete a secret handshake.
|
|
||||||
The [programming guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#handshake)
|
|
||||||
is once again a good reference.
|
|
||||||
|
|
||||||
The box stream and RPC protocol can both be implemented from the
|
|
||||||
[same documentation](https://ssbc.github.io/scuttlebutt-protocol-guide/#box-stream)
|
|
||||||
without surprises.
|
|
||||||
|
|
||||||
## Synchronizing Data
|
|
||||||
|
|
||||||
... `ebt.replicate` or `createHistoryStream` ...
|
|
||||||
|
|
||||||
## Rooms
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## References
|
|
||||||
* [https://ssbc.github.io/scuttlebutt-protocol-guide/](https://ssbc.github.io/scuttlebutt-protocol-guide/)
|
|
||||||
* [https://dev.planetary.social/](https://dev.planetary.social/)
|
|
||||||
* [https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints](https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints)
|
|
@ -1,65 +0,0 @@
|
|||||||
# Tilde Friends Structure
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
Tilde Friends is a mostly-self-contained executable written in C.
|
|
||||||
|
|
||||||
In combines the following key components:
|
|
||||||
- A Secure Scuttlebutt (SSB) client/server. This talks with other SSB
|
|
||||||
instances, storing messages and blobs for anyone visible to local
|
|
||||||
users as they are encountered and sharing anything published locally
|
|
||||||
as appropriate.
|
|
||||||
- An sqlite database. This is where the SSB instance stores its data.
|
|
||||||
The general schema involves a `messages` table, storing mostly JSON,
|
|
||||||
a `blobs` table storing arbitrary blob data, and a `properties` table,
|
|
||||||
storing arbitrary state gleaned from `messages` and `blobs`, generally
|
|
||||||
updated on demand and incrementally.
|
|
||||||
- A QuickJS runtime. The core process runs stock scripts and has access
|
|
||||||
and permission to use all resources. All other processes, which
|
|
||||||
includes everything which runs untrusted code created by Tilde Friends
|
|
||||||
users, are strictly sandboxed in ways similar to how web browsers run
|
|
||||||
untrusted code. All attempts to access potentially sensitive resources
|
|
||||||
are mediated through the core process.
|
|
||||||
|
|
||||||
When run with no arguments, it starts a web server on
|
|
||||||
[http://localhost:12345/](http://localhost:12345/) and an SSB node.
|
|
||||||
|
|
||||||
## Web Interface
|
|
||||||
The Tilde Friends web server provides access to Tilde Friends applications,
|
|
||||||
which are arbitrary user-defined web applications.
|
|
||||||
|
|
||||||
At the top left, in addition to some basic navigation links, is an `edit`
|
|
||||||
link. Anyone can view, modify, and run in-place the code to any Tilde
|
|
||||||
Friends application by using the in-browser editor.
|
|
||||||
|
|
||||||
At the top right, one can `login` (to save work in their own space)
|
|
||||||
or `logout` (proceeding as a guest).
|
|
||||||
|
|
||||||
The rest of the page is an iframe belonging to the application.
|
|
||||||
|
|
||||||
## Special Paths
|
|
||||||
|
|
||||||
- `/~user/app/` - Tilde Friends application paths take the form `/~user/app/`, where `user`
|
|
||||||
is a username of a Tilde Friends account, and `app` is an arbitrary name
|
|
||||||
of an application saved by the given user.
|
|
||||||
- `/~user/app/file` - A raw file in an app.
|
|
||||||
- `/&blobid.ed25519` - A raw blob. Content-Type is inferred for at least
|
|
||||||
a few common image types.
|
|
||||||
|
|
||||||
## Communication Channels
|
|
||||||
Web Browser <-> Core <-> Sandbox
|
|
||||||
|
|
||||||
Visiting an application path delivers stock HTML and JavaScript which
|
|
||||||
establishes a WebSocket connection back to the server.
|
|
||||||
|
|
||||||
At this point, a new sandbox process is started in Tilde Friends, much
|
|
||||||
as a new sandboxed process might be started for a new tab in a web
|
|
||||||
browser. This process has a custom RPC connection to the core process
|
|
||||||
which holds the WebSocket connection to the browser.
|
|
||||||
|
|
||||||
The custom RPC communication between the sandbox process and the core
|
|
||||||
process facilitates passing and calling functions remotely. Calling a
|
|
||||||
function in another process returns a `Promise`.
|
|
||||||
|
|
||||||
An application will typically call `app.setDocument()` at startup to
|
|
||||||
populate the app's iframe in the web browser with its own client web
|
|
||||||
application resources.
|
|
@ -1,63 +0,0 @@
|
|||||||
# Tilde Friends TODO
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
## MVP3
|
|
||||||
- Sync status (problem feeds, messages/seconds stats, ...)
|
|
||||||
- app: wiki
|
|
||||||
- app: public blog
|
|
||||||
- Content-Disposition: download
|
|
||||||
- remove SSB credentials
|
|
||||||
- export SSB credentials
|
|
||||||
- initial: better empty news screen
|
|
||||||
- initial: remembered wrong user across login/logout
|
|
||||||
- initial: bad experience when following nobody
|
|
||||||
- make a cool independent app
|
|
||||||
- indicate when workspace differs from installed
|
|
||||||
- / => Something good.
|
|
||||||
- update docs
|
|
||||||
- audit + document API exposed to apps
|
|
||||||
- fix weird HTTP warnings
|
|
||||||
- channels
|
|
||||||
- placeholder/missing images
|
|
||||||
- no denial of service
|
|
||||||
- package standalone executable
|
|
||||||
- editor without app iframe
|
|
||||||
- sequence_before_author -> flags
|
|
||||||
- linkify ssb: links
|
|
||||||
- perfect rooms support
|
|
||||||
- connections 2.0
|
|
||||||
- make a better connections API
|
|
||||||
|
|
||||||
## Maybe Done
|
|
||||||
- blob_wants 2.0
|
|
||||||
- image downsample
|
|
||||||
- app: todo
|
|
||||||
- app: build archive
|
|
||||||
- update README
|
|
||||||
- administrators config
|
|
||||||
- apps name characters
|
|
||||||
- initial: can't switch to account when there is only one
|
|
||||||
- get tarball under 5MB
|
|
||||||
- rooms
|
|
||||||
- initial: doesn't refresh when create identity
|
|
||||||
- tf account timeout why
|
|
||||||
- ssb don't overflow boxes
|
|
||||||
- jwt for session tokens
|
|
||||||
- linkify https://...
|
|
||||||
- emoji reaction picker
|
|
||||||
- expose loads of stats
|
|
||||||
- confirm posting all new messages
|
|
||||||
- multiple identities per user, in database
|
|
||||||
- auto-populate data on initial launch
|
|
||||||
- make the docker image good / test it / use it
|
|
||||||
- leaking imports / exports
|
|
||||||
- file upload widget
|
|
||||||
- keep working on good error feedback
|
|
||||||
- build for windows
|
|
||||||
- installable apps (bring back an app message?)
|
|
||||||
- sqlStream => sqlExec or something
|
|
||||||
- !ssb from child process?
|
|
||||||
|
|
||||||
## Done
|
|
||||||
- update LICENSE
|
|
||||||
- logging to browser
|
|
@ -2,7 +2,7 @@ let g_about_cache = {};
|
|||||||
|
|
||||||
async function query(sql, args) {
|
async function query(sql, args) {
|
||||||
let result = [];
|
let result = [];
|
||||||
await ssb.sqlAsync(sql, args, function(row) {
|
await ssb.sqlAsync(sql, args, function (row) {
|
||||||
result.push(row);
|
result.push(row);
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
@ -21,7 +21,8 @@ async function contacts_internal(id, last_row_id, following, max_row_id) {
|
|||||||
json_extract(content, '$.type') = 'contact'
|
json_extract(content, '$.type') = 'contact'
|
||||||
ORDER BY sequence
|
ORDER BY sequence
|
||||||
`,
|
`,
|
||||||
[id, last_row_id, max_row_id]);
|
[id, last_row_id, max_row_id]
|
||||||
|
);
|
||||||
for (let row of contacts) {
|
for (let row of contacts) {
|
||||||
let contact = JSON.parse(row.content);
|
let contact = JSON.parse(row.content);
|
||||||
if (contact.following === true) {
|
if (contact.following === true) {
|
||||||
@ -42,15 +43,34 @@ async function contact(id, last_row_id, following, max_row_id) {
|
|||||||
return await contacts_internal(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) {
|
async function following_deep_internal(
|
||||||
let contacts = await Promise.all([...new Set(ids)].map(x => contact(x, last_row_id, following, max_row_id)));
|
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 = {};
|
let result = {};
|
||||||
for (let i = 0; i < ids.length; i++) {
|
for (let i = 0; i < ids.length; i++) {
|
||||||
let id = ids[i];
|
let id = ids[i];
|
||||||
let contact = contacts[i];
|
let contact = contacts[i];
|
||||||
let all_blocking = Object.assign({}, contact.blocking, blocking);
|
let all_blocking = Object.assign({}, contact.blocking, blocking);
|
||||||
let found = Object.keys(contact.following).filter(y => !all_blocking[y]);
|
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) : [];
|
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];
|
result[id] = [id, ...found, ...deeper];
|
||||||
}
|
}
|
||||||
return [...new Set(Object.values(result).flat())];
|
return [...new Set(Object.values(result).flat())];
|
||||||
@ -68,10 +88,22 @@ async function following_deep(ids, depth, blocking) {
|
|||||||
last_row_id: 0,
|
last_row_id: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let max_row_id = (await query(`
|
let max_row_id = (
|
||||||
|
await query(
|
||||||
|
`
|
||||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
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);
|
[]
|
||||||
|
)
|
||||||
|
)[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;
|
cache.last_row_id = max_row_id;
|
||||||
let store = JSON.stringify(cache);
|
let store = JSON.stringify(cache);
|
||||||
await db.set('following', store);
|
await db.set('following', store);
|
||||||
@ -90,13 +122,15 @@ async function fetch_about(db, ids, users) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
let max_row_id = 0;
|
let max_row_id = 0;
|
||||||
await ssb.sqlAsync(`
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
`,
|
`,
|
||||||
[],
|
[],
|
||||||
function(row) {
|
function (row) {
|
||||||
max_row_id = row.max_row_id;
|
max_row_id = row.max_row_id;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
for (let id of Object.keys(cache.about)) {
|
for (let id of Object.keys(cache.about)) {
|
||||||
if (ids.indexOf(id) == -1) {
|
if (ids.indexOf(id) == -1) {
|
||||||
delete cache.about[id];
|
delete cache.about[id];
|
||||||
@ -129,17 +163,21 @@ async function fetch_about(db, ids, users) {
|
|||||||
ORDER BY messages.author, messages.sequence
|
ORDER BY messages.author, messages.sequence
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
JSON.stringify(ids.filter(id => cache.about[id])),
|
JSON.stringify(ids.filter((id) => cache.about[id])),
|
||||||
JSON.stringify(ids.filter(id => !cache.about[id])),
|
JSON.stringify(ids.filter((id) => !cache.about[id])),
|
||||||
cache.last_row_id,
|
cache.last_row_id,
|
||||||
max_row_id,
|
max_row_id,
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
for (let about of abouts) {
|
for (let about of abouts) {
|
||||||
let content = JSON.parse(about.content);
|
let content = JSON.parse(about.content);
|
||||||
if (content.about === about.author) {
|
if (content.about === about.author) {
|
||||||
delete content.type;
|
delete content.type;
|
||||||
delete content.about;
|
delete content.about;
|
||||||
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
|
cache.about[about.author] = Object.assign(
|
||||||
|
cache.about[about.author] || {},
|
||||||
|
content
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache.last_row_id = max_row_id;
|
cache.last_row_id = max_row_id;
|
||||||
@ -155,41 +193,41 @@ async function getAbout(db, id) {
|
|||||||
if (g_about_cache[id]) {
|
if (g_about_cache[id]) {
|
||||||
return g_about_cache[id];
|
return g_about_cache[id];
|
||||||
}
|
}
|
||||||
let o = await db.get(id + ":about");
|
let o = await db.get(id + ':about');
|
||||||
const k_version = 4;
|
const k_version = 4;
|
||||||
let f = o ? JSON.parse(o) : o;
|
let f = o ? JSON.parse(o) : o;
|
||||||
if (!f || f.version != k_version) {
|
if (!f || f.version != k_version) {
|
||||||
f = {about: {}, sequence: 0, version: k_version};
|
f = {about: {}, sequence: 0, version: k_version};
|
||||||
}
|
}
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
"SELECT "+
|
'SELECT ' +
|
||||||
" sequence, "+
|
' sequence, ' +
|
||||||
" content "+
|
' content ' +
|
||||||
"FROM messages "+
|
'FROM messages ' +
|
||||||
"WHERE "+
|
'WHERE ' +
|
||||||
" author = ?1 AND "+
|
' author = ?1 AND ' +
|
||||||
" sequence > ?2 AND "+
|
' sequence > ?2 AND ' +
|
||||||
" json_extract(content, '$.type') = 'about' AND "+
|
" json_extract(content, '$.type') = 'about' AND " +
|
||||||
" json_extract(content, '$.about') = ?1 "+
|
" json_extract(content, '$.about') = ?1 " +
|
||||||
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
|
'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
|
||||||
"ORDER BY sequence",
|
'ORDER BY sequence',
|
||||||
[id, f.sequence],
|
[id, f.sequence],
|
||||||
function(row) {
|
function (row) {
|
||||||
f.sequence = row.sequence;
|
f.sequence = row.sequence;
|
||||||
if (row.content) {
|
if (row.content) {
|
||||||
let about = {};
|
let about = {};
|
||||||
try {
|
try {
|
||||||
about = JSON.parse(row.content);
|
about = JSON.parse(row.content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
delete about.about;
|
delete about.about;
|
||||||
delete about.type;
|
delete about.type;
|
||||||
f.about = Object.assign(f.about, about);
|
f.about = Object.assign(f.about, about);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
let j = JSON.stringify(f);
|
let j = JSON.stringify(f);
|
||||||
if (o != j) {
|
if (o != j) {
|
||||||
await db.set(id + ":about", j);
|
await db.set(id + ':about', j);
|
||||||
}
|
}
|
||||||
g_about_cache[id] = f.about;
|
g_about_cache[id] = f.about;
|
||||||
return f.about;
|
return f.about;
|
||||||
@ -198,15 +236,15 @@ async function getAbout(db, id) {
|
|||||||
async function getSize(db, id) {
|
async function getSize(db, id) {
|
||||||
let size = 0;
|
let size = 0;
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
"SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
|
'SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1',
|
||||||
[id],
|
[id],
|
||||||
function (row) {
|
function (row) {
|
||||||
size += row.size;
|
size += row.size;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function getSizes(ids) {
|
async function getSizes(ids) {
|
||||||
let sizes = {};
|
let sizes = {};
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
@ -221,7 +259,8 @@ async function getSizes(ids) {
|
|||||||
[JSON.stringify(ids)],
|
[JSON.stringify(ids)],
|
||||||
function (row) {
|
function (row) {
|
||||||
sizes[row.author] = row.size;
|
sizes[row.author] = row.size;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return sizes;
|
return sizes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +280,10 @@ function niceSize(bytes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function escape(value) {
|
function escape(value) {
|
||||||
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
return value
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@ -249,19 +291,27 @@ async function main() {
|
|||||||
let db = await database('ssb');
|
let db = await database('ssb');
|
||||||
let whoami = await ssb.getIdentities();
|
let whoami = await ssb.getIdentities();
|
||||||
let tree = '';
|
let tree = '';
|
||||||
await app.setDocument(`<pre style="color: #fff">Enumerating followed users...</pre>`);
|
await app.setDocument(
|
||||||
|
`<pre style="color: #fff">Enumerating followed users...</pre>`
|
||||||
|
);
|
||||||
let following = await following_deep(whoami, 2, {});
|
let following = await following_deep(whoami, 2, {});
|
||||||
await app.setDocument(`<pre style="color: #fff">Getting names and sizes...</pre>`);
|
await app.setDocument(
|
||||||
|
`<pre style="color: #fff">Getting names and sizes...</pre>`
|
||||||
|
);
|
||||||
let [about, sizes] = await Promise.all([
|
let [about, sizes] = await Promise.all([
|
||||||
fetch_about(db, following, {}),
|
fetch_about(db, following, {}),
|
||||||
getSizes(following),
|
getSizes(following),
|
||||||
]);
|
]);
|
||||||
await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
|
await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
|
||||||
following.sort((a, b) => ((sizes[b] ?? 0) - (sizes[a] ?? 0)));
|
following.sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0));
|
||||||
for (let id of following) {
|
for (let id of following) {
|
||||||
tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
|
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>');
|
await app.setDocument(
|
||||||
|
'<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' +
|
||||||
|
tree +
|
||||||
|
'</ul>\n</body>\n</html>'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "tildefriends-app",
|
|
||||||
"emoji": "🗺",
|
|
||||||
"previous": "&0XSp+xdQwVtQ88bXzvWdH15Ex63hv5zUKTa4zx7HBGM=.sha256"
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
import * as tfrpc from '/tfrpc.js';
|
|
||||||
import * as strava from './strava.js';
|
|
||||||
|
|
||||||
let g_database;
|
|
||||||
let g_shared_database;
|
|
||||||
|
|
||||||
tfrpc.register(async function createIdentity() {
|
|
||||||
return ssb.createIdentity();
|
|
||||||
});
|
|
||||||
tfrpc.register(async function appendMessage(id, message) {
|
|
||||||
print('APPEND', JSON.stringify(message));
|
|
||||||
return ssb.appendMessageWithIdentity(id, message);
|
|
||||||
});
|
|
||||||
tfrpc.register(function url() {
|
|
||||||
return core.url;
|
|
||||||
});
|
|
||||||
tfrpc.register(async function getUser() {
|
|
||||||
return core.user;
|
|
||||||
});
|
|
||||||
tfrpc.register(function getIdentities() {
|
|
||||||
return ssb.getIdentities();
|
|
||||||
});
|
|
||||||
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 databaseRemove(key, value) {
|
|
||||||
return g_database ? g_database.remove(key, value) : undefined;
|
|
||||||
});
|
|
||||||
tfrpc.register(async function sharedDatabaseGet(key) {
|
|
||||||
return g_shared_database ? g_shared_database.get(key) : undefined;
|
|
||||||
});
|
|
||||||
tfrpc.register(async function sharedDatabaseSet(key, value) {
|
|
||||||
return g_shared_database ? g_shared_database.set(key, value) : undefined;
|
|
||||||
});
|
|
||||||
tfrpc.register(async function sharedDatabaseRemove(key, value) {
|
|
||||||
return g_shared_database ? g_shared_database.remove(key, value) : undefined;
|
|
||||||
});
|
|
||||||
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 (typeof(blob) == 'string') {
|
|
||||||
blob = utf8Encode(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(strava.refresh_token);
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
g_shared_database = await shared_database('state');
|
|
||||||
if (core.user.credentials?.session?.name) {
|
|
||||||
g_database = await database('state');
|
|
||||||
}
|
|
||||||
|
|
||||||
let attempt;
|
|
||||||
if (core.user.credentials?.session?.name) {
|
|
||||||
let shared_db = await shared_database('state');
|
|
||||||
attempt = await shared_db.get(core.user.credentials.session.name);
|
|
||||||
}
|
|
||||||
app.setDocument(utf8Decode(getFile('index.html')).replace('${data}', JSON.stringify({
|
|
||||||
attempt: attempt,
|
|
||||||
state: core.user?.credentials?.session?.name,
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
File diff suppressed because one or more lines are too long
@ -1,81 +0,0 @@
|
|||||||
function xml_parse(xml) {
|
|
||||||
let result;
|
|
||||||
let path = [];
|
|
||||||
let tag_begin;
|
|
||||||
let text_begin;
|
|
||||||
for (let i = 0; i < xml.length; i++) {
|
|
||||||
let c = xml.charAt(i);
|
|
||||||
if (!tag_begin && c == '<') {
|
|
||||||
if (i > text_begin && path.length) {
|
|
||||||
let value = xml.substring(text_begin, i);
|
|
||||||
if (!/^\s*$/.test(value)) {
|
|
||||||
path[path.length - 1].value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tag_begin = i + 1;
|
|
||||||
} else if (tag_begin && c == '>') {
|
|
||||||
let tag = xml.substring(tag_begin, i).trim();
|
|
||||||
if (tag.startsWith('?') && tag.endsWith('?')) {
|
|
||||||
/* Ignore directives. */
|
|
||||||
} else if (tag.startsWith('/')) {
|
|
||||||
path.pop();
|
|
||||||
} else {
|
|
||||||
let parts = tag.split(' ');
|
|
||||||
let attributes = {};
|
|
||||||
for (let j = 1; j < parts.length; j++) {
|
|
||||||
let eq = parts[j].indexOf('=');
|
|
||||||
let value = parts[j].substring(eq + 1);
|
|
||||||
if (value.startsWith('"') && value.endsWith('"')) {
|
|
||||||
value = value.substring(1, value.length - 1);
|
|
||||||
}
|
|
||||||
attributes[parts[j].substring(0, eq)] = value;
|
|
||||||
}
|
|
||||||
let next = {name: parts[0], children: [], attributes: attributes};
|
|
||||||
if (path.length) {
|
|
||||||
path[path.length - 1].children.push(next);
|
|
||||||
} else {
|
|
||||||
result = next;
|
|
||||||
}
|
|
||||||
if (!tag.endsWith('/')) {
|
|
||||||
path.push(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tag_begin = undefined;
|
|
||||||
text_begin = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function* xml_each(node, name) {
|
|
||||||
for (let child of node.children) {
|
|
||||||
if (child.name == name) {
|
|
||||||
yield child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function gpx_parse(xml) {
|
|
||||||
let result = {segments: []};
|
|
||||||
let tree = xml_parse(xml);
|
|
||||||
if (tree?.name == 'gpx') {
|
|
||||||
for (let trk of xml_each(tree, 'trk')) {
|
|
||||||
for (let trkseg of xml_each(trk, 'trkseg')) {
|
|
||||||
let segment = [];
|
|
||||||
for (let trkpt of xml_each(trkseg, 'trkpt')) {
|
|
||||||
segment.push({lat: parseFloat(trkpt.attributes.lat), lon: parseFloat(trkpt.attributes.lon)});
|
|
||||||
}
|
|
||||||
result.segments.push(segment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let metadata of xml_each(tree, 'metadata')) {
|
|
||||||
for (let link of xml_each(metadata, 'link')) {
|
|
||||||
result.link = link.attributes.href;
|
|
||||||
}
|
|
||||||
for (let time of xml_each(metadata, 'time')) {
|
|
||||||
result.time = time.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import * as strava from './strava.js';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
print('handler running');
|
|
||||||
let r = await strava.authorization_code(request.query.code);
|
|
||||||
print('state =', request.query.state);
|
|
||||||
print('body = ', r.body);
|
|
||||||
if (request.query.state && r.body) {
|
|
||||||
let shared_db = await shared_database('state');
|
|
||||||
await shared_db.set(request.query.state, utf8Decode(r.body));
|
|
||||||
}
|
|
||||||
await respond({
|
|
||||||
data: r.body,
|
|
||||||
content_type: 'text/plain',
|
|
||||||
headers: {
|
|
||||||
Location: 'https://tildefriends.net/~cory/gg/',
|
|
||||||
},
|
|
||||||
status_code: 307,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
main();
|
|
@ -1,14 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html style="width: 100%; height: 100%; margin: 0; padding: 0">
|
|
||||||
<head>
|
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
|
||||||
<script>
|
|
||||||
let g_data = ${data};
|
|
||||||
</script>
|
|
||||||
<script src="script.js" type="module"></script>
|
|
||||||
<script src="leaflet.js"></script>
|
|
||||||
</head>
|
|
||||||
<body style="color: #fff; display: flex; flex-flow: column; height: 100%; width: 100%; margin: 0; padding: 0">
|
|
||||||
<gg-app style="width: 100%; height: 100%" id="ggapp"></gg-app>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,661 +0,0 @@
|
|||||||
/* required styles */
|
|
||||||
|
|
||||||
.leaflet-pane,
|
|
||||||
.leaflet-tile,
|
|
||||||
.leaflet-marker-icon,
|
|
||||||
.leaflet-marker-shadow,
|
|
||||||
.leaflet-tile-container,
|
|
||||||
.leaflet-pane > svg,
|
|
||||||
.leaflet-pane > canvas,
|
|
||||||
.leaflet-zoom-box,
|
|
||||||
.leaflet-image-layer,
|
|
||||||
.leaflet-layer {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
.leaflet-container {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.leaflet-tile,
|
|
||||||
.leaflet-marker-icon,
|
|
||||||
.leaflet-marker-shadow {
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-drag: none;
|
|
||||||
}
|
|
||||||
/* Prevents IE11 from highlighting tiles in blue */
|
|
||||||
.leaflet-tile::selection {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
|
||||||
.leaflet-safari .leaflet-tile {
|
|
||||||
image-rendering: -webkit-optimize-contrast;
|
|
||||||
}
|
|
||||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
|
||||||
.leaflet-safari .leaflet-tile-container {
|
|
||||||
width: 1600px;
|
|
||||||
height: 1600px;
|
|
||||||
-webkit-transform-origin: 0 0;
|
|
||||||
}
|
|
||||||
.leaflet-marker-icon,
|
|
||||||
.leaflet-marker-shadow {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
|
||||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
|
||||||
.leaflet-container .leaflet-overlay-pane svg {
|
|
||||||
max-width: none !important;
|
|
||||||
max-height: none !important;
|
|
||||||
}
|
|
||||||
.leaflet-container .leaflet-marker-pane img,
|
|
||||||
.leaflet-container .leaflet-shadow-pane img,
|
|
||||||
.leaflet-container .leaflet-tile-pane img,
|
|
||||||
.leaflet-container img.leaflet-image-layer,
|
|
||||||
.leaflet-container .leaflet-tile {
|
|
||||||
max-width: none !important;
|
|
||||||
max-height: none !important;
|
|
||||||
width: auto;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-container img.leaflet-tile {
|
|
||||||
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
|
||||||
mix-blend-mode: plus-lighter;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-container.leaflet-touch-zoom {
|
|
||||||
-ms-touch-action: pan-x pan-y;
|
|
||||||
touch-action: pan-x pan-y;
|
|
||||||
}
|
|
||||||
.leaflet-container.leaflet-touch-drag {
|
|
||||||
-ms-touch-action: pinch-zoom;
|
|
||||||
/* Fallback for FF which doesn't support pinch-zoom */
|
|
||||||
touch-action: none;
|
|
||||||
touch-action: pinch-zoom;
|
|
||||||
}
|
|
||||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
|
||||||
-ms-touch-action: none;
|
|
||||||
touch-action: none;
|
|
||||||
}
|
|
||||||
.leaflet-container {
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
.leaflet-container a {
|
|
||||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
|
||||||
}
|
|
||||||
.leaflet-tile {
|
|
||||||
filter: inherit;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
.leaflet-tile-loaded {
|
|
||||||
visibility: inherit;
|
|
||||||
}
|
|
||||||
.leaflet-zoom-box {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
z-index: 800;
|
|
||||||
}
|
|
||||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
|
||||||
.leaflet-overlay-pane svg {
|
|
||||||
-moz-user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-pane { z-index: 400; }
|
|
||||||
|
|
||||||
.leaflet-tile-pane { z-index: 200; }
|
|
||||||
.leaflet-overlay-pane { z-index: 400; }
|
|
||||||
.leaflet-shadow-pane { z-index: 500; }
|
|
||||||
.leaflet-marker-pane { z-index: 600; }
|
|
||||||
.leaflet-tooltip-pane { z-index: 650; }
|
|
||||||
.leaflet-popup-pane { z-index: 700; }
|
|
||||||
|
|
||||||
.leaflet-map-pane canvas { z-index: 100; }
|
|
||||||
.leaflet-map-pane svg { z-index: 200; }
|
|
||||||
|
|
||||||
.leaflet-vml-shape {
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
}
|
|
||||||
.lvml {
|
|
||||||
behavior: url(#default#VML);
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* control positioning */
|
|
||||||
|
|
||||||
.leaflet-control {
|
|
||||||
position: relative;
|
|
||||||
z-index: 800;
|
|
||||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
.leaflet-top,
|
|
||||||
.leaflet-bottom {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1000;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.leaflet-top {
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
.leaflet-right {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
.leaflet-bottom {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
.leaflet-left {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
.leaflet-control {
|
|
||||||
float: left;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
.leaflet-right .leaflet-control {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
.leaflet-top .leaflet-control {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
.leaflet-bottom .leaflet-control {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.leaflet-left .leaflet-control {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.leaflet-right .leaflet-control {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* zoom and fade animations */
|
|
||||||
|
|
||||||
.leaflet-fade-anim .leaflet-popup {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transition: opacity 0.2s linear;
|
|
||||||
-moz-transition: opacity 0.2s linear;
|
|
||||||
transition: opacity 0.2s linear;
|
|
||||||
}
|
|
||||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.leaflet-zoom-animated {
|
|
||||||
-webkit-transform-origin: 0 0;
|
|
||||||
-ms-transform-origin: 0 0;
|
|
||||||
transform-origin: 0 0;
|
|
||||||
}
|
|
||||||
svg.leaflet-zoom-animated {
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
|
||||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
|
||||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
|
||||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
|
||||||
}
|
|
||||||
.leaflet-zoom-anim .leaflet-tile,
|
|
||||||
.leaflet-pan-anim .leaflet-tile {
|
|
||||||
-webkit-transition: none;
|
|
||||||
-moz-transition: none;
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* cursors */
|
|
||||||
|
|
||||||
.leaflet-interactive {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.leaflet-grab {
|
|
||||||
cursor: -webkit-grab;
|
|
||||||
cursor: -moz-grab;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
.leaflet-crosshair,
|
|
||||||
.leaflet-crosshair .leaflet-interactive {
|
|
||||||
cursor: crosshair;
|
|
||||||
}
|
|
||||||
.leaflet-popup-pane,
|
|
||||||
.leaflet-control {
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
.leaflet-dragging .leaflet-grab,
|
|
||||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
|
||||||
.leaflet-dragging .leaflet-marker-draggable {
|
|
||||||
cursor: move;
|
|
||||||
cursor: -webkit-grabbing;
|
|
||||||
cursor: -moz-grabbing;
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* marker & overlays interactivity */
|
|
||||||
.leaflet-marker-icon,
|
|
||||||
.leaflet-marker-shadow,
|
|
||||||
.leaflet-image-layer,
|
|
||||||
.leaflet-pane > svg path,
|
|
||||||
.leaflet-tile-container {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-marker-icon.leaflet-interactive,
|
|
||||||
.leaflet-image-layer.leaflet-interactive,
|
|
||||||
.leaflet-pane > svg path.leaflet-interactive,
|
|
||||||
svg.leaflet-image-layer.leaflet-interactive path {
|
|
||||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* visual tweaks */
|
|
||||||
|
|
||||||
.leaflet-container {
|
|
||||||
background: #ddd;
|
|
||||||
outline-offset: 1px;
|
|
||||||
}
|
|
||||||
.leaflet-container a {
|
|
||||||
color: #0078A8;
|
|
||||||
}
|
|
||||||
.leaflet-zoom-box {
|
|
||||||
border: 2px dotted #38f;
|
|
||||||
background: rgba(255,255,255,0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* general typography */
|
|
||||||
.leaflet-container {
|
|
||||||
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
|
||||||
font-size: 12px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* general toolbar styles */
|
|
||||||
|
|
||||||
.leaflet-bar {
|
|
||||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.leaflet-bar a {
|
|
||||||
background-color: #fff;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
line-height: 26px;
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
.leaflet-bar a,
|
|
||||||
.leaflet-control-layers-toggle {
|
|
||||||
background-position: 50% 50%;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.leaflet-bar a:hover,
|
|
||||||
.leaflet-bar a:focus {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
.leaflet-bar a:first-child {
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
}
|
|
||||||
.leaflet-bar a:last-child {
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
border-bottom-right-radius: 4px;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
.leaflet-bar a.leaflet-disabled {
|
|
||||||
cursor: default;
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
color: #bbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-touch .leaflet-bar a {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
.leaflet-touch .leaflet-bar a:first-child {
|
|
||||||
border-top-left-radius: 2px;
|
|
||||||
border-top-right-radius: 2px;
|
|
||||||
}
|
|
||||||
.leaflet-touch .leaflet-bar a:last-child {
|
|
||||||
border-bottom-left-radius: 2px;
|
|
||||||
border-bottom-right-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* zoom control */
|
|
||||||
|
|
||||||
.leaflet-control-zoom-in,
|
|
||||||
.leaflet-control-zoom-out {
|
|
||||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
|
||||||
text-indent: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* layers control */
|
|
||||||
|
|
||||||
.leaflet-control-layers {
|
|
||||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-toggle {
|
|
||||||
background-image: url(images/layers.png);
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
.leaflet-retina .leaflet-control-layers-toggle {
|
|
||||||
background-image: url(images/layers-2x.png);
|
|
||||||
background-size: 26px 26px;
|
|
||||||
}
|
|
||||||
.leaflet-touch .leaflet-control-layers-toggle {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers .leaflet-control-layers-list,
|
|
||||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-expanded {
|
|
||||||
padding: 6px 10px 6px 6px;
|
|
||||||
color: #333;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-scrollbar {
|
|
||||||
overflow-y: scroll;
|
|
||||||
overflow-x: hidden;
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-selector {
|
|
||||||
margin-top: 2px;
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers label {
|
|
||||||
display: block;
|
|
||||||
font-size: 13px;
|
|
||||||
font-size: 1.08333em;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-separator {
|
|
||||||
height: 0;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
margin: 5px -10px 5px -6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Default icon URLs */
|
|
||||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
|
||||||
background-image: url(images/marker-icon.png);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* attribution and scale controls */
|
|
||||||
|
|
||||||
.leaflet-container .leaflet-control-attribution {
|
|
||||||
background: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.leaflet-control-attribution,
|
|
||||||
.leaflet-control-scale-line {
|
|
||||||
padding: 0 5px;
|
|
||||||
color: #333;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
.leaflet-control-attribution a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.leaflet-control-attribution a:hover,
|
|
||||||
.leaflet-control-attribution a:focus {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.leaflet-attribution-flag {
|
|
||||||
display: inline !important;
|
|
||||||
vertical-align: baseline !important;
|
|
||||||
width: 1em;
|
|
||||||
height: 0.6669em;
|
|
||||||
}
|
|
||||||
.leaflet-left .leaflet-control-scale {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
.leaflet-bottom .leaflet-control-scale {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.leaflet-control-scale-line {
|
|
||||||
border: 2px solid #777;
|
|
||||||
border-top: none;
|
|
||||||
line-height: 1.1;
|
|
||||||
padding: 2px 5px 1px;
|
|
||||||
white-space: nowrap;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
text-shadow: 1px 1px #fff;
|
|
||||||
}
|
|
||||||
.leaflet-control-scale-line:not(:first-child) {
|
|
||||||
border-top: 2px solid #777;
|
|
||||||
border-bottom: none;
|
|
||||||
margin-top: -2px;
|
|
||||||
}
|
|
||||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
|
||||||
border-bottom: 2px solid #777;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-touch .leaflet-control-attribution,
|
|
||||||
.leaflet-touch .leaflet-control-layers,
|
|
||||||
.leaflet-touch .leaflet-bar {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
.leaflet-touch .leaflet-control-layers,
|
|
||||||
.leaflet-touch .leaflet-bar {
|
|
||||||
border: 2px solid rgba(0,0,0,0.2);
|
|
||||||
background-clip: padding-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* popup */
|
|
||||||
|
|
||||||
.leaflet-popup {
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.leaflet-popup-content-wrapper {
|
|
||||||
padding: 1px;
|
|
||||||
text-align: left;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
.leaflet-popup-content {
|
|
||||||
margin: 13px 24px 13px 20px;
|
|
||||||
line-height: 1.3;
|
|
||||||
font-size: 13px;
|
|
||||||
font-size: 1.08333em;
|
|
||||||
min-height: 1px;
|
|
||||||
}
|
|
||||||
.leaflet-popup-content p {
|
|
||||||
margin: 17px 0;
|
|
||||||
margin: 1.3em 0;
|
|
||||||
}
|
|
||||||
.leaflet-popup-tip-container {
|
|
||||||
width: 40px;
|
|
||||||
height: 20px;
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
margin-top: -1px;
|
|
||||||
margin-left: -20px;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.leaflet-popup-tip {
|
|
||||||
width: 17px;
|
|
||||||
height: 17px;
|
|
||||||
padding: 1px;
|
|
||||||
|
|
||||||
margin: -10px auto 0;
|
|
||||||
pointer-events: auto;
|
|
||||||
|
|
||||||
-webkit-transform: rotate(45deg);
|
|
||||||
-moz-transform: rotate(45deg);
|
|
||||||
-ms-transform: rotate(45deg);
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
.leaflet-popup-content-wrapper,
|
|
||||||
.leaflet-popup-tip {
|
|
||||||
background: white;
|
|
||||||
color: #333;
|
|
||||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
|
||||||
}
|
|
||||||
.leaflet-container a.leaflet-popup-close-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
border: none;
|
|
||||||
text-align: center;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
font: 16px/24px Tahoma, Verdana, sans-serif;
|
|
||||||
color: #757575;
|
|
||||||
text-decoration: none;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.leaflet-container a.leaflet-popup-close-button:hover,
|
|
||||||
.leaflet-container a.leaflet-popup-close-button:focus {
|
|
||||||
color: #585858;
|
|
||||||
}
|
|
||||||
.leaflet-popup-scrolled {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
|
||||||
-ms-zoom: 1;
|
|
||||||
}
|
|
||||||
.leaflet-oldie .leaflet-popup-tip {
|
|
||||||
width: 24px;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
|
||||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-oldie .leaflet-control-zoom,
|
|
||||||
.leaflet-oldie .leaflet-control-layers,
|
|
||||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
|
||||||
.leaflet-oldie .leaflet-popup-tip {
|
|
||||||
border: 1px solid #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* div icon */
|
|
||||||
|
|
||||||
.leaflet-div-icon {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Tooltip */
|
|
||||||
/* Base styles for the element that has a tooltip */
|
|
||||||
.leaflet-tooltip {
|
|
||||||
position: absolute;
|
|
||||||
padding: 6px;
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #fff;
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #222;
|
|
||||||
white-space: nowrap;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
pointer-events: none;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
|
||||||
}
|
|
||||||
.leaflet-tooltip.leaflet-interactive {
|
|
||||||
cursor: pointer;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-top:before,
|
|
||||||
.leaflet-tooltip-bottom:before,
|
|
||||||
.leaflet-tooltip-left:before,
|
|
||||||
.leaflet-tooltip-right:before {
|
|
||||||
position: absolute;
|
|
||||||
pointer-events: none;
|
|
||||||
border: 6px solid transparent;
|
|
||||||
background: transparent;
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Directions */
|
|
||||||
|
|
||||||
.leaflet-tooltip-bottom {
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-top {
|
|
||||||
margin-top: -6px;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-bottom:before,
|
|
||||||
.leaflet-tooltip-top:before {
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -6px;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-top:before {
|
|
||||||
bottom: 0;
|
|
||||||
margin-bottom: -12px;
|
|
||||||
border-top-color: #fff;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-bottom:before {
|
|
||||||
top: 0;
|
|
||||||
margin-top: -12px;
|
|
||||||
margin-left: -6px;
|
|
||||||
border-bottom-color: #fff;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-left {
|
|
||||||
margin-left: -6px;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-right {
|
|
||||||
margin-left: 6px;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-left:before,
|
|
||||||
.leaflet-tooltip-right:before {
|
|
||||||
top: 50%;
|
|
||||||
margin-top: -6px;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-left:before {
|
|
||||||
right: 0;
|
|
||||||
margin-right: -12px;
|
|
||||||
border-left-color: #fff;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-right:before {
|
|
||||||
left: 0;
|
|
||||||
margin-left: -12px;
|
|
||||||
border-right-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Printing */
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
/* Prevent printers from removing background-images of controls. */
|
|
||||||
.leaflet-control {
|
|
||||||
-webkit-print-color-adjust: exact;
|
|
||||||
print-color-adjust: exact;
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
120
apps/gg/lit-all.min.js
vendored
120
apps/gg/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,158 +0,0 @@
|
|||||||
/**
|
|
||||||
* Based off of [the offical Google document](https://developers.google.com/maps/documentation/utilities/polylinealgorithm)
|
|
||||||
*
|
|
||||||
* Some parts from [this implementation](http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/PolylineEncoder.js)
|
|
||||||
* by [Mark McClure](http://facstaff.unca.edu/mcmcclur/)
|
|
||||||
*
|
|
||||||
* @module polyline
|
|
||||||
*/
|
|
||||||
|
|
||||||
var polyline = {};
|
|
||||||
|
|
||||||
function py2_round(value) {
|
|
||||||
// Google's polyline algorithm uses the same rounding strategy as Python 2, which is different from JS for negative values
|
|
||||||
return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function encode(current, previous, factor) {
|
|
||||||
current = py2_round(current * factor);
|
|
||||||
previous = py2_round(previous * factor);
|
|
||||||
var coordinate = (current - previous) * 2;
|
|
||||||
if (coordinate < 0) {
|
|
||||||
coordinate = -coordinate - 1
|
|
||||||
}
|
|
||||||
var output = '';
|
|
||||||
while (coordinate >= 0x20) {
|
|
||||||
output += String.fromCharCode((0x20 | (coordinate & 0x1f)) + 63);
|
|
||||||
coordinate /= 32;
|
|
||||||
}
|
|
||||||
output += String.fromCharCode((coordinate | 0) + 63);
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes to a [latitude, longitude] coordinates array.
|
|
||||||
*
|
|
||||||
* This is adapted from the implementation in Project-OSRM.
|
|
||||||
*
|
|
||||||
* @param {String} str
|
|
||||||
* @param {Number} precision
|
|
||||||
* @returns {Array}
|
|
||||||
*
|
|
||||||
* @see https://github.com/Project-OSRM/osrm-frontend/blob/master/WebContent/routing/OSRM.RoutingGeometry.js
|
|
||||||
*/
|
|
||||||
polyline.decode = function(str, precision) {
|
|
||||||
var index = 0,
|
|
||||||
lat = 0,
|
|
||||||
lng = 0,
|
|
||||||
coordinates = [],
|
|
||||||
shift = 0,
|
|
||||||
result = 0,
|
|
||||||
byte = null,
|
|
||||||
latitude_change,
|
|
||||||
longitude_change,
|
|
||||||
factor = Math.pow(10, Number.isInteger(precision) ? precision : 5);
|
|
||||||
|
|
||||||
// Coordinates have variable length when encoded, so just keep
|
|
||||||
// track of whether we've hit the end of the string. In each
|
|
||||||
// loop iteration, a single coordinate is decoded.
|
|
||||||
while (index < str.length) {
|
|
||||||
|
|
||||||
// Reset shift, result, and byte
|
|
||||||
byte = null;
|
|
||||||
shift = 1;
|
|
||||||
result = 0;
|
|
||||||
|
|
||||||
do {
|
|
||||||
byte = str.charCodeAt(index++) - 63;
|
|
||||||
result += (byte & 0x1f) * shift;
|
|
||||||
shift *= 32;
|
|
||||||
} while (byte >= 0x20);
|
|
||||||
|
|
||||||
latitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2);
|
|
||||||
|
|
||||||
shift = 1;
|
|
||||||
result = 0;
|
|
||||||
|
|
||||||
do {
|
|
||||||
byte = str.charCodeAt(index++) - 63;
|
|
||||||
result += (byte & 0x1f) * shift;
|
|
||||||
shift *= 32;
|
|
||||||
} while (byte >= 0x20);
|
|
||||||
|
|
||||||
longitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2);
|
|
||||||
|
|
||||||
lat += latitude_change;
|
|
||||||
lng += longitude_change;
|
|
||||||
|
|
||||||
coordinates.push([lat / factor, lng / factor]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return coordinates;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the given [latitude, longitude] coordinates array.
|
|
||||||
*
|
|
||||||
* @param {Array.<Array.<Number>>} coordinates
|
|
||||||
* @param {Number} precision
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
polyline.encode = function(coordinates, precision) {
|
|
||||||
if (!coordinates.length) { return ''; }
|
|
||||||
|
|
||||||
var factor = Math.pow(10, Number.isInteger(precision) ? precision : 5),
|
|
||||||
output = encode(coordinates[0][0], 0, factor) + encode(coordinates[0][1], 0, factor);
|
|
||||||
|
|
||||||
for (var i = 1; i < coordinates.length; i++) {
|
|
||||||
var a = coordinates[i], b = coordinates[i - 1];
|
|
||||||
output += encode(a[0], b[0], factor);
|
|
||||||
output += encode(a[1], b[1], factor);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
|
|
||||||
function flipped(coords) {
|
|
||||||
var flipped = [];
|
|
||||||
for (var i = 0; i < coords.length; i++) {
|
|
||||||
var coord = coords[i].slice();
|
|
||||||
flipped.push([coord[1], coord[0]]);
|
|
||||||
}
|
|
||||||
return flipped;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a GeoJSON LineString feature/geometry.
|
|
||||||
*
|
|
||||||
* @param {Object} geojson
|
|
||||||
* @param {Number} precision
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
polyline.fromGeoJSON = function(geojson, precision) {
|
|
||||||
if (geojson && geojson.type === 'Feature') {
|
|
||||||
geojson = geojson.geometry;
|
|
||||||
}
|
|
||||||
if (!geojson || geojson.type !== 'LineString') {
|
|
||||||
throw new Error('Input must be a GeoJSON LineString');
|
|
||||||
}
|
|
||||||
return polyline.encode(flipped(geojson.coordinates), precision);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes to a GeoJSON LineString geometry.
|
|
||||||
*
|
|
||||||
* @param {String} str
|
|
||||||
* @param {Number} precision
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
polyline.toGeoJSON = function(str, precision) {
|
|
||||||
var coords = polyline.decode(str, precision);
|
|
||||||
return {
|
|
||||||
type: 'LineString',
|
|
||||||
coordinates: flipped(coords)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
let polyline_decode = polyline.decode;
|
|
||||||
export { polyline_decode as decode };
|
|
@ -1,807 +0,0 @@
|
|||||||
import {LitElement, html, unsafeHTML, css, guard, until} from './lit-all.min.js';
|
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
|
||||||
import * as polyline from './polyline.js';
|
|
||||||
import {gpx_parse} from './gpx.js';
|
|
||||||
|
|
||||||
const k_client_id = '28276';
|
|
||||||
const k_redirect_url = 'https://tildefriends.net/~cory/gg/login';
|
|
||||||
|
|
||||||
const k_color_snow = [128, 128, 255, 255];
|
|
||||||
const k_color_ice = [160, 160, 255, 255];
|
|
||||||
const k_color_water = [0, 0, 255, 255];
|
|
||||||
const k_color_dirt = [128, 129, 130, 255];
|
|
||||||
const k_color_pavement = [32, 32, 32, 255];
|
|
||||||
const k_color_grass = [0, 255, 0, 255];
|
|
||||||
const k_color_default = [128, 128, 128, 255];
|
|
||||||
|
|
||||||
const k_store = {
|
|
||||||
'🦞': 15,
|
|
||||||
'🛶': 10,
|
|
||||||
'🏠': 10,
|
|
||||||
'⛰': 10,
|
|
||||||
'🐠': 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
const k_marker_snap = {x: 5, y: 4};
|
|
||||||
|
|
||||||
class GgAppElement extends LitElement {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
user: {type: Object},
|
|
||||||
strava: {type: Object},
|
|
||||||
activities: {type: Array},
|
|
||||||
activity: {type: Object},
|
|
||||||
world: {type: Object},
|
|
||||||
whoami: {type: String},
|
|
||||||
status: {type: Object},
|
|
||||||
tab: {type: String},
|
|
||||||
url: {type: String},
|
|
||||||
currency: {type: Number},
|
|
||||||
to_build: {type: String},
|
|
||||||
emoji_of_the_day: {type: String},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.activities = [];
|
|
||||||
this.activity = {};
|
|
||||||
this.loaded_activities = [];
|
|
||||||
this.placed_emojis = [];
|
|
||||||
this.strava = {};
|
|
||||||
this.min_lat = Number.MAX_VALUE;
|
|
||||||
this.min_lon = Number.MAX_VALUE;
|
|
||||||
this.max_lat = -Number.MAX_VALUE;
|
|
||||||
this.max_lon = -Number.MAX_VALUE;
|
|
||||||
this.focus = undefined;
|
|
||||||
this.status = undefined;
|
|
||||||
this.tab = 'map';
|
|
||||||
this.load().catch(function(e) {
|
|
||||||
console.log('load error', e);
|
|
||||||
});
|
|
||||||
this.to_build = '🏠';
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
console.log('load');
|
|
||||||
let emojis = await (await fetch('emojis.json')).json();
|
|
||||||
emojis = Object.values(emojis).map(x => Object.values(x)).flat();
|
|
||||||
let today = new Date();
|
|
||||||
let date_index = today.getYear() * 356 + today.getMonth() * 31 + today.getDate();
|
|
||||||
this.emoji_of_the_day = emojis[(date_index * 123457) % emojis.length];
|
|
||||||
this.user = await tfrpc.rpc.getUser();
|
|
||||||
this.url = (await tfrpc.rpc.url()).split('?')[0];
|
|
||||||
try {
|
|
||||||
await this.update_credentials();
|
|
||||||
} catch (e) {
|
|
||||||
console.log('update_credentials failed', e);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await this.update_activities();
|
|
||||||
} catch (e) {
|
|
||||||
console.log('update_activities failed', e);
|
|
||||||
}
|
|
||||||
await this.acquire_ssb_identity();
|
|
||||||
if (this.whoami && this.activities?.length) {
|
|
||||||
await this.sync_activities();
|
|
||||||
}
|
|
||||||
await this.get_activities_from_ssb();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* https://gist.github.com/jcouyang/632709f30e12a7879a73e9e132c0d56b?permalink_comment_id=3591045#gistcomment-3591045 */
|
|
||||||
async promise_all(promises, max_concurrent) {
|
|
||||||
let index = 0;
|
|
||||||
let results = [];
|
|
||||||
async function exec_thread() {
|
|
||||||
while (index < promises.length) {
|
|
||||||
const current = index++;
|
|
||||||
results[current] = await promises[current];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const threads = [];
|
|
||||||
for (let thread = 0; thread < max_concurrent; thread++) {
|
|
||||||
threads.push(exec_thread());
|
|
||||||
}
|
|
||||||
await Promise.all(threads);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get_activities_from_ssb() {
|
|
||||||
this.status = {text: 'loading activities'};
|
|
||||||
this.loaded_activities = [];
|
|
||||||
let rows = await tfrpc.rpc.query(`
|
|
||||||
SELECT messages.author, json_extract(mention.value, '$.link') AS blob_id
|
|
||||||
FROM messages_fts('"gg-activity"')
|
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid,
|
|
||||||
json_each(messages.content, '$.mentions') as mention
|
|
||||||
WHERE json_extract(messages.content, '$.type') = 'gg-activity' AND
|
|
||||||
json_extract(mention.value, '$.name') = 'activity_data'
|
|
||||||
ORDER BY messages.timestamp DESC
|
|
||||||
`, []);
|
|
||||||
this.status = {text: 'loading activity data'};
|
|
||||||
let authors = rows.map(x => x.author);
|
|
||||||
let blobs = await this.promise_all(rows.map(x => tfrpc.rpc.get_blob(x.blob_id)), 8);
|
|
||||||
this.status = {text: 'processing activity data'};
|
|
||||||
for (let [index, blob] of blobs.entries()) {
|
|
||||||
let activity;
|
|
||||||
try {
|
|
||||||
activity = JSON.parse(blob);
|
|
||||||
} catch {
|
|
||||||
activity = gpx_parse(blob);
|
|
||||||
}
|
|
||||||
if (activity) {
|
|
||||||
activity.author = authors[index];
|
|
||||||
this.loaded_activities.push(activity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.status = {text: 'calculating balance'};
|
|
||||||
rows = await tfrpc.rpc.query(`
|
|
||||||
SELECT count(*) AS currency FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-activity'
|
|
||||||
`, [this.whoami]);
|
|
||||||
let currency = rows[0].currency;
|
|
||||||
rows = await tfrpc.rpc.query(`
|
|
||||||
SELECT SUM(json_extract(content, '$.cost')) AS cost FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-place'
|
|
||||||
`, [this.whoami]);
|
|
||||||
let spent = rows[0].cost;
|
|
||||||
this.currency = currency - spent;
|
|
||||||
this.status = {text: 'getting placed emojis'};
|
|
||||||
rows = await tfrpc.rpc.query(`
|
|
||||||
SELECT messages.content
|
|
||||||
FROM messages_fts('"gg-place"')
|
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
|
||||||
WHERE json_extract(messages.content, '$.type') = 'gg-place'
|
|
||||||
ORDER BY messages.timestamp
|
|
||||||
`);
|
|
||||||
for (let row of rows) {
|
|
||||||
console.log(row.content);
|
|
||||||
let content = JSON.parse(row.content);
|
|
||||||
this.placed_emojis.push({
|
|
||||||
position: content.position,
|
|
||||||
emoji: content.emoji,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log(this.placed_emojis);
|
|
||||||
this.status = undefined;
|
|
||||||
this.update_map();
|
|
||||||
}
|
|
||||||
|
|
||||||
async sync_activities() {
|
|
||||||
let ids = this.activities.map(x => `https://www.strava.com/activities/${x.id}`);
|
|
||||||
let missing = await tfrpc.rpc.query(`
|
|
||||||
WITH my_activities AS (
|
|
||||||
SELECT json_extract(mention.value, '$.link') AS url
|
|
||||||
FROM messages, json_each(messages.content, '$.mentions') AS mention
|
|
||||||
WHERE
|
|
||||||
author = ? AND
|
|
||||||
json_extract(messages.content, '$.type') = 'gg-activity' AND
|
|
||||||
json_extract(mention.value, '$.name') = 'activity_url')
|
|
||||||
SELECT from_strava.value FROM json_each(?) AS from_strava
|
|
||||||
LEFT OUTER JOIN my_activities ON from_strava.value = my_activities.url
|
|
||||||
WHERE my_activities.url IS NULL
|
|
||||||
`, [this.whoami, JSON.stringify(ids)]);
|
|
||||||
console.log('missing = ', missing);
|
|
||||||
for (let [index, row] of missing.entries()) {
|
|
||||||
this.status = {text: 'syncing from strava', value: index, max: missing.length};
|
|
||||||
let url = row.value;
|
|
||||||
let id = url.match(/.*\/(\d+)/)[1];
|
|
||||||
let response = await fetch(`https://www.strava.com/api/v3/activities/${id}`, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${this.strava.access_token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
let activity = await response.json();
|
|
||||||
let blob_id = await tfrpc.rpc.store_blob(JSON.stringify(activity));
|
|
||||||
let message = {
|
|
||||||
type: 'gg-activity',
|
|
||||||
mentions: [
|
|
||||||
{
|
|
||||||
link: url,
|
|
||||||
name: 'activity_url',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: blob_id,
|
|
||||||
name: 'activity_data',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
};
|
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message);
|
|
||||||
}
|
|
||||||
this.status = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async acquire_ssb_identity() {
|
|
||||||
let user = await tfrpc.rpc.getUser();
|
|
||||||
if (!user?.credentials?.session?.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let ids = await tfrpc.rpc.getIdentities();
|
|
||||||
let players = ids.length ? (await tfrpc.rpc.query(`
|
|
||||||
SELECT author FROM messages JOIN json_each(?) ON messages.author = json_each.value
|
|
||||||
WHERE
|
|
||||||
json_extract(messages.content, '$.type') = 'gg-player' AND
|
|
||||||
json_extract(messages.content, '$.active')
|
|
||||||
ORDER BY timestamp DESC limit 1
|
|
||||||
`, [JSON.stringify(ids)])).map(row => row.author) : [];
|
|
||||||
if (!players.length) {
|
|
||||||
this.whoami = await tfrpc.rpc.createIdentity();
|
|
||||||
if (this.whoami) {
|
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, {
|
|
||||||
type: 'gg-player',
|
|
||||||
active: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
players.sort();
|
|
||||||
this.whoami = players[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update_credentials() {
|
|
||||||
let name = this.user?.credentials?.session?.name;
|
|
||||||
if (!name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let shared = await tfrpc.rpc.sharedDatabaseGet(name);
|
|
||||||
if (shared) {
|
|
||||||
await tfrpc.rpc.databaseSet('strava', shared);
|
|
||||||
await tfrpc.rpc.sharedDatabaseRemove(name);
|
|
||||||
}
|
|
||||||
this.strava = JSON.parse(await tfrpc.rpc.databaseGet('strava') || '{}');
|
|
||||||
if (new Date().valueOf() / 1000 > this.strava.expires_at) {
|
|
||||||
console.log('this looks expired', new Date().valueOf() / 1000, '>', this.strava.expires_at);
|
|
||||||
let x = await tfrpc.rpc.refresh_token(this.strava);
|
|
||||||
if (x) {
|
|
||||||
this.strava = x;
|
|
||||||
await tfrpc.rpc.databaseSet('strava', JSON.stringify(x));
|
|
||||||
} else {
|
|
||||||
this.strava = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update_activities() {
|
|
||||||
if (this?.strava?.access_token) {
|
|
||||||
let response = await fetch('https://www.strava.com/api/v3/athlete/activities', {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${this.strava.access_token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.activities = await response.json();
|
|
||||||
this.activities.sort((a, b) => (a.id - b.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
color_to_emoji(color) {
|
|
||||||
const k_map = [
|
|
||||||
[k_color_snow, '⬜'],
|
|
||||||
[k_color_ice, '🟦'],
|
|
||||||
[k_color_water, '🟦'],
|
|
||||||
[k_color_dirt, '🟫'],
|
|
||||||
[k_color_pavement, '⬛'],
|
|
||||||
[k_color_grass, '🟩'],
|
|
||||||
[k_color_default, '🟧'],
|
|
||||||
];
|
|
||||||
for (let m of k_map) {
|
|
||||||
if (m[0][0] == color[0] &&
|
|
||||||
m[0][1] == color[1] &&
|
|
||||||
m[0][2] == color[2] &&
|
|
||||||
m[0][3] == color[3]) {
|
|
||||||
return m[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activity_bounds(activity) {
|
|
||||||
let min_lat = Number.MAX_VALUE;
|
|
||||||
let min_lon = Number.MAX_VALUE;
|
|
||||||
let max_lat = -Number.MAX_VALUE;
|
|
||||||
let max_lon = -Number.MAX_VALUE;
|
|
||||||
if (activity?.map?.polyline) {
|
|
||||||
for (let pt of polyline.decode(activity.map.polyline)) {
|
|
||||||
min_lat = Math.min(min_lat, pt[0]);
|
|
||||||
min_lon = Math.min(min_lon, pt[1]);
|
|
||||||
max_lat = Math.max(max_lat, pt[0]);
|
|
||||||
max_lon = Math.max(max_lon, pt[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (activity?.segments) {
|
|
||||||
for (let segment of activity.segments) {
|
|
||||||
for (let pt of segment) {
|
|
||||||
min_lat = Math.min(min_lat, pt.lat);
|
|
||||||
min_lon = Math.min(min_lon, pt.lon);
|
|
||||||
max_lat = Math.max(max_lat, pt.lat);
|
|
||||||
max_lon = Math.max(max_lon, pt.lon);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
min: {
|
|
||||||
lat: min_lat,
|
|
||||||
lng: min_lon,
|
|
||||||
},
|
|
||||||
max: {
|
|
||||||
lat: max_lat,
|
|
||||||
lng: max_lon,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
on_click(event) {
|
|
||||||
let popup = L.popup()
|
|
||||||
.setLatLng(event.latlng)
|
|
||||||
.setContent(`
|
|
||||||
<div><a target="_top" href="https://www.google.com/maps/search/?api=1&query=${event.latlng.lat},${event.latlng.lng}">${event.latlng.lat}, ${event.latlng.lng}</a></div>
|
|
||||||
`)
|
|
||||||
.openOn(this.leaflet);
|
|
||||||
}
|
|
||||||
|
|
||||||
async build() {
|
|
||||||
if (this.popup) {
|
|
||||||
this.popup.remove();
|
|
||||||
}
|
|
||||||
if (!this.marker) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let latlng = this.marker.getLatLng();
|
|
||||||
|
|
||||||
let cost = k_store[this.to_build];
|
|
||||||
if (cost > this.currency) {
|
|
||||||
alert('Insufficient funds.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let message = {
|
|
||||||
type: 'gg-place',
|
|
||||||
position: {lat: latlng.lat, lng: latlng.lng},
|
|
||||||
emoji: this.to_build,
|
|
||||||
cost: cost,
|
|
||||||
};
|
|
||||||
let id = await tfrpc.rpc.appendMessage(this.whoami, message);
|
|
||||||
this.marker.remove();
|
|
||||||
this.placed_emojis.push({
|
|
||||||
position: {lat: latlng.lat, lng: latlng.lng},
|
|
||||||
emoji: this.to_build,
|
|
||||||
});
|
|
||||||
this.currency -= cost;
|
|
||||||
return this.update_map();
|
|
||||||
}
|
|
||||||
|
|
||||||
on_marker_click(event) {
|
|
||||||
this.popup = L.popup()
|
|
||||||
.setLatLng(event.latlng)
|
|
||||||
.setContent(`
|
|
||||||
${this.to_build} (-${k_store[this.to_build]}) <input type="button" value="Build" onclick="document.getElementById('ggapp').build()"></input>
|
|
||||||
`)
|
|
||||||
.openOn(this.leaflet);
|
|
||||||
}
|
|
||||||
|
|
||||||
snap_to_grid(latlng, fudge, zoom) {
|
|
||||||
let position = this.leaflet.options.crs.latLngToPoint(latlng, zoom ?? this.leaflet.getZoom());
|
|
||||||
position.x = Math.round(position.x / 16) * 16 + (fudge?.x ?? 0);
|
|
||||||
position.y = Math.round(position.y / 16) * 16 + (fudge?.y ?? 0);
|
|
||||||
position = this.leaflet.options.crs.pointToLatLng(position, zoom ?? this.leaflet.getZoom());
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
on_marker_move(event) {
|
|
||||||
if (!this.no_snap && this.marker) {
|
|
||||||
this.no_snap = true;
|
|
||||||
this.marker.setLatLng(this.snap_to_grid(this.marker.getLatLng(), k_marker_snap));
|
|
||||||
this.no_snap = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
on_zoom(event) {
|
|
||||||
if (this.marker) {
|
|
||||||
this.marker.setLatLng(this.snap_to_grid(this.marker.getLatLng(), k_marker_snap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
on_mouse_down(event) {
|
|
||||||
if (this.marker) {
|
|
||||||
this.marker.remove();
|
|
||||||
this.marker = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.to_build) {
|
|
||||||
this.marker = L.marker(this.snap_to_grid(event.latlng, k_marker_snap), {icon: L.divIcon({className: 'build-icon'}), draggable: true}).addTo(this.leaflet);
|
|
||||||
this.marker.on({click: this.on_marker_click.bind(this)});
|
|
||||||
this.marker.on({drag: this.on_marker_move.bind(this)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update_map() {
|
|
||||||
let map = this.shadowRoot.getElementById('map');
|
|
||||||
if (!map || !this.loaded_activities.length) {
|
|
||||||
this.leaflet = undefined;
|
|
||||||
this.grid_layer = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.leaflet) {
|
|
||||||
this.leaflet = L.map(map, {attributionControl: false, maxZoom: 16, bounceAtZoomLimits: false});
|
|
||||||
this.leaflet.on({contextmenu: this.on_click.bind(this)});
|
|
||||||
this.leaflet.on({click: this.on_mouse_down.bind(this)});
|
|
||||||
this.leaflet.on({zoom: this.on_zoom.bind(this)});
|
|
||||||
}
|
|
||||||
let self = this;
|
|
||||||
let grid_layer = L.GridLayer.extend({
|
|
||||||
createTile: function(coords) {
|
|
||||||
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
|
|
||||||
var size = this.getTileSize();
|
|
||||||
tile.width = size.x;
|
|
||||||
tile.height = size.y;
|
|
||||||
var context = tile.getContext('2d');
|
|
||||||
context.font = '10pt sans';
|
|
||||||
let bounds = this._tileCoordsToBounds(coords);
|
|
||||||
let degrees = 360.0 / (2 ** coords.z);
|
|
||||||
let ul = bounds.getNorthWest();
|
|
||||||
let lr = bounds.getSouthEast();
|
|
||||||
|
|
||||||
let mini = document.createElement('canvas');
|
|
||||||
mini.width = Math.floor(size.x / 16.0);
|
|
||||||
mini.height = Math.floor(size.y / 16.0);
|
|
||||||
let mini_context = mini.getContext('2d');
|
|
||||||
let image_data = context.getImageData(0, 0, mini.width, mini.height);
|
|
||||||
for (let activity of self.loaded_activities) {
|
|
||||||
self.draw_activity_to_tile(image_data, mini.width, mini.height, ul, lr, activity);
|
|
||||||
}
|
|
||||||
context.textAlign = 'left';
|
|
||||||
context.textBaseline = 'bottom';
|
|
||||||
for (let x = 0; x < mini.width; x++) {
|
|
||||||
for (let y = 0; y < mini.height; y++) {
|
|
||||||
let start = (y * mini.width + x) * 4;
|
|
||||||
let pixel = self.color_to_emoji(image_data.data.slice(start, start + 4));
|
|
||||||
if (pixel) {
|
|
||||||
//context.fillRect(x * size.x / mini.width, y * size.y / mini.height, size.x / mini.width, size.y / mini.height);
|
|
||||||
context.fillText(pixel, x * size.x / mini.width, y * size.y / mini.height + mini.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let placed of self.placed_emojis) {
|
|
||||||
let position = self.leaflet.options.crs.latLngToPoint(self.snap_to_grid(placed.position, undefined, coords.z), coords.z);
|
|
||||||
let tile_x = Math.floor(position.x / size.x);
|
|
||||||
let tile_y = Math.floor(position.y / size.y);
|
|
||||||
position.x = position.x - tile_x * size.x;
|
|
||||||
position.y = position.y - tile_y * size.y;
|
|
||||||
if (tile_x == coords.x && tile_y == coords.y) {
|
|
||||||
//context.fillRect(position.x, position.y, size.x / mini.width, size.y / mini.height);
|
|
||||||
context.fillText(placed.emoji, position.x, position.y + mini.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tile;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (this.grid_layer) {
|
|
||||||
this.grid_layer.redraw();
|
|
||||||
} else {
|
|
||||||
this.grid_layer = new grid_layer();
|
|
||||||
this.grid_layer.addTo(this.leaflet);
|
|
||||||
}
|
|
||||||
for (let activity of this.loaded_activities) {
|
|
||||||
let bounds = this.activity_bounds(activity);
|
|
||||||
this.min_lat = Math.min(this.min_lat, bounds.min.lat);
|
|
||||||
this.min_lon = Math.min(this.min_lon, bounds.min.lng);
|
|
||||||
this.max_lat = Math.max(this.max_lat, bounds.max.lat);
|
|
||||||
this.max_lon = Math.max(this.max_lon, bounds.max.lng);
|
|
||||||
}
|
|
||||||
if (this.focus) {
|
|
||||||
this.leaflet.fitBounds([
|
|
||||||
this.focus.min,
|
|
||||||
this.focus.max,
|
|
||||||
]);
|
|
||||||
this.focus = undefined;
|
|
||||||
} else {
|
|
||||||
this.leaflet.fitBounds([
|
|
||||||
[this.min_lat, this.min_lon],
|
|
||||||
[this.max_lat, this.max_lon],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activity_to_color(activity) {
|
|
||||||
let color = [0, 0, 0, 255];
|
|
||||||
switch (activity.sport_type) {
|
|
||||||
/* Implies snow. */
|
|
||||||
case 'AlpineSki':
|
|
||||||
case 'BackcountrySki':
|
|
||||||
case 'NordicSki':
|
|
||||||
case 'Snowshoe':
|
|
||||||
case 'Snowboard':
|
|
||||||
color = k_color_snow;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Implies ice. */
|
|
||||||
case 'IceSkate':
|
|
||||||
case 'InlineSkate':
|
|
||||||
color = k_color_ice;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Implies water. */
|
|
||||||
case 'Canoeing':
|
|
||||||
case 'Kayaking':
|
|
||||||
case 'Kitesurf':
|
|
||||||
case 'Rowing':
|
|
||||||
case 'Sail':
|
|
||||||
case 'StandUpPaddling':
|
|
||||||
case 'Surfing':
|
|
||||||
case 'Swim':
|
|
||||||
case 'Windsurf':
|
|
||||||
color = k_color_water;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Implies dirt. */
|
|
||||||
case 'EMountainBikeRide':
|
|
||||||
case 'Hike':
|
|
||||||
case 'MountainBikeRide':
|
|
||||||
case 'RockClimbing':
|
|
||||||
case 'TrailRun':
|
|
||||||
color = k_color_dirt;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Implies pavement. */
|
|
||||||
case 'EBikeRide':
|
|
||||||
case 'GravelRide':
|
|
||||||
case 'Handcycle':
|
|
||||||
case 'Ride':
|
|
||||||
case 'RollerSki':
|
|
||||||
case 'Run':
|
|
||||||
case 'Skateboard':
|
|
||||||
case 'Badminton':
|
|
||||||
case 'Tennis':
|
|
||||||
case 'Velomobile':
|
|
||||||
case 'Walk':
|
|
||||||
case 'Wheelchair':
|
|
||||||
color = k_color_pavement;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Grass, maybe? */
|
|
||||||
case 'Golf':
|
|
||||||
case 'Soccer':
|
|
||||||
case 'Squash':
|
|
||||||
color = k_color_grass;
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Crossfit,
|
|
||||||
// Elliptical
|
|
||||||
// HighIntensityIntervalTraining
|
|
||||||
// Pickleball
|
|
||||||
// Pilates
|
|
||||||
// Racquetball
|
|
||||||
// StairStepper
|
|
||||||
// TableTennis,
|
|
||||||
// VirtualRide
|
|
||||||
// VirtualRow
|
|
||||||
// VirtualRun
|
|
||||||
// WeightTraining
|
|
||||||
// Workout
|
|
||||||
// Yoga
|
|
||||||
default:
|
|
||||||
color = k_color_default;
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
line(image_data, x0, y0, x1, y1, value) {
|
|
||||||
/* <3 https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm */
|
|
||||||
let dx = Math.abs(x1 - x0);
|
|
||||||
let sx = x0 < x1 ? 1 : -1;
|
|
||||||
let dy = -Math.abs(y1 - y0);
|
|
||||||
let sy = y0 < y1 ? 1 : -1;
|
|
||||||
let error = dx + dy;
|
|
||||||
while (true) {
|
|
||||||
if (x0 >= 0 && y0 >= 0 && x0 < image_data.width && y0 < image_data.height) {
|
|
||||||
let base = (y0 * image_data.width + x0) * 4;
|
|
||||||
image_data.data[base + 0] = value[0];
|
|
||||||
image_data.data[base + 1] = value[1];
|
|
||||||
image_data.data[base + 2] = value[2];
|
|
||||||
image_data.data[base + 3] = value[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x0 == x1 && y0 == y1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let e2 = 2 * error;
|
|
||||||
if (e2 >= dy) {
|
|
||||||
if (x0 == x1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
error += dy;
|
|
||||||
x0 = Math.round(x0 + sx);
|
|
||||||
}
|
|
||||||
if (e2 <= dx) {
|
|
||||||
if (y0 == y1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
error += dx;
|
|
||||||
y0 = Math.round(y0 + sy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
draw_activity_to_tile(image_data, width, height, ul, lr, activity) {
|
|
||||||
let color = this.activity_to_color(activity);
|
|
||||||
if (activity?.map?.polyline) {
|
|
||||||
let last;
|
|
||||||
for (let pt of polyline.decode(activity.map.polyline)) {
|
|
||||||
let px = [
|
|
||||||
Math.floor(width * (pt[1] - ul.lng) / (lr.lng - ul.lng)),
|
|
||||||
Math.floor(height * (pt[0] - ul.lat) / (lr.lat - ul.lat)),
|
|
||||||
];
|
|
||||||
if (last) {
|
|
||||||
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
|
||||||
}
|
|
||||||
last = px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (activity?.segments) {
|
|
||||||
for (let segment of activity.segments) {
|
|
||||||
let last;
|
|
||||||
for (let pt of segment) {
|
|
||||||
let px = [
|
|
||||||
Math.floor(width * (pt.lon - ul.lng) / (lr.lng - ul.lng)),
|
|
||||||
Math.floor(height * (pt.lat - ul.lat) / (lr.lat - ul.lat)),
|
|
||||||
];
|
|
||||||
if (last) {
|
|
||||||
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
|
||||||
}
|
|
||||||
last = px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async on_upload(event) {
|
|
||||||
try {
|
|
||||||
let file = event.srcElement.files[0];
|
|
||||||
let xml = await file.text();
|
|
||||||
let gpx = gpx_parse(xml);
|
|
||||||
let blob_id = await tfrpc.rpc.store_blob(xml);
|
|
||||||
console.log('blob_id = ', blob_id);
|
|
||||||
console.log(gpx);
|
|
||||||
let message = {
|
|
||||||
type: 'gg-activity',
|
|
||||||
mentions: [
|
|
||||||
{
|
|
||||||
link: `https://${gpx.link}/activity/${gpx.time}`,
|
|
||||||
name: 'activity_url',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: blob_id,
|
|
||||||
name: 'activity_data',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
};
|
|
||||||
console.log('id =', this.whoami, 'message = ', message);
|
|
||||||
let id = await tfrpc.rpc.appendMessage(this.whoami, message);
|
|
||||||
console.log('appended message', id);
|
|
||||||
alert('Activity uploaded.');
|
|
||||||
await this.get_activities_from_ssb();
|
|
||||||
} catch (e) {
|
|
||||||
alert(`Error: ${JSON.stringify(e, null, 2)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
upload() {
|
|
||||||
let input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.onchange = (event) => this.on_upload(event);
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
this.update_map();
|
|
||||||
}
|
|
||||||
|
|
||||||
focus_map(activity) {
|
|
||||||
let bounds = this.activity_bounds(activity);
|
|
||||||
if (bounds.min.lat < bounds.max.lat &&
|
|
||||||
bounds.min.lng < bounds.max.lng) {
|
|
||||||
this.tab = 'map';
|
|
||||||
this.focus = bounds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render_news() {
|
|
||||||
return html`
|
|
||||||
<ul>
|
|
||||||
${this.loaded_activities.map(x => html`
|
|
||||||
<li style="cursor: pointer" @click=${() => this.focus_map(x)}>${x.author} ${x.name ?? x.time}</li>
|
|
||||||
`)}
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render_store_item(item) {
|
|
||||||
let [emoji, cost] = item;
|
|
||||||
return html`
|
|
||||||
<div>
|
|
||||||
<input type="button" value="${emoji}" @click=${() => this.to_build = emoji}></input> ${cost} ${emoji == this.to_build ? '<-- Will be built next' : undefined}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render_store() {
|
|
||||||
let store = Object.assign({}, k_store);
|
|
||||||
store[this.emoji_of_the_day] = 5;
|
|
||||||
return html`
|
|
||||||
<h2>Store</h2>
|
|
||||||
<div><b>Your balance:</b> ${this.currency}</div>
|
|
||||||
${Object.entries(store).map(this.render_store_item.bind(this))}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let header;
|
|
||||||
if (!this.user?.credentials?.session?.name) {
|
|
||||||
header = html`<div style="flex: 1 0">Please <a target="_top" href="/login?return=${this.url}">login</a> to Tilde Friends, first.</div>`;
|
|
||||||
} else if (!this.strava?.access_token) {
|
|
||||||
let strava_url = `https://www.strava.com/oauth/authorize?client_id=${k_client_id}&redirect_uri=${k_redirect_url}&response_type=code&approval_prompt=auto&scope=activity%3Aread&state=${g_data.state}`;
|
|
||||||
header = html`
|
|
||||||
<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
|
|
||||||
<div style="flex: 1 1">Please <a target="_top" href=${strava_url}>login</a> to Strava.</div>
|
|
||||||
<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
|
|
||||||
<input type="button" value="📁" @click=${this.upload}></input>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
header = html`
|
|
||||||
<div>
|
|
||||||
<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
|
|
||||||
<h1>Welcome, ${this.user.credentials.session.name}</h1>
|
|
||||||
<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
|
|
||||||
<input type="button" value="📁" @click=${this.upload}></input>
|
|
||||||
</div>
|
|
||||||
<h3 ?hidden=${!this.status?.text}>${this.status?.text} <progress ?hidden=${!this.status?.max} value=${this.status?.value} max=${this.status?.max}>${this.status?.value}</progress></h3>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let navigation = html`
|
|
||||||
<style>
|
|
||||||
#navigation input[type="button"] {
|
|
||||||
min-width: 3em;
|
|
||||||
min-height: 3em;
|
|
||||||
flex: 1 0;
|
|
||||||
font-size: large;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div id="navigation" style="display: flex; flex-direction: row">
|
|
||||||
<input type="button" id="button_map" @click=${() => this.tab = 'map'} value="🗺️Map"></input>
|
|
||||||
<input type="button" id="button_news" @click=${() => this.tab = 'news'} value="🏃News"></input>
|
|
||||||
<input type="button" id="button_friends" @click=${() => this.tab = 'friends'} value="👫Friends"></input>
|
|
||||||
<input type="button" id="button_store" @click=${() => this.tab = 'store'} value="🏗️Store"></input>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
let content;
|
|
||||||
switch (this.tab) {
|
|
||||||
case 'map':
|
|
||||||
content = html`<div id="map" style="width: 100%; height: 100%"></div>`;
|
|
||||||
break;
|
|
||||||
case 'news':
|
|
||||||
content = this.render_news();
|
|
||||||
break;
|
|
||||||
case 'friends':
|
|
||||||
content = html`<div>Friends</div>`;
|
|
||||||
break;
|
|
||||||
case 'store':
|
|
||||||
content = this.render_store();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
.build-icon::before {
|
|
||||||
content: '📍';
|
|
||||||
border: 2px solid red;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<link rel="stylesheet" href="leaflet.css"/>
|
|
||||||
<div style="width: 100%; height: 100%; display: flex; flex-direction: column">
|
|
||||||
${header}
|
|
||||||
<div style="flex: 1 0; overflow: scroll">${content}</div>
|
|
||||||
${navigation}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define('gg-app', GgAppElement);
|
|
@ -1,20 +0,0 @@
|
|||||||
const k_client_id = '28276';
|
|
||||||
const k_client_secret = '3123f1f5afe132d9731111066d1d17bdb22ef27e';
|
|
||||||
const k_access_token = 'f753e77764c26252bd2d80e7c5cc17ace51a8864';
|
|
||||||
const k_refresh_token = 'f58d8e1b5a3ec3bf96e681589d5014f9a294f5a4';
|
|
||||||
const k_redirect_url = 'https://tildefriends.net/~cory/gg/login';
|
|
||||||
|
|
||||||
export async function refresh_token(token) {
|
|
||||||
let r = await fetch('https://www.strava.com/api/v3/oauth/token', {
|
|
||||||
method: 'POST',
|
|
||||||
body: `client_id=${k_client_id}&client_secret=${k_client_secret}&refresh_token=${token.refresh_token}&grant_type=refresh_token`,
|
|
||||||
});
|
|
||||||
return r?.body ? JSON.parse(utf8Decode(r.body)) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function authorization_code(code) {
|
|
||||||
return await fetch('https://www.strava.com/api/v3/oauth/token', {
|
|
||||||
method: 'POST',
|
|
||||||
body: `client_id=${k_client_id}&client_secret=${k_client_secret}&code=${code}&grant_type=authorization_code`,
|
|
||||||
});
|
|
||||||
}
|
|
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}
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🦟",
|
"emoji": "🦟",
|
||||||
"previous": "&TegdzvFE+im94shygaHkgDYSaSrwY2h0OKUXSRPBQDM=.sha256"
|
"previous": "&cUqvSDUls3jn0haD85LPFAGdkc8wFuy347TtATNcJgg=.sha256"
|
||||||
}
|
}
|
@ -67,9 +67,6 @@ tfrpc.register(function getHash(id, message) {
|
|||||||
tfrpc.register(function setHash(hash) {
|
tfrpc.register(function setHash(hash) {
|
||||||
return app.setHash(hash);
|
return app.setHash(hash);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('message', async function(id) {
|
|
||||||
await tfrpc.rpc.notifyNewMessage(id);
|
|
||||||
});
|
|
||||||
tfrpc.register(async function store_blob(blob) {
|
tfrpc.register(async function store_blob(blob) {
|
||||||
if (Array.isArray(blob)) {
|
if (Array.isArray(blob)) {
|
||||||
blob = Uint8Array.from(blob);
|
blob = Uint8Array.from(blob);
|
||||||
@ -85,19 +82,24 @@ tfrpc.register(async function store_message(message) {
|
|||||||
tfrpc.register(function apps() {
|
tfrpc.register(function apps() {
|
||||||
return core.apps();
|
return core.apps();
|
||||||
});
|
});
|
||||||
|
tfrpc.register(function getActiveIdentity() {
|
||||||
|
return ssb.getActiveIdentity();
|
||||||
|
});
|
||||||
tfrpc.register(async function try_decrypt(id, content) {
|
tfrpc.register(async function try_decrypt(id, content) {
|
||||||
return await ssb.privateMessageDecrypt(id, content);
|
return await ssb.privateMessageDecrypt(id, content);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('broadcasts', async function() {
|
core.register('onMessage', async function (id) {
|
||||||
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
|
});
|
||||||
|
core.register('onBroadcastsChanged', async function () {
|
||||||
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
});
|
});
|
||||||
|
core.register('onConnectionsChanged', async function () {
|
||||||
core.register('onConnectionsChanged', async function() {
|
|
||||||
await tfrpc.rpc.set('connections', await ssb.connections());
|
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||||
});
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (typeof(database) !== 'undefined') {
|
if (typeof database !== 'undefined') {
|
||||||
g_database = await database('ssb');
|
g_database = await database('ssb');
|
||||||
}
|
}
|
||||||
await app.setDocument(utf8Decode(await getFile('index.html')));
|
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||||
|
2
apps/issues/commonmark.min.js
vendored
2
apps/issues/commonmark.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,12 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="color: #fff">
|
<html style="color: #fff">
|
||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends</title>
|
<title>Tilde Friends</title>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<tf-issues-app/>
|
<tf-issues-app />
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="commonmark.min.js"></script>
|
<script src="commonmark.min.js"></script>
|
||||||
<script src="commonmark-linkify.js" type="module"></script>
|
<script src="commonmark-linkify.js" type="module"></script>
|
||||||
<script src="script.js" type="module"></script>
|
<script src="script.js" type="module"></script>
|
||||||
|
42
apps/issues/lit-all.min.js
vendored
42
apps/issues/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -4,43 +4,6 @@ import * as tfutils from './tf-utils.js';
|
|||||||
|
|
||||||
const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256';
|
const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256';
|
||||||
|
|
||||||
class TfIdPickerElement extends LitElement {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
ids: {type: Array},
|
|
||||||
selected: {type: String},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
this.selected = await tfrpc.rpc.localStorageGet('whoami');
|
|
||||||
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
changed(event) {
|
|
||||||
this.selected = event.srcElement.value;
|
|
||||||
tfrpc.rpc.localStorageSet('whoami', this.selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.ids) {
|
|
||||||
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>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
return html`<div>Loading...</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define('tf-id-picker', TfIdPickerElement);
|
|
||||||
|
|
||||||
class TfComposeElement extends LitElement {
|
class TfComposeElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
@ -57,13 +20,15 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
this.dispatchEvent(new CustomEvent('tf-submit', {
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-submit', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
detail: {
|
detail: {
|
||||||
value: this.renderRoot.getElementById('input').value,
|
value: this.renderRoot.getElementById('input').value,
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
this.renderRoot.getElementById('input').value = '';
|
this.renderRoot.getElementById('input').value = '';
|
||||||
this.input();
|
this.input();
|
||||||
}
|
}
|
||||||
@ -96,18 +61,21 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
let issues = {};
|
let issues = {};
|
||||||
let messages = await tfrpc.rpc.query(`
|
let messages = await tfrpc.rpc.query(
|
||||||
WITH issues AS (SELECT messages.* FROM messages_refs JOIN messages ON
|
`
|
||||||
|
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
|
messages.id = messages_refs.message
|
||||||
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
|
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
|
||||||
edits AS (SELECT messages.* FROM issues JOIN messages_refs ON
|
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
|
issues.id = messages_refs.ref JOIN messages ON
|
||||||
messages.id = messages_refs.message
|
messages.id = messages_refs.message
|
||||||
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
|
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
|
||||||
SELECT * FROM issues
|
SELECT * FROM issues
|
||||||
UNION
|
UNION
|
||||||
SELECT * FROM edits ORDER BY timestamp
|
SELECT * FROM edits ORDER BY timestamp
|
||||||
`, [k_project]);
|
`,
|
||||||
|
[k_project]
|
||||||
|
);
|
||||||
for (let message of messages) {
|
for (let message of messages) {
|
||||||
let content = JSON.parse(message.content);
|
let content = JSON.parse(message.content);
|
||||||
switch (content.type) {
|
switch (content.type) {
|
||||||
@ -123,7 +91,7 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
break;
|
break;
|
||||||
case 'issue-edit':
|
case 'issue-edit':
|
||||||
case 'post':
|
case 'post':
|
||||||
for (let issue of (content.issues || [])) {
|
for (let issue of content.issues || []) {
|
||||||
if (issues[issue.link]) {
|
if (issues[issue.link]) {
|
||||||
if (issue.open !== undefined) {
|
if (issue.open !== undefined) {
|
||||||
issues[issue.link].open = issue.open;
|
issues[issue.link].open = issue.open;
|
||||||
@ -136,7 +104,9 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.issues = Object.values(issues).sort((x, y) => (y.open - x.open) || (y.created - x.created));
|
this.issues = Object.values(issues).sort(
|
||||||
|
(x, y) => y.open - x.open || y.created - x.created
|
||||||
|
);
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
for (let issue of this.issues) {
|
for (let issue of this.issues) {
|
||||||
if (issue.id == this.selected.id) {
|
if (issue.id == this.selected.id) {
|
||||||
@ -150,11 +120,20 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<tr>
|
<tr>
|
||||||
<td>${issue.open ? '☐ open' : '☑ closed'}</td>
|
<td>${issue.open ? '☐ open' : '☑ closed'}</td>
|
||||||
<td style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis">${issue.author}</td>
|
<td
|
||||||
<td style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer" @click=${() => this.selected = issue}>
|
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]}
|
${issue.text.split('\n')?.[0]}
|
||||||
</td>
|
</td>
|
||||||
<td>${new Date(issue.updated ?? issue.created).toLocaleDateString()}</td>
|
<td>
|
||||||
|
${new Date(issue.updated ?? issue.created).toLocaleDateString()}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -170,14 +149,22 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
<div>${new Date(update.timestamp).toLocaleString()}</div>
|
<div>${new Date(update.timestamp).toLocaleString()}</div>
|
||||||
<div>${update.author}</div>
|
<div>${update.author}</div>
|
||||||
<div>${message}</div>
|
<div>${message}</div>
|
||||||
<div>${update.open !== undefined ? (update.open ? 'issue opened' : 'issue closed') : undefined}</div>
|
<div>
|
||||||
|
${update.open !== undefined
|
||||||
|
? update.open
|
||||||
|
? 'issue opened'
|
||||||
|
: 'issue closed'
|
||||||
|
: undefined}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async set_open(id, open) {
|
async set_open(id, open) {
|
||||||
if (confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)) {
|
if (
|
||||||
let whoami = this.shadowRoot.getElementById('picker').selected;
|
confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)
|
||||||
|
) {
|
||||||
|
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
await tfrpc.rpc.appendMessage(whoami, {
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
type: 'issue-edit',
|
type: 'issue-edit',
|
||||||
issues: [
|
issues: [
|
||||||
@ -192,7 +179,7 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create_issue(event) {
|
async create_issue(event) {
|
||||||
let whoami = this.shadowRoot.getElementById('picker').selected;
|
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
await tfrpc.rpc.appendMessage(whoami, {
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
type: 'issue',
|
type: 'issue',
|
||||||
project: k_project,
|
project: k_project,
|
||||||
@ -202,12 +189,14 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async reply_to_issue(event) {
|
async reply_to_issue(event) {
|
||||||
let whoami = this.shadowRoot.getElementById('picker').selected;
|
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
await tfrpc.rpc.appendMessage(whoami, {
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
type: 'post',
|
type: 'post',
|
||||||
text: event.detail.value,
|
text: event.detail.value,
|
||||||
root: this.selected.id,
|
root: this.selected.id,
|
||||||
branch: this.selected.updates.length ? this.selected.updates[this.selected.updates.length - 1].id : this.selected.id,
|
branch: this.selected.updates.length
|
||||||
|
? this.selected.updates[this.selected.updates.length - 1].id
|
||||||
|
: this.selected.id,
|
||||||
issues: [
|
issues: [
|
||||||
{
|
{
|
||||||
link: this.selected.id,
|
link: this.selected.id,
|
||||||
@ -218,24 +207,23 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let header = html`
|
let header = html` <h1>Tilde Friends Issues</h1> `;
|
||||||
<h1>Tilde Friends Issues</h1>
|
|
||||||
<tf-id-picker id="picker"></tf-id-picker>
|
|
||||||
`;
|
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
return html`
|
return html`
|
||||||
${header}
|
${header}
|
||||||
<div>
|
<div>
|
||||||
<input type="button" value="Back" @click=${() => this.selected = undefined}></input>
|
<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>` :
|
this.selected.open
|
||||||
html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`}
|
? 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>
|
||||||
<div>${new Date(this.selected.created).toLocaleString()}</div>
|
<div>${new Date(this.selected.created).toLocaleString()}</div>
|
||||||
<div>${this.selected.author}</div>
|
<div>${this.selected.author}</div>
|
||||||
<div>${this.selected.id}</div>
|
<div>${this.selected.id}</div>
|
||||||
<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
|
<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
|
||||||
${this.selected.updates.map(x => this.render_update(x))}
|
${this.selected.updates.map((x) => this.render_update(x))}
|
||||||
<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
|
<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
@ -250,7 +238,7 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
</tr>
|
</tr>
|
||||||
${this.issues.map(x => this.render_issue_table_row(x))}
|
${this.issues.map((x) => this.render_issue_table_row(x))}
|
||||||
</table>
|
</table>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,32 @@
|
|||||||
import * as linkify from './commonmark-linkify.js';
|
import * as linkify from './commonmark-linkify.js';
|
||||||
|
|
||||||
function image(node, entering) {
|
function image(node, entering) {
|
||||||
if (node.firstChild?.type === 'text' &&
|
if (
|
||||||
node.firstChild.literal.startsWith('video:')) {
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('video:')
|
||||||
|
) {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
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.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
this.disableTags += 1;
|
this.disableTags += 1;
|
||||||
} else {
|
} else {
|
||||||
this.disableTags -= 1;
|
this.disableTags -= 1;
|
||||||
this.lit('</video>');
|
this.lit('</video>');
|
||||||
}
|
}
|
||||||
} else if (node.firstChild?.type === 'text' &&
|
} else if (
|
||||||
node.firstChild.literal.startsWith('audio:')) {
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('audio:')
|
||||||
|
) {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
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.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
this.disableTags += 1;
|
this.disableTags += 1;
|
||||||
} else {
|
} else {
|
||||||
@ -24,7 +36,11 @@ function image(node, entering) {
|
|||||||
} else {
|
} else {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
if (this.disableTags === 0) {
|
if (this.disableTags === 0) {
|
||||||
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>');
|
this.lit(
|
||||||
|
'<div class="img_caption">' +
|
||||||
|
this.esc(node.firstChild?.literal || node.destination) +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
||||||
this.lit('<img src="" alt="');
|
this.lit('<img src="" alt="');
|
||||||
} else {
|
} else {
|
||||||
@ -56,14 +72,20 @@ export function markdown(md) {
|
|||||||
node = event.node;
|
node = event.node;
|
||||||
if (event.entering) {
|
if (event.entering) {
|
||||||
if (node.type == 'link') {
|
if (node.type == 'link') {
|
||||||
if (node.destination.startsWith('@') &&
|
if (
|
||||||
node.destination.endsWith('.ed25519')) {
|
node.destination.startsWith('@') &&
|
||||||
|
node.destination.endsWith('.ed25519')
|
||||||
|
) {
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (node.destination.startsWith('%') &&
|
} else if (
|
||||||
node.destination.endsWith('.sha256')) {
|
node.destination.startsWith('%') &&
|
||||||
|
node.destination.endsWith('.sha256')
|
||||||
|
) {
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (node.destination.startsWith('&') &&
|
} else if (
|
||||||
node.destination.endsWith('.sha256')) {
|
node.destination.startsWith('&') &&
|
||||||
|
node.destination.endsWith('.sha256')
|
||||||
|
) {
|
||||||
node.destination = '/' + node.destination + '/view';
|
node.destination = '/' + node.destination + '/view';
|
||||||
}
|
}
|
||||||
} else if (node.type == 'image') {
|
} else if (node.type == 'image') {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "📝",
|
"emoji": "📝",
|
||||||
"previous": "&2hdIDbBrAg63T2X1MzdGSF7yiqHvlnfF0PnInQLp0DA=.sha256"
|
"previous": "&b//KqE4Vx6kOSBRODK1p/8wjOLKZJ+CBB5IkaBt5YsM=.sha256"
|
||||||
}
|
}
|
@ -47,7 +47,7 @@ tfrpc.register(async function get_blob(id) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let g_new_message_resolve;
|
let g_new_message_resolve;
|
||||||
let g_new_message_promise = new Promise(function(resolve, reject) {
|
let g_new_message_promise = new Promise(function (resolve, reject) {
|
||||||
g_new_message_resolve = resolve;
|
g_new_message_resolve = resolve;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -55,9 +55,9 @@ function new_message() {
|
|||||||
return g_new_message_promise;
|
return g_new_message_promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssb.addEventListener('message', function(id) {
|
core.register('onMessage', function (id) {
|
||||||
let resolve = g_new_message_resolve;
|
let resolve = g_new_message_resolve;
|
||||||
g_new_message_promise = new Promise(function(resolve, reject) {
|
g_new_message_promise = new Promise(function (resolve, reject) {
|
||||||
g_new_message_resolve = resolve;
|
g_new_message_resolve = resolve;
|
||||||
});
|
});
|
||||||
if (resolve) {
|
if (resolve) {
|
||||||
@ -104,8 +104,7 @@ async function process_message(whoami, collection, message, kind, parent) {
|
|||||||
if (!x) {
|
if (!x) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (content.type !== kind ||
|
if (content.type !== kind || (parent && content.parent !== parent)) {
|
||||||
(parent && content.parent !== parent)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,7 +112,10 @@ async function process_message(whoami, collection, message, kind, parent) {
|
|||||||
if (content?.tombstone) {
|
if (content?.tombstone) {
|
||||||
delete collection[content.key];
|
delete collection[content.key];
|
||||||
} else {
|
} else {
|
||||||
collection[content.key] = Object.assign(collection[content.key] || {}, content);
|
collection[content.key] = Object.assign(
|
||||||
|
collection[content.key] || {},
|
||||||
|
content
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
collection[message.id] = Object.assign(content, {id: message.id});
|
collection[message.id] = Object.assign(content, {id: message.id});
|
||||||
@ -125,20 +127,29 @@ tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
|
|||||||
let whoami = await ssb.getIdentities();
|
let whoami = await ssb.getIdentities();
|
||||||
data = data ?? {};
|
data = data ?? {};
|
||||||
let rowid = 0;
|
let rowid = 0;
|
||||||
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
await ssb.sqlAsync(
|
||||||
|
'SELECT MAX(rowid) AS rowid FROM messages',
|
||||||
|
[],
|
||||||
|
function (row) {
|
||||||
rowid = row.rowid;
|
rowid = row.rowid;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
while (true) {
|
while (true) {
|
||||||
if (rowid == max_rowid) {
|
if (rowid == max_rowid) {
|
||||||
await new_message();
|
await new_message();
|
||||||
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
await ssb.sqlAsync(
|
||||||
|
'SELECT MAX(rowid) AS rowid FROM messages',
|
||||||
|
[],
|
||||||
|
function (row) {
|
||||||
rowid = row.rowid;
|
rowid = row.rowid;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let modified = false;
|
let modified = false;
|
||||||
let rows = [];
|
let rows = [];
|
||||||
await ssb.sqlAsync(`
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
SELECT messages.id, author, content, timestamp
|
SELECT messages.id, author, content, timestamp
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?1) AS id ON messages.author = id.value
|
JOIN json_each(?1) AS id ON messages.author = id.value
|
||||||
@ -150,9 +161,10 @@ tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
|
|||||||
content LIKE '"%')
|
content LIKE '"%')
|
||||||
`,
|
`,
|
||||||
[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent],
|
[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent],
|
||||||
function(row) {
|
function (row) {
|
||||||
rows.push(row);
|
rows.push(row);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
max_rowid = rowid;
|
max_rowid = rowid;
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
if (await process_message(whoami, data, row, kind, parent)) {
|
if (await process_message(whoami, data, row, kind, parent)) {
|
||||||
|
2
apps/journal/commonmark.min.js
vendored
2
apps/journal/commonmark.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,12 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff">
|
<body style="color: #fff">
|
||||||
<tf-journal-app></tf-journal-app>
|
<tf-journal-app></tf-journal-app>
|
||||||
<script src="commonmark.min.js"></script>
|
<script src="commonmark.min.js"></script>
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="tf-journal-app.js" type="module"></script>
|
<script src="tf-journal-app.js" type="module"></script>
|
||||||
<script src="tf-journal-entry.js" type="module"></script>
|
<script src="tf-journal-entry.js" type="module"></script>
|
||||||
<script src="tf-id-picker.js" type="module"></script>
|
<script src="tf-id-picker.js" type="module"></script>
|
||||||
|
42
apps/journal/lit-all.min.js
vendored
42
apps/journal/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -2,8 +2,8 @@ import {LitElement, html} from './lit-all.min.js';
|
|||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** Provide a list of IDs, and this lets the user pick one.
|
** Provide a list of IDs, and this lets the user pick one.
|
||||||
*/
|
*/
|
||||||
class TfIdentityPickerElement extends LitElement {
|
class TfIdentityPickerElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
@ -19,15 +19,22 @@ class TfIdentityPickerElement extends LitElement {
|
|||||||
|
|
||||||
changed(event) {
|
changed(event) {
|
||||||
this.selected = event.srcElement.value;
|
this.selected = event.srcElement.value;
|
||||||
this.dispatchEvent(new Event('change', {
|
this.dispatchEvent(
|
||||||
|
new Event('change', {
|
||||||
srcElement: this,
|
srcElement: this,
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<select @change=${this.changed} style="max-width: 100%">
|
<select @change=${this.changed} style="max-width: 100%">
|
||||||
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
${(this.ids ?? []).map(
|
||||||
|
(id) =>
|
||||||
|
html`<option ?selected=${id == this.selected} value=${id}>
|
||||||
|
${id}
|
||||||
|
</option>`
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -28,9 +28,14 @@ class TfJournalAppElement extends LitElement {
|
|||||||
async read_journals() {
|
async read_journals() {
|
||||||
let max_rowid;
|
let max_rowid;
|
||||||
let journals;
|
let journals;
|
||||||
while (true)
|
while (true) {
|
||||||
{
|
[max_rowid, journals] = await tfrpc.rpc.collection(
|
||||||
[max_rowid, journals] = await tfrpc.rpc.collection([this.whoami], 'journal-entry', undefined, max_rowid, journals);
|
[this.whoami],
|
||||||
|
'journal-entry',
|
||||||
|
undefined,
|
||||||
|
max_rowid,
|
||||||
|
journals
|
||||||
|
);
|
||||||
this.journals = Object.assign({}, journals);
|
this.journals = Object.assign({}, journals);
|
||||||
console.log('JOURNALS', this.journals);
|
console.log('JOURNALS', this.journals);
|
||||||
}
|
}
|
||||||
@ -52,7 +57,11 @@ class TfJournalAppElement extends LitElement {
|
|||||||
};
|
};
|
||||||
message.recps = [this.whoami];
|
message.recps = [this.whoami];
|
||||||
print(message);
|
print(message);
|
||||||
message = await tfrpc.rpc.encrypt(this.whoami, message.recps, JSON.stringify(message));
|
message = await tfrpc.rpc.encrypt(
|
||||||
|
this.whoami,
|
||||||
|
message.recps,
|
||||||
|
JSON.stringify(message)
|
||||||
|
);
|
||||||
print(message);
|
print(message);
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message);
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
}
|
}
|
||||||
@ -62,12 +71,17 @@ class TfJournalAppElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed}></tf-id-picker>
|
<tf-id-picker
|
||||||
|
.ids=${this.ids}
|
||||||
|
selected=${this.whoami}
|
||||||
|
@change=${this.on_whoami_changed}
|
||||||
|
></tf-id-picker>
|
||||||
</div>
|
</div>
|
||||||
<tf-journal-entry
|
<tf-journal-entry
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.journals=${this.journals}
|
.journals=${this.journals}
|
||||||
@publish=${this.on_journal_publish}></tf-journal-entry>
|
@publish=${this.on_journal_publish}
|
||||||
|
></tf-journal-entry>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,13 +30,15 @@ class TfJournalEntryElement extends LitElement {
|
|||||||
|
|
||||||
async on_publish() {
|
async on_publish() {
|
||||||
console.log('publish', this.text);
|
console.log('publish', this.text);
|
||||||
this.dispatchEvent(new CustomEvent('publish', {
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('publish', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: {
|
detail: {
|
||||||
key: this.shadowRoot.getElementById('date_picker').value,
|
key: this.shadowRoot.getElementById('date_picker').value,
|
||||||
text: this.text,
|
text: this.text,
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
back_dates(count) {
|
back_dates(count) {
|
||||||
@ -63,19 +65,30 @@ class TfJournalEntryElement extends LitElement {
|
|||||||
console.log('RENDER ENTRY', this.key, this.journals?.[this.key]);
|
console.log('RENDER ENTRY', this.key, this.journals?.[this.key]);
|
||||||
return html`
|
return html`
|
||||||
<select id="date_picker" @change=${this.on_date_change}>
|
<select id="date_picker" @change=${this.on_date_change}>
|
||||||
${this.back_dates(10).map(x => html`
|
${this.back_dates(10).map(
|
||||||
<option value=${x}>${x}</option>
|
(x) => html` <option value=${x}>${x}</option> `
|
||||||
`)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
<div style="display: inline-flex; flex-direction: row">
|
<div style="display: inline-flex; flex-direction: row">
|
||||||
<button ?disabled=${this.text == this.journals?.[this.key]?.text} @click=${this.on_publish}>Publish</button>
|
<button
|
||||||
|
?disabled=${this.text == this.journals?.[this.key]?.text}
|
||||||
|
@click=${this.on_publish}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
<button @click=${this.on_discard}>Discard</button>
|
<button @click=${this.on_discard}>Discard</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<textarea
|
<textarea
|
||||||
style="flex: 1 1; min-height: 10em"
|
style="flex: 1 1; min-height: 10em"
|
||||||
@input=${this.on_edit} .value=${this.text ?? this.journals?.[this.key]?.text ?? ''}></textarea>
|
@input=${this.on_edit}
|
||||||
<div style="flex: 1 1">${unsafeHTML(this.markdown(this.text ?? this.journals?.[this.key]?.text))}</div>
|
.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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
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();
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "👟"
|
"emoji": "👟",
|
||||||
|
"previous": "&lYZRnT2UGQxXxYISbuaZewik9AuxBpcJumakwrePw5c=.sha256"
|
||||||
}
|
}
|
@ -1,12 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="color: #fff">
|
<html style="color: #fff">
|
||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends</title>
|
<title>Tilde Friends</title>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<tf-sneaker-app/>
|
<tf-sneaker-app />
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="filesaver.min.js"></script>
|
<script src="filesaver.min.js"></script>
|
||||||
<script src="jszip.min.js"></script>
|
<script src="jszip.min.js"></script>
|
||||||
<script src="script.js" type="module"></script>
|
<script src="script.js" type="module"></script>
|
||||||
|
42
apps/sneaker/lit-all.min.js
vendored
42
apps/sneaker/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -19,7 +19,8 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
|
|
||||||
async search() {
|
async search() {
|
||||||
let q = this.renderRoot.getElementById('search').value;
|
let q = this.renderRoot.getElementById('search').value;
|
||||||
let result = await tfrpc.rpc.query(`
|
let result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name
|
SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name
|
||||||
FROM messages_fts(?)
|
FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
@ -31,15 +32,17 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
HAVING MAX(messages.sequence)
|
HAVING MAX(messages.sequence)
|
||||||
ORDER BY COUNT(*) DESC
|
ORDER BY COUNT(*) DESC
|
||||||
`,
|
`,
|
||||||
[`"${q.replaceAll('"', '""')}"`]);
|
[`"${q.replaceAll('"', '""')}"`]
|
||||||
this.feeds = Object.fromEntries(result.map(x => [x.id, x.name]));
|
);
|
||||||
|
this.feeds = Object.fromEntries(result.map((x) => [x.id, x.name]));
|
||||||
}
|
}
|
||||||
|
|
||||||
format_message(message) {
|
format_message(message) {
|
||||||
|
const k_flag_sequence_before_author = 1;
|
||||||
let out = {
|
let out = {
|
||||||
previous: message.previous ?? null,
|
previous: message.previous ?? null,
|
||||||
};
|
};
|
||||||
if (message.sequence_before_author) {
|
if (message.flags & k_flag_sequence_before_author) {
|
||||||
out.sequence = message.sequence;
|
out.sequence = message.sequence;
|
||||||
out.author = message.author;
|
out.author = message.author;
|
||||||
} else {
|
} else {
|
||||||
@ -70,24 +73,104 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
|
if (
|
||||||
startsWith(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
|
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, 0xee]) ||
|
||||||
startsWith(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
|
startsWith(data, [
|
||||||
|
0xff,
|
||||||
|
0xd8,
|
||||||
|
0xff,
|
||||||
|
0xe1,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x45,
|
||||||
|
0x78,
|
||||||
|
0x69,
|
||||||
|
0x66,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
])
|
||||||
|
) {
|
||||||
return '.jpg';
|
return '.jpg';
|
||||||
} else if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
} else if (
|
||||||
|
startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
||||||
|
) {
|
||||||
return '.png';
|
return '.png';
|
||||||
} else if (startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
} else if (
|
||||||
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
|
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
||||||
|
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])
|
||||||
|
) {
|
||||||
return '.gif';
|
return '.gif';
|
||||||
} else if (startsWith(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) {
|
} else if (
|
||||||
|
startsWith(data, [
|
||||||
|
0x52,
|
||||||
|
0x49,
|
||||||
|
0x46,
|
||||||
|
0x46,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x57,
|
||||||
|
0x45,
|
||||||
|
0x42,
|
||||||
|
0x50,
|
||||||
|
])
|
||||||
|
) {
|
||||||
return '.webp';
|
return '.webp';
|
||||||
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
|
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
|
||||||
return '.svg';
|
return '.svg';
|
||||||
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
} else if (
|
||||||
|
startsWith(data, [
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x66,
|
||||||
|
0x74,
|
||||||
|
0x79,
|
||||||
|
0x70,
|
||||||
|
0x6d,
|
||||||
|
0x70,
|
||||||
|
0x34,
|
||||||
|
0x32,
|
||||||
|
])
|
||||||
|
) {
|
||||||
return '.mp3';
|
return '.mp3';
|
||||||
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) ||
|
} else if (
|
||||||
startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
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';
|
return '.mp4';
|
||||||
} else {
|
} else {
|
||||||
return '.bin';
|
return '.bin';
|
||||||
@ -98,17 +181,34 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
let all_messages = '';
|
let all_messages = '';
|
||||||
let sequence = -1;
|
let sequence = -1;
|
||||||
let messages_done = 0;
|
let messages_done = 0;
|
||||||
let messages_max = (await tfrpc.rpc.query('SELECT MAX(sequence) AS total FROM messages WHERE author = ?', [id]))[0].total;
|
let messages_max = (
|
||||||
|
await tfrpc.rpc.query(
|
||||||
|
'SELECT MAX(sequence) AS total FROM messages WHERE author = ?',
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
)[0].total;
|
||||||
while (true) {
|
while (true) {
|
||||||
let messages = await tfrpc.rpc.query(
|
let messages = await tfrpc.rpc.query(
|
||||||
'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100',
|
`
|
||||||
|
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]
|
[id, sequence]
|
||||||
);
|
);
|
||||||
if (messages?.length) {
|
if (messages?.length) {
|
||||||
all_messages += messages.map(x => JSON.stringify(this.format_message(x))).join('\n') + '\n';
|
all_messages +=
|
||||||
|
messages
|
||||||
|
.map((x) => JSON.stringify(this.format_message(x)))
|
||||||
|
.join('\n') + '\n';
|
||||||
sequence = messages[messages.length - 1].sequence;
|
sequence = messages[messages.length - 1].sequence;
|
||||||
messages_done += messages.length;
|
messages_done += messages.length;
|
||||||
this.progress = {name: 'messages', value: messages_done, max: messages_max};
|
this.progress = {
|
||||||
|
name: 'messages',
|
||||||
|
value: messages_done,
|
||||||
|
max: messages_max,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -122,7 +222,8 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
FROM messages
|
FROM messages
|
||||||
JOIN messages_refs ON messages.id = messages_refs.message
|
JOIN messages_refs ON messages.id = messages_refs.message
|
||||||
WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
|
WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
|
||||||
[id]);
|
[id]
|
||||||
|
);
|
||||||
let blobs_done = 0;
|
let blobs_done = 0;
|
||||||
for (let row of blobs) {
|
for (let row of blobs) {
|
||||||
this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
|
this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
|
||||||
@ -133,7 +234,10 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
console.log(`Failed to get ${row.id}: ${e.message}`);
|
console.log(`Failed to get ${row.id}: ${e.message}`);
|
||||||
}
|
}
|
||||||
if (blob) {
|
if (blob) {
|
||||||
zip.file(`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, new Uint8Array(blob));
|
zip.file(
|
||||||
|
`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`,
|
||||||
|
new Uint8Array(blob)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
blobs_done++;
|
blobs_done++;
|
||||||
}
|
}
|
||||||
@ -161,7 +265,7 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
file = await zip.loadAsync(file);
|
file = await zip.loadAsync(file);
|
||||||
let messages = [];
|
let messages = [];
|
||||||
let blobs = [];
|
let blobs = [];
|
||||||
file.forEach(function(path, entry) {
|
file.forEach(function (path, entry) {
|
||||||
if (!entry.dir) {
|
if (!entry.dir) {
|
||||||
if (path.startsWith('message/classic/')) {
|
if (path.startsWith('message/classic/')) {
|
||||||
messages.push(entry);
|
messages.push(entry);
|
||||||
@ -181,7 +285,11 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let message = JSON.parse(line);
|
let message = JSON.parse(line);
|
||||||
this.progress = {name: 'messages', value: progress++, max: total_messages};
|
this.progress = {
|
||||||
|
name: 'messages',
|
||||||
|
value: progress++,
|
||||||
|
max: total_messages,
|
||||||
|
};
|
||||||
if (await tfrpc.rpc.store_message(message.value)) {
|
if (await tfrpc.rpc.store_message(message.value)) {
|
||||||
success.messages++;
|
success.messages++;
|
||||||
}
|
}
|
||||||
@ -202,7 +310,13 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
let progress;
|
let progress;
|
||||||
if (this.progress) {
|
if (this.progress) {
|
||||||
if (this.progress.max) {
|
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>`;
|
progress = html`<div>
|
||||||
|
<label for="progress">${this.progress.name}</label
|
||||||
|
><progress
|
||||||
|
value=${this.progress.value}
|
||||||
|
max=${this.progress.max}
|
||||||
|
></progress>
|
||||||
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
progress = html`<div><span>${this.progress.name}</span></div>`;
|
progress = html`<div><span>${this.progress.name}</span></div>`;
|
||||||
}
|
}
|
||||||
@ -218,13 +332,17 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
<input type="text" id="search" @keypress=${this.keypress}></input>
|
<input type="text" id="search" @keypress=${this.keypress}></input>
|
||||||
<input type="button" value="Search Users" @click=${this.search}></input>
|
<input type="button" value="Search Users" @click=${this.search}></input>
|
||||||
<ul>
|
<ul>
|
||||||
${Object.entries(this.feeds).map(([id, name]) => html`
|
${Object.entries(this.feeds).map(
|
||||||
|
([id, name]) => html`
|
||||||
<li>
|
<li>
|
||||||
${this.progress ? undefined : html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
|
${this.progress
|
||||||
|
? undefined
|
||||||
|
: html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
|
||||||
${name}
|
${name}
|
||||||
<code style="color: #ccc">${id}</code>
|
<code style="color: #ccc">${id}</code>
|
||||||
</li>
|
</li>
|
||||||
`)}
|
`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🐌",
|
"emoji": "🐌",
|
||||||
"previous": "&dO6ckMIPVv9QvSc+0TOg0S59qe+rirPo2a6p9xSHj9M=.sha256"
|
"previous": "&0ZjcRFTyAmJMm3qhqKy5Ffrue23G1CAUfc1i2WYZrzQ=.sha256"
|
||||||
}
|
}
|
@ -76,7 +76,7 @@ tfrpc.register(function getHash(id, message) {
|
|||||||
tfrpc.register(function setHash(hash) {
|
tfrpc.register(function setHash(hash) {
|
||||||
return app.setHash(hash);
|
return app.setHash(hash);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('message', async function(id) {
|
core.register('onMessage', async function (id) {
|
||||||
await tfrpc.rpc.notifyNewMessage(id);
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
});
|
});
|
||||||
tfrpc.register(async function store_blob(blob) {
|
tfrpc.register(async function store_blob(blob) {
|
||||||
@ -100,16 +100,25 @@ tfrpc.register(async function try_decrypt(id, content) {
|
|||||||
tfrpc.register(async function encrypt(id, recipients, content) {
|
tfrpc.register(async function encrypt(id, recipients, content) {
|
||||||
return await ssb.privateMessageEncrypt(id, recipients, content);
|
return await ssb.privateMessageEncrypt(id, recipients, content);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('broadcasts', async function() {
|
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());
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
});
|
});
|
||||||
|
|
||||||
core.register('onConnectionsChanged', async function() {
|
core.register('onConnectionsChanged', async function () {
|
||||||
await tfrpc.rpc.set('connections', await ssb.connections());
|
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||||
});
|
});
|
||||||
|
core.register('setActiveIdentity', async function (id) {
|
||||||
|
await tfrpc.rpc.set('identity', id);
|
||||||
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (typeof(database) !== 'undefined') {
|
if (typeof database !== 'undefined') {
|
||||||
g_database = await database('ssb');
|
g_database = await database('ssb');
|
||||||
}
|
}
|
||||||
await app.setDocument(utf8Decode(await getFile('index.html')));
|
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
function textNode(text) {
|
function textNode(text) {
|
||||||
const node = new commonmark.Node("text", undefined);
|
const node = new commonmark.Node('text', undefined);
|
||||||
node.literal = text;
|
node.literal = text;
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkNode(text, link) {
|
function linkNode(text, link) {
|
||||||
const linkNode = new commonmark.Node("link", undefined);
|
const linkNode = new commonmark.Node('link', undefined);
|
||||||
|
if (link.startsWith('#')) {
|
||||||
linkNode.destination = `#q=${encodeURIComponent(link)}`;
|
linkNode.destination = `#q=${encodeURIComponent(link)}`;
|
||||||
|
} else {
|
||||||
|
linkNode.destination = link;
|
||||||
|
}
|
||||||
linkNode.appendChild(textNode(text));
|
linkNode.appendChild(textNode(text));
|
||||||
return linkNode;
|
return linkNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitMatches(text, regexp) {
|
function splitMatches(text, regexp) {
|
||||||
// Regexp must be sticky.
|
// Regexp must be sticky.
|
||||||
regexp = new RegExp(regexp, "gm");
|
regexp = new RegExp(regexp, 'gm');
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const result = [];
|
const result = [];
|
||||||
@ -39,13 +43,13 @@ function splitMatches(text, regexp) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const regex = new RegExp("(?<!\w)#[\\w-]+");
|
const regex = new RegExp('(?:https?://[^ ]+[^ .,])|(?:(?<!\\w)#[\\w-]+)|(?:@[A-Za-z0-9+/]+=.ed25519)|(?:[%&][A-Za-z0-9+/]+=.sha256)');
|
||||||
|
|
||||||
function split(textNodes) {
|
function split(textNodes) {
|
||||||
const text = textNodes.map(n => n.literal).join("");
|
const text = textNodes.map((n) => n.literal).join('');
|
||||||
const parts = splitMatches(text, regex);
|
const parts = splitMatches(text, regex);
|
||||||
|
|
||||||
return parts.map(part => {
|
return parts.map((part) => {
|
||||||
if (part[1]) {
|
if (part[1]) {
|
||||||
return linkNode(part[0], part[0]);
|
return linkNode(part[0], part[0]);
|
||||||
} else {
|
} else {
|
||||||
@ -61,17 +65,17 @@ export function transform(parsed) {
|
|||||||
let nodes = [];
|
let nodes = [];
|
||||||
while ((event = walker.next())) {
|
while ((event = walker.next())) {
|
||||||
const node = event.node;
|
const node = event.node;
|
||||||
if (event.entering && node.type === "text") {
|
if (event.entering && node.type === 'text') {
|
||||||
nodes.push(node);
|
nodes.push(node);
|
||||||
} else {
|
} else {
|
||||||
if (nodes.length > 0) {
|
if (nodes.length > 0) {
|
||||||
split(nodes)
|
split(nodes)
|
||||||
.reverse()
|
.reverse()
|
||||||
.forEach(newNode => {
|
.forEach((newNode) => {
|
||||||
nodes[0].insertAfter(newNode);
|
nodes[0].insertAfter(newNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
nodes.forEach(n => n.unlink());
|
nodes.forEach((n) => n.unlink());
|
||||||
nodes = [];
|
nodes = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,10 +84,10 @@ export function transform(parsed) {
|
|||||||
if (nodes.length > 0) {
|
if (nodes.length > 0) {
|
||||||
split(nodes)
|
split(nodes)
|
||||||
.reverse()
|
.reverse()
|
||||||
.forEach(newNode => {
|
.forEach((newNode) => {
|
||||||
nodes[0].insertAfter(newNode);
|
nodes[0].insertAfter(newNode);
|
||||||
});
|
});
|
||||||
nodes.forEach(n => n.unlink());
|
nodes.forEach((n) => n.unlink());
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
|
@ -1,91 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
2
apps/ssb/commonmark.min.js
vendored
2
apps/ssb/commonmark.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,28 +1,43 @@
|
|||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {html, render} from './lit-all.min.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
let g_emojis;
|
let g_emojis;
|
||||||
|
|
||||||
function get_emojis() {
|
function get_emojis() {
|
||||||
if (g_emojis) {
|
if (g_emojis) {
|
||||||
return Promise.resolve(g_emojis);
|
return Promise.resolve(g_emojis);
|
||||||
}
|
}
|
||||||
return fetch('emojis.json').then(function(result) {
|
return fetch('emojis.json').then(function (result) {
|
||||||
g_emojis = result.json();
|
g_emojis = result.json();
|
||||||
return g_emojis;
|
return g_emojis;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function picker(callback, anchor) {
|
async function get_recent(author) {
|
||||||
get_emojis().then(function(json) {
|
let recent = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
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');
|
let div = document.createElement('div');
|
||||||
div.id = 'emoji_picker';
|
div.id = 'emoji_picker';
|
||||||
div.style.color = '#000';
|
div.style.color = '#000';
|
||||||
div.style.background = '#fff';
|
div.style.background = '#fff';
|
||||||
div.style.border = '1px solid #000';
|
div.style.border = '1px solid #000';
|
||||||
div.style.display = 'block';
|
div.style.display = 'block';
|
||||||
div.style.position = 'absolute';
|
|
||||||
div.style.minWidth = 'min(16em, 90vw)';
|
|
||||||
div.style.width = 'min(16em, 90vw)';
|
|
||||||
div.style.maxWidth = 'min(16em, 90vw)';
|
|
||||||
div.style.maxHeight = '16em';
|
|
||||||
div.style.overflow = 'scroll';
|
div.style.overflow = 'scroll';
|
||||||
div.style.fontWeight = 'bold';
|
div.style.fontWeight = 'bold';
|
||||||
div.style.fontSize = 'xx-large';
|
div.style.fontSize = 'xx-large';
|
||||||
@ -36,18 +51,10 @@ export function picker(callback, anchor) {
|
|||||||
div.appendChild(input);
|
div.appendChild(input);
|
||||||
let list = document.createElement('div');
|
let list = document.createElement('div');
|
||||||
div.appendChild(list);
|
div.appendChild(list);
|
||||||
div.addEventListener('mousedown', function(event) {
|
div.addEventListener('mousedown', function (event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
function cleanup() {
|
|
||||||
console.log('emoji cleanup');
|
|
||||||
div.parentElement.removeChild(div);
|
|
||||||
window.removeEventListener('keydown', key_down);
|
|
||||||
console.log('removing click');
|
|
||||||
document.body.removeEventListener('mousedown', cleanup);
|
|
||||||
}
|
|
||||||
|
|
||||||
function key_down(event) {
|
function key_down(event) {
|
||||||
if (event.key == 'Escape') {
|
if (event.key == 'Escape') {
|
||||||
cleanup();
|
cleanup();
|
||||||
@ -66,15 +73,51 @@ export function picker(callback, anchor) {
|
|||||||
}
|
}
|
||||||
let search = input.value.toLowerCase();
|
let search = input.value.toLowerCase();
|
||||||
let any_at_all = false;
|
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)) {
|
for (let row of Object.entries(json)) {
|
||||||
let header = document.createElement('div');
|
let header = document.createElement('div');
|
||||||
header.appendChild(document.createTextNode(row[0]));
|
header.appendChild(document.createTextNode(row[0]));
|
||||||
list.appendChild(header);
|
list.appendChild(header);
|
||||||
let any = false;
|
let any = false;
|
||||||
for (let entry of Object.entries(row[1])) {
|
for (let entry of Object.entries(row[1])) {
|
||||||
if (search &&
|
if (
|
||||||
|
search &&
|
||||||
search.length &&
|
search.length &&
|
||||||
entry[0].toLowerCase().indexOf(search) == -1) {
|
entry[0].toLowerCase().indexOf(search) == -1
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let emoji = document.createElement('span');
|
let emoji = document.createElement('span');
|
||||||
@ -99,14 +142,23 @@ export function picker(callback, anchor) {
|
|||||||
}
|
}
|
||||||
refresh();
|
refresh();
|
||||||
input.oninput = refresh;
|
input.oninput = refresh;
|
||||||
document.body.appendChild(div);
|
let modal = html`
|
||||||
div.style.position = 'fixed';
|
<style>
|
||||||
div.style.top = '50%';
|
${styles}
|
||||||
div.style.left = '50%';
|
</style>
|
||||||
div.style.transform = 'translate(-50%, -50%)';
|
<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();
|
input.focus();
|
||||||
console.log('adding click');
|
|
||||||
document.body.addEventListener('mousedown', cleanup);
|
document.body.addEventListener('mousedown', cleanup);
|
||||||
window.addEventListener('keydown', key_down);
|
window.addEventListener('keydown', key_down);
|
||||||
});
|
|
||||||
}
|
}
|
@ -1,8 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="color: #fff">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends</title>
|
<title>Tilde Friends</title>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
<link rel="stylesheet" href="tribute.css" />
|
<link rel="stylesheet" href="tribute.css" />
|
||||||
<style>
|
<style>
|
||||||
.tribute-container {
|
.tribute-container {
|
||||||
@ -10,12 +10,14 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body style="margin: 0; padding: 0">
|
||||||
<tf-app/>
|
<tf-app></tf-app>
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<tf-reactions-modal id="reactions_modal"></tf-reactions-modal>
|
||||||
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="filesaver.min.js"></script>
|
<script src="filesaver.min.js"></script>
|
||||||
<script src="commonmark.min.js"></script>
|
<script src="commonmark.min.js"></script>
|
||||||
<script src="commonmark-linkify.js" type="module"></script>
|
|
||||||
<script src="commonmark-hashtag.js" type="module"></script>
|
<script src="commonmark-hashtag.js" type="module"></script>
|
||||||
<script src="script.js" type="module"></script>
|
<script src="script.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
42
apps/ssb/lit-all.min.js
vendored
42
apps/ssb/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,13 +1,13 @@
|
|||||||
import {LitElement, html} from './lit-all.min.js';
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
import * as tf_id_picker from './tf-id-picker.js';
|
|
||||||
import * as tf_app from './tf-app.js';
|
import * as tf_app from './tf-app.js';
|
||||||
import * as tf_message from './tf-message.js';
|
import * as tf_message from './tf-message.js';
|
||||||
import * as tf_user from './tf-user.js';
|
import * as tf_user from './tf-user.js';
|
||||||
import * as tf_compose from './tf-compose.js';
|
import * as tf_compose from './tf-compose.js';
|
||||||
import * as tf_news from './tf-news.js';
|
import * as tf_news from './tf-news.js';
|
||||||
import * as tf_profile from './tf-profile.js';
|
import * as tf_profile from './tf-profile.js';
|
||||||
|
import * as tf_reactions_modal from './tf-reactions-modal.js';
|
||||||
import * as tf_tab_mentions from './tf-tab-mentions.js';
|
import * as tf_tab_mentions from './tf-tab-mentions.js';
|
||||||
import * as tf_tab_news from './tf-tab-news.js';
|
import * as tf_tab_news from './tf-tab-news.js';
|
||||||
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
|
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
|
||||||
|
@ -34,9 +34,13 @@ class TfElement extends LitElement {
|
|||||||
this.users = {};
|
this.users = {};
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.tags = [];
|
this.tags = [];
|
||||||
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || []; });
|
tfrpc.rpc.getBroadcasts().then((b) => {
|
||||||
tfrpc.rpc.getConnections().then(c => { self.connections = c || []; });
|
self.broadcasts = b || [];
|
||||||
tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
|
});
|
||||||
|
tfrpc.rpc.getConnections().then((c) => {
|
||||||
|
self.connections = c || [];
|
||||||
|
});
|
||||||
|
tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
|
||||||
tfrpc.register(function hashChanged(hash) {
|
tfrpc.register(function hashChanged(hash) {
|
||||||
self.set_hash(hash);
|
self.set_hash(hash);
|
||||||
});
|
});
|
||||||
@ -48,20 +52,22 @@ class TfElement extends LitElement {
|
|||||||
self.broadcasts = value;
|
self.broadcasts = value;
|
||||||
} else if (name === 'connections') {
|
} else if (name === 'connections') {
|
||||||
self.connections = value;
|
self.connections = value;
|
||||||
|
} else if (name === 'identity') {
|
||||||
|
self.whoami = value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.initial_load();
|
this.initial_load();
|
||||||
}
|
}
|
||||||
|
|
||||||
async initial_load() {
|
async initial_load() {
|
||||||
let whoami = await tfrpc.rpc.localStorageGet('whoami');
|
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
let ids = (await tfrpc.rpc.getIdentities()) || [];
|
let ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
|
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
|
||||||
this.ids = ids;
|
this.ids = ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_hash(hash) {
|
set_hash(hash) {
|
||||||
this.hash = hash || '#';
|
this.hash = decodeURIComponent(hash || '#');
|
||||||
if (this.hash.startsWith('#q=')) {
|
if (this.hash.startsWith('#q=')) {
|
||||||
this.tab = 'search';
|
this.tab = 'search';
|
||||||
} else if (this.hash === '#connections') {
|
} else if (this.hash === '#connections') {
|
||||||
@ -86,9 +92,14 @@ class TfElement extends LitElement {
|
|||||||
last_row_id: 0,
|
last_row_id: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let max_row_id = (await tfrpc.rpc.query(`
|
let max_row_id = (
|
||||||
|
await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
`, []))[0].max_row_id;
|
`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
)[0].max_row_id;
|
||||||
for (let id of Object.keys(cache.about)) {
|
for (let id of Object.keys(cache.about)) {
|
||||||
if (ids.indexOf(id) == -1) {
|
if (ids.indexOf(id) == -1) {
|
||||||
delete cache.about[id];
|
delete cache.about[id];
|
||||||
@ -98,7 +109,7 @@ class TfElement extends LitElement {
|
|||||||
let abouts = await tfrpc.rpc.query(
|
let abouts = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
messages.*
|
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM
|
FROM
|
||||||
messages,
|
messages,
|
||||||
json_each(?1) AS following
|
json_each(?1) AS following
|
||||||
@ -109,7 +120,7 @@ class TfElement extends LitElement {
|
|||||||
json_extract(messages.content, '$.type') = 'about'
|
json_extract(messages.content, '$.type') = 'about'
|
||||||
UNION
|
UNION
|
||||||
SELECT
|
SELECT
|
||||||
messages.*
|
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM
|
FROM
|
||||||
messages,
|
messages,
|
||||||
json_each(?2) AS following
|
json_each(?2) AS following
|
||||||
@ -120,17 +131,21 @@ class TfElement extends LitElement {
|
|||||||
ORDER BY messages.author, messages.sequence
|
ORDER BY messages.author, messages.sequence
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
JSON.stringify(ids.filter(id => cache.about[id])),
|
JSON.stringify(ids.filter((id) => cache.about[id])),
|
||||||
JSON.stringify(ids.filter(id => !cache.about[id])),
|
JSON.stringify(ids.filter((id) => !cache.about[id])),
|
||||||
cache.last_row_id,
|
cache.last_row_id,
|
||||||
max_row_id,
|
max_row_id,
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
for (let about of abouts) {
|
for (let about of abouts) {
|
||||||
let content = JSON.parse(about.content);
|
let content = JSON.parse(about.content);
|
||||||
if (content.about === about.author) {
|
if (content.about === about.author) {
|
||||||
delete content.type;
|
delete content.type;
|
||||||
delete content.about;
|
delete content.about;
|
||||||
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
|
cache.about[about.author] = Object.assign(
|
||||||
|
cache.about[about.author] || {},
|
||||||
|
content
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache.last_row_id = max_row_id;
|
cache.last_row_id = max_row_id;
|
||||||
@ -145,15 +160,13 @@ class TfElement extends LitElement {
|
|||||||
async fetch_new_message(id) {
|
async fetch_new_message(id) {
|
||||||
let messages = await tfrpc.rpc.query(
|
let messages = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT messages.*
|
SELECT messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.id = ?
|
WHERE messages.id = ?
|
||||||
`,
|
`,
|
||||||
[
|
[JSON.stringify(this.following), id]
|
||||||
JSON.stringify(this.following),
|
);
|
||||||
id,
|
|
||||||
]);
|
|
||||||
if (messages && messages.length) {
|
if (messages && messages.length) {
|
||||||
this.unread = [...this.unread, ...messages];
|
this.unread = [...this.unread, ...messages];
|
||||||
this.unread = this.unread.slice(this.unread.length - 1024);
|
this.unread = this.unread.slice(this.unread.length - 1024);
|
||||||
@ -173,7 +186,7 @@ class TfElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create_identity() {
|
async create_identity() {
|
||||||
if (confirm("Are you sure you want to create a new identity?")) {
|
if (confirm('Are you sure you want to create a new identity?')) {
|
||||||
await tfrpc.rpc.createIdentity();
|
await tfrpc.rpc.createIdentity();
|
||||||
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
if (this.ids && !this.whoami) {
|
if (this.ids && !this.whoami) {
|
||||||
@ -182,18 +195,12 @@ class TfElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_id_picker() {
|
|
||||||
return html`
|
|
||||||
<tf-id-picker id="picker" selected=${this.whoami} .ids=${this.ids} .users=${this.users} @change=${this._handle_whoami_changed}></tf-id-picker>
|
|
||||||
<button @click=${this.create_identity} id="create_identity">Create Identity</button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async load_recent_tags() {
|
async load_recent_tags() {
|
||||||
let start = new Date();
|
let start = new Date();
|
||||||
this.tags = await tfrpc.rpc.query(`
|
this.tags = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
WITH
|
WITH
|
||||||
recent AS (SELECT id, content FROM messages
|
recent AS (SELECT id, json(content) AS content FROM messages
|
||||||
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
|
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
|
||||||
ORDER BY timestamp DESC LIMIT 1024),
|
ORDER BY timestamp DESC LIMIT 1024),
|
||||||
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
|
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
|
||||||
@ -205,7 +212,9 @@ class TfElement extends LitElement {
|
|||||||
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
|
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)
|
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
|
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
|
||||||
`, [new Date() - 7 * 24 * 60 * 60 * 1000]);
|
`,
|
||||||
|
[new Date() - 7 * 24 * 60 * 60 * 1000]
|
||||||
|
);
|
||||||
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
|
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,7 +234,15 @@ class TfElement extends LitElement {
|
|||||||
by_count.push({count: v.of, id: id});
|
by_count.push({count: v.of, id: id});
|
||||||
}
|
}
|
||||||
console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
|
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);
|
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.following = Object.keys(following);
|
||||||
this.users = users;
|
this.users = users;
|
||||||
await tags;
|
await tags;
|
||||||
@ -239,23 +256,54 @@ class TfElement extends LitElement {
|
|||||||
let users = this.users;
|
let users = this.users;
|
||||||
if (this.tab === 'news') {
|
if (this.tab === 'news') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-news id="tf-tab-news" .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
|
<tf-tab-news
|
||||||
|
id="tf-tab-news"
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
hash=${this.hash}
|
||||||
|
.unread=${this.unread}
|
||||||
|
@refresh=${() => (this.unread = [])}
|
||||||
|
?loading=${this.loading}
|
||||||
|
></tf-tab-news>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'connections') {
|
} else if (this.tab === 'connections') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
|
<tf-tab-connections
|
||||||
|
.users=${this.users}
|
||||||
|
.connections=${this.connections}
|
||||||
|
.broadcasts=${this.broadcasts}
|
||||||
|
></tf-tab-connections>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'mentions') {
|
} else if (this.tab === 'mentions') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-mentions .following=${this.following} whoami=${this.whoami} .users=${this.users}}></tf-tab-mentions>
|
<tf-tab-mentions
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users="${this.users}}"
|
||||||
|
></tf-tab-mentions>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'search') {
|
} else if (this.tab === 'search') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
|
<tf-tab-search
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
query=${this.hash?.startsWith('#q=')
|
||||||
|
? decodeURIComponent(this.hash.substring(3))
|
||||||
|
: null}
|
||||||
|
></tf-tab-search>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'query') {
|
} else if (this.tab === 'query') {
|
||||||
return html`
|
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>
|
<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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -278,31 +326,62 @@ class TfElement extends LitElement {
|
|||||||
|
|
||||||
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.load().finally(function() {
|
this.load().finally(function () {
|
||||||
self.loading = false;
|
self.loading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const k_tabs = {
|
||||||
|
'📰': 'news',
|
||||||
|
'📡': 'connections',
|
||||||
|
'@': 'mentions',
|
||||||
|
'🔍': 'search',
|
||||||
|
'👩💻': 'query',
|
||||||
|
};
|
||||||
|
|
||||||
let tabs = html`
|
let tabs = html`
|
||||||
<div>
|
<div class="w3-bar w3-theme-l1">
|
||||||
<input type="button" class="tab" value="News" ?disabled=${self.tab == 'news'} @click=${() => self.set_tab('news')}></input>
|
${Object.entries(k_tabs).map(
|
||||||
<input type="button" class="tab" value="Connections" ?disabled=${self.tab == 'connections'} @click=${() => self.set_tab('connections')}></input>
|
([k, v]) => html`
|
||||||
<input type="button" class="tab" value="Mentions" ?disabled=${self.tab == 'mentions'} @click=${() => self.set_tab('mentions')}></input>
|
<button
|
||||||
<input type="button" class="tab" value="Search" ?disabled=${self.tab == 'search'} @click=${() => self.set_tab('search')}></input>
|
title=${v}
|
||||||
<input type="button" class="tab" value="Query" ?disabled=${self.tab == 'query'} @click=${() => self.set_tab('query')}></input>
|
class="w3-bar-item w3-padding w3-hover-theme tab ${self.tab == v
|
||||||
|
? 'w3-theme-l2'
|
||||||
|
: 'w3-theme-l1'}"
|
||||||
|
@click=${() => self.set_tab(v)}
|
||||||
|
>
|
||||||
|
${k}
|
||||||
|
<span class=${self.tab == v ? '' : 'w3-hide-small'}
|
||||||
|
>${v.charAt(0).toUpperCase() + v.substring(1)}</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
let contents =
|
let contents = !this.loaded
|
||||||
!this.loaded ?
|
? this.loading
|
||||||
this.loading ?
|
? html`<div
|
||||||
html`<div>Loading...</div>` :
|
class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge"
|
||||||
html`<div>Select or create an identity.</div>` :
|
>
|
||||||
this.render_tab();
|
Loading...
|
||||||
|
</div>
|
||||||
|
${this.render_tab()}`
|
||||||
|
: html`<div>Select or create an identity.</div>`
|
||||||
|
: this.render_tab();
|
||||||
return html`
|
return html`
|
||||||
${this.render_id_picker()}
|
<div
|
||||||
|
style="width: 100vw; min-height: 100vh; height: 100%"
|
||||||
|
class="w3-theme-dark"
|
||||||
|
>
|
||||||
${tabs}
|
${tabs}
|
||||||
${this.tags.map(x => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`)}
|
<div style="padding: 8px">
|
||||||
|
${this.tags.map(
|
||||||
|
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
|
||||||
|
)}
|
||||||
${contents}
|
${contents}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
import {LitElement, html, unsafeHTML, live} from './lit-all.min.js';
|
||||||
import * as tfutils from './tf-utils.js';
|
import * as tfutils from './tf-utils.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles} from './tf-styles.js';
|
||||||
@ -13,6 +13,7 @@ class TfComposeElement extends LitElement {
|
|||||||
branch: {type: String},
|
branch: {type: String},
|
||||||
apps: {type: Object},
|
apps: {type: Object},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
|
author: {type: String},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ class TfComposeElement extends LitElement {
|
|||||||
this.branch = undefined;
|
this.branch = undefined;
|
||||||
this.apps = undefined;
|
this.apps = undefined;
|
||||||
this.drafts = {};
|
this.drafts = {};
|
||||||
|
this.author = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
process_text(text) {
|
process_text(text) {
|
||||||
@ -58,11 +60,13 @@ class TfComposeElement extends LitElement {
|
|||||||
link: link,
|
link: link,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
|
draft.mentions[link].name = name.startsWith('@')
|
||||||
|
? name.substring(1)
|
||||||
|
: name;
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
if (updated) {
|
if (updated) {
|
||||||
this.requestUpdate();
|
setTimeout(() => this.notify(draft), 0);
|
||||||
}
|
}
|
||||||
return tfutils.markdown(text);
|
return tfutils.markdown(text);
|
||||||
}
|
}
|
||||||
@ -70,36 +74,31 @@ class TfComposeElement extends LitElement {
|
|||||||
input(event) {
|
input(event) {
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
let preview = this.renderRoot.getElementById('preview');
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
preview.innerHTML = this.process_text(edit.value);
|
preview.innerHTML = this.process_text(edit.innerText);
|
||||||
let content_warning = this.renderRoot.getElementById('content_warning');
|
let content_warning = this.renderRoot.getElementById('content_warning');
|
||||||
let content_warning_preview = this.renderRoot.getElementById('content_warning_preview');
|
let draft = this.get_draft();
|
||||||
if (content_warning && content_warning_preview) {
|
draft.text = edit.innerText;
|
||||||
content_warning_preview.innerText = content_warning.value;
|
draft.content_warning = content_warning?.value;
|
||||||
}
|
setTimeout(() => this.notify(draft), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(draft) {
|
notify(draft) {
|
||||||
this.dispatchEvent(new CustomEvent('tf-draft', {
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-draft', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
detail: {
|
detail: {
|
||||||
id: this.branch,
|
id: this.branch,
|
||||||
draft: draft
|
draft: draft,
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
}
|
);
|
||||||
|
|
||||||
change() {
|
|
||||||
let draft = this.get_draft();
|
|
||||||
draft.text = this.renderRoot.getElementById('edit')?.value;
|
|
||||||
draft.content_warning = this.renderRoot.getElementById('content_warning')?.value;
|
|
||||||
this.notify(draft);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
convert_to_format(buffer, type, mime_type) {
|
convert_to_format(buffer, type, mime_type) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
let img = new Image();
|
let img = new Image();
|
||||||
img.onload = function() {
|
img.onload = function () {
|
||||||
let canvas = document.createElement('canvas');
|
let canvas = document.createElement('canvas');
|
||||||
let width_scale = Math.min(img.width, 1024) / img.width;
|
let width_scale = Math.min(img.width, 1024) / img.width;
|
||||||
let height_scale = Math.min(img.height, 1024) / img.height;
|
let height_scale = Math.min(img.height, 1024) / img.height;
|
||||||
@ -109,13 +108,17 @@ class TfComposeElement extends LitElement {
|
|||||||
let context = canvas.getContext('2d');
|
let context = canvas.getContext('2d');
|
||||||
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
let data_url = canvas.toDataURL(mime_type);
|
let data_url = canvas.toDataURL(mime_type);
|
||||||
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
|
let result = atob(data_url.split(',')[1])
|
||||||
|
.split('')
|
||||||
|
.map((x) => x.charCodeAt(0));
|
||||||
resolve(result);
|
resolve(result);
|
||||||
};
|
};
|
||||||
img.onerror = function(event) {
|
img.onerror = function (event) {
|
||||||
reject(new Error('Failed to load image.'));
|
reject(new Error('Failed to load image.'));
|
||||||
};
|
};
|
||||||
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
|
let raw = Array.from(new Uint8Array(buffer))
|
||||||
|
.map((b) => String.fromCharCode(b))
|
||||||
|
.join('');
|
||||||
let original = `data:${type};base64,${btoa(raw)}`;
|
let original = `data:${type};base64,${btoa(raw)}`;
|
||||||
img.src = original;
|
img.src = original;
|
||||||
});
|
});
|
||||||
@ -131,7 +134,11 @@ class TfComposeElement extends LitElement {
|
|||||||
let best_buffer;
|
let best_buffer;
|
||||||
let best_type;
|
let best_type;
|
||||||
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
||||||
let test_buffer = await self.convert_to_format(buffer, file.type, format);
|
let test_buffer = await self.convert_to_format(
|
||||||
|
buffer,
|
||||||
|
file.type,
|
||||||
|
format
|
||||||
|
);
|
||||||
if (!best_buffer || test_buffer.length < best_buffer.length) {
|
if (!best_buffer || test_buffer.length < best_buffer.length) {
|
||||||
best_buffer = test_buffer;
|
best_buffer = test_buffer;
|
||||||
best_type = format;
|
best_type = format;
|
||||||
@ -154,10 +161,9 @@ class TfComposeElement extends LitElement {
|
|||||||
size: buffer.length ?? buffer.byteLength,
|
size: buffer.length ?? buffer.byteLength,
|
||||||
};
|
};
|
||||||
let edit = self.renderRoot.getElementById('edit');
|
let edit = self.renderRoot.getElementById('edit');
|
||||||
edit.value += `\n![${name}](${id})`;
|
edit.innerText += `\n![${name}](${id})`;
|
||||||
self.change();
|
|
||||||
self.input();
|
self.input();
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
alert(e?.message);
|
alert(e?.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -174,6 +180,13 @@ class TfComposeElement extends LitElement {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
document.execCommand(
|
||||||
|
'insertText',
|
||||||
|
false,
|
||||||
|
event.clipboardData.getData('text/plain')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
@ -182,7 +195,7 @@ class TfComposeElement extends LitElement {
|
|||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
let message = {
|
let message = {
|
||||||
type: 'post',
|
type: 'post',
|
||||||
text: edit.value,
|
text: edit.innerText,
|
||||||
};
|
};
|
||||||
if (this.root || this.branch) {
|
if (this.root || this.branch) {
|
||||||
message.root = this.root;
|
message.root = this.root;
|
||||||
@ -201,36 +214,30 @@ class TfComposeElement extends LitElement {
|
|||||||
to = [...to];
|
to = [...to];
|
||||||
message.recps = to;
|
message.recps = to;
|
||||||
console.log('message is now', message);
|
console.log('message is now', message);
|
||||||
message = await tfrpc.rpc.encrypt(this.whoami, to, JSON.stringify(message));
|
message = await tfrpc.rpc.encrypt(
|
||||||
|
this.whoami,
|
||||||
|
to,
|
||||||
|
JSON.stringify(message)
|
||||||
|
);
|
||||||
console.log('encrypted as', message);
|
console.log('encrypted as', message);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
edit.value = '';
|
|
||||||
self.change();
|
|
||||||
self.notify(undefined);
|
self.notify(undefined);
|
||||||
self.requestUpdate();
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message);
|
alert(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
discard() {
|
discard() {
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
|
||||||
edit.value = '';
|
|
||||||
this.change();
|
|
||||||
let preview = this.renderRoot.getElementById('preview');
|
|
||||||
preview.innerHTML = '';
|
|
||||||
this.notify(undefined);
|
this.notify(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
attach() {
|
attach() {
|
||||||
let self = this;
|
let self = this;
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
|
||||||
let input = document.createElement('input');
|
let input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.onchange = function(event) {
|
input.onchange = function (event) {
|
||||||
let file = event.target.files[0];
|
let file = event.target.files[0];
|
||||||
self.add_file(file);
|
self.add_file(file);
|
||||||
};
|
};
|
||||||
@ -241,12 +248,15 @@ class TfComposeElement extends LitElement {
|
|||||||
this.last_autocomplete = text;
|
this.last_autocomplete = text;
|
||||||
let results = [];
|
let results = [];
|
||||||
try {
|
try {
|
||||||
let rows = await tfrpc.rpc.query(`
|
let rows = await tfrpc.rpc.query(
|
||||||
SELECT messages.content FROM messages_fts(?)
|
`
|
||||||
|
SELECT json(messages.content) AS content FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
WHERE messages.content LIKE ?
|
WHERE json(messages.content) LIKE ?
|
||||||
ORDER BY timestamp DESC LIMIT 10
|
ORDER BY timestamp DESC LIMIT 10
|
||||||
`, ['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]);
|
`,
|
||||||
|
['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]
|
||||||
|
);
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
|
for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
|
||||||
if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
|
if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
|
||||||
@ -262,19 +272,39 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
|
let values = Object.entries(this.users).map((x) => ({
|
||||||
|
key: x[1].name ?? x[0],
|
||||||
|
value: x[0],
|
||||||
|
}));
|
||||||
|
if (this.author) {
|
||||||
|
values = [].concat(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: this.users[this.author]?.name,
|
||||||
|
value: this.author,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
values
|
||||||
|
);
|
||||||
|
}
|
||||||
let tribute = new Tribute({
|
let tribute = new Tribute({
|
||||||
|
iframe: this.shadowRoot,
|
||||||
collection: [
|
collection: [
|
||||||
{
|
{
|
||||||
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
values: values,
|
||||||
selectTemplate: function(item) {
|
selectTemplate: function (item) {
|
||||||
return `[@${item.original.key}](${item.original.value})`;
|
return item
|
||||||
|
? `[@${item.original.key}](${item.original.value})`
|
||||||
|
: undefined;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
trigger: '&',
|
trigger: '&',
|
||||||
values: this.autocomplete,
|
values: this.autocomplete,
|
||||||
selectTemplate: function(item) {
|
selectTemplate: function (item) {
|
||||||
return `![${item.original.key}](${item.original.value})`;
|
return item
|
||||||
|
? `![${item.original.key}](${item.original.value})`
|
||||||
|
: undefined;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -285,16 +315,20 @@ class TfComposeElement extends LitElement {
|
|||||||
updated() {
|
updated() {
|
||||||
super.updated();
|
super.updated();
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
if (this.last_updated_text !== edit.value) {
|
if (this.last_updated_text !== edit.innerText) {
|
||||||
let preview = this.renderRoot.getElementById('preview');
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
preview.innerHTML = this.process_text(edit.value);
|
preview.innerHTML = this.process_text(edit.innerText);
|
||||||
this.last_updated_text = edit.value;
|
this.last_updated_text = edit.innerText;
|
||||||
}
|
}
|
||||||
let encrypt = this.renderRoot.getElementById('encrypt_to');
|
let encrypt = this.renderRoot.getElementById('encrypt_to');
|
||||||
if (encrypt) {
|
if (encrypt) {
|
||||||
let tribute = new Tribute({
|
let tribute = new Tribute({
|
||||||
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
iframe: this.shadowRoot,
|
||||||
selectTemplate: function(item) {
|
values: Object.entries(this.users).map((x) => ({
|
||||||
|
key: x[1].name,
|
||||||
|
value: x[0],
|
||||||
|
})),
|
||||||
|
selectTemplate: function (item) {
|
||||||
return item.original.value;
|
return item.original.value;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -305,23 +339,32 @@ class TfComposeElement extends LitElement {
|
|||||||
remove_mention(id) {
|
remove_mention(id) {
|
||||||
let draft = this.get_draft();
|
let draft = this.get_draft();
|
||||||
delete draft.mentions[id];
|
delete draft.mentions[id];
|
||||||
this.notify(draft);
|
setTimeout(() => this.notify(), 0);
|
||||||
this.requestUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render_mention(mention) {
|
render_mention(mention) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html` <div style="display: flex; flex-direction: row">
|
||||||
<div style="display: flex; flex-direction: row">
|
|
||||||
<div style="align-self: center; margin: 0.5em">
|
<div style="align-self: center; margin: 0.5em">
|
||||||
<input type="button" value="🚮" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}></input>
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
title="Remove ${mention.name} mention"
|
||||||
|
@click=${() => self.remove_mention(mention.link)}
|
||||||
|
>
|
||||||
|
🚮
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column">
|
<div style="display: flex; flex-direction: column">
|
||||||
<h3>${mention.name}</h3>
|
<h3>${mention.name}</h3>
|
||||||
<div style="padding-left: 1em">
|
<div style="padding-left: 1em">
|
||||||
${Object.entries(mention)
|
${Object.entries(mention)
|
||||||
.filter(x => x[0] != 'name')
|
.filter((x) => x[0] != 'name')
|
||||||
.map(x => html`<div><span style="font-weight: bold">${x[0]}</span>: ${x[1]}</div>`)}
|
.map(
|
||||||
|
(x) =>
|
||||||
|
html`<div>
|
||||||
|
<span style="font-weight: bold">${x[0]}</span>: ${x[1]}
|
||||||
|
</div>`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -357,12 +400,21 @@ class TfComposeElement extends LitElement {
|
|||||||
|
|
||||||
if (this.apps) {
|
if (this.apps) {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div class="w3-card-4 w3-margin w3-padding">
|
||||||
<select id="select">
|
<select id="select" class="w3-select w3-theme-d1">
|
||||||
${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
|
${Object.keys(self.apps).map(
|
||||||
|
(app) => html`<option value=${app}>${app}</option>`
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
<input type="button" value="Attach" @click=${attach_selected_app}></input>
|
<button class="w3-button w3-theme-d1" @click=${attach_selected_app}>
|
||||||
<input type="button" value="Cancel" @click=${() => this.apps = null}></input>
|
Attach
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (this.apps = null)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -374,9 +426,16 @@ class TfComposeElement extends LitElement {
|
|||||||
self.apps = await tfrpc.rpc.apps();
|
self.apps = await tfrpc.rpc.apps();
|
||||||
}
|
}
|
||||||
if (!this.apps) {
|
if (!this.apps) {
|
||||||
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`;
|
return html`<button class="w3-button w3-theme-d1" @click=${attach_app}>
|
||||||
|
Attach App
|
||||||
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`;
|
return html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (this.apps = null)}
|
||||||
|
>
|
||||||
|
Discard App
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,15 +451,17 @@ class TfComposeElement extends LitElement {
|
|||||||
let draft = this.get_draft();
|
let draft = this.get_draft();
|
||||||
if (draft.content_warning !== undefined) {
|
if (draft.content_warning !== undefined) {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div class="w3-container w3-padding">
|
||||||
<input type="checkbox" id="cw" @change=${() => self.set_content_warning(undefined)} checked></input>
|
<p>
|
||||||
|
<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>
|
<label for="cw">CW</label>
|
||||||
<input type="text" id="content_warning" @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
|
</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>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<input type="checkbox" id="cw" @change=${() => self.set_content_warning('')}></input>
|
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input>
|
||||||
<label for="cw">CW</label>
|
<label for="cw">CW</label>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -430,14 +491,16 @@ class TfComposeElement extends LitElement {
|
|||||||
<div style="display: flex; flex-direction: row; width: 100%">
|
<div style="display: flex; flex-direction: row; width: 100%">
|
||||||
<label for="encrypt_to">🔐 To:</label>
|
<label for="encrypt_to">🔐 To:</label>
|
||||||
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
|
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
|
||||||
<input type="button" value="🚮" @click=${() => this.set_encrypt(undefined)}></input>
|
<button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
${draft.encrypt_to.map(x => html`
|
${draft.encrypt_to.map(
|
||||||
|
(x) => html`
|
||||||
<li>
|
<li>
|
||||||
<tf-user id=${x} .users=${this.users}></tf-user>
|
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||||
<input type="button" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter(id => id != x))}></input>
|
<input type="button" class="w3-button w3-theme-d1" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
|
||||||
</li>`)}
|
</li>`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -453,29 +516,59 @@ class TfComposeElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
let draft = self.get_draft();
|
let draft = self.get_draft();
|
||||||
let content_warning =
|
let content_warning =
|
||||||
draft.content_warning !== undefined ?
|
draft.content_warning !== undefined
|
||||||
html`<div id="content_warning_preview" class="content_warning">${draft.content_warning}</div>` :
|
? html`<div class="w3-panel w3-round-xlarge w3-theme-d2">
|
||||||
undefined;
|
<p id="content_warning_preview">${draft.content_warning}</p>
|
||||||
let encrypt = draft.encrypt_to !== undefined ?
|
</div>`
|
||||||
undefined :
|
: undefined;
|
||||||
html`<input type="button" value="🔐" @click=${() => this.set_encrypt([])}></input>`;
|
let encrypt =
|
||||||
|
draft.encrypt_to !== undefined
|
||||||
|
? undefined
|
||||||
|
: html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => this.set_encrypt([])}
|
||||||
|
>
|
||||||
|
🔐
|
||||||
|
</button>`;
|
||||||
let result = html`
|
let result = html`
|
||||||
|
<div
|
||||||
|
class="w3-card-4 w3-theme-d4 w3-padding-small"
|
||||||
|
style="box-sizing: border-box"
|
||||||
|
>
|
||||||
${this.render_encrypt()}
|
${this.render_encrypt()}
|
||||||
<div style="display: flex; flex-direction: row; width: 100%">
|
<div class="w3-container w3-padding-small">
|
||||||
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${draft.text}</textarea>
|
<div class="w3-half">
|
||||||
<div style="flex: 1 0 50%">
|
<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}
|
${content_warning}
|
||||||
<div id="preview"></div>
|
<div id="preview"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
|
${Object.values(draft.mentions || {}).map((x) =>
|
||||||
${this.render_content_warning()}
|
self.render_mention(x)
|
||||||
${this.render_attach_app()}
|
)}
|
||||||
<input type="button" id="submit" value="Submit" @click=${this.submit}></input>
|
${this.render_attach_app()} ${this.render_content_warning()}
|
||||||
<input type="button" value="Attach" @click=${this.attach}></input>
|
<button class="w3-button w3-theme-d1" id="submit" @click=${this.submit}>
|
||||||
${this.render_attach_app_button()}
|
Submit
|
||||||
${encrypt}
|
</button>
|
||||||
<input type="button" value="Discard" @click=${this.discard}></input>
|
<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;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
import {LitElement, html} from './lit-all.min.js';
|
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
|
||||||
|
|
||||||
/*
|
|
||||||
** Provide a list of IDs, and this lets the user pick one.
|
|
||||||
*/
|
|
||||||
class TfIdentityPickerElement extends LitElement {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
ids: {type: Array},
|
|
||||||
selected: {type: String},
|
|
||||||
users: {type: Object},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.ids = [];
|
|
||||||
this.users = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
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}>${this.users[id]?.name ? (this.users[id]?.name + ' - ') : undefined}<small>${id}</small></option>`)}
|
|
||||||
</select>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('tf-id-picker', TfIdentityPickerElement);
|
|
@ -1,4 +1,4 @@
|
|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
import {LitElement, html, render, unsafeHTML} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import * as tfutils from './tf-utils.js';
|
import * as tfutils from './tf-utils.js';
|
||||||
import * as emojis from './emojis.js';
|
import * as emojis from './emojis.js';
|
||||||
@ -31,14 +31,33 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show_reply() {
|
show_reply() {
|
||||||
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: {
|
let event = new CustomEvent('tf-draft', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
id: this.message?.id,
|
||||||
|
draft: {
|
||||||
encrypt_to: this.message?.decrypted?.recps,
|
encrypt_to: this.message?.decrypted?.recps,
|
||||||
}}});
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
discard_reply() {
|
discard_reply() {
|
||||||
this.dispatchEvent(new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.id, draft: undefined}}));
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-draft', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {id: this.id, draft: undefined},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
show_reactions() {
|
||||||
|
let modal = document.getElementById('reactions_modal');
|
||||||
|
modal.users = this.users;
|
||||||
|
modal.votes = this.message?.votes || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
render_votes() {
|
render_votes() {
|
||||||
@ -53,12 +72,26 @@ class TfMessageElement extends LitElement {
|
|||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return html`<div>${(this.message.votes || []).map(
|
if (this.message?.votes?.length) {
|
||||||
vote => html`
|
return html` <div class="w3-container">
|
||||||
<span title="${this.users[vote.author]?.name ?? vote.author} ${new Date(vote.timestamp)}">
|
<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)}
|
${normalize_expression(vote.content.vote.expression)}
|
||||||
</span>
|
</span>
|
||||||
`)}</div>`;
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_raw() {
|
render_raw() {
|
||||||
@ -72,30 +105,40 @@ class TfMessageElement extends LitElement {
|
|||||||
content: this.message?.content,
|
content: this.message?.content,
|
||||||
signature: this.message?.signature,
|
signature: this.message?.signature,
|
||||||
};
|
};
|
||||||
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`;
|
return html`<div style="white-space: pre-wrap">
|
||||||
|
${JSON.stringify(raw, null, 2)}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
vote(emoji) {
|
vote(emoji) {
|
||||||
let reaction = emoji;
|
let reaction = emoji;
|
||||||
let message = this.message.id;
|
let message = this.message.id;
|
||||||
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
|
if (
|
||||||
tfrpc.rpc.appendMessage(
|
confirm(
|
||||||
this.whoami,
|
'Are you sure you want to react with ' +
|
||||||
{
|
reaction +
|
||||||
|
' to ' +
|
||||||
|
message +
|
||||||
|
'?'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
tfrpc.rpc
|
||||||
|
.appendMessage(this.whoami, {
|
||||||
type: 'vote',
|
type: 'vote',
|
||||||
vote: {
|
vote: {
|
||||||
link: message,
|
link: message,
|
||||||
value: 1,
|
value: 1,
|
||||||
expression: reaction,
|
expression: reaction,
|
||||||
},
|
},
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
react(event) {
|
react(event) {
|
||||||
emojis.picker(x => this.vote(x));
|
emojis.picker((x) => this.vote(x), null, this.whoami);
|
||||||
}
|
}
|
||||||
|
|
||||||
show_image(link) {
|
show_image(link) {
|
||||||
@ -129,9 +172,12 @@ class TfMessageElement extends LitElement {
|
|||||||
body_click(event) {
|
body_click(event) {
|
||||||
if (event.srcElement.tagName == 'IMG') {
|
if (event.srcElement.tagName == 'IMG') {
|
||||||
this.show_image(event.srcElement.src);
|
this.show_image(event.srcElement.src);
|
||||||
} else if (event.srcElement.tagName == 'DIV' && event.srcElement.classList.contains('img_caption')) {
|
} else if (
|
||||||
|
event.srcElement.tagName == 'DIV' &&
|
||||||
|
event.srcElement.classList.contains('img_caption')
|
||||||
|
) {
|
||||||
let next = event.srcElement.nextSibling;
|
let next = event.srcElement.nextSibling;
|
||||||
if (next.style.display == 'block') {
|
if (next.style.display != 'none') {
|
||||||
next.style.display = 'none';
|
next.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
next.style.display = 'block';
|
next.style.display = 'block';
|
||||||
@ -140,50 +186,75 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_mention(mention) {
|
render_mention(mention) {
|
||||||
if (!mention?.link || typeof(mention.link) != 'string') {
|
if (!mention?.link || typeof mention.link != 'string') {
|
||||||
return html` <pre>${JSON.stringify(mention)}</pre>`;
|
return html` <pre>${JSON.stringify(mention)}</pre>`;
|
||||||
} else if (mention?.link?.startsWith('&') &&
|
} else if (
|
||||||
mention?.type?.startsWith('image/')) {
|
mention?.link?.startsWith('&') &&
|
||||||
|
mention?.type?.startsWith('image/')
|
||||||
|
) {
|
||||||
return html`
|
return html`
|
||||||
<img src=${'/' + mention.link + '/view'} style="max-width: 128px; max-height: 128px" title=${mention.name} @click=${() => this.show_image('/' + mention.link + '/view')}>
|
<img
|
||||||
|
src=${'/' + mention.link + '/view'}
|
||||||
|
style="max-width: 128px; max-height: 128px"
|
||||||
|
title=${mention.name}
|
||||||
|
@click=${() => this.show_image('/' + mention.link + '/view')}
|
||||||
|
/>
|
||||||
`;
|
`;
|
||||||
} else if (mention.link?.startsWith('&') &&
|
} else if (
|
||||||
mention.name?.startsWith('audio:')) {
|
mention.link?.startsWith('&') &&
|
||||||
|
mention.name?.startsWith('audio:')
|
||||||
|
) {
|
||||||
return html`
|
return html`
|
||||||
<audio controls style="height: 32px">
|
<audio controls style="height: 32px">
|
||||||
<source src=${'/' + mention.link + '/view'}></source>
|
<source src=${'/' + mention.link + '/view'}></source>
|
||||||
</audio>
|
</audio>
|
||||||
`;
|
`;
|
||||||
} else if (mention.link?.startsWith('&') &&
|
} else if (
|
||||||
mention.name?.startsWith('video:')) {
|
mention.link?.startsWith('&') &&
|
||||||
|
mention.name?.startsWith('video:')
|
||||||
|
) {
|
||||||
return html`
|
return html`
|
||||||
<video controls style="max-height: 240px; max-width: 128px">
|
<video controls style="max-height: 240px; max-width: 128px">
|
||||||
<source src=${'/' + mention.link + '/view'}></source>
|
<source src=${'/' + mention.link + '/view'}></source>
|
||||||
</video>
|
</video>
|
||||||
`;
|
`;
|
||||||
} else if (mention.link?.startsWith('&') &&
|
} else if (
|
||||||
mention?.type === 'application/tildefriends') {
|
mention.link?.startsWith('&') &&
|
||||||
|
mention?.type === 'application/tildefriends'
|
||||||
|
) {
|
||||||
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
|
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
|
||||||
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
|
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
|
||||||
return html` <a href=${'#' + encodeURIComponent(mention.link)}>${mention.name}</a>`;
|
return html` <a href=${'#' + encodeURIComponent(mention.link)}
|
||||||
|
>${mention.name}</a
|
||||||
|
>`;
|
||||||
} else if (mention.link?.startsWith('#')) {
|
} else if (mention.link?.startsWith('#')) {
|
||||||
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}>${mention.link}</a>`;
|
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}
|
||||||
} else if (Object.keys(mention).length == 2 && mention.link && mention.name) {
|
>${mention.link}</a
|
||||||
|
>`;
|
||||||
|
} else if (
|
||||||
|
Object.keys(mention).length == 2 &&
|
||||||
|
mention.link &&
|
||||||
|
mention.name
|
||||||
|
) {
|
||||||
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
|
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
|
||||||
} else {
|
} else {
|
||||||
return html` <pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>`;
|
return html` <pre style="white-space: pre-wrap">
|
||||||
|
${JSON.stringify(mention, null, 2)}</pre
|
||||||
|
>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_mentions() {
|
render_mentions() {
|
||||||
let mentions = this.message?.content?.mentions || [];
|
let mentions = this.message?.content?.mentions || [];
|
||||||
mentions = mentions.filter(x => this.message?.content?.text?.indexOf(x.link) === -1);
|
mentions = mentions.filter(
|
||||||
|
(x) => this.message?.content?.text?.indexOf(x.link) === -1
|
||||||
|
);
|
||||||
if (mentions.length) {
|
if (mentions.length) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<fieldset style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black">
|
<fieldset style="padding: 0.5em; border: 1px solid black">
|
||||||
<legend>Mentions</legend>
|
<legend>Mentions</legend>
|
||||||
${mentions.map(x => self.render_mention(x))}
|
${mentions.map((x) => self.render_mention(x))}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -194,28 +265,55 @@ class TfMessageElement extends LitElement {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let total = message.child_messages.length;
|
let total = message.child_messages.length;
|
||||||
for (let m of message.child_messages)
|
for (let m of message.child_messages) {
|
||||||
{
|
|
||||||
total += this.total_child_messages(m);
|
total += this.total_child_messages(m);
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_expanded(expanded, tag) {
|
set_expanded(expanded, tag) {
|
||||||
this.dispatchEvent(new CustomEvent('tf-expand', {bubbles: true, composed: true, detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded}}));
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-expand', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded},
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle_expanded(tag) {
|
toggle_expanded(tag) {
|
||||||
this.set_expanded(!this.expanded[(this.message.id || '') + (tag || '')], tag);
|
this.set_expanded(
|
||||||
|
!this.expanded[(this.message.id || '') + (tag || '')],
|
||||||
|
tag
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render_children() {
|
render_children() {
|
||||||
let self = this;
|
let self = this;
|
||||||
if (this.message.child_messages?.length) {
|
if (this.message.child_messages?.length) {
|
||||||
if (!this.expanded[this.message.id]) {
|
if (!this.expanded[this.message.id]) {
|
||||||
return html`<input type="button" value=${this.total_child_messages(this.message) + ' More'} @click=${() => self.set_expanded(true)}></input>`;
|
return html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => self.set_expanded(true)}
|
||||||
|
>
|
||||||
|
+ ${this.total_child_messages(this.message) + ' More'}
|
||||||
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
return html`<input type="button" value="Collapse" @click=${() => self.set_expanded(false)}></input>${(this.message.child_messages || []).map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`)}`;
|
return html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => self.set_expanded(false)}
|
||||||
|
>
|
||||||
|
Collapse</button
|
||||||
|
>${(this.message.child_messages || []).map(
|
||||||
|
(x) =>
|
||||||
|
html`<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-message>`
|
||||||
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -231,13 +329,12 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
if (Array.isArray(content.mentions)) {
|
if (Array.isArray(content.mentions)) {
|
||||||
for (let mention of content.mentions) {
|
for (let mention of content.mentions) {
|
||||||
if (typeof mention?.link === 'string' &&
|
if (typeof mention?.link === 'string' && mention.link.startsWith('#')) {
|
||||||
mention.link.startsWith('#')) {
|
|
||||||
channels.push(mention.link);
|
channels.push(mention.link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return channels.map(x => html`<tf-tag tag=${x}></tf-tag>`);
|
return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -245,59 +342,129 @@ class TfMessageElement extends LitElement {
|
|||||||
if (this.message?.decrypted?.type == 'post') {
|
if (this.message?.decrypted?.type == 'post') {
|
||||||
content = this.message.decrypted;
|
content = this.message.decrypted;
|
||||||
}
|
}
|
||||||
|
let class_background = this.message?.decrypted
|
||||||
|
? 'w3-pale-red'
|
||||||
|
: 'w3-theme-d4';
|
||||||
let self = this;
|
let self = this;
|
||||||
let raw_button;
|
let raw_button;
|
||||||
switch (this.format) {
|
switch (this.format) {
|
||||||
case 'raw':
|
case 'raw':
|
||||||
if (content?.type == 'post' || content?.type == 'blog') {
|
if (content?.type == 'post' || content?.type == 'blog') {
|
||||||
raw_button = html`<input type="button" value="Markdown" @click=${() => self.format = 'md'}></input>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (self.format = 'md')}
|
||||||
|
>
|
||||||
|
Markdown
|
||||||
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
raw_button = html`<input type="button" value="Message" @click=${() => self.format = 'message'}></input>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (self.format = 'message')}
|
||||||
|
>
|
||||||
|
Message
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'md':
|
case 'md':
|
||||||
raw_button = html`<input type="button" value="Message" @click=${() => self.format = 'message'}></input>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (self.format = 'message')}
|
||||||
|
>
|
||||||
|
Message
|
||||||
|
</button>`;
|
||||||
break;
|
break;
|
||||||
case 'decrypted':
|
case 'decrypted':
|
||||||
raw_button = html`<input type="button" value="Raw" @click=${() => self.format = 'raw'}></input>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (self.format = 'raw')}
|
||||||
|
>
|
||||||
|
Raw
|
||||||
|
</button>`;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (this.message.decrypted) {
|
if (this.message.decrypted) {
|
||||||
raw_button = html`<input type="button" value="Decrypted" @click=${() => self.format = 'decrypted'}></input>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (self.format = 'decrypted')}
|
||||||
|
>
|
||||||
|
Decrypted
|
||||||
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
raw_button = html`<input type="button" value="Raw" @click=${() => self.format = 'raw'}></input>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (self.format = 'raw')}
|
||||||
|
>
|
||||||
|
Raw
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
function small_frame(inner) {
|
function small_frame(inner) {
|
||||||
let body;
|
let body;
|
||||||
return html`
|
return html`
|
||||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere">
|
<div
|
||||||
|
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||||
|
style="margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
|
||||||
|
>
|
||||||
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
||||||
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
|
<span style="padding-right: 8px"
|
||||||
${raw_button}
|
><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(
|
||||||
${self.format == 'raw' ? self.render_raw() : inner}
|
self.message.timestamp
|
||||||
|
).toLocaleString()}</span
|
||||||
|
>
|
||||||
|
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
|
||||||
${self.render_votes()}
|
${self.render_votes()}
|
||||||
|
${(self.message.child_messages || []).map(
|
||||||
|
(x) => html`
|
||||||
|
<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${self.whoami}
|
||||||
|
.users=${self.users}
|
||||||
|
.drafts=${self.drafts}
|
||||||
|
.expanded=${self.expanded}
|
||||||
|
></tf-message>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (this.message?.type === 'contact_group') {
|
if (this.message?.type === 'contact_group') {
|
||||||
return html`
|
return html` <div
|
||||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
|
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||||
${this.message.messages.map(x =>
|
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||||
html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`
|
>
|
||||||
|
${this.message.messages.map(
|
||||||
|
(x) =>
|
||||||
|
html`<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-message>`
|
||||||
)}
|
)}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (this.message.placeholder) {
|
} else if (this.message.placeholder) {
|
||||||
return html`
|
return html` <div
|
||||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
|
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||||
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
|
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||||
|
>
|
||||||
|
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
|
||||||
|
(placeholder)
|
||||||
<div>${this.render_votes()}</div>
|
<div>${this.render_votes()}</div>
|
||||||
${(this.message.child_messages || []).map(x => html`
|
${(this.message.child_messages || []).map(
|
||||||
<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>
|
(x) => html`
|
||||||
`)}
|
<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-message>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (typeof(content?.type === 'string')) {
|
} else if (typeof (content?.type === 'string')) {
|
||||||
if (content.type == 'about') {
|
if (content.type == 'about') {
|
||||||
let name;
|
let name;
|
||||||
let image;
|
let image;
|
||||||
@ -307,7 +474,7 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
if (content.image !== undefined) {
|
if (content.image !== undefined) {
|
||||||
image = html`
|
image = html`
|
||||||
<div><img src=${'/' + (typeof(content.image?.link) == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
|
<div><img src=${'/' + (typeof content.image?.link == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (content.description !== undefined) {
|
if (content.description !== undefined) {
|
||||||
@ -317,41 +484,52 @@ class TfMessageElement extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
let update = content.about == this.message.author ?
|
let update =
|
||||||
html`<div style="font-weight: bold">Updated profile.</div>` :
|
content.about == this.message.author
|
||||||
html`<div style="font-weight: bold">Updated profile for <tf-user id=${content.about} .users=${this.users}></tf-user>.</div>`;
|
? html`<div style="font-weight: bold">Updated profile.</div>`
|
||||||
return small_frame(html`
|
: html`<div style="font-weight: bold">
|
||||||
${update}
|
Updated profile for
|
||||||
${name}
|
<tf-user id=${content.about} .users=${this.users}></tf-user>.
|
||||||
${image}
|
</div>`;
|
||||||
${description}
|
return small_frame(html` ${update} ${name} ${image} ${description} `);
|
||||||
`);
|
|
||||||
} else if (content.type == 'contact') {
|
} else if (content.type == 'contact') {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
is
|
is
|
||||||
${
|
${content.blocking === true
|
||||||
content.blocking === true ? 'blocking' :
|
? 'blocking'
|
||||||
content.blocking === false ? 'no longer blocking' :
|
: content.blocking === false
|
||||||
content.following === true ? 'following' :
|
? 'no longer blocking'
|
||||||
content.following === false ? 'no longer following' :
|
: content.following === true
|
||||||
'?'
|
? 'following'
|
||||||
}
|
: content.following === false
|
||||||
<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user>
|
? 'no longer following'
|
||||||
|
: '?'}
|
||||||
|
<tf-user
|
||||||
|
id=${this.message.content.contact}
|
||||||
|
.users=${this.users}
|
||||||
|
></tf-user>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type == 'post') {
|
} else if (content.type == 'post') {
|
||||||
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
|
let reply =
|
||||||
|
this.drafts[this.message?.id] !== undefined
|
||||||
|
? html`
|
||||||
<tf-compose
|
<tf-compose
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.users=${this.users}
|
.users=${this.users}
|
||||||
root=${this.message.content.root || this.message.id}
|
root=${content.root || this.message.id}
|
||||||
branch=${this.message.id}
|
branch=${this.message.id}
|
||||||
.drafts=${this.drafts}
|
.drafts=${this.drafts}
|
||||||
@tf-discard=${this.discard_reply}></tf-compose>
|
@tf-discard=${this.discard_reply}
|
||||||
` : html`
|
author=${this.message.author}
|
||||||
<input type="button" value="Reply" @click=${this.show_reply}></input>
|
></tf-compose>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
|
||||||
|
Reply
|
||||||
|
</button>
|
||||||
`;
|
`;
|
||||||
let self = this;
|
let self = this;
|
||||||
let body;
|
let body;
|
||||||
@ -360,32 +538,44 @@ class TfMessageElement extends LitElement {
|
|||||||
body = this.render_raw();
|
body = this.render_raw();
|
||||||
break;
|
break;
|
||||||
case 'md':
|
case 'md':
|
||||||
body = html`<code style="white-space: pre-wrap; overflow-wrap: anywhere">${content.text}</code>`;
|
body = html`<code
|
||||||
|
style="white-space: pre-wrap; overflow-wrap: anywhere"
|
||||||
|
>${content.text}</code
|
||||||
|
>`;
|
||||||
break;
|
break;
|
||||||
case 'message':
|
case 'message':
|
||||||
body = unsafeHTML(tfutils.markdown(content.text));
|
body = unsafeHTML(tfutils.markdown(content.text));
|
||||||
break;
|
break;
|
||||||
|
case 'decrypted':
|
||||||
|
body = html`<pre
|
||||||
|
style="white-space: pre-wrap; overflow-wrap: anywhere"
|
||||||
|
>
|
||||||
|
${JSON.stringify(content, null, 2)}</pre
|
||||||
|
>`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
let content_warning = html`
|
let content_warning = html`
|
||||||
<div class="content_warning" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
|
<div
|
||||||
|
class="w3-panel w3-round-xlarge w3-theme-l4"
|
||||||
|
style="cursor: pointer"
|
||||||
|
@click=${(x) => this.toggle_expanded(':cw')}
|
||||||
|
>
|
||||||
|
<p>${content.contentWarning}</p>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
let content_html =
|
let content_html = html`
|
||||||
html`
|
|
||||||
${this.render_channels()}
|
${this.render_channels()}
|
||||||
<div @click=${this.body_click}>${body}</div>
|
<div @click=${this.body_click}>${body}</div>
|
||||||
${this.render_mentions()}
|
${this.render_mentions()}
|
||||||
`;
|
`;
|
||||||
let payload =
|
let payload = content.contentWarning
|
||||||
content.contentWarning ?
|
? self.expanded[(this.message.id || '') + ':cw']
|
||||||
self.expanded[(this.message.id || '') + ':cw'] ?
|
? html` ${content_warning} ${content_html} `
|
||||||
html`
|
: content_warning
|
||||||
${content_warning}
|
: content_html;
|
||||||
${content_html}
|
let is_encrypted = this.message?.decrypted
|
||||||
` :
|
? html`<span style="align-self: center">🔓</span>`
|
||||||
content_warning :
|
: undefined;
|
||||||
content_html;
|
|
||||||
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
|
|
||||||
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
|
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
code {
|
code {
|
||||||
@ -401,26 +591,34 @@ class TfMessageElement extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
<div
|
||||||
|
class="w3-card-4 ${class_background} w3-border-theme"
|
||||||
|
style="margin-top: 8px; padding: 16px"
|
||||||
|
>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
${is_encrypted}
|
${is_encrypted}
|
||||||
<span style="flex: 1"></span>
|
<span style="flex: 1"></span>
|
||||||
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
<span style="padding-right: 8px"
|
||||||
|
><a target="_top" href=${'#' + self.message.id}>%</a>
|
||||||
|
${new Date(this.message.timestamp).toLocaleString()}</span
|
||||||
|
>
|
||||||
<span>${raw_button}</span>
|
<span>${raw_button}</span>
|
||||||
</div>
|
</div>
|
||||||
${payload}
|
${payload} ${this.render_votes()}
|
||||||
${this.render_votes()}
|
<p>
|
||||||
<div>
|
|
||||||
${reply}
|
${reply}
|
||||||
<input type="button" value="React" @click=${this.react}></input>
|
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||||
</div>
|
React
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
${this.render_children()}
|
${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type === 'issue') {
|
} else if (content.type === 'issue') {
|
||||||
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
|
let is_encrypted = this.message?.decrypted
|
||||||
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
|
? html`<span style="align-self: center">🔓</span>`
|
||||||
|
: undefined;
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
code {
|
code {
|
||||||
@ -436,31 +634,41 @@ class TfMessageElement extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
<div
|
||||||
|
class="w3-card-4 ${class_background} w3-border-theme"
|
||||||
|
style="margin-top: 8px; padding: 16px"
|
||||||
|
>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
${is_encrypted}
|
${is_encrypted}
|
||||||
<span style="flex: 1"></span>
|
<span style="flex: 1"></span>
|
||||||
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
<span style="padding-right: 8px"
|
||||||
|
><a target="_top" href=${'#' + self.message.id}>%</a>
|
||||||
|
${new Date(this.message.timestamp).toLocaleString()}</span
|
||||||
|
>
|
||||||
<span>${raw_button}</span>
|
<span>${raw_button}</span>
|
||||||
</div>
|
</div>
|
||||||
${content.text}
|
${content.text} ${this.render_votes()}
|
||||||
${this.render_votes()}
|
<p>
|
||||||
<div>
|
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||||
<input type="button" value="React" @click=${this.react}></input>
|
React
|
||||||
</div>
|
</button>
|
||||||
|
</p>
|
||||||
${this.render_children()}
|
${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type === 'blog') {
|
} else if (content.type === 'blog') {
|
||||||
let self = this;
|
let self = this;
|
||||||
tfrpc.rpc.get_blob(content.blog).then(function(data) {
|
tfrpc.rpc.get_blob(content.blog).then(function (data) {
|
||||||
self.blog_data = data;
|
self.blog_data = data;
|
||||||
});
|
});
|
||||||
let payload =
|
let payload = this.expanded[(this.message.id || '') + ':blog']
|
||||||
this.expanded[(this.message.id || '') + ':blog'] ?
|
? html`<div>
|
||||||
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
|
${this.blog_data
|
||||||
undefined;
|
? unsafeHTML(tfutils.markdown(this.blog_data))
|
||||||
|
: 'Loading...'}
|
||||||
|
</div>`
|
||||||
|
: undefined;
|
||||||
let body;
|
let body;
|
||||||
switch (this.format) {
|
switch (this.format) {
|
||||||
case 'raw':
|
case 'raw':
|
||||||
@ -473,7 +681,7 @@ class TfMessageElement extends LitElement {
|
|||||||
body = html`
|
body = html`
|
||||||
<div
|
<div
|
||||||
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
||||||
@click=${x => self.toggle_expanded(':blog')}>
|
@click=${(x) => self.toggle_expanded(':blog')}>
|
||||||
<h2>${content.title}</h2>
|
<h2>${content.title}</h2>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<img src=/${content.thumbnail}/view></img>
|
<img src=/${content.thumbnail}/view></img>
|
||||||
@ -484,6 +692,24 @@ class TfMessageElement extends LitElement {
|
|||||||
`;
|
`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
let reply =
|
||||||
|
this.drafts[this.message?.id] !== undefined
|
||||||
|
? html`
|
||||||
|
<tf-compose
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
root=${content.root || this.message.id}
|
||||||
|
branch=${this.message.id}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
@tf-discard=${this.discard_reply}
|
||||||
|
author=${this.message.author}
|
||||||
|
></tf-compose>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
|
||||||
|
Reply
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
code {
|
code {
|
||||||
@ -499,44 +725,70 @@ class TfMessageElement extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
|
<div
|
||||||
|
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||||
|
style="margin-top: 8px; padding: 16px"
|
||||||
|
>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
<span style="flex: 1"></span>
|
<span style="flex: 1"></span>
|
||||||
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
<span style="padding-right: 8px"
|
||||||
|
><a target="_top" href=${'#' + self.message.id}>%</a>
|
||||||
|
${new Date(this.message.timestamp).toLocaleString()}</span
|
||||||
|
>
|
||||||
<span>${raw_button}</span>
|
<span>${raw_button}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>${body}</div>
|
<div>${body}</div>
|
||||||
${this.render_mentions()}
|
${this.render_mentions()}
|
||||||
${this.render_votes()}
|
<div>
|
||||||
|
${reply}
|
||||||
|
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||||
|
React
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${this.render_votes()} ${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type === 'pub') {
|
} else if (content.type === 'pub') {
|
||||||
return small_frame(html`
|
return small_frame(
|
||||||
<style>
|
html` <style>
|
||||||
span {
|
span {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<span>
|
<span>
|
||||||
<div>
|
<div>
|
||||||
🍻 <tf-user .users=${this.users} id=${content.address.key}></tf-user>
|
🍻
|
||||||
|
<tf-user
|
||||||
|
.users=${this.users}
|
||||||
|
id=${content.address.key}
|
||||||
|
></tf-user>
|
||||||
</div>
|
</div>
|
||||||
<pre>${content.address.host}:${content.address.port}</pre>
|
<pre>${content.address.host}:${content.address.port}</pre>
|
||||||
</span>`);
|
</span>`
|
||||||
|
);
|
||||||
} else if (content.type === 'channel') {
|
} else if (content.type === 'channel') {
|
||||||
return small_frame(html`
|
return small_frame(html`
|
||||||
<div>
|
<div>
|
||||||
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} <a href=${'#q=' + encodeURIComponent('#' + content.channel)}>#${content.channel}</a>
|
${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
|
||||||
|
<a href=${'#q=' + encodeURIComponent('#' + content.channel)}
|
||||||
|
>#${content.channel}</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
} else if (typeof(this.message.content) == 'string') {
|
} else if (typeof this.message.content == 'string') {
|
||||||
if (this.message?.decrypted) {
|
if (this.message?.decrypted) {
|
||||||
if (this.format == 'decrypted') {
|
if (this.format == 'decrypted') {
|
||||||
return small_frame(html`<span>🔓</span><pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`);
|
return small_frame(
|
||||||
|
html`<span>🔓</span>
|
||||||
|
<pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return small_frame(html`<span>🔓</span><div>${this.message.decrypted.type}</div>`);
|
return small_frame(
|
||||||
|
html`<span>🔓</span>
|
||||||
|
<div>${this.message.decrypted.type}</div>`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return small_frame(html`<span>🔒</span>`);
|
return small_frame(html`<span>🔒</span>`);
|
||||||
|
@ -61,7 +61,7 @@ class TfNewsElement extends LitElement {
|
|||||||
message.parent_message = message.content.vote.link;
|
message.parent_message = message.content.vote.link;
|
||||||
} else if (message.content.type == 'post') {
|
} else if (message.content.type == 'post') {
|
||||||
if (message.content.root) {
|
if (message.content.root) {
|
||||||
if (typeof(message.content.root) === 'string') {
|
if (typeof message.content.root === 'string') {
|
||||||
let m = ensure_message(message.content.root);
|
let m = ensure_message(message.content.root);
|
||||||
if (!m.child_messages) {
|
if (!m.child_messages) {
|
||||||
m.child_messages = [];
|
m.child_messages = [];
|
||||||
@ -89,8 +89,7 @@ class TfNewsElement extends LitElement {
|
|||||||
for (let message of messages) {
|
for (let message of messages) {
|
||||||
try {
|
try {
|
||||||
message.content = JSON.parse(message.content);
|
message.content = JSON.parse(message.content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
if (!messages_by_id[message.id]) {
|
if (!messages_by_id[message.id]) {
|
||||||
messages_by_id[message.id] = message;
|
messages_by_id[message.id] = message;
|
||||||
link_message(message);
|
link_message(message);
|
||||||
@ -100,8 +99,12 @@ class TfNewsElement extends LitElement {
|
|||||||
message.parent_message = placeholder.parent_message;
|
message.parent_message = placeholder.parent_message;
|
||||||
message.child_messages = placeholder.child_messages;
|
message.child_messages = placeholder.child_messages;
|
||||||
message.votes = placeholder.votes;
|
message.votes = placeholder.votes;
|
||||||
if (placeholder.parent_message && messages_by_id[placeholder.parent_message]) {
|
if (
|
||||||
let children = messages_by_id[placeholder.parent_message].child_messages;
|
placeholder.parent_message &&
|
||||||
|
messages_by_id[placeholder.parent_message]
|
||||||
|
) {
|
||||||
|
let children =
|
||||||
|
messages_by_id[placeholder.parent_message].child_messages;
|
||||||
children.splice(children.indexOf(placeholder), 1);
|
children.splice(children.indexOf(placeholder), 1);
|
||||||
children.push(message);
|
children.push(message);
|
||||||
}
|
}
|
||||||
@ -116,7 +119,10 @@ class TfNewsElement extends LitElement {
|
|||||||
let latest = 0;
|
let latest = 0;
|
||||||
for (let message of messages || []) {
|
for (let message of messages || []) {
|
||||||
if (message.latest_subtree_timestamp === undefined) {
|
if (message.latest_subtree_timestamp === undefined) {
|
||||||
message.latest_subtree_timestamp = Math.max(message.timestamp ?? 0, this.update_latest_subtree_timestamp(message.child_messages));
|
message.latest_subtree_timestamp = Math.max(
|
||||||
|
message.timestamp ?? 0,
|
||||||
|
this.update_latest_subtree_timestamp(message.child_messages)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
latest = Math.max(latest, message.latest_subtree_timestamp);
|
latest = Math.max(latest, message.latest_subtree_timestamp);
|
||||||
}
|
}
|
||||||
@ -127,20 +133,22 @@ class TfNewsElement extends LitElement {
|
|||||||
function recursive_sort(messages, top) {
|
function recursive_sort(messages, top) {
|
||||||
if (messages) {
|
if (messages) {
|
||||||
if (top) {
|
if (top) {
|
||||||
messages.sort((a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp);
|
messages.sort(
|
||||||
|
(a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
messages.sort((a, b) => a.timestamp - b.timestamp);
|
messages.sort((a, b) => a.timestamp - b.timestamp);
|
||||||
}
|
}
|
||||||
for (let message of messages) {
|
for (let message of messages) {
|
||||||
recursive_sort(message.child_messages, false);
|
recursive_sort(message.child_messages, false);
|
||||||
}
|
}
|
||||||
return messages.map(x => Object.assign({}, x));
|
return messages.map((x) => Object.assign({}, x));
|
||||||
} else {
|
} else {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let roots = Object.values(messages_by_id).filter(x => !x.parent_message);
|
let roots = Object.values(messages_by_id).filter((x) => !x.parent_message);
|
||||||
this.update_latest_subtree_timestamp(roots);
|
this.update_latest_subtree_timestamp(roots);
|
||||||
return recursive_sort(roots, true);
|
return recursive_sort(roots, true);
|
||||||
}
|
}
|
||||||
@ -167,10 +175,22 @@ class TfNewsElement extends LitElement {
|
|||||||
|
|
||||||
load_and_render(messages) {
|
load_and_render(messages) {
|
||||||
let messages_by_id = this.process_messages(messages);
|
let messages_by_id = this.process_messages(messages);
|
||||||
let final_messages = this.group_following(this.finalize_messages(messages_by_id));
|
let final_messages = this.group_following(
|
||||||
|
this.finalize_messages(messages_by_id)
|
||||||
|
);
|
||||||
return html`
|
return html`
|
||||||
<div style="display: flex; flex-direction: column">
|
<div style="display: flex; flex-direction: column">
|
||||||
${final_messages.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded} collapsed=true></tf-message>`)}
|
${final_messages.map(
|
||||||
|
(x) =>
|
||||||
|
html`<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
collapsed="true"
|
||||||
|
></tf-message>`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -36,23 +36,29 @@ class TfProfileElement extends LitElement {
|
|||||||
this.following = undefined;
|
this.following = undefined;
|
||||||
this.blocking = undefined;
|
this.blocking = undefined;
|
||||||
|
|
||||||
let result = await tfrpc.rpc.query(`
|
let result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT json_extract(content, '$.following') AS following
|
SELECT json_extract(content, '$.following') AS following
|
||||||
FROM messages WHERE author = ? AND
|
FROM messages WHERE author = ? AND
|
||||||
json_extract(content, '$.type') = 'contact' AND
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
json_extract(content, '$.contact') = ? AND
|
json_extract(content, '$.contact') = ? AND
|
||||||
following IS NOT NULL
|
following IS NOT NULL
|
||||||
ORDER BY sequence DESC LIMIT 1
|
ORDER BY sequence DESC LIMIT 1
|
||||||
`, [this.whoami, this.id]);
|
`,
|
||||||
|
[this.whoami, this.id]
|
||||||
|
);
|
||||||
this.following = result?.[0]?.following ?? false;
|
this.following = result?.[0]?.following ?? false;
|
||||||
result = await tfrpc.rpc.query(`
|
result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT json_extract(content, '$.blocking') AS blocking
|
SELECT json_extract(content, '$.blocking') AS blocking
|
||||||
FROM messages WHERE author = ? AND
|
FROM messages WHERE author = ? AND
|
||||||
json_extract(content, '$.type') = 'contact' AND
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
json_extract(content, '$.contact') = ? AND
|
json_extract(content, '$.contact') = ? AND
|
||||||
blocking IS NOT NULL
|
blocking IS NOT NULL
|
||||||
ORDER BY sequence DESC LIMIT 1
|
ORDER BY sequence DESC LIMIT 1
|
||||||
`, [this.whoami, this.id]);
|
`,
|
||||||
|
[this.whoami, this.id]
|
||||||
|
);
|
||||||
this.blocking = result?.[0]?.blocking ?? false;
|
this.blocking = result?.[0]?.blocking ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,13 +66,16 @@ class TfProfileElement extends LitElement {
|
|||||||
async initial_load() {
|
async initial_load() {
|
||||||
this.server_follows_me = undefined;
|
this.server_follows_me = undefined;
|
||||||
let server_id = await tfrpc.rpc.getServerIdentity();
|
let server_id = await tfrpc.rpc.getServerIdentity();
|
||||||
let followed = await tfrpc.rpc.query(`
|
let followed = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT json_extract(content, '$.following') AS following
|
SELECT json_extract(content, '$.following') AS following
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE author = ? AND
|
WHERE author = ? AND
|
||||||
json_extract(content, '$.type') = 'contact' AND
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
|
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
|
||||||
`, [server_id, this.whoami]);
|
`,
|
||||||
|
[server_id, this.whoami]
|
||||||
|
);
|
||||||
let is_followed = false;
|
let is_followed = false;
|
||||||
for (let row of followed) {
|
for (let row of followed) {
|
||||||
is_followed = row.following != 0;
|
is_followed = row.following != 0;
|
||||||
@ -75,11 +84,18 @@ class TfProfileElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
modify(change) {
|
modify(change) {
|
||||||
tfrpc.rpc.appendMessage(this.whoami,
|
tfrpc.rpc
|
||||||
Object.assign({
|
.appendMessage(
|
||||||
|
this.whoami,
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
type: 'contact',
|
type: 'contact',
|
||||||
contact: this.id,
|
contact: this.id,
|
||||||
}, change)).catch(function(error) {
|
},
|
||||||
|
change
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.catch(function (error) {
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -122,9 +138,12 @@ class TfProfileElement extends LitElement {
|
|||||||
message[key] = this.editing[key];
|
message[key] = this.editing[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
tfrpc.rpc
|
||||||
|
.appendMessage(this.whoami, message)
|
||||||
|
.then(function () {
|
||||||
self.editing = null;
|
self.editing = null;
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -137,15 +156,19 @@ class TfProfileElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
let input = document.createElement('input');
|
let input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.onchange = function(event) {
|
input.onchange = function (event) {
|
||||||
let file = event.target.files[0];
|
let file = event.target.files[0];
|
||||||
file.arrayBuffer().then(function(buffer) {
|
file
|
||||||
|
.arrayBuffer()
|
||||||
|
.then(function (buffer) {
|
||||||
let bin = Array.from(new Uint8Array(buffer));
|
let bin = Array.from(new Uint8Array(buffer));
|
||||||
return tfrpc.rpc.store_blob(bin);
|
return tfrpc.rpc.store_blob(bin);
|
||||||
}).then(function(id) {
|
})
|
||||||
|
.then(function (id) {
|
||||||
self.editing = Object.assign({}, self.editing, {image: id});
|
self.editing = Object.assign({}, self.editing, {image: id});
|
||||||
console.log(self.editing);
|
console.log(self.editing);
|
||||||
}).catch(function(e) {
|
})
|
||||||
|
.catch(function (e) {
|
||||||
alert(e.message);
|
alert(e.message);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -165,16 +188,27 @@ class TfProfileElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copy_id() {
|
||||||
|
navigator.clipboard.writeText(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.id == this.whoami && this.editing && this.server_follows_me === undefined) {
|
if (
|
||||||
|
this.id == this.whoami &&
|
||||||
|
this.editing &&
|
||||||
|
this.server_follows_me === undefined
|
||||||
|
) {
|
||||||
this.initial_load();
|
this.initial_load();
|
||||||
}
|
}
|
||||||
this.load();
|
this.load();
|
||||||
let self = this;
|
let self = this;
|
||||||
let profile = this.users[this.id] || {};
|
let profile = this.users[this.id] || {};
|
||||||
tfrpc.rpc.query(
|
tfrpc.rpc
|
||||||
|
.query(
|
||||||
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
|
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
|
||||||
[this.id]).then(function(result) {
|
[this.id]
|
||||||
|
)
|
||||||
|
.then(function (result) {
|
||||||
self.size = result[0].size;
|
self.size = result[0].size;
|
||||||
});
|
});
|
||||||
let edit;
|
let edit;
|
||||||
@ -184,54 +218,81 @@ class TfProfileElement extends LitElement {
|
|||||||
if (this.editing) {
|
if (this.editing) {
|
||||||
let server_follow;
|
let server_follow;
|
||||||
if (this.server_follows_me === true) {
|
if (this.server_follows_me === true) {
|
||||||
server_follow = html`<input type="button" value="Server, Stop Following Me" @click=${() => this.server_follow_me(false)}></input>`;
|
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) {
|
} else if (this.server_follows_me === false) {
|
||||||
server_follow = html`<input type="button" value="Server, Follow Me" @click=${() => this.server_follow_me(true)}></input>`;
|
server_follow = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => this.server_follow_me(true)}
|
||||||
|
>
|
||||||
|
Server, Follow Me
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
edit = html`
|
edit = html`
|
||||||
<input type="button" value="Save Profile" @click=${this.save_edits}></input>
|
<button class="w3-button w3-theme-d1" @click=${this.save_edits}>
|
||||||
<input type="button" value="Discard" @click=${this.discard_edits}></input>
|
Save Profile
|
||||||
|
</button>
|
||||||
|
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
${server_follow}
|
${server_follow}
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
edit = html`<input type="button" value="Edit Profile" @click=${this.edit}></input>`;
|
edit = html`<button class="w3-button w3-theme-d1" @click=${this.edit}>
|
||||||
|
Edit Profile
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami &&
|
if (this.id !== this.whoami && this.following !== undefined) {
|
||||||
this.following !== undefined) {
|
follow = this.following
|
||||||
follow =
|
? html`<button class="w3-button w3-theme-d1" @click=${this.unfollow}>
|
||||||
this.following ?
|
Unfollow
|
||||||
html`<input type="button" value="Unfollow" @click=${this.unfollow}></input>` :
|
</button>`
|
||||||
html`<input type="button" value="Follow" @click=${this.follow}></input>`;
|
: html`<button class="w3-button w3-theme-d1" @click=${this.follow}>
|
||||||
|
Follow
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami &&
|
if (this.id !== this.whoami && this.blocking !== undefined) {
|
||||||
this.blocking !== undefined) {
|
block = this.blocking
|
||||||
block =
|
? html`<button class="w3-button w3-theme-d1" @click=${this.unblock}>
|
||||||
this.blocking ?
|
Unblock
|
||||||
html`<input type="button" value="Unblock" @click=${this.unblock}></input>` :
|
</button>`
|
||||||
html`<input type="button" value="Block" @click=${this.block}></input>`;
|
: html`<button class="w3-button w3-theme-d1" @click=${this.block}>
|
||||||
|
Block
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
let edit_profile = this.editing ? html`
|
let edit_profile = this.editing
|
||||||
<div style="flex: 1 0 50%; display: flex; flex-direction: column">
|
? html`
|
||||||
|
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
|
||||||
|
<div class="w3-container">
|
||||||
<div>
|
<div>
|
||||||
<label for="name">Name:</label>
|
<label for="name">Name:</label>
|
||||||
<input type="text" id="name" value=${this.editing.name} @input=${event => this.editing = Object.assign({}, this.editing, {name: event.srcElement.value})}></input>
|
<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
|
||||||
</div>
|
</div>
|
||||||
<div><label for="description">Description:</label></div>
|
<div><label for="description">Description:</label></div>
|
||||||
<textarea style="flex: 1 0" id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
|
<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
|
||||||
<div>
|
<div>
|
||||||
<label for="public_web_hosting">Public Web Hosting:</label>
|
<label for="public_web_hosting">Public Web Hosting:</label>
|
||||||
<input type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${event => self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked})}></input>
|
<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="button" value="Attach Image" @click=${this.attach_image}></input>
|
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
|
||||||
</div>
|
</div>
|
||||||
</div>` : null;
|
</div>
|
||||||
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
|
</div>`
|
||||||
|
: null;
|
||||||
|
let image =
|
||||||
|
typeof profile.image == 'string' ? profile.image : profile.image?.link;
|
||||||
image = this.editing?.image ?? image;
|
image = this.editing?.image ?? image;
|
||||||
let description = this.editing?.description ?? profile.description;
|
let description = this.editing?.description ?? profile.description;
|
||||||
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
|
return html`<div class="w3-container" style="box-sizing: border-box; border: 2px solid black; background-color: rgba(255, 255, 255, 0.2)">
|
||||||
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
|
<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">
|
<div style="display: flex; flex-direction: row; gap: 1em">
|
||||||
${edit_profile}
|
${edit_profile}
|
||||||
<div style="flex: 1 0 50%">
|
<div style="flex: 1 0 50%">
|
||||||
@ -245,11 +306,11 @@ class TfProfileElement extends LitElement {
|
|||||||
Blocking ${profile.blocking} identities.
|
Blocking ${profile.blocking} identities.
|
||||||
Blocked by ${profile.blocked} identities.
|
Blocked by ${profile.blocked} identities.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<p>
|
||||||
${edit}
|
${edit}
|
||||||
${follow}
|
${follow}
|
||||||
${block}
|
${block}
|
||||||
</div>
|
</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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);
|
@ -1,54 +1,314 @@
|
|||||||
import {css} from './lit-all.min.js';
|
import {css} from './lit-all.min.js';
|
||||||
|
|
||||||
export let styles = css`
|
const tf = css`
|
||||||
a:link {
|
img {
|
||||||
color: #bbf;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:visited {
|
|
||||||
color: #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #ddf;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: min(640px, 100%);
|
max-width: min(640px, 100%);
|
||||||
max-height: min(480px, auto);
|
max-height: min(480px, auto);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab:disabled {
|
.tab:disabled {
|
||||||
color: #088;
|
color: #088;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content_warning {
|
.content_warning {
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.img_caption {
|
div.img_caption {
|
||||||
color: #888;
|
color: #888;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.img_caption::after {
|
div.img_caption::after {
|
||||||
content: ' ±';
|
content: ' ±';
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
pre code {
|
||||||
|
display: block;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
border-left: 4px solid #fff;
|
border-left: 4px solid #fff;
|
||||||
margin-left: 0px;
|
padding: 8px;
|
||||||
padding-left: 8px;
|
padding-left: 12px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
const w3 = css`
|
||||||
|
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
||||||
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
|
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
|
||||||
|
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
|
||||||
|
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
|
||||||
|
a{background-color:transparent}a:active,a:hover{outline-width:0}
|
||||||
|
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
|
||||||
|
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
|
||||||
|
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||||
|
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
|
||||||
|
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
|
||||||
|
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
|
||||||
|
button,input{overflow:visible}button,select{text-transform:none}
|
||||||
|
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
|
||||||
|
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
|
||||||
|
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
|
||||||
|
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
|
||||||
|
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
|
||||||
|
[type=checkbox],[type=radio]{padding:0}
|
||||||
|
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
|
||||||
|
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
|
||||||
|
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
|
||||||
|
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
|
||||||
|
/* End extract */
|
||||||
|
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
|
||||||
|
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
|
||||||
|
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
|
||||||
|
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
|
||||||
|
hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
||||||
|
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
|
||||||
|
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
|
||||||
|
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
|
||||||
|
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
|
||||||
|
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
|
||||||
|
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
|
||||||
|
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
||||||
|
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
|
||||||
|
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
|
||||||
|
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
|
||||||
|
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
|
||||||
|
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
|
||||||
|
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
|
||||||
|
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
|
||||||
|
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
|
||||||
|
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
|
||||||
|
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
|
||||||
|
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
|
||||||
|
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
|
||||||
|
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
|
||||||
|
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
|
||||||
|
.w3-main,#main{transition:margin-left .4s}
|
||||||
|
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
|
||||||
|
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
|
||||||
|
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
|
||||||
|
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
|
||||||
|
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
|
||||||
|
.w3-bar .w3-button{white-space:normal}
|
||||||
|
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
|
||||||
|
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
|
||||||
|
.w3-responsive{display:block;overflow-x:auto}
|
||||||
|
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
|
||||||
|
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
|
||||||
|
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
|
||||||
|
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
|
||||||
|
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
|
||||||
|
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
|
||||||
|
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
|
||||||
|
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
|
||||||
|
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
|
||||||
|
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
|
||||||
|
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
|
||||||
|
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
|
||||||
|
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
|
||||||
|
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
|
||||||
|
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
|
||||||
|
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
|
||||||
|
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
|
||||||
|
@media (max-width:1205px){.w3-auto{max-width:95%}}
|
||||||
|
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
|
||||||
|
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
|
||||||
|
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
|
||||||
|
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
|
||||||
|
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
|
||||||
|
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
|
||||||
|
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
|
||||||
|
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
|
||||||
|
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
|
||||||
|
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
|
||||||
|
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
|
||||||
|
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
|
||||||
|
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
|
||||||
|
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
|
||||||
|
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
|
||||||
|
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
|
||||||
|
.w3-display-position{position:absolute}
|
||||||
|
.w3-circle{border-radius:50%}
|
||||||
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
|
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
|
||||||
|
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
|
||||||
|
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
|
||||||
|
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
|
||||||
|
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
|
||||||
|
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
|
||||||
|
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
|
||||||
|
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
|
||||||
|
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
|
||||||
|
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
|
||||||
|
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
|
||||||
|
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
|
||||||
|
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
|
||||||
|
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
|
||||||
|
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
|
||||||
|
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
|
||||||
|
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
|
||||||
|
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
|
||||||
|
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
|
||||||
|
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
|
||||||
|
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
|
||||||
|
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
|
||||||
|
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
|
||||||
|
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
|
||||||
|
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
|
||||||
|
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
|
||||||
|
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
|
||||||
|
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
|
||||||
|
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
|
||||||
|
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
|
||||||
|
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
|
||||||
|
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
|
||||||
|
.w3-left{float:left!important}.w3-right{float:right!important}
|
||||||
|
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||||
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
|
/* Colors */
|
||||||
|
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||||
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
|
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
|
||||||
|
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
|
||||||
|
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
|
||||||
|
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
|
||||||
|
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
|
||||||
|
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
|
||||||
|
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
|
||||||
|
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
|
||||||
|
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
|
||||||
|
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
|
||||||
|
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
|
||||||
|
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
|
||||||
|
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
|
||||||
|
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
|
||||||
|
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
|
||||||
|
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
|
||||||
|
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
|
||||||
|
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
|
||||||
|
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
|
||||||
|
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
|
||||||
|
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
|
||||||
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||||
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
|
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
|
||||||
|
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
|
||||||
|
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
|
||||||
|
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
|
||||||
|
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
|
||||||
|
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
|
||||||
|
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
|
||||||
|
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
|
||||||
|
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
|
||||||
|
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
|
||||||
|
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
|
||||||
|
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
|
||||||
|
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
|
||||||
|
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
|
||||||
|
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
|
||||||
|
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
|
||||||
|
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
|
||||||
|
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
|
||||||
|
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
|
||||||
|
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
|
||||||
|
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
|
||||||
|
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
|
||||||
|
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
|
||||||
|
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
|
||||||
|
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
|
||||||
|
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
|
||||||
|
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
|
||||||
|
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
|
||||||
|
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
|
||||||
|
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
|
||||||
|
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
|
||||||
|
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
|
||||||
|
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
|
||||||
|
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
|
||||||
|
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
|
||||||
|
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
|
||||||
|
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
|
||||||
|
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
|
||||||
|
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
|
||||||
|
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
|
||||||
|
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
|
||||||
|
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
|
||||||
|
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
|
||||||
|
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
|
||||||
|
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
|
||||||
|
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
|
||||||
|
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
|
||||||
|
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
|
||||||
|
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
|
||||||
|
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
|
||||||
|
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
|
||||||
|
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
|
||||||
|
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
|
||||||
|
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
|
||||||
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
|
const w3_2016_riverside = css`
|
||||||
|
.w3-theme-l5 {color:#000 !important; background-color:#f4f6f9 !important}
|
||||||
|
.w3-theme-l4 {color:#000 !important; background-color:#d9e1ec !important}
|
||||||
|
.w3-theme-l3 {color:#000 !important; background-color:#b4c3d8 !important}
|
||||||
|
.w3-theme-l2 {color:#fff !important; background-color:#8ea6c5 !important}
|
||||||
|
.w3-theme-l1 {color:#fff !important; background-color:#6888b1 !important}
|
||||||
|
.w3-theme-d1 {color:#fff !important; background-color:#456185 !important}
|
||||||
|
.w3-theme-d2 {color:#fff !important; background-color:#3d5676 !important}
|
||||||
|
.w3-theme-d3 {color:#fff !important; background-color:#354b68 !important}
|
||||||
|
.w3-theme-d4 {color:#fff !important; background-color:#2e4059 !important}
|
||||||
|
.w3-theme-d5 {color:#fff !important; background-color:#26364a !important}
|
||||||
|
|
||||||
|
.w3-theme-light {color:#000 !important; background-color:#f4f6f9 !important}
|
||||||
|
.w3-theme-dark {color:#fff !important; background-color:#26364a !important}
|
||||||
|
.w3-theme-action {color:#fff !important; background-color:#26364a !important}
|
||||||
|
|
||||||
|
.w3-theme {color:#fff !important; background-color:#4c6a92 !important}
|
||||||
|
.w3-text-theme {color:#4c6a92 !important}
|
||||||
|
.w3-border-theme {border-color:#4c6a92 !important}
|
||||||
|
|
||||||
|
.w3-hover-theme:hover {color:#fff !important; background-color:#4c6a92 !important}
|
||||||
|
.w3-hover-text-theme:hover {color:#4c6a92 !important}
|
||||||
|
.w3-hover-border-theme:hover {border-color:#4c6a92 !important}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export let styles = [tf, w3, w3_2016_riverside];
|
||||||
|
@ -1,38 +1,58 @@
|
|||||||
import {LitElement, html} from './lit-all.min.js';
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
class TfTabConnectionsElement extends LitElement {
|
class TfTabConnectionsElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
broadcasts: {type: Array},
|
broadcasts: {type: Array},
|
||||||
identities: {type: Array},
|
identities: {type: Array},
|
||||||
|
my_identities: {type: Array},
|
||||||
connections: {type: Array},
|
connections: {type: Array},
|
||||||
stored_connections: {type: Array},
|
stored_connections: {type: Array},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
|
server_identity: {type: String},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
static k_broadcast_emojis = {
|
||||||
|
discovery: '🏓',
|
||||||
|
room: '🚪',
|
||||||
|
peer_exchange: '🕸',
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
let self = this;
|
let self = this;
|
||||||
this.broadcasts = [];
|
this.broadcasts = [];
|
||||||
this.identities = [];
|
this.identities = [];
|
||||||
|
this.my_identities = [];
|
||||||
this.connections = [];
|
this.connections = [];
|
||||||
this.stored_connections = [];
|
this.stored_connections = [];
|
||||||
this.users = {};
|
this.users = {};
|
||||||
tfrpc.rpc.getAllIdentities().then(function(identities) {
|
tfrpc.rpc.getIdentities().then(function (identities) {
|
||||||
|
self.my_identities = identities || [];
|
||||||
|
});
|
||||||
|
tfrpc.rpc.getAllIdentities().then(function (identities) {
|
||||||
self.identities = identities || [];
|
self.identities = identities || [];
|
||||||
});
|
});
|
||||||
tfrpc.rpc.getStoredConnections().then(function(connections) {
|
tfrpc.rpc.getStoredConnections().then(function (connections) {
|
||||||
self.stored_connections = connections || [];
|
self.stored_connections = connections || [];
|
||||||
});
|
});
|
||||||
|
tfrpc.rpc.getServerIdentity().then(function (identity) {
|
||||||
|
self.server_identity = identity;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render_connection_summary(connection) {
|
render_connection_summary(connection) {
|
||||||
if (connection.address && connection.port) {
|
if (connection.address && connection.port) {
|
||||||
return html`(<small>${connection.address}:${connection.port}</small>)`;
|
return html`<div>
|
||||||
|
<small>${connection.address}:${connection.port}</small>
|
||||||
|
</div>`;
|
||||||
} else if (connection.tunnel) {
|
} else if (connection.tunnel) {
|
||||||
return html`(room peer)`;
|
return html`<div>room peer</div>`;
|
||||||
} else {
|
} else {
|
||||||
return JSON.stringify(connection);
|
return JSON.stringify(connection);
|
||||||
}
|
}
|
||||||
@ -40,10 +60,12 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
|
|
||||||
render_room_peers(connection) {
|
render_room_peers(connection) {
|
||||||
let self = this;
|
let self = this;
|
||||||
let peers = this.broadcasts.filter(x => x.tunnel?.id == connection);
|
let peers = this.broadcasts.filter((x) => x.tunnel?.id == connection);
|
||||||
if (peers.length) {
|
if (peers.length) {
|
||||||
let connections = this.connections.map(x => x.id);
|
let connections = this.connections.map((x) => x.id);
|
||||||
return html`${peers.filter(x => connections.indexOf(x.pubkey) == -1).map(x => html`${self.render_room_peer(x)}`)}`;
|
return html`${peers
|
||||||
|
.filter((x) => connections.indexOf(x.pubkey) == -1)
|
||||||
|
.map((x) => html`${self.render_room_peer(x)}`)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +77,12 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<li>
|
<li>
|
||||||
<input type="button" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} value="Connect"></input>
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
@ -63,10 +90,18 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
|
|
||||||
render_broadcast(connection) {
|
render_broadcast(connection) {
|
||||||
return html`
|
return html`
|
||||||
<li>
|
<li class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap">
|
||||||
<input type="button" @click=${() => tfrpc.rpc.connect(connection)} value="Connect"></input>
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
|
@click=${() => tfrpc.rpc.connect(connection)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
<div class="w3-bar-item">
|
||||||
|
${TfTabConnectionsElement.k_broadcast_emojis[connection.origin]}
|
||||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
||||||
${this.render_connection_summary(connection)}
|
${this.render_connection_summary(connection)}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -77,49 +112,137 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_connection(connection) {
|
render_connection(connection) {
|
||||||
|
let requests = Object.values(
|
||||||
|
connection.requests.reduce(function (accumulator, value) {
|
||||||
|
let key = `${value.name}:${Math.sign(value.request_number)}`;
|
||||||
|
if (!accumulator[key]) {
|
||||||
|
accumulator[key] = Object.assign({count: 0}, value);
|
||||||
|
}
|
||||||
|
accumulator[key].count++;
|
||||||
|
return accumulator;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
return html`
|
return html`
|
||||||
<input type="button" @click=${() => tfrpc.rpc.closeConnection(connection.id)} value="Close"></input>
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
${connection.flags.one_shot ? '🔃' : undefined}
|
||||||
<tf-user id=${connection.id} .users=${this.users}></tf-user>
|
<tf-user id=${connection.id} .users=${this.users}></tf-user>
|
||||||
${connection.tunnel !== undefined ? '🚇' : html`(${connection.host}:${connection.port})`}
|
${connection.tunnel !== undefined
|
||||||
|
? '🚇'
|
||||||
|
: html`(${connection.host}:${connection.port})`}
|
||||||
|
<div>
|
||||||
|
${requests.map(
|
||||||
|
(x) => html`
|
||||||
|
<span
|
||||||
|
class=${'w3-tag w3-small ' +
|
||||||
|
(x.active ? 'w3-theme-l3' : 'w3-theme-d3')}
|
||||||
|
>${x.request_number > 0 ? '🟩' : '🟥'} ${x.name}
|
||||||
|
<span
|
||||||
|
class="w3-badge w3-white"
|
||||||
|
style=${x.count > 1 ? undefined : 'display: none'}
|
||||||
|
>${x.count}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
${this.connections.filter(x => x.tunnel === this.connections.indexOf(connection)).map(x => html`<li>${this.render_connection(x)}</li>`)}
|
${this.connections
|
||||||
|
.filter((x) => x.tunnel === this.connections.indexOf(connection))
|
||||||
|
.map((x) => html`<li>${this.render_connection(x)}</li>`)}
|
||||||
${this.render_room_peers(connection.id)}
|
${this.render_room_peers(connection.id)}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
tfrpc.rpc.sync();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
|
<div class="w3-container" style="box-sizing: border-box">
|
||||||
|
<button
|
||||||
|
class="w3-button w3-theme-l3 w3-circle w3-ripple w3-large"
|
||||||
|
@click=${this.refresh}
|
||||||
|
>
|
||||||
|
🔃
|
||||||
|
</button>
|
||||||
<h2>New Connection</h2>
|
<h2>New Connection</h2>
|
||||||
<div style="display: flex; flex-direction: column">
|
<textarea class="w3-input w3-theme-d1" id="code"></textarea>
|
||||||
<textarea id="code"></textarea>
|
<button
|
||||||
</div>
|
class="w3-button w3-theme-d1"
|
||||||
<input type="button" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)} value="Connect"></input>
|
@click=${() =>
|
||||||
|
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
<h2>Broadcasts</h2>
|
<h2>Broadcasts</h2>
|
||||||
<ul>
|
<ul class="w3-ul w3-border">
|
||||||
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
|
${this.broadcasts
|
||||||
|
.filter((x) => x.address)
|
||||||
|
.map((x) => self.render_broadcast(x))}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Connections</h2>
|
<h2>Connections</h2>
|
||||||
<ul>
|
<ul class="w3-ul w3-border">
|
||||||
${this.connections.filter(x => x.tunnel === undefined).map(x => html`
|
${this.connections
|
||||||
<li>${this.render_connection(x)}</li>
|
.filter((x) => x.tunnel === undefined)
|
||||||
`)}
|
.map(
|
||||||
|
(x) => html`
|
||||||
|
<li class="w3-bar">${this.render_connection(x)}</li>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Stored Connections (WIP)</h2>
|
<h2>Stored Connections</h2>
|
||||||
<ul>
|
<ul class="w3-ul w3-border">
|
||||||
${this.stored_connections.map(x => html`
|
${this.stored_connections.map(
|
||||||
<li>
|
(x) => html`
|
||||||
<input type="button" @click=${() => self.forget_stored_connection(x)} value="Forget"></input>
|
<li class="w3-bar">
|
||||||
<input type="button" @click=${() => tfrpc.rpc.connect(x)} value="Connect"></input>
|
<button
|
||||||
${x.address}:${x.port} <tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
|
@click=${() => self.forget_stored_connection(x)}
|
||||||
|
>
|
||||||
|
Forget
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
|
@click=${() => tfrpc.rpc.connect(x)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
<div class="w3-bar-item">
|
||||||
|
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
||||||
|
<div><small>${x.address}:${x.port}</small></div>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
`)}
|
`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Local Accounts</h2>
|
<h2>Local Accounts</h2>
|
||||||
<ul>
|
<ul class="w3-ul w3-border">
|
||||||
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
${this.identities.map(
|
||||||
|
(x) =>
|
||||||
|
html`<li class="w3-bar">
|
||||||
|
${x == this.server_identity
|
||||||
|
? html`<span class="w3-tag w3-medium w3-round w3-theme-l1"
|
||||||
|
>🖥 local server</span
|
||||||
|
>`
|
||||||
|
: undefined}
|
||||||
|
${this.my_identities.indexOf(x) != -1
|
||||||
|
? html`<span class="w3-tag w3-medium w3-round w3-theme-d1"
|
||||||
|
>😎 you</span
|
||||||
|
>`
|
||||||
|
: undefined}
|
||||||
|
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||||
|
</li>`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,15 +27,21 @@ class TfTabMentionsElement extends LitElement {
|
|||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
console.log('Loading...', this.whoami);
|
console.log('Loading...', this.whoami);
|
||||||
let results = await tfrpc.rpc.query(`
|
let results = await tfrpc.rpc.query(
|
||||||
SELECT messages.*
|
`
|
||||||
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages_fts(?)
|
FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.author != ?
|
WHERE messages.author != ?
|
||||||
ORDER BY timestamp DESC limit 20
|
ORDER BY timestamp DESC limit 20
|
||||||
`,
|
`,
|
||||||
['"' + this.whoami.replace('"', '""') + '"', JSON.stringify(this.following), this.whoami]);
|
[
|
||||||
|
'"' + this.whoami.replace('"', '""') + '"',
|
||||||
|
JSON.stringify(this.following),
|
||||||
|
this.whoami,
|
||||||
|
]
|
||||||
|
);
|
||||||
console.log('Done.');
|
console.log('Done.');
|
||||||
this.messages = results;
|
this.messages = results;
|
||||||
}
|
}
|
||||||
@ -58,7 +64,14 @@ class TfTabMentionsElement extends LitElement {
|
|||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
|
<tf-news
|
||||||
|
id="news"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.messages=${this.messages}
|
||||||
|
.users=${this.users}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@tf-expand=${this.on_expand}
|
||||||
|
></tf-news>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,54 +33,53 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
if (this.hash.startsWith('#@')) {
|
if (this.hash.startsWith('#@')) {
|
||||||
let r = await tfrpc.rpc.query(
|
let r = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH mine AS (SELECT messages.*
|
WITH mine AS (SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE messages.author = ?
|
WHERE messages.author = ?
|
||||||
ORDER BY sequence DESC
|
ORDER BY sequence DESC
|
||||||
LIMIT 20)
|
LIMIT 20)
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM mine
|
FROM mine
|
||||||
JOIN messages_refs ON mine.id = messages_refs.ref
|
JOIN messages_refs ON mine.id = messages_refs.ref
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
UNION
|
UNION
|
||||||
SELECT * FROM mine
|
SELECT * FROM mine
|
||||||
`,
|
`,
|
||||||
[
|
[this.hash.substring(1)]
|
||||||
this.hash.substring(1),
|
);
|
||||||
]);
|
|
||||||
return r;
|
return r;
|
||||||
} else if (this.hash.startsWith('#%')) {
|
} else if (this.hash.startsWith('#%')) {
|
||||||
return await tfrpc.rpc.query(
|
return await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT messages.*
|
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE id = ?1
|
WHERE id = ?1
|
||||||
UNION
|
UNION
|
||||||
SELECT messages.*
|
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages JOIN messages_refs
|
FROM messages JOIN messages_refs
|
||||||
ON messages.id = messages_refs.message
|
ON messages.id = messages_refs.message
|
||||||
WHERE messages_refs.ref = ?1
|
WHERE messages_refs.ref = ?1
|
||||||
`,
|
`,
|
||||||
[
|
[this.hash.substring(1)]
|
||||||
this.hash.substring(1),
|
);
|
||||||
]);
|
|
||||||
} else {
|
} else {
|
||||||
let promises = [];
|
let promises = [];
|
||||||
const k_following_limit = 256;
|
const k_following_limit = 256;
|
||||||
for (let i = 0; i < this.following.length; i += k_following_limit) {
|
for (let i = 0; i < this.following.length; i += k_following_limit) {
|
||||||
promises.push(tfrpc.rpc.query(
|
promises.push(
|
||||||
|
tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH news AS (SELECT messages.*
|
WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.timestamp > ? AND messages.timestamp < ?
|
WHERE messages.timestamp > ? AND messages.timestamp < ?
|
||||||
ORDER BY messages.timestamp DESC)
|
ORDER BY messages.timestamp DESC)
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM news
|
FROM news
|
||||||
JOIN messages_refs ON news.id = messages_refs.ref
|
JOIN messages_refs ON news.id = messages_refs.ref
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
UNION
|
UNION
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM news
|
FROM news
|
||||||
JOIN messages_refs ON news.id = messages_refs.message
|
JOIN messages_refs ON news.id = messages_refs.message
|
||||||
JOIN messages ON messages_refs.ref = messages.id
|
JOIN messages ON messages_refs.ref = messages.id
|
||||||
@ -95,7 +94,9 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
** messages with far-future timestamps from staying at the top forever.
|
** messages with far-future timestamps from staying at the top forever.
|
||||||
*/
|
*/
|
||||||
new Date().valueOf() + 24 * 60 * 60 * 1000,
|
new Date().valueOf() + 24 * 60 * 60 * 1000,
|
||||||
]));
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return [].concat(...(await Promise.all(promises)));
|
return [].concat(...(await Promise.all(promises)));
|
||||||
}
|
}
|
||||||
@ -106,29 +107,26 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
|
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
|
||||||
let more = await tfrpc.rpc.query(
|
let more = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH news AS (SELECT messages.*
|
WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.timestamp > ?
|
WHERE messages.timestamp > ?
|
||||||
AND messages.timestamp <= ?
|
AND messages.timestamp <= ?
|
||||||
ORDER BY messages.timestamp DESC)
|
ORDER BY messages.timestamp DESC)
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM news
|
FROM news
|
||||||
JOIN messages_refs ON news.id = messages_refs.ref
|
JOIN messages_refs ON news.id = messages_refs.ref
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
UNION
|
UNION
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM news
|
FROM news
|
||||||
JOIN messages_refs ON news.id = messages_refs.message
|
JOIN messages_refs ON news.id = messages_refs.message
|
||||||
JOIN messages ON messages_refs.ref = messages.id
|
JOIN messages ON messages_refs.ref = messages.id
|
||||||
UNION
|
UNION
|
||||||
SELECT news.* FROM news
|
SELECT news.* FROM news
|
||||||
`,
|
`,
|
||||||
[
|
[JSON.stringify(this.following), this.start_time, last_start_time]
|
||||||
JSON.stringify(this.following),
|
);
|
||||||
this.start_time,
|
|
||||||
last_start_time,
|
|
||||||
]);
|
|
||||||
this.messages = await this.decrypt([...more, ...this.messages]);
|
this.messages = await this.decrypt([...more, ...this.messages]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,14 +137,12 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
let content;
|
let content;
|
||||||
try {
|
try {
|
||||||
content = JSON.parse(message?.content);
|
content = JSON.parse(message?.content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
if (typeof content === 'string') {
|
||||||
if (typeof(content) === 'string') {
|
|
||||||
let decrypted;
|
let decrypted;
|
||||||
try {
|
try {
|
||||||
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
|
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
if (decrypted) {
|
if (decrypted) {
|
||||||
try {
|
try {
|
||||||
message.decrypted = JSON.parse(decrypted);
|
message.decrypted = JSON.parse(decrypted);
|
||||||
@ -165,29 +161,48 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.messages ||
|
if (
|
||||||
|
!this.messages ||
|
||||||
this._messages_hash !== this.hash ||
|
this._messages_hash !== this.hash ||
|
||||||
this._messages_following !== this.following) {
|
this._messages_following !== this.following
|
||||||
console.log(`loading messages for ${this.whoami} (following ${this.following.length})`);
|
) {
|
||||||
|
console.log(
|
||||||
|
`loading messages for ${this.whoami} (following ${this.following.length})`
|
||||||
|
);
|
||||||
let self = this;
|
let self = this;
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this._messages_hash = this.hash;
|
this._messages_hash = this.hash;
|
||||||
this._messages_following = this.following;
|
this._messages_following = this.following;
|
||||||
this.fetch_messages().then(this.decrypt.bind(this)).then(function(messages) {
|
this.fetch_messages()
|
||||||
|
.then(this.decrypt.bind(this))
|
||||||
|
.then(function (messages) {
|
||||||
self.messages = messages;
|
self.messages = messages;
|
||||||
console.log(`loading mesages done for ${self.whoami}`);
|
console.log(`loading mesages done for ${self.whoami}`);
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
alert(JSON.stringify(error, null, 2));
|
alert(JSON.stringify(error, null, 2));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let more;
|
let more;
|
||||||
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
||||||
more = html`
|
more = html`
|
||||||
<input type="button" value="Load More" @click=${this.load_more}></input>
|
<p>
|
||||||
|
<button class="w3-button w3-theme-d1" @click=${this.load_more}>
|
||||||
|
Load More
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<tf-news id="news" whoami=${this.whoami} .users=${this.users} .messages=${this.messages} .following=${this.following} .drafts=${this.drafts} .expanded=${this.expanded}></tf-news>
|
<tf-news
|
||||||
|
id="news"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.messages=${this.messages}
|
||||||
|
.following=${this.following}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-news>
|
||||||
${more}
|
${more}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ class TfTabNewsElement extends LitElement {
|
|||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
|
loading: {type: Boolean},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ class TfTabNewsElement extends LitElement {
|
|||||||
this.cache = {};
|
this.cache = {};
|
||||||
this.drafts = {};
|
this.drafts = {};
|
||||||
this.expanded = {};
|
this.expanded = {};
|
||||||
tfrpc.rpc.localStorageGet('drafts').then(function(d) {
|
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
|
||||||
self.drafts = JSON.parse(d || '{}');
|
self.drafts = JSON.parse(d || '{}');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -48,7 +49,9 @@ class TfTabNewsElement extends LitElement {
|
|||||||
let news = this.shadowRoot?.getElementById('news');
|
let news = this.shadowRoot?.getElementById('news');
|
||||||
if (news) {
|
if (news) {
|
||||||
console.log('injecting messages', news.messages);
|
console.log('injecting messages', news.messages);
|
||||||
news.add_messages(Object.values(Object.fromEntries(this.unread.map(x => [x.id, x]))));
|
news.add_messages(
|
||||||
|
Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x])))
|
||||||
|
);
|
||||||
this.dispatchEvent(new CustomEvent('refresh'));
|
this.dispatchEvent(new CustomEvent('refresh'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,11 +65,16 @@ class TfTabNewsElement extends LitElement {
|
|||||||
let type = 'private';
|
let type = 'private';
|
||||||
try {
|
try {
|
||||||
type = JSON.parse(message.content).type || type;
|
type = JSON.parse(message.content).type || type;
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
counts[type] = (counts[type] || 0) + 1;
|
counts[type] = (counts[type] || 0) + 1;
|
||||||
}
|
}
|
||||||
return 'Show New: ' + Object.keys(counts).sort().map(x => (counts[x].toString() + ' ' + x + 's')).join(', ');
|
return (
|
||||||
|
'↻ Show New: ' +
|
||||||
|
Object.keys(counts)
|
||||||
|
.sort()
|
||||||
|
.map((x) => counts[x].toString() + ' ' + x + 's')
|
||||||
|
.join(', ')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
draft(event) {
|
draft(event) {
|
||||||
@ -77,10 +85,7 @@ class TfTabNewsElement extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
delete this.drafts[id];
|
delete this.drafts[id];
|
||||||
}
|
}
|
||||||
/* Only trigger a re-render if we're creating a new draft or discarding an old one. */
|
|
||||||
if ((previous !== undefined) != (event.detail.draft !== undefined)) {
|
|
||||||
this.drafts = Object.assign({}, this.drafts);
|
this.drafts = Object.assign({}, this.drafts);
|
||||||
}
|
|
||||||
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
|
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,22 +101,66 @@ class TfTabNewsElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
on_keypress(event) {
|
on_keypress(event) {
|
||||||
if (event.target === document.body &&
|
if (event.target === document.body && event.key == '.') {
|
||||||
event.key == '.') {
|
|
||||||
this.show_more();
|
this.show_more();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let profile = this.hash.startsWith('#@') ?
|
let profile = this.hash.startsWith('#@')
|
||||||
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
|
? html`<tf-profile
|
||||||
|
id=${this.hash.substring(1)}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
></tf-profile>`
|
||||||
|
: undefined;
|
||||||
|
let edit_profile;
|
||||||
|
if (
|
||||||
|
!this.loading &&
|
||||||
|
this.users[this.whoami]?.name === undefined &&
|
||||||
|
this.hash.substring(1) != this.whoami
|
||||||
|
) {
|
||||||
|
edit_profile = html` <div
|
||||||
|
class="w3-panel w3-padding w3-round w3-card-4 w3-theme-l3"
|
||||||
|
>
|
||||||
|
ℹ️ Follow your identity link ☝️ above to edit your profile and set your
|
||||||
|
name.
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
<div><input type="button" value=${this.new_messages_text()} @click=${this.show_more}></input></div>
|
<p class="w3-bar">
|
||||||
<a target="_top" href="#" ?hidden=${this.hash.length <= 1}>🏠Home</a>
|
<button
|
||||||
<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div>
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
<div><tf-compose id="tf-compose" whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
|
@click=${this.show_more}
|
||||||
|
>
|
||||||
|
${this.new_messages_text()}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<div class="w3-bar">
|
||||||
|
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
|
||||||
|
${edit_profile}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<tf-compose
|
||||||
|
id="tf-compose"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
@tf-draft=${this.draft}
|
||||||
|
></tf-compose>
|
||||||
|
</div>
|
||||||
${profile}
|
${profile}
|
||||||
<tf-tab-news-feed id="news" whoami=${this.whoami} .users=${this.users} .following=${this.following} hash=${this.hash} .drafts=${this.drafts} .expanded=${this.expanded} @tf-draft=${this.draft} @tf-expand=${this.on_expand}></tf-tab-news-feed>
|
<tf-tab-news-feed
|
||||||
|
id="news"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.following=${this.following}
|
||||||
|
hash=${this.hash}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@tf-draft=${this.draft}
|
||||||
|
@tf-expand=${this.on_expand}
|
||||||
|
></tf-tab-news-feed>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user