forked from cory/tildefriends
Compare commits
714 Commits
Author | SHA1 | Date | |
---|---|---|---|
13d43d8319 | |||
7bcdbd3813 | |||
60ada22674 | |||
637119d46d | |||
40f3da6a65 | |||
f4697fe7f7 | |||
3bc18b9021 | |||
c21581aefa | |||
165f25db69 | |||
9aa0617aa1 | |||
ddce88dce6 | |||
6aa2bce2be | |||
a43c1d3d1e | |||
1ed0e817e8 | |||
709ca55e65 | |||
8c13f5dbba | |||
4cb82d81b7 | |||
0c42921387 | |||
70a3e7fc7d | |||
d5267be38c | |||
8e7e0ed490 | |||
8cf2837725 | |||
63ae186c76 | |||
dbf5c7b832 | |||
bfbfc01e99 | |||
8fa9d0e843 | |||
2d3e108fd9 | |||
7822b30dcb | |||
2701b7d04e | |||
e361c3f975 | |||
260706c172 | |||
390668ec34 | |||
1d5cdf9607 | |||
a4bf3542e0 | |||
df82cfe66b | |||
f23414adaf | |||
41024ddb79 | |||
53f9547cc5 | |||
4bfd9de100 | |||
c01e00d77d | |||
825191c08f | |||
9dc6670795 | |||
1db8eee9f7 | |||
1bc50cb62c | |||
450b07fd08 | |||
12c7515ee8 | |||
ed65da4340 | |||
d9d2917cf5 | |||
ce5ca1875b | |||
4f869252a2 | |||
17b92126de | |||
6e88c44229 | |||
6c3d338c12 | |||
4ebd44cb4e | |||
75cb9f7fd2 | |||
eacca9d2ab | |||
d0e11bc68b | |||
1958623a7a | |||
498d8b6520 | |||
a12f2fec5a | |||
22bf046643 | |||
dca48fae36 | |||
9af4068bb6 | |||
2992d8ec12 | |||
33dd2560e0 | |||
aeb5c6ee25 | |||
08a2436b8f | |||
fbc3cfeda4 | |||
c8812b1add | |||
8d82e80639 | |||
ed741d53d7 | |||
685754895b | |||
e7791d38ff | |||
9f14653001 | |||
6c5a7b0751 | |||
51a327c52d | |||
5a978bb30d | |||
6801758cb3 | |||
14de3dd9e5 | |||
ed2d57fb4b | |||
e87acc6286 | |||
0de932bc9e | |||
d021d9f757 | |||
eb5da26004 | |||
6765254f43 | |||
e98802f5b2 | |||
af54b6483e | |||
96167c3167 | |||
eecfdf482f | |||
7ceb865206 | |||
b919670706 | |||
72120b8842 | |||
1e53c08d9d | |||
2d1b6a09e9 | |||
7f661d9af9 | |||
81c66bdddd | |||
4bd46a1657 | |||
244a752ae1 | |||
72369ab745 | |||
b62a05f627 | |||
03eb8e7fae | |||
a5a00b6987 | |||
542162c78a | |||
8cfe0fb7d2 | |||
49c831cb62 | |||
2c79e03094 | |||
21e6cf10b6 | |||
dc655bb359 | |||
b9987580ee | |||
cb2dfc696d | |||
7f0643f9c0 | |||
14a4117aff | |||
55fb5dce1a | |||
923d6f9835 | |||
08b5ade8ec | |||
91f41c7497 | |||
fa06282ff9 | |||
48b967f5b6 | |||
f479165aac | |||
2f83ecc1ac | |||
01efc215fd | |||
00ba74a6c4 | |||
44b5ba1a9a | |||
843e172e56 | |||
a0df336abe | |||
0db4bb06c9 | |||
ad2b49b838 | |||
ab519342e8 | |||
1f0b9012e3 | |||
1ddad6be93 | |||
cf311003c0 | |||
64249976a8 | |||
6ecb3ccd08 | |||
4867bacb72 | |||
7d029d3d7a | |||
455befc18f | |||
6e57845512 | |||
1335a6e1e5 | |||
1eab44464c | |||
c3e2da3d51 | |||
1716f71c12 | |||
b52e99c958 | |||
86531bfd7e | |||
874a22325e | |||
2380b65853 | |||
f72e8cbd91 | |||
24e418344e | |||
2b7077ca70 | |||
10d438e723 | |||
331846ee2e | |||
dc0e58afc1 | |||
18e9252998 | |||
b2e3c04036 | |||
4fd155e68a | |||
59ac0b5f20 | |||
f4979c841a | |||
74eb74deb1 | |||
9e5e7b70d4 | |||
2384fc9fa9 | |||
576e58b1e3 | |||
a0af058f5e | |||
b40457d774 | |||
2353b43514 | |||
b11d5192c2 | |||
d38c58ce1d | |||
a0f390b7dc | |||
cb12799111 | |||
86fb5c53a1 | |||
29fc728509 | |||
0fb341f378 | |||
8a1a182479 | |||
49907bc8ee | |||
21d4a9b328 | |||
d5ede43a13 | |||
b73f5011cf | |||
32ebfa78cd | |||
39c942a205 | |||
ebc4533b10 | |||
4e5f9c86a8 | |||
d89a7a5556 | |||
8ab53f2da3 | |||
4c8eab2692 | |||
08989f54d9 | |||
c78753f3ff | |||
34a87d8b3b | |||
7516524d69 | |||
549d7ffec8 | |||
ccafc23d3c | |||
709b57d84f | |||
9ef909c9a1 | |||
d7c0ffaac4 | |||
e4cd5312f1 | |||
197fca6d3b | |||
04af1f0053 | |||
93d9b1ed93 | |||
2d73116bc0 | |||
f2f6d78790 | |||
797509fc11 | |||
6920504762 | |||
9d1476a760 | |||
c1890775dc | |||
72e5fe5b8f | |||
c81ec214e2 | |||
0dcc879eb1 | |||
4f3f4295ea | |||
d02f17a8cf | |||
2f6a92168e | |||
b6a3923b27 | |||
d556cbc835 | |||
f186806117 | |||
f4f560b164 | |||
14b7f9237b | |||
f3518b3d0f | |||
7964524e0a | |||
8ab8335baa | |||
cd43bf9dfa | |||
ccebf831e7 | |||
9f2f9bd8b0 | |||
adf8c14536 | |||
606e82d718 | |||
1621f1753a | |||
196ab66e14 | |||
38ab32dad9 | |||
86046e52f0 | |||
9e7c860414 | |||
7dc8b86ee2 | |||
6ecbfe3de6 | |||
f9940fc436 | |||
58e75ee276 | |||
e7771f539d | |||
c2f62cd8e0 | |||
f4b6812675 | |||
03e4b37c04 | |||
7b3a9e0f63 | |||
067f546580 | |||
2f7697b7ec | |||
1d214f89ed | |||
0b47207949 | |||
94dd573a81 | |||
6fa4896155 | |||
28c99f9d8b | |||
88fbb5f73b | |||
402c185dd4 | |||
ae2015a604 | |||
023731fc3f | |||
99998aac8a | |||
360d0bc110 | |||
817838e522 | |||
deb3cfb4b6 | |||
af61519632 | |||
b1714cf554 | |||
f0984b19f2 | |||
eb3c9cd6f3 | |||
e677b0ac3c | |||
dd909bfe53 | |||
b13b111614 | |||
5511530926 | |||
5e1ef01bc0 | |||
a060eadab7 | |||
70db31bb8f | |||
1292775a75 | |||
0fbc84d364 | |||
0dd0b835ec | |||
d96e0a7497 | |||
625504b8eb | |||
a185ded47e | |||
5a0c77d06a | |||
e54bd316d5 | |||
f908b45cc7 | |||
d02751ee08 | |||
13ab9786f7 | |||
ba13a08e78 | |||
8c92a5ff7b | |||
37f728835b | |||
a6f1eaa09e | |||
dff8eca16e | |||
a3cca9aae6 | |||
edabd7735c | |||
461e7b7d5a | |||
06ea8d4781 | |||
62ef0bcb42 | |||
a0043ec49f | |||
305f5232e7 | |||
d467c4dd8a | |||
5b2ace80d4 | |||
1484a87cad | |||
b23bc0b16b | |||
a6c8dd846c | |||
5fff3b8161 | |||
b4236d0ec0 | |||
5e8cd12760 | |||
699438602c | |||
52aa6eed0d | |||
6cdf207dcd | |||
607e9ef71b | |||
367c489fc3 | |||
b3c9ad2fcb | |||
ee9cb63327 | |||
889773c38d | |||
b83704a218 | |||
dfe5d51d04 | |||
280dee0438 | |||
aa10ab69f6 | |||
6ef14d985d | |||
61a3226e14 | |||
f9c370212b | |||
021f3ad5bc | |||
8bdc27bf5c | |||
00eb5222f8 | |||
d06f490cc2 | |||
b087a09d37 | |||
4cedc54d2d | |||
8fe7adc50e | |||
bf5236e68b | |||
da1d686705 | |||
2ce5bc73d5 | |||
c6ae9313cc | |||
8f3883563f | |||
950273da41 | |||
391da742fd | |||
4414676076 | |||
0da45b7b40 | |||
7e02cb90f6 | |||
01ba90fdba | |||
b394140f9e | |||
4a1d136721 | |||
ab3009f771 | |||
bf340f3de4 | |||
68f5827dee | |||
b695a4ba3b | |||
d84ab2734e | |||
4f0cc793c7 | |||
b7a4ac22b2 | |||
5db9acae1d | |||
c299c1432c | |||
d8551ab732 | |||
25bc1279c2 | |||
f6d9d23456 | |||
b20d95d616 | |||
071c2f1c20 | |||
566d00f0df | |||
0550aa4e98 | |||
baf69355a5 | |||
17c0266998 | |||
14a2207064 | |||
7c721fc6eb | |||
ab03692a4c | |||
626fa4f27b | |||
15676e0f4f | |||
8709ce8ad5 | |||
bb924d79d6 | |||
8e075e33d9 | |||
b0b002104a | |||
43d1f34fa3 | |||
6db1a097aa | |||
6dae2f0749 | |||
dc87c26b99 | |||
234d597083 | |||
b74c347c7c | |||
996996e609 | |||
cd9050f61f | |||
b70b309977 | |||
ee510f3f3f | |||
8a70b8ea3e | |||
27ee73bb89 | |||
3aeb47e447 | |||
b9ceb30ecf | |||
5b6ee20b2d | |||
d062ec0dfe | |||
d5a0daa0d3 | |||
8c4ec71e26 | |||
1e5aa0ba93 | |||
0326a1f8cc | |||
8311404a09 | |||
c81111c2cf | |||
e499be12ae | |||
e785f7f10a | |||
c3da10bef6 | |||
c8c8cb305e | |||
efdd046ef8 | |||
496eefd2ee | |||
9da79b3a21 | |||
1b2b0970fb | |||
0bb45a7fa8 | |||
15a25a41aa | |||
76b6ff78cd | |||
5285b3f222 | |||
0c993c251b | |||
b3a1f17452 | |||
11929e8c68 | |||
fc9a081250 | |||
2583221117 | |||
a69e551968 | |||
8bd0027e71 | |||
84eaa3e2fd | |||
a57916b3db | |||
87e769786a | |||
05a7e941cf | |||
ed307e6b3b | |||
e8aa957209 | |||
a39f820ff1 | |||
1c621a602f | |||
7169f4a6cb | |||
e202c1a40e | |||
0f4b4da0aa | |||
1f96413bd3 | |||
9695621c91 | |||
2d67f5ead6 | |||
29d2a45abc | |||
13c8b05f9a | |||
2c1a5359c6 | |||
d8530f228e | |||
575f6c2f0a | |||
62cdc592c0 | |||
11373faf23 | |||
0473eec0a2 | |||
7fc23dc085 | |||
c741cc06b2 | |||
39dbfdec82 | |||
0e5d6056e4 | |||
d711993b3b | |||
615bf7fe43 | |||
424b9b5a2f | |||
d1e494b730 | |||
623e4b8fff | |||
b5dd1f2f86 | |||
ec83f9c747 | |||
31af27529e | |||
6302565942 | |||
cda724b2da | |||
c2e2ba2a40 | |||
b5d3f5faa7 | |||
71f3910055 | |||
70ed8c3b32 | |||
af13bfc920 | |||
e24fd92f85 | |||
7e27cefe6a | |||
450cf6424e | |||
54898d3dbb | |||
dd851a2b25 | |||
4c6b44eb30 | |||
74a3efe78d | |||
51301fc49e | |||
02dd8c3dd0 | |||
26a778c3b2 | |||
9fecbd97e8 | |||
e1383e3903 | |||
47532b8512 | |||
3c4959433a | |||
e921b4a86a | |||
b23b0ca239 | |||
191b45f054 | |||
15d0383349 | |||
d2485583fd | |||
2b94704916 | |||
85ac6c215a | |||
e83e665db9 | |||
645aafef16 | |||
152c893a6f | |||
7c130dda56 | |||
2d82dad806 | |||
e8ac5b759d | |||
4833d18968 | |||
6eafded1f6 | |||
7b440b720e | |||
e20ba7384f | |||
45231c6ede | |||
35475defb5 | |||
8741841f27 | |||
5282d19b55 | |||
d9782aa0fb | |||
9751facfb4 | |||
e0110203e7 | |||
088b44cc2c | |||
8f63bcbfbf | |||
c8029388c9 | |||
d9c4d847a1 | |||
df9d9425ec | |||
90bb3c684e | |||
9c81b6de8a | |||
6383498041 | |||
daeb88785d | |||
dcea08f73b | |||
b252b921f8 | |||
172826bf13 | |||
060f1980f5 | |||
e223d35252 | |||
99dba1a4c6 | |||
b52026c81f | |||
47b8c86426 | |||
2e55c68648 | |||
b7362dd84d | |||
01637b31e1 | |||
0e9a39608a | |||
79404e4d41 | |||
35c21fbdaf | |||
8c7bd7dc11 | |||
09ad4f0320 | |||
d96b836bef | |||
59b2ffaf95 | |||
f1b55ddd64 | |||
85acac3a30 | |||
befff5c1e5 | |||
d72ba81a67 | |||
fef88e2032 | |||
20557e8ce4 | |||
99c905e908 | |||
d7b58ee2c5 | |||
faca2d387b | |||
358d02d97f | |||
b66dac7465 | |||
f7d201859a | |||
61d2ef5469 | |||
ac994b9c62 | |||
264dcbc331 | |||
e5425c0ffb | |||
e10803de68 | |||
07b1a0e403 | |||
6ed2c702d8 | |||
5c1c33d33e | |||
70d37c88b5 | |||
1ba37d95b5 | |||
0d82198849 | |||
39927e75f2 | |||
e6fd33b969 | |||
e8fe32d5af | |||
bfc8bb864d | |||
9179746763 | |||
d0177d24cb | |||
0573008c9c | |||
9506f518c2 | |||
0f0ae9153b | |||
09c7c8ac64 | |||
5e2dfff148 | |||
958b47548d | |||
16155ef746 | |||
5755b61ea6 | |||
353847a77f | |||
bdf64edeb8 | |||
b5768dd927 | |||
3e5abf3a4d | |||
d3029639de | |||
d21d7e4add | |||
afde69b5d9 | |||
3319df3df0 | |||
1102feaac3 | |||
deede728be | |||
fc3dd84122 | |||
9239441d73 | |||
b984811851 | |||
1c52446331 | |||
b6dffa8e66 | |||
315d650d27 | |||
07c121044a | |||
f3169afcf5 | |||
c371fc2a8e | |||
6889e11fd1 | |||
fb73fd0afc | |||
6fcebd7a08 | |||
15ea62a546 | |||
b0cd58f5aa | |||
7fe8f66fd3 | |||
68ca99e9d9 | |||
a2542c658b | |||
eb203c7e62 | |||
6ef466f3ed | |||
5074246462 | |||
73bbcebddb | |||
18128303b6 | |||
c4a2d790a3 | |||
c1ec150696 | |||
f4b856df15 | |||
85b87553dd | |||
5decdf3afa | |||
a4acee4939 | |||
d06aea2831 | |||
ae0a8b0a33 | |||
f0452704a1 | |||
b8b1f1ba80 | |||
caf7478da4 | |||
0e40ba78a4 | |||
d1eac6c9eb | |||
8f5201b2bc | |||
6022001d66 | |||
f018c367ed | |||
48c47f097a | |||
39ac215b5a | |||
7d562ce85c | |||
51b317233a | |||
87ce715011 | |||
ef5afc1e23 | |||
486212f22a | |||
0e8867dd6e | |||
ca28b5ca82 | |||
19e26c1759 | |||
790f6643a4 | |||
2158ad3c0b | |||
d904d8922f | |||
da50792500 | |||
b4629acc48 | |||
0cf4118330 | |||
dd61a6ecc3 | |||
8e6f1284e1 | |||
813d3cd492 | |||
f421606e21 | |||
1ccb9183b4 | |||
7d9b627f37 | |||
3038138909 | |||
2ca08d21e4 | |||
478e96fc5f | |||
e237c7ea1d | |||
bf9ff088fd | |||
e073ebedd1 | |||
10d4ae7dcc | |||
5b8bdbb3e4 | |||
c807e21c6b | |||
cc92d0e316 | |||
09c396d5a3 | |||
bc5bbca951 | |||
ed4faedcd7 | |||
251556ebed | |||
1324afb459 | |||
1119804fc2 | |||
cdf6440197 | |||
8727fe00af | |||
7da7890bb6 | |||
706bd2c51f | |||
acabec940e | |||
470b998b61 | |||
80fad05f23 | |||
07a912fb9a | |||
e9d83262c4 | |||
74323c22f9 | |||
2614e89b1b | |||
e092fe1399 | |||
9cbe895cb8 | |||
b0b0f74e83 | |||
d9eaa92c37 | |||
566d07117e | |||
2bffdb1168 | |||
1359b48c9f | |||
a69fb5eeac | |||
38e313350e | |||
5052dc04f2 | |||
9ef3a3aca0 | |||
7b91a2ec37 | |||
2926f855a1 | |||
639419db60 | |||
54747c127c | |||
791c3dd787 | |||
b00d75ab7c | |||
956ea0df56 | |||
30014040e7 | |||
ab055c3394 | |||
1e37eeea05 | |||
84aec0278d | |||
06642f58c5 | |||
e6d44b32f4 | |||
1f3f6e2b92 | |||
8f2d3e3bcd | |||
2df2fc5792 | |||
20b0337e0a | |||
e86b9dae48 | |||
71de897419 | |||
3edfaf9137 | |||
19c1784864 | |||
0d9fac7363 | |||
2fb91fccc0 | |||
24e1ab12ab | |||
10ea885d8d | |||
ec65faa12d | |||
53692a1ea8 | |||
ebef51b4ea | |||
a94d6f9271 | |||
3d2c88c201 | |||
bdeee7fc0e | |||
33a037e0ea | |||
2dc2d9ebf6 | |||
9748f0ed8b | |||
d6be2f7d54 | |||
63615747a7 | |||
fbb657a85c | |||
bdac0c7879 | |||
54dde76a8a | |||
2bbe22bc7a | |||
ad8532f7ac | |||
602941104e | |||
d38b41687c | |||
08125cd1e8 | |||
2ce2097a3f | |||
a5da17e1b1 | |||
2b0962f087 | |||
37173cce4c | |||
37edbd9824 | |||
a32bb02223 | |||
2ab1b84432 | |||
52ae19220c | |||
10bfa65a4e | |||
2a3b1a1e33 | |||
f74f4f6da9 | |||
12a8b7a058 | |||
400f07660f | |||
d532795b7f | |||
6064ed6a3a | |||
2c1a43df2e | |||
bf72782c9f | |||
63dcab30c3 | |||
50e48af7c4 | |||
9127a18ff0 | |||
61ff466908 | |||
1c10768aa4 | |||
992b123853 | |||
f736756b20 | |||
28d73f5b37 |
20
.clang-format
Normal file
20
.clang-format
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Format Style Options - Created with Clang Power Tools
|
||||||
|
---
|
||||||
|
BasedOnStyle: WebKit
|
||||||
|
AlignEscapedNewlines: DontAlign
|
||||||
|
AlignOperands: DontAlign
|
||||||
|
AllowShortCaseLabelsOnASingleLine: false
|
||||||
|
AllowShortFunctionsOnASingleLine: false
|
||||||
|
BreakBeforeBinaryOperators: None
|
||||||
|
BreakBeforeBraces: Allman
|
||||||
|
ColumnLimit: 180
|
||||||
|
ContinuationIndentWidth: 4
|
||||||
|
IndentCaseBlocks: true
|
||||||
|
IndentWidth: 4
|
||||||
|
MaxEmptyLinesToKeep: 1
|
||||||
|
ObjCBlockIndentWidth: 4
|
||||||
|
ObjCBreakBeforeNestedBlockParam: false
|
||||||
|
SortIncludes: false
|
||||||
|
TabWidth: 4
|
||||||
|
UseTab: Always
|
||||||
|
...
|
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Add prettier to the project
|
||||||
|
41024ddb7961b04a5688bbc997cb74de6fab4763
|
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
**/node_modules
|
||||||
|
.keys
|
||||||
|
.zsign_cache/
|
||||||
|
db.*
|
||||||
|
deps/ios_toolchain/
|
||||||
|
deps/openssl/
|
||||||
|
dist/
|
||||||
|
out
|
14
.prettierignore
Normal file
14
.prettierignore
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
node_modules
|
||||||
|
src
|
||||||
|
deps
|
||||||
|
.clang-format
|
||||||
|
|
||||||
|
# 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
|
882
GNUmakefile
Normal file
882
GNUmakefile
Normal file
@ -0,0 +1,882 @@
|
|||||||
|
.ONESHELL:
|
||||||
|
.DELETE_ON_ERROR:
|
||||||
|
MAKEFLAGS += --warn-undefined-variables
|
||||||
|
MAKEFLAGS += --no-builtin-rules
|
||||||
|
|
||||||
|
VERSION_CODE := 16
|
||||||
|
VERSION_NUMBER := 0.0.16
|
||||||
|
VERSION_NAME := Now with 38% more process.
|
||||||
|
|
||||||
|
PROJECT = tildefriends
|
||||||
|
BUILD_DIR ?= out
|
||||||
|
UNAME_S := $(shell uname -s)
|
||||||
|
UNAME_M := $(shell uname -m)
|
||||||
|
|
||||||
|
ANDROID_SDK ?= ~/Android/Sdk
|
||||||
|
|
||||||
|
ifeq ($(UNAME_S),Darwin)
|
||||||
|
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
|
||||||
|
else ifeq ($(UNAME_S),Linux)
|
||||||
|
BUILD_TYPES := debug release
|
||||||
|
HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1,0)
|
||||||
|
HAVE_LINUX_IOS = $(if $(shell which deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang),1,0)
|
||||||
|
HAVE_WIN = $(if $(shell which x86_64-w64-mingw32-gcc-win32),1,0)
|
||||||
|
else ifeq ($(UNAME_S),Haiku)
|
||||||
|
BUILD_TYPES := debug release
|
||||||
|
CFLAGS += -Dstatic_assert=_Static_assert
|
||||||
|
LDFLAGS += \
|
||||||
|
-lbsd \
|
||||||
|
-lnetwork
|
||||||
|
else ifeq ($(UNAME_S),OpenBSD)
|
||||||
|
BUILD_TYPES := debug release
|
||||||
|
CFLAGS += \
|
||||||
|
-Wno-unknown-warning-option
|
||||||
|
LDFLAGS += \
|
||||||
|
-lexecinfo \
|
||||||
|
-lc++abi
|
||||||
|
HAVE_ANDROID := 0
|
||||||
|
HAVE_LINUX_IOS := 0
|
||||||
|
HAVE_WIN := 0
|
||||||
|
else
|
||||||
|
$(error Unexpected host platform $(UNAME_S).)
|
||||||
|
endif
|
||||||
|
|
||||||
|
CFLAGS += \
|
||||||
|
-std=gnu11 \
|
||||||
|
-Wall \
|
||||||
|
-Wextra \
|
||||||
|
-Wno-unused-parameter \
|
||||||
|
-MMD \
|
||||||
|
-MP \
|
||||||
|
-ffunction-sections \
|
||||||
|
-fdata-sections \
|
||||||
|
-fno-exceptions \
|
||||||
|
-g
|
||||||
|
|
||||||
|
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
|
||||||
|
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-34
|
||||||
|
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.1.10909125
|
||||||
|
ANDROID_MIN_SDK_VERSION := 24
|
||||||
|
ANDROID_TARGET_SDK_VERSION := 34
|
||||||
|
|
||||||
|
ANDROID_ARMV7A_TARGETS := \
|
||||||
|
out/androiddebug-armv7a/tildefriends \
|
||||||
|
out/androidrelease-armv7a/tildefriends
|
||||||
|
ANDROID_ARM64_TARGETS := \
|
||||||
|
out/androiddebug/tildefriends \
|
||||||
|
out/androidrelease/tildefriends
|
||||||
|
ANDROID_X86_TARGETS := \
|
||||||
|
out/androiddebug-x86/tildefriends \
|
||||||
|
out/androidrelease-x86/tildefriends
|
||||||
|
ANDROID_X86_64_TARGETS := \
|
||||||
|
out/androiddebug-x86_64/tildefriends \
|
||||||
|
out/androidrelease-x86_64/tildefriends
|
||||||
|
ANDROID_TARGETS := \
|
||||||
|
$(ANDROID_X86_TARGETS) \
|
||||||
|
$(ANDROID_X86_64_TARGETS) \
|
||||||
|
$(ANDROID_ARMV7A_TARGETS) \
|
||||||
|
$(ANDROID_ARM64_TARGETS)
|
||||||
|
ifeq ($(HAVE_ANDROID),1)
|
||||||
|
BUILD_TYPES += \
|
||||||
|
androiddebug \
|
||||||
|
androidrelease \
|
||||||
|
androiddebug-armv7a \
|
||||||
|
androidrelease-armv7a \
|
||||||
|
androiddebug-x86 \
|
||||||
|
androidrelease-x86 \
|
||||||
|
androiddebug-x86_64 \
|
||||||
|
androidrelease-x86_64
|
||||||
|
all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk
|
||||||
|
endif
|
||||||
|
|
||||||
|
WINDOWS_TARGETS := \
|
||||||
|
out/windebug/tildefriends.exe \
|
||||||
|
out/winrelease/tildefriends.exe
|
||||||
|
ifeq ($(HAVE_WIN),1)
|
||||||
|
BUILD_TYPES += windebug winrelease
|
||||||
|
endif
|
||||||
|
|
||||||
|
LINUX_TARGETS := \
|
||||||
|
out/debug/tildefriends \
|
||||||
|
out/release/tildefriends
|
||||||
|
MACOS_TARGETS := \
|
||||||
|
out/macosdebug/tildefriends \
|
||||||
|
out/macosrelease/tildefriends
|
||||||
|
IOS_TARGETS := \
|
||||||
|
out/iosdebug/tildefriends \
|
||||||
|
out/iosrelease/tildefriends
|
||||||
|
IOSSIM_TARGETS := \
|
||||||
|
out/iossimdebug/tildefriends \
|
||||||
|
out/iossimrelease/tildefriends
|
||||||
|
IOS_APPS = \
|
||||||
|
out/tildefriends-iosdebug.app/tildefriends \
|
||||||
|
out/tildefriends-iosrelease.app/tildefriends
|
||||||
|
ifeq ($(HAVE_LINUX_IOS),1)
|
||||||
|
BUILD_TYPES += iosdebug iosrelease
|
||||||
|
all: $(IOS_APPS)
|
||||||
|
endif
|
||||||
|
ifeq ($(UNAME_S),Darwin)
|
||||||
|
all: $(IOS_APPS) \
|
||||||
|
out/tildefriends-iossimdebug.app/tildefriends \
|
||||||
|
out/tildefriends-iossimrelease.app/tildefriends
|
||||||
|
endif
|
||||||
|
|
||||||
|
DEBUG_TARGETS := \
|
||||||
|
out/debug/tildefriends \
|
||||||
|
out/windebug/tildefriends.exe \
|
||||||
|
out/iosdebug/tildefriends \
|
||||||
|
out/iossimdebug/tildefriends \
|
||||||
|
out/macosdebug/tildefriends \
|
||||||
|
out/androiddebug/tildefriends \
|
||||||
|
out/androiddebug-armv7a/tildefriends \
|
||||||
|
out/androiddebug-x86_64/tildefriends \
|
||||||
|
out/androiddebug-x86/tildefriends
|
||||||
|
RELEASE_TARGETS := \
|
||||||
|
out/release/tildefriends \
|
||||||
|
out/winrelease/tildefriends.exe \
|
||||||
|
out/iosrelease/tildefriends \
|
||||||
|
out/iossimrelease/tildefriends \
|
||||||
|
out/macosrelease/tildefriends \
|
||||||
|
out/androidrelease/tildefriends \
|
||||||
|
out/androidrelease-armv7a/tildefriends \
|
||||||
|
out/androidrelease-x86_64/tildefriends \
|
||||||
|
out/androidrelease-x86/tildefriends
|
||||||
|
ALL_TARGETS = $(DEBUG_TARGETS) $(RELEASE_TARGETS)
|
||||||
|
ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
|
||||||
|
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
|
||||||
|
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(ALL_TARGETS))
|
||||||
|
NONMACOS_TARGETS := $(filter-out $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS),$(ALL_TARGETS))
|
||||||
|
|
||||||
|
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
|
||||||
|
$(filter-out $(ANDROID_TARGETS) $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
|
||||||
|
$(ANDROID_TARGETS): CFLAGS += \
|
||||||
|
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
|
||||||
|
-fPIC \
|
||||||
|
-fdebug-compilation-dir . \
|
||||||
|
-fomit-frame-pointer \
|
||||||
|
-fno-asynchronous-unwind-tables \
|
||||||
|
-funwind-tables
|
||||||
|
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
|
||||||
|
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
|
||||||
|
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
|
||||||
|
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
|
||||||
|
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
|
||||||
|
$(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32
|
||||||
|
$(WINDOWS_TARGETS): AS = $(CC)
|
||||||
|
$(WINDOWS_TARGETS): CFLAGS += \
|
||||||
|
-D_WIN32_WINNT=0x0A00 \
|
||||||
|
-DWINVER=0x0A00 \
|
||||||
|
-DNTDDI_VERSION=NTDDI_WIN10 \
|
||||||
|
-Ideps/openssl/mingw64/usr/local/include
|
||||||
|
$(WINDOWS_TARGETS): LDFLAGS += \
|
||||||
|
-static \
|
||||||
|
-lm \
|
||||||
|
-Ldeps/openssl/mingw64/usr/local/lib
|
||||||
|
ifeq ($(UNAME_S),Darwin)
|
||||||
|
$(MACOS_TARGETS): CC = xcrun clang
|
||||||
|
$(IOS_TARGETS): IOS_SYSROOT := $(shell xcrun --sdk iphoneos --show-sdk-path)
|
||||||
|
$(IOS_TARGETS): CC = xcrun --sdk iphoneos clang -isysroot $(IOS_SYSROOT) -arch arm64
|
||||||
|
$(IOSSIM_TARGETS): IOSSIM_SYSROOT := $(shell xcrun --sdk iphonesimulator --show-sdk-path)
|
||||||
|
$(IOSSIM_TARGETS): CC = xcrun --sdk iphonesimulator clang -isysroot $(IOSSIM_SYSROOT) -arch x86_64
|
||||||
|
else ifeq ($(UNAME_S),Linux)
|
||||||
|
$(IOS_TARGETS): IOS_SYSROOT := deps/iPhoneOS17.0.sdk
|
||||||
|
$(IOS_TARGETS): CC = PATH=$$PATH:deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang
|
||||||
|
endif
|
||||||
|
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
|
||||||
|
$(ANDROID_X86_TARGETS): ANDROID_NDK_TARGET_TRIPLE := i686-linux-android
|
||||||
|
$(ANDROID_ARMV7A_TARGETS): ANDROID_NDK_TARGET_TRIPLE := armv7a-linux-androideabi
|
||||||
|
$(ANDROID_ARM64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := aarch64-linux-android
|
||||||
|
$(ANDROID_TARGETS): CC = $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/$(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION)-clang
|
||||||
|
$(ANDROID_TARGETS): AS = $(CC)
|
||||||
|
$(ANDROID_TARGETS): CFLAGS += \
|
||||||
|
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
|
||||||
|
-Wno-unknown-warning-option
|
||||||
|
$(ANDROID_ARMV7A_TARGETS): CFLAGS += -Ideps/openssl/android/armeabi-v7a/usr/local/include
|
||||||
|
$(ANDROID_ARMV7A_TARGETS): LDFLAGS += -Ldeps/openssl/android/armeabi-v7a/usr/local/lib
|
||||||
|
$(ANDROID_ARM64_TARGETS): CFLAGS += -Ideps/openssl/android/arm64-v8a/usr/local/include
|
||||||
|
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Ldeps/openssl/android/arm64-v8a/usr/local/lib
|
||||||
|
$(ANDROID_X86_TARGETS): CFLAGS += -Ideps/openssl/android/x86/usr/local/include
|
||||||
|
$(ANDROID_X86_TARGETS): CFLAGS += -Wno-atomic-alignment
|
||||||
|
$(ANDROID_X86_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86/usr/local/lib
|
||||||
|
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
|
||||||
|
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
|
||||||
|
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
|
||||||
|
$(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections
|
||||||
|
$(IOS_TARGETS): CFLAGS += -mios-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
|
||||||
|
$(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
|
||||||
|
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
|
||||||
|
$(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/lib
|
||||||
|
|
||||||
|
ifeq ($(UNAME_M),x86_64)
|
||||||
|
ifneq ($(UNAME_S),Haiku)
|
||||||
|
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||||
|
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifeq ($(UNAME_M),aarch64)
|
||||||
|
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||||
|
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||||
|
endif
|
||||||
|
|
||||||
|
get_objs = \
|
||||||
|
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
|
||||||
|
$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
|
||||||
|
$(foreach build_type,windebug winrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_win))))) \
|
||||||
|
$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androiddebug-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
|
||||||
|
$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androidrelease-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
|
||||||
|
$(foreach build_type,macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_macos))))) \
|
||||||
|
$(foreach build_type,iosdebug iosrelease iossimdebug iossimrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_ios))))) \
|
||||||
|
$(foreach build_type,androiddebug-x86 androidrelease-x86,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_x86)))))
|
||||||
|
|
||||||
|
APP_SOURCES := $(wildcard src/*.c)
|
||||||
|
APP_SOURCES_ios := $(wildcard src/*.m)
|
||||||
|
APP_OBJS := $(call get_objs,APP_SOURCES)
|
||||||
|
$(APP_OBJS): CFLAGS += \
|
||||||
|
-Ideps/base64c/include \
|
||||||
|
-Ideps/crypt_blowfish \
|
||||||
|
-Ideps/libbacktrace \
|
||||||
|
-Ideps/libsodium \
|
||||||
|
-Ideps/libsodium/src/libsodium/include \
|
||||||
|
-Ideps/libuv/include \
|
||||||
|
-Ideps/zlib \
|
||||||
|
-Ideps/zlib/contrib/minizip \
|
||||||
|
-Ideps/picohttpparser \
|
||||||
|
-Ideps/quickjs \
|
||||||
|
-Ideps/sqlite \
|
||||||
|
-Ideps/valgrind \
|
||||||
|
-Wdouble-promotion \
|
||||||
|
-Werror
|
||||||
|
ifeq ($(UNAME_M),x86_64)
|
||||||
|
$(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/macos% $(BUILD_DIR)/ios%,$(APP_OBJS)): CFLAGS += \
|
||||||
|
-fanalyzer
|
||||||
|
endif
|
||||||
|
|
||||||
|
BLOWFISH_SOURCES := \
|
||||||
|
deps/crypt_blowfish/crypt_blowfish.c \
|
||||||
|
deps/crypt_blowfish/crypt_gensalt.c \
|
||||||
|
deps/crypt_blowfish/wrapper.c
|
||||||
|
BLOWFISH_SOURCES_win := \
|
||||||
|
deps/crypt_blowfish/x86.S
|
||||||
|
BLOWFISH_SOURCES_x86 := \
|
||||||
|
deps/crypt_blowfish/x86.S
|
||||||
|
BLOWFISH_OBJS := $(call get_objs,BLOWFISH_SOURCES)
|
||||||
|
|
||||||
|
UV_SOURCES := \
|
||||||
|
deps/libuv/src/fs-poll.c \
|
||||||
|
deps/libuv/src/idna.c \
|
||||||
|
deps/libuv/src/inet.c \
|
||||||
|
deps/libuv/src/random.c \
|
||||||
|
deps/libuv/src/strscpy.c \
|
||||||
|
deps/libuv/src/strtok.c \
|
||||||
|
deps/libuv/src/threadpool.c \
|
||||||
|
deps/libuv/src/timer.c \
|
||||||
|
deps/libuv/src/uv-common.c \
|
||||||
|
deps/libuv/src/uv-data-getter-setters.c \
|
||||||
|
deps/libuv/src/version.c
|
||||||
|
UV_SOURCES_unix := \
|
||||||
|
deps/libuv/src/unix/async.c \
|
||||||
|
deps/libuv/src/unix/core.c \
|
||||||
|
deps/libuv/src/unix/dl.c \
|
||||||
|
deps/libuv/src/unix/fs.c \
|
||||||
|
deps/libuv/src/unix/getaddrinfo.c \
|
||||||
|
deps/libuv/src/unix/getnameinfo.c \
|
||||||
|
deps/libuv/src/unix/loop-watcher.c \
|
||||||
|
deps/libuv/src/unix/loop.c \
|
||||||
|
deps/libuv/src/unix/pipe.c \
|
||||||
|
deps/libuv/src/unix/poll.c \
|
||||||
|
deps/libuv/src/unix/process.c \
|
||||||
|
deps/libuv/src/unix/random-devurandom.c \
|
||||||
|
deps/libuv/src/unix/random-getrandom.c \
|
||||||
|
deps/libuv/src/unix/signal.c \
|
||||||
|
deps/libuv/src/unix/stream.c \
|
||||||
|
deps/libuv/src/unix/tcp.c \
|
||||||
|
deps/libuv/src/unix/thread.c \
|
||||||
|
deps/libuv/src/unix/tty.c \
|
||||||
|
deps/libuv/src/unix/udp.c
|
||||||
|
ifeq ($(UNAME_S),Linux)
|
||||||
|
UV_SOURCES_unix += \
|
||||||
|
deps/libuv/src/unix/linux.c \
|
||||||
|
deps/libuv/src/unix/procfs-exepath.c \
|
||||||
|
deps/libuv/src/unix/proctitle.c \
|
||||||
|
deps/libuv/src/unix/random-sysctl-linux.c
|
||||||
|
else ifeq ($(UNAME_S),Haiku)
|
||||||
|
UV_SOURCES_unix += \
|
||||||
|
deps/libuv/src/unix/bsd-ifaddrs.c \
|
||||||
|
deps/libuv/src/unix/haiku.c \
|
||||||
|
deps/libuv/src/unix/no-fsevents.c \
|
||||||
|
deps/libuv/src/unix/no-proctitle.c \
|
||||||
|
deps/libuv/src/unix/posix-hrtime.c \
|
||||||
|
deps/libuv/src/unix/posix-poll.c
|
||||||
|
else ifeq ($(UNAME_S),OpenBSD)
|
||||||
|
UV_SOURCES_unix += \
|
||||||
|
deps/libuv/src/unix/bsd-ifaddrs.c \
|
||||||
|
deps/libuv/src/unix/kqueue.c \
|
||||||
|
deps/libuv/src/unix/no-proctitle.c \
|
||||||
|
deps/libuv/src/unix/openbsd.c \
|
||||||
|
deps/libuv/src/unix/posix-hrtime.c \
|
||||||
|
deps/libuv/src/unix/random-getentropy.c
|
||||||
|
endif
|
||||||
|
UV_SOURCES_android := \
|
||||||
|
deps/libuv/src/unix/random-getentropy.c
|
||||||
|
UV_SOURCES_win := \
|
||||||
|
deps/libuv/src/win/async.c \
|
||||||
|
deps/libuv/src/win/core.c \
|
||||||
|
deps/libuv/src/win/detect-wakeup.c \
|
||||||
|
deps/libuv/src/win/dl.c \
|
||||||
|
deps/libuv/src/win/error.c \
|
||||||
|
deps/libuv/src/win/fs-event.c \
|
||||||
|
deps/libuv/src/win/fs.c \
|
||||||
|
deps/libuv/src/win/getaddrinfo.c \
|
||||||
|
deps/libuv/src/win/getnameinfo.c \
|
||||||
|
deps/libuv/src/win/handle.c \
|
||||||
|
deps/libuv/src/win/loop-watcher.c \
|
||||||
|
deps/libuv/src/win/pipe.c \
|
||||||
|
deps/libuv/src/win/poll.c \
|
||||||
|
deps/libuv/src/win/process-stdio.c \
|
||||||
|
deps/libuv/src/win/process.c \
|
||||||
|
deps/libuv/src/win/signal.c \
|
||||||
|
deps/libuv/src/win/snprintf.c \
|
||||||
|
deps/libuv/src/win/stream.c \
|
||||||
|
deps/libuv/src/win/tcp.c \
|
||||||
|
deps/libuv/src/win/thread.c \
|
||||||
|
deps/libuv/src/win/tty.c \
|
||||||
|
deps/libuv/src/win/udp.c \
|
||||||
|
deps/libuv/src/win/util.c \
|
||||||
|
deps/libuv/src/win/winapi.c \
|
||||||
|
deps/libuv/src/win/winsock.c
|
||||||
|
UV_SOURCES_macos := \
|
||||||
|
deps/libuv/src/unix/async.c \
|
||||||
|
deps/libuv/src/unix/bsd-ifaddrs.c \
|
||||||
|
deps/libuv/src/unix/core.c \
|
||||||
|
deps/libuv/src/unix/darwin.c \
|
||||||
|
deps/libuv/src/unix/darwin-proctitle.c \
|
||||||
|
deps/libuv/src/unix/dl.c \
|
||||||
|
deps/libuv/src/unix/fs.c \
|
||||||
|
deps/libuv/src/unix/fsevents.c \
|
||||||
|
deps/libuv/src/unix/getaddrinfo.c \
|
||||||
|
deps/libuv/src/unix/getnameinfo.c \
|
||||||
|
deps/libuv/src/unix/kqueue.c \
|
||||||
|
deps/libuv/src/unix/loop-watcher.c \
|
||||||
|
deps/libuv/src/unix/loop.c \
|
||||||
|
deps/libuv/src/unix/pipe.c \
|
||||||
|
deps/libuv/src/unix/poll.c \
|
||||||
|
deps/libuv/src/unix/process.c \
|
||||||
|
deps/libuv/src/unix/proctitle.c \
|
||||||
|
deps/libuv/src/unix/random-devurandom.c \
|
||||||
|
deps/libuv/src/unix/random-getentropy.c \
|
||||||
|
deps/libuv/src/unix/signal.c \
|
||||||
|
deps/libuv/src/unix/stream.c \
|
||||||
|
deps/libuv/src/unix/tcp.c \
|
||||||
|
deps/libuv/src/unix/thread.c \
|
||||||
|
deps/libuv/src/unix/tty.c \
|
||||||
|
deps/libuv/src/unix/udp.c
|
||||||
|
UV_OBJS := $(call get_objs,UV_SOURCES)
|
||||||
|
$(UV_OBJS): CFLAGS += \
|
||||||
|
-Ideps/libuv/include \
|
||||||
|
-Ideps/libuv/src \
|
||||||
|
-Wno-dangling-pointer \
|
||||||
|
-Wno-incompatible-pointer-types \
|
||||||
|
-Wno-maybe-uninitialized \
|
||||||
|
-Wno-sign-compare \
|
||||||
|
-Wno-unused-but-set-parameter \
|
||||||
|
-Wno-unused-but-set-variable \
|
||||||
|
-Wno-unused-result \
|
||||||
|
-Wno-unused-variable
|
||||||
|
ifeq ($(UNAME_S),Linux)
|
||||||
|
$(UV_OBJS): CFLAGS += \
|
||||||
|
-D_GNU_SOURCE
|
||||||
|
else ifeq ($(UNAME_S),Haiku)
|
||||||
|
$(UV_OBJS): CFLAGS += \
|
||||||
|
-D_BSD_SOURCE \
|
||||||
|
-Wno-format-truncation
|
||||||
|
endif
|
||||||
|
|
||||||
|
SODIUM_SOURCES := \
|
||||||
|
deps/libsodium/src/libsodium/crypto_aead/aegis128l/aead_aegis128l.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_aead/aegis128l/aegis128l_soft.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_aead/aegis256/aead_aegis256.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_aead/aegis256/aegis256_soft.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_auth/hmacsha512/auth_hmacsha512.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_auth/hmacsha512256/auth_hmacsha512256.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_box/crypto_box.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_box/curve25519xsalsa20poly1305/box_curve25519xsalsa20poly1305.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_core/ed25519/ref10/ed25519_ref10.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_core/hsalsa20/ref2/core_hsalsa20_ref2.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_core/salsa/ref/core_salsa_ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_core/softaes/softaes.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-compress-ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/generichash_blake2b.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_hash/sha256/cp/hash_sha256_cp.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_hash/sha256/hash_sha256.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_hash/sha512/cp/hash_sha512_cp.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/donna/poly1305_donna.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/onetimeauth_poly1305.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-core.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-fill-block-ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_pwhash/argon2/blake2b-long.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_scalarmult/crypto_scalarmult.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/sandy2x/curve25519_sandy2x.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/scalarmult_curve25519.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_secretbox/crypto_secretbox_easy.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_secretbox/xsalsa20poly1305/secretbox_xsalsa20poly1305.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/crypto_sign.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/keypair.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/open.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/sign.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_sign/ed25519/sign_ed25519.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/chacha20/ref/chacha20_ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/chacha20/stream_chacha20.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/salsa20/ref/salsa20_ref.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/salsa20/stream_salsa20.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_stream/xsalsa20/stream_xsalsa20.c \
|
||||||
|
deps/libsodium/src/libsodium/crypto_verify/verify.c \
|
||||||
|
deps/libsodium/src/libsodium/randombytes/randombytes.c \
|
||||||
|
deps/libsodium/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/core.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/codecs.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/runtime.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/utils.c \
|
||||||
|
deps/libsodium/src/libsodium/sodium/version.c
|
||||||
|
SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES)
|
||||||
|
$(SODIUM_OBJS): CFLAGS += \
|
||||||
|
-DCONFIGURED=1 \
|
||||||
|
-DMINIMAL=1 \
|
||||||
|
-DHAVE_ALLOCA \
|
||||||
|
-DHAVE_CPUID_V \
|
||||||
|
-DHAVE_GCC_MEMORY_FENCES \
|
||||||
|
-Wno-unused-function \
|
||||||
|
-Wno-unused-variable \
|
||||||
|
-Wno-type-limits \
|
||||||
|
-Wno-unknown-pragmas \
|
||||||
|
-Wno-attributes \
|
||||||
|
-Ideps/libsodium/builds/msvc \
|
||||||
|
-Ideps/libsodium/src/libsodium/include/sodium
|
||||||
|
ifneq ($(UNAME_S),OpenBSD)
|
||||||
|
$(filter-out $(BUILD_DIR)/win%,$(SODIUM_OBJS)): CFLAGS += \
|
||||||
|
-DHAVE_ALLOCA_H
|
||||||
|
endif
|
||||||
|
|
||||||
|
SQLITE_SOURCES := deps/sqlite/sqlite3.c
|
||||||
|
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
|
||||||
|
$(SQLITE_OBJS): CFLAGS += \
|
||||||
|
-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
|
||||||
|
-DSQLITE_DEFAULT_MEMSTATUS=0 \
|
||||||
|
-DSQLITE_DQS=0 \
|
||||||
|
-DSQLITE_ENABLE_MEMSYS5 \
|
||||||
|
-DSQLITE_ENABLE_FTS5 \
|
||||||
|
-DSQLITE_ENABLE_JSON1 \
|
||||||
|
-DSQLITE_LIKE_DOESNT_MATCH_BLOBS \
|
||||||
|
-DSQLITE_MAX_ATTACHED=1 \
|
||||||
|
-DSQLITE_MAX_COLUMN=100 \
|
||||||
|
-DSQLITE_MAX_COMPOUND_SELECT=300 \
|
||||||
|
-DSQLITE_MAX_EXPR_DEPTH=40 \
|
||||||
|
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
||||||
|
-DSQLITE_MAX_LENGTH=5242880 \
|
||||||
|
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
|
||||||
|
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
||||||
|
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
|
||||||
|
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
|
||||||
|
-DSQLITE_MAX_VDBE_OP=25000 \
|
||||||
|
-DSQLITE_OMIT_DEPRECATED \
|
||||||
|
-DSQLITE_OMIT_DESERIALIZE \
|
||||||
|
-DSQLITE_OMIT_LOAD_EXTENSION \
|
||||||
|
-DSQLITE_OMIT_TCL_VARIABLE \
|
||||||
|
-DSQLITE_PRAGMA_DEFAULT_WAL_SYNCHRONOUS=1 \
|
||||||
|
-DSQLITE_SECURE_DELETE \
|
||||||
|
-DSQLITE_THREADSAFE=2 \
|
||||||
|
-DSQLITE_UNTESTABLE \
|
||||||
|
-DSQLITE_USE_ALLOCA \
|
||||||
|
-DHAVE_ISNAN \
|
||||||
|
-DHAVE_GETHOSTUUID=0 \
|
||||||
|
-Wno-implicit-fallthrough \
|
||||||
|
-Wno-unused-but-set-variable \
|
||||||
|
-Wno-unused-function \
|
||||||
|
-Wno-unused-variable
|
||||||
|
|
||||||
|
QUICKJS_SOURCES := \
|
||||||
|
deps/quickjs/cutils.c \
|
||||||
|
deps/quickjs/libbf.c \
|
||||||
|
deps/quickjs/libregexp.c \
|
||||||
|
deps/quickjs/libunicode.c \
|
||||||
|
deps/quickjs/quickjs.c
|
||||||
|
QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES)
|
||||||
|
$(QUICKJS_OBJS): CFLAGS += \
|
||||||
|
-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
|
||||||
|
-DCONFIG_BIGNUM \
|
||||||
|
-D_GNU_SOURCE \
|
||||||
|
-Wno-enum-conversion \
|
||||||
|
-Wno-implicit-const-int-float-conversion \
|
||||||
|
-Wno-implicit-fallthrough \
|
||||||
|
-Wno-sign-compare \
|
||||||
|
-Wno-unused-but-set-variable \
|
||||||
|
-Wno-unused-variable
|
||||||
|
$(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS
|
||||||
|
|
||||||
|
ifeq ($(UNAME_S),Haiku)
|
||||||
|
$(QUICKJS_OBJS): CFLAGS += "-Dmalloc_usable_size(x)=0"
|
||||||
|
else ifeq ($(UNAME_S),OpenBSD)
|
||||||
|
$(QUICKJS_OBJS): CFLAGS += "-Dmalloc_usable_size(x)=0"
|
||||||
|
endif
|
||||||
|
|
||||||
|
LIBBACKTRACE_SOURCES := \
|
||||||
|
deps/libbacktrace/atomic.c \
|
||||||
|
deps/libbacktrace/backtrace.c \
|
||||||
|
deps/libbacktrace/dwarf.c \
|
||||||
|
deps/libbacktrace/fileline.c \
|
||||||
|
deps/libbacktrace/print.c \
|
||||||
|
deps/libbacktrace/simple.c \
|
||||||
|
deps/libbacktrace/sort.c \
|
||||||
|
deps/libbacktrace/state.c
|
||||||
|
LIBBACKTRACE_SOURCES_unix := \
|
||||||
|
deps/libbacktrace/elf.c \
|
||||||
|
deps/libbacktrace/mmap.c \
|
||||||
|
deps/libbacktrace/mmapio.c \
|
||||||
|
deps/libbacktrace/posix.c
|
||||||
|
LIBBACKTRACE_SOURCES_win := \
|
||||||
|
deps/libbacktrace/alloc.c \
|
||||||
|
deps/libbacktrace/pecoff.c \
|
||||||
|
deps/libbacktrace/posix.c \
|
||||||
|
deps/libbacktrace/read.c
|
||||||
|
LIBBACKTRACE_SOURCES_macos := \
|
||||||
|
deps/libbacktrace/dwarf.c \
|
||||||
|
deps/libbacktrace/macho.c \
|
||||||
|
deps/libbacktrace/mmap.c \
|
||||||
|
deps/libbacktrace/mmapio.c \
|
||||||
|
deps/libbacktrace/posix.c
|
||||||
|
LIBBACKTRACE_OBJS := $(call get_objs,LIBBACKTRACE_SOURCES)
|
||||||
|
$(LIBBACKTRACE_OBJS): CFLAGS += \
|
||||||
|
-Ideps/libbacktrace_config \
|
||||||
|
-Wno-unused-but-set-variable \
|
||||||
|
-Wno-maybe-initialized \
|
||||||
|
-Wno-unused-function \
|
||||||
|
-DBACKTRACE_ELF_SIZE=64
|
||||||
|
|
||||||
|
PICOHTTPPARSER_SOURCES := \
|
||||||
|
deps/picohttpparser/picohttpparser.c
|
||||||
|
PICOHTTPPARSER_OBJS := $(call get_objs,PICOHTTPPARSER_SOURCES)
|
||||||
|
|
||||||
|
MINIUNZIP_SOURCES := \
|
||||||
|
deps/zlib/contrib/minizip/unzip.c \
|
||||||
|
deps/zlib/contrib/minizip/ioapi.c \
|
||||||
|
deps/zlib/adler32.c \
|
||||||
|
deps/zlib/crc32.c \
|
||||||
|
deps/zlib/inffast.c \
|
||||||
|
deps/zlib/inflate.c \
|
||||||
|
deps/zlib/inftrees.c \
|
||||||
|
deps/zlib/zutil.c
|
||||||
|
MINIUNZIP_OBJS := $(call get_objs,MINIUNZIP_SOURCES)
|
||||||
|
$(MINIUNZIP_OBJS): CFLAGS += \
|
||||||
|
-Ideps/zlib \
|
||||||
|
-Wno-maybe-uninitialized
|
||||||
|
|
||||||
|
LDFLAGS += \
|
||||||
|
-pthread \
|
||||||
|
-lm
|
||||||
|
debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
||||||
|
-lssl \
|
||||||
|
-lcrypto
|
||||||
|
ifneq ($(UNAME_S),Haiku)
|
||||||
|
ifneq ($(UNAME_S),OpenBSD)
|
||||||
|
debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
||||||
|
-ldl
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
$(WINDOWS_TARGETS): LDFLAGS += \
|
||||||
|
-lssl \
|
||||||
|
-lcrypto \
|
||||||
|
-lcrypt32 \
|
||||||
|
-ldbghelp \
|
||||||
|
-liphlpapi \
|
||||||
|
-lkernel32 \
|
||||||
|
-lole32 \
|
||||||
|
-luserenv \
|
||||||
|
-luuid \
|
||||||
|
-lws2_32 \
|
||||||
|
-lwsock32
|
||||||
|
$(ANDROID_TARGETS): LDFLAGS += \
|
||||||
|
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
|
||||||
|
-ldl \
|
||||||
|
-llog \
|
||||||
|
-lssl \
|
||||||
|
-lcrypto
|
||||||
|
$(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): CFLAGS += \
|
||||||
|
-Wno-unknown-warning-option
|
||||||
|
$(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
||||||
|
-framework Foundation \
|
||||||
|
-framework CoreFoundation \
|
||||||
|
-framework UIKit \
|
||||||
|
-framework WebKit
|
||||||
|
|
||||||
|
unix: debug release
|
||||||
|
win: windebug winrelease
|
||||||
|
all: $(BUILD_TYPES)
|
||||||
|
.PHONY: all win unix
|
||||||
|
|
||||||
|
ALL_APP_OBJS := \
|
||||||
|
$(APP_OBJS) \
|
||||||
|
$(BLOWFISH_OBJS) \
|
||||||
|
$(LIBBACKTRACE_OBJS) \
|
||||||
|
$(MINIUNZIP_OBJS) \
|
||||||
|
$(PICOHTTPPARSER_OBJS) \
|
||||||
|
$(QUICKJS_OBJS) \
|
||||||
|
$(SODIUM_OBJS) \
|
||||||
|
$(SQLITE_OBJS) \
|
||||||
|
$(UV_OBJS)
|
||||||
|
|
||||||
|
DEPS = $(ALL_APP_OBJS:.o=.d)
|
||||||
|
-include $(DEPS)
|
||||||
|
|
||||||
|
define build_rules
|
||||||
|
$(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
|
||||||
|
.PHONY: $(1)
|
||||||
|
|
||||||
|
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
|
||||||
|
@echo "[link] $$@"
|
||||||
|
@$$(CC) -o $$@ $$^ $$(LDFLAGS)
|
||||||
|
|
||||||
|
$(BUILD_DIR)/$(1)/%.o: %.c
|
||||||
|
@mkdir -p $$(dir $$@)
|
||||||
|
@echo "[c] $$@"
|
||||||
|
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
||||||
|
|
||||||
|
$(BUILD_DIR)/$(1)/%.o: %.m
|
||||||
|
@mkdir -p $$(dir $$@)
|
||||||
|
@echo "[m] $$@"
|
||||||
|
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
||||||
|
|
||||||
|
$(BUILD_DIR)/$(1)/%.o: %.S
|
||||||
|
@mkdir -p $$(dir $$@)
|
||||||
|
@echo "[as] $$@"
|
||||||
|
@$$(AS) -c $$< -o $$@
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
|
||||||
|
|
||||||
|
src/version.h : $(firstword $(MAKEFILE_LIST))
|
||||||
|
@echo "[version] $@"
|
||||||
|
@echo "#define VERSION_NUMBER \"$(VERSION_NUMBER)\"" > $@
|
||||||
|
@echo "#define VERSION_NAME \"$(VERSION_NAME)\"" >> $@
|
||||||
|
|
||||||
|
src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
|
||||||
|
@echo "[android_version] $@"
|
||||||
|
@sed -i \
|
||||||
|
-e 's/versionCode=".*"/versionCode="$(VERSION_CODE)"/' \
|
||||||
|
-e 's/versionName=".*"/versionName="$(VERSION_NUMBER)"/' \
|
||||||
|
-e 's/android:minSdkVersion="[[:digit:]]*"/android:minSdkVersion="$(ANDROID_MIN_SDK_VERSION)"/' \
|
||||||
|
-e 's/android:targetSdkVersion="[[:digit:]]*"/android:targetSdkVersion="$(ANDROID_TARGET_SDK_VERSION)"/' \
|
||||||
|
$@
|
||||||
|
|
||||||
|
# Android support.
|
||||||
|
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@echo "[aapt2] $@"
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
|
||||||
|
|
||||||
|
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@echo "[aapt2] $@"
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
|
||||||
|
|
||||||
|
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
|
||||||
|
|
||||||
|
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
|
||||||
|
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
|
||||||
|
|
||||||
|
$(CLASS_FILES) &: $(JAVA_FILES)
|
||||||
|
@echo "[javac] $(CLASS_FILES)"
|
||||||
|
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
|
||||||
|
|
||||||
|
out/apk/classes.dex: $(CLASS_FILES)
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@echo "[d8] $@"
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
|
||||||
|
|
||||||
|
PACKAGE_DIRS := \
|
||||||
|
apps/ \
|
||||||
|
core/ \
|
||||||
|
deps/codemirror/ \
|
||||||
|
deps/prettier/ \
|
||||||
|
deps/lit/
|
||||||
|
|
||||||
|
RAW_FILES := $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
|
||||||
|
|
||||||
|
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
|
||||||
|
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
|
||||||
|
out/apk/TildeFriends-x86-debug.unsigned.apk: BUILD_TYPE := debug
|
||||||
|
out/apk/TildeFriends-x86-release.unsigned.apk: BUILD_TYPE := release
|
||||||
|
|
||||||
|
out/apk/TildeFriends-arm-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
out/apk/TildeFriends-arm-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
out/apk/TildeFriends-x86-debug.unsigned.apk: out/apk/classes.dex out/androiddebug-x86_64/tildefriends out/androiddebug-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
out/apk/TildeFriends-x86-release.unsigned.apk: out/apk/classes.dex out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
|
||||||
|
out/apk/TildeFriends-arm-%.unsigned.apk:
|
||||||
|
@mkdir -p $(dir $@) out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/
|
||||||
|
@echo "[aapt] $@"
|
||||||
|
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
|
||||||
|
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
||||||
|
@cp out/apk/res.apk $@.zip
|
||||||
|
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
|
||||||
|
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
|
||||||
|
@zip -u $@.zip -q $(RAW_FILES)
|
||||||
|
@rm -f $@ && $(ANDROID_BUILD_TOOLS)/zipalign -z 4 $@.zip $@
|
||||||
|
|
||||||
|
out/apk/TildeFriends-x86-%.unsigned.apk:
|
||||||
|
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
|
||||||
|
@echo "[aapt] $@"
|
||||||
|
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
||||||
|
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
||||||
|
@cp out/apk/res.apk $@.zip
|
||||||
|
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
|
||||||
|
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
|
||||||
|
@zip -u $@.zip -q $(RAW_FILES)
|
||||||
|
@rm -f $@ && $(ANDROID_BUILD_TOOLS)/zipalign -z 4 $@.zip $@
|
||||||
|
|
||||||
|
out/%.apk: out/apk/%.unsigned.apk
|
||||||
|
@echo "[apksigner] $(notdir $@)"
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $<
|
||||||
|
|
||||||
|
release-apk: out/TildeFriends-arm-release.apk out/TildeFriends-x86-release.apk
|
||||||
|
.PHONY: release-apk
|
||||||
|
|
||||||
|
releaseapkgo: out/TildeFriends-arm-release.apk
|
||||||
|
@adb install -r $<
|
||||||
|
@adb shell am start com.unprompted.tildefriends/.MainActivity
|
||||||
|
.PHONY: releaseapkgo
|
||||||
|
|
||||||
|
# iOS Support
|
||||||
|
out/%.app/Info.plist: src/ios/Info.plist
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@cp -v $< $@
|
||||||
|
out/%.app/tildefriends.png: src/ios/tildefriends.png
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@cp -v $< $@
|
||||||
|
|
||||||
|
out/%/data.zip: $(RAW_FILES)
|
||||||
|
@zip -u $@ -q -9 $(RAW_FILES)
|
||||||
|
|
||||||
|
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/tildefriends-%.app/data.zip
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
@cp -v $< $@
|
||||||
|
ifeq ($(HAVE_LINUX_IOS),1)
|
||||||
|
@zsign -q -k .keys/apple.p12 -f -m src/ios/embedded.mobileprovision $(realpath $(dir $@))
|
||||||
|
endif
|
||||||
|
.SECONDARY:
|
||||||
|
out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
|
||||||
|
@echo "[ipa] $@"
|
||||||
|
@rm -rf $@.tmp $@
|
||||||
|
@mkdir -p $@.tmp/Payload/tildefriends.app/
|
||||||
|
@cp -R $(dir $<)/* $@.tmp/Payload/tildefriends.app/
|
||||||
|
@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
|
||||||
|
@rm -rf $@.tmp/
|
||||||
|
|
||||||
|
iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends
|
||||||
|
iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends
|
||||||
|
iosdebug-app: out/tildefriends-iosdebug.app/tildefriends
|
||||||
|
iosrelease-app: out/tildefriends-iosrelease.app/tildefriends
|
||||||
|
|
||||||
|
iosdebug-ipa: out/tildefriends-debug.ipa
|
||||||
|
iosrelease-ipa: out/tildefriends-release.ipa
|
||||||
|
.PHONY: iossimdebug-app iossimrelease-app iosdebug-app iosrelease-app
|
||||||
|
|
||||||
|
ios%go: out/tildefriends-ios%.app/tildefriends
|
||||||
|
ideviceinstaller -i $(realpath $(dir $<))
|
||||||
|
|
||||||
|
iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends
|
||||||
|
xcrun simctl install booted out/tildefriends-iossimdebug.app/
|
||||||
|
xcrun simctl launch booted com.unprompted.tildefriends
|
||||||
|
.PHONY: iossimdebuggo
|
||||||
|
|
||||||
|
apklog:
|
||||||
|
@adb logcat *:S tildefriends
|
||||||
|
.PHONY: apklog
|
||||||
|
|
||||||
|
fetchdeps:
|
||||||
|
@echo "[fetch] libuv"
|
||||||
|
@test -f out/deps/libuv.tar.gz || (mkdir -p out/deps/ && curl -q https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz -o out/deps/libuv.tar.gz)
|
||||||
|
@test -d deps/libuv/ || (mkdir -p deps/libuv/ && tar -C deps/libuv/ -m --strip=1 -xf out/deps/libuv.tar.gz)
|
||||||
|
@echo "[fetch] sqlite"
|
||||||
|
@test -f out/deps/sqlite.zip || (mkdir -p out/deps/ && curl -q https://www.sqlite.org/2024/sqlite-amalgamation-3450100.zip -o out/deps/sqlite.zip)
|
||||||
|
@test -d deps/sqlite/ || (mkdir -p deps/sqlite/ && unzip -qDj -d deps/sqlite/ out/deps/sqlite.zip)
|
||||||
|
@echo "[fetch] prettier"
|
||||||
|
@test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs
|
||||||
|
@test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs
|
||||||
|
@test -f deps/prettier/babel.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/babel.mjs
|
||||||
|
@test -f deps/prettier/estree.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/estree.mjs
|
||||||
|
.PHONY: fetchdeps
|
||||||
|
|
||||||
|
ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a
|
||||||
|
$(ANDROID_DEPS):
|
||||||
|
+@tools/ssl-android
|
||||||
|
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
|
||||||
|
|
||||||
|
ifeq ($(HAVE_WIN),1)
|
||||||
|
WINDOWS_DEPS := deps/openssl/mingw64/usr/local/lib/libssl.a
|
||||||
|
$(WINDOWS_DEPS):
|
||||||
|
+@tools/ssl-mingw64
|
||||||
|
$(filter $(BUILD_DIR)/win%,$(APP_OBJS)): | $(WINDOWS_DEPS)
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifeq ($(UNAME_S),Darwin)
|
||||||
|
IOS_DEPS := deps/openssl/ios/ios64-xcrun/usr/local/lib/libssl.a
|
||||||
|
$(IOS_DEPS):
|
||||||
|
+@tools/ssl-ios
|
||||||
|
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
|
||||||
|
endif
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILD_DIR)
|
||||||
|
.PHONY: clean
|
||||||
|
|
||||||
|
dist: release-apk iosrelease-ipa
|
||||||
|
@echo [archive] dist/tildefriends-$(VERSION_NUMBER).tar.xz
|
||||||
|
@rm -rf out/tildefriends-$(VERSION_NUMBER)
|
||||||
|
@mkdir -p dist/ out/tildefriends-$(VERSION_NUMBER)
|
||||||
|
@git archive main | tar -x -C out/tildefriends-$(VERSION_NUMBER)
|
||||||
|
@tar \
|
||||||
|
--exclude=apps/welcome* \
|
||||||
|
--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
|
||||||
|
--exclude=deps/libsodium/builds/msvc/vs* \
|
||||||
|
--exclude=deps/libsodium/builds/msvc/build \
|
||||||
|
--exclude=deps/libsodium/builds/msvc/properties \
|
||||||
|
--exclude=deps/libsodium/configure \
|
||||||
|
--exclude=deps/libsodium/test \
|
||||||
|
--exclude=deps/libuv/docs \
|
||||||
|
--exclude=deps/libuv/test \
|
||||||
|
--exclude=deps/openssl \
|
||||||
|
--exclude=deps/speedscope/*.map \
|
||||||
|
--exclude=deps/sqlite/shell.c \
|
||||||
|
--exclude=deps/zlib/contrib/vstudio \
|
||||||
|
--exclude=deps/zlib/doc \
|
||||||
|
-caf dist/tildefriends-$(VERSION_NUMBER).tar.xz out/tildefriends-$(VERSION_NUMBER)
|
||||||
|
@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk"
|
||||||
|
@cp out/TildeFriends-x86-release.apk dist/TildeFriends-x86-$(VERSION_NUMBER).apk
|
||||||
|
@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk"
|
||||||
|
@cp out/TildeFriends-arm-release.apk dist/TildeFriends-arm-$(VERSION_NUMBER).apk
|
||||||
|
@echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
|
||||||
|
@cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
|
||||||
|
.PHONY: dist
|
||||||
|
|
||||||
|
dist-test: dist
|
||||||
|
@tar -xf tildefriends-$(VERSION_NUMBER).tar.xz
|
||||||
|
@$(MAKE) -C tildefriends-$(VERSION_NUMBER)/ debug release
|
||||||
|
@docker build tildefriends-$(VERSION_NUMBER)/
|
||||||
|
@rm -rf tildefriends-$(VERSION_NUMBER)
|
||||||
|
.PHONY: dist-test
|
||||||
|
|
||||||
|
format:
|
||||||
|
@clang-format -i $(wildcard src/*.c src/*.h src/*.m)
|
||||||
|
.PHONY: format
|
||||||
|
|
||||||
|
docs:
|
||||||
|
@doxygen
|
||||||
|
.PHONY: docs
|
365
Makefile
365
Makefile
@ -1,365 +0,0 @@
|
|||||||
.ONESHELL:
|
|
||||||
.DELETE_ON_ERROR:
|
|
||||||
MAKEFLAGS += --warn-undefined-variables
|
|
||||||
MAKEFLAGS += --no-builtin-rules
|
|
||||||
|
|
||||||
PROJECT = tildefriends
|
|
||||||
BUILD_DIR ?= out
|
|
||||||
BUILD_TYPES := debug release windebug winrelease androiddebug androidrelease
|
|
||||||
UNAME_M := $(shell uname -m)
|
|
||||||
|
|
||||||
CFLAGS += \
|
|
||||||
-Wall \
|
|
||||||
-Wextra \
|
|
||||||
-Wno-unused-parameter \
|
|
||||||
-Wno-cast-function-type \
|
|
||||||
-MMD \
|
|
||||||
-ffunction-sections \
|
|
||||||
-fdata-sections \
|
|
||||||
-fno-omit-frame-pointer \
|
|
||||||
-g
|
|
||||||
LDFLAGS += -Wl,--gc-sections
|
|
||||||
|
|
||||||
NDK_PATH := /usr/lib/android-sdk/ndk-bundle
|
|
||||||
NDK_API_VERSION := 30
|
|
||||||
NDK_TARGET_TRIPLE := aarch64-linux-android
|
|
||||||
|
|
||||||
debug windebug androiddebug: CFLAGS += -Og
|
|
||||||
debug release androidrelease: LDFLAGS += -rdynamic
|
|
||||||
release winrelease: CFLAGS += -DNDEBUG -O3
|
|
||||||
windebug winrelease: CC = x86_64-w64-mingw32-gcc-win32
|
|
||||||
windebug winrelease: AS = $(CC)
|
|
||||||
windebug winrelease: CFLAGS += \
|
|
||||||
-D_WIN32_WINNT=0x0A00 \
|
|
||||||
-DWINVER=0x0A00 \
|
|
||||||
-DNTDDI_VERSION=NTDDI_WIN10 \
|
|
||||||
-Ideps/openssl/mingw64/include
|
|
||||||
windebug winrelease: LDFLAGS += \
|
|
||||||
-static \
|
|
||||||
-lm \
|
|
||||||
-Ldeps/openssl/mingw64/lib
|
|
||||||
androiddebug androidrelease: CC = $(NDK_PATH)/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
|
|
||||||
androiddebug androidrelease: AS = $(CC)
|
|
||||||
androiddebug androidrelease: CFLAGS += \
|
|
||||||
-target $(NDK_TARGET_TRIPLE)$(NDK_API_VERSION) \
|
|
||||||
-Ideps/openssl/android/arm64-v8a/usr/local/include \
|
|
||||||
-Wno-unknown-warning-option
|
|
||||||
androiddebug androidrelease: LDFLAGS += \
|
|
||||||
-target $(NDK_TARGET_TRIPLE)$(NDK_API_VERSION) \
|
|
||||||
-Ldeps/openssl/android/arm64-v8a/usr/local/lib
|
|
||||||
|
|
||||||
ifeq ($(UNAME_M),x86_64)
|
|
||||||
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
|
||||||
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
|
||||||
endif
|
|
||||||
|
|
||||||
get_objs = \
|
|
||||||
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
|
|
||||||
$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
|
|
||||||
$(foreach build_type,windebug winrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_win))))) \
|
|
||||||
$(foreach build_type,androiddebug androidrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
|
|
||||||
$(foreach build_type,androiddebug androidrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix)))))
|
|
||||||
|
|
||||||
APP_SOURCES := $(wildcard src/*.c)
|
|
||||||
APP_OBJS := $(call get_objs,APP_SOURCES)
|
|
||||||
$(APP_OBJS): CFLAGS += \
|
|
||||||
-Ideps/base64c/include \
|
|
||||||
-Ideps/crypt_blowfish \
|
|
||||||
-Ideps/libbacktrace \
|
|
||||||
-Ideps/libsodium \
|
|
||||||
-Ideps/libsodium/src/libsodium/include \
|
|
||||||
-Ideps/libuv/include \
|
|
||||||
-Ideps/picohttpparser \
|
|
||||||
-Ideps/quickjs \
|
|
||||||
-Ideps/sqlite \
|
|
||||||
-Ideps/valgrind \
|
|
||||||
-Ideps/xopt \
|
|
||||||
-Werror
|
|
||||||
|
|
||||||
BLOWFISH_SOURCES := \
|
|
||||||
deps/crypt_blowfish/crypt_blowfish.c \
|
|
||||||
deps/crypt_blowfish/crypt_gensalt.c \
|
|
||||||
deps/crypt_blowfish/wrapper.c
|
|
||||||
BLOWFISH_SOURCES_win = \
|
|
||||||
deps/crypt_blowfish/x86.S
|
|
||||||
BLOWFISH_OBJS := $(call get_objs,BLOWFISH_SOURCES)
|
|
||||||
|
|
||||||
UV_SOURCES := \
|
|
||||||
deps/libuv/src/fs-poll.c \
|
|
||||||
deps/libuv/src/idna.c \
|
|
||||||
deps/libuv/src/inet.c \
|
|
||||||
deps/libuv/src/random.c \
|
|
||||||
deps/libuv/src/strscpy.c \
|
|
||||||
deps/libuv/src/strtok.c \
|
|
||||||
deps/libuv/src/threadpool.c \
|
|
||||||
deps/libuv/src/timer.c \
|
|
||||||
deps/libuv/src/uv-common.c \
|
|
||||||
deps/libuv/src/uv-data-getter-setters.c \
|
|
||||||
deps/libuv/src/version.c
|
|
||||||
UV_SOURCES_unix := \
|
|
||||||
deps/libuv/src/unix/async.c \
|
|
||||||
deps/libuv/src/unix/core.c \
|
|
||||||
deps/libuv/src/unix/dl.c \
|
|
||||||
deps/libuv/src/unix/epoll.c \
|
|
||||||
deps/libuv/src/unix/fs.c \
|
|
||||||
deps/libuv/src/unix/getaddrinfo.c \
|
|
||||||
deps/libuv/src/unix/getnameinfo.c \
|
|
||||||
deps/libuv/src/unix/linux-core.c \
|
|
||||||
deps/libuv/src/unix/linux-inotify.c \
|
|
||||||
deps/libuv/src/unix/linux-syscalls.c \
|
|
||||||
deps/libuv/src/unix/loop-watcher.c \
|
|
||||||
deps/libuv/src/unix/loop.c \
|
|
||||||
deps/libuv/src/unix/pipe.c \
|
|
||||||
deps/libuv/src/unix/poll.c \
|
|
||||||
deps/libuv/src/unix/process.c \
|
|
||||||
deps/libuv/src/unix/procfs-exepath.c \
|
|
||||||
deps/libuv/src/unix/proctitle.c \
|
|
||||||
deps/libuv/src/unix/random-devurandom.c \
|
|
||||||
deps/libuv/src/unix/random-getrandom.c \
|
|
||||||
deps/libuv/src/unix/random-sysctl-linux.c \
|
|
||||||
deps/libuv/src/unix/signal.c \
|
|
||||||
deps/libuv/src/unix/stream.c \
|
|
||||||
deps/libuv/src/unix/tcp.c \
|
|
||||||
deps/libuv/src/unix/thread.c \
|
|
||||||
deps/libuv/src/unix/tty.c \
|
|
||||||
deps/libuv/src/unix/udp.c
|
|
||||||
UV_SOURCES_android := \
|
|
||||||
deps/libuv/src/unix/pthread-fixes.c \
|
|
||||||
deps/libuv/src/unix/random-getentropy.c
|
|
||||||
UV_SOURCES_win := \
|
|
||||||
deps/libuv/src/win/async.c \
|
|
||||||
deps/libuv/src/win/core.c \
|
|
||||||
deps/libuv/src/win/detect-wakeup.c \
|
|
||||||
deps/libuv/src/win/dl.c \
|
|
||||||
deps/libuv/src/win/error.c \
|
|
||||||
deps/libuv/src/win/fs-event.c \
|
|
||||||
deps/libuv/src/win/fs.c \
|
|
||||||
deps/libuv/src/win/getaddrinfo.c \
|
|
||||||
deps/libuv/src/win/getnameinfo.c \
|
|
||||||
deps/libuv/src/win/handle.c \
|
|
||||||
deps/libuv/src/win/loop-watcher.c \
|
|
||||||
deps/libuv/src/win/pipe.c \
|
|
||||||
deps/libuv/src/win/poll.c \
|
|
||||||
deps/libuv/src/win/process-stdio.c \
|
|
||||||
deps/libuv/src/win/process.c \
|
|
||||||
deps/libuv/src/win/signal.c \
|
|
||||||
deps/libuv/src/win/snprintf.c \
|
|
||||||
deps/libuv/src/win/stream.c \
|
|
||||||
deps/libuv/src/win/tcp.c \
|
|
||||||
deps/libuv/src/win/thread.c \
|
|
||||||
deps/libuv/src/win/tty.c \
|
|
||||||
deps/libuv/src/win/udp.c \
|
|
||||||
deps/libuv/src/win/util.c \
|
|
||||||
deps/libuv/src/win/winapi.c \
|
|
||||||
deps/libuv/src/win/winsock.c
|
|
||||||
UV_OBJS := $(call get_objs,UV_SOURCES)
|
|
||||||
$(UV_OBJS): CFLAGS += \
|
|
||||||
-Ideps/libuv/include \
|
|
||||||
-Ideps/libuv/src \
|
|
||||||
-Wno-unused-but-set-variable \
|
|
||||||
-Wno-incompatible-pointer-types \
|
|
||||||
-Wno-sign-compare \
|
|
||||||
-Wno-unused-variable \
|
|
||||||
-Wno-dangling-pointer \
|
|
||||||
-Wno-maybe-uninitialized \
|
|
||||||
-D_GNU_SOURCE
|
|
||||||
|
|
||||||
SODIUM_SOURCES := \
|
|
||||||
deps/libsodium/src/libsodium/crypto_auth/hmacsha512/auth_hmacsha512.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_auth/hmacsha512256/auth_hmacsha512256.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_box/crypto_box.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_box/curve25519xsalsa20poly1305/box_curve25519xsalsa20poly1305.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_core/ed25519/ref10/ed25519_ref10.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_core/hsalsa20/ref2/core_hsalsa20_ref2.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_core/salsa/ref/core_salsa_ref.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-compress-ref.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-ref.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/generichash_blake2b.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_hash/sha256/cp/hash_sha256_cp.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_hash/sha256/hash_sha256.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_hash/sha512/cp/hash_sha512_cp.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/donna/poly1305_donna.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/onetimeauth_poly1305.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-core.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-fill-block-ref.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_pwhash/argon2/blake2b-long.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_scalarmult/crypto_scalarmult.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/scalarmult_curve25519.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_secretbox/crypto_secretbox_easy.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_secretbox/xsalsa20poly1305/secretbox_xsalsa20poly1305.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_sign/crypto_sign.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/keypair.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/open.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/sign.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_sign/ed25519/sign_ed25519.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_stream/chacha20/ref/chacha20_ref.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_stream/chacha20/stream_chacha20.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_stream/salsa20/ref/salsa20_ref.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_stream/salsa20/stream_salsa20.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_stream/xsalsa20/stream_xsalsa20.c \
|
|
||||||
deps/libsodium/src/libsodium/crypto_verify/sodium/verify.c \
|
|
||||||
deps/libsodium/src/libsodium/randombytes/randombytes.c \
|
|
||||||
deps/libsodium/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c \
|
|
||||||
deps/libsodium/src/libsodium/sodium/core.c \
|
|
||||||
deps/libsodium/src/libsodium/sodium/codecs.c \
|
|
||||||
deps/libsodium/src/libsodium/sodium/runtime.c \
|
|
||||||
deps/libsodium/src/libsodium/sodium/utils.c
|
|
||||||
SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES)
|
|
||||||
$(SODIUM_OBJS): CFLAGS += \
|
|
||||||
-DCONFIGURED=1 \
|
|
||||||
-DMINIMAL=1 \
|
|
||||||
-Wno-unused-function \
|
|
||||||
-Wno-unused-variable \
|
|
||||||
-Wno-type-limits \
|
|
||||||
-Wno-unknown-pragmas \
|
|
||||||
-Ideps/libsodium/src/libsodium/include/sodium
|
|
||||||
|
|
||||||
SQLITE_SOURCES := deps/sqlite/sqlite3.c
|
|
||||||
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
|
|
||||||
$(SQLITE_OBJS): CFLAGS += \
|
|
||||||
-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
|
|
||||||
-DSQLITE_ENABLE_FTS5 \
|
|
||||||
-DSQLITE_ENABLE_JSON1 \
|
|
||||||
-DSQLITE_THREADSAFE=1 \
|
|
||||||
-DSQLITE_MAX_LENGTH=5242880 \
|
|
||||||
-DSQLITE_MAX_SQL_LENGTH=100000 \
|
|
||||||
-DSQLITE_MAX_COLUMN=100 \
|
|
||||||
-DSQLITE_MAX_EXPR_DEPTH=40 \
|
|
||||||
-DSQLITE_MAX_COMPOUND_SELECT=300 \
|
|
||||||
-DSQLITE_MAX_VDBE_OP=25000 \
|
|
||||||
-DSQLITE_MAX_FUNCTION_ARG=8 \
|
|
||||||
-DSQLITE_MAX_ATTACHED=0 \
|
|
||||||
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
|
|
||||||
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
|
|
||||||
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
|
|
||||||
-DSQLITE_SECURE_DELETE \
|
|
||||||
-Wno-implicit-fallthrough \
|
|
||||||
-Wno-unused-but-set-variable \
|
|
||||||
-Wno-unused-function
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
QUICKJS_SOURCES := \
|
|
||||||
deps/quickjs/cutils.c \
|
|
||||||
deps/quickjs/libbf.c \
|
|
||||||
deps/quickjs/libregexp.c \
|
|
||||||
deps/quickjs/libunicode.c \
|
|
||||||
deps/quickjs/quickjs-libc.c \
|
|
||||||
deps/quickjs/quickjs.c
|
|
||||||
QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES)
|
|
||||||
$(QUICKJS_OBJS): CFLAGS += \
|
|
||||||
-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
|
|
||||||
-DCONFIG_BIGNUM \
|
|
||||||
-DDUMP_LEAKS \
|
|
||||||
-D_GNU_SOURCE \
|
|
||||||
-Wno-sign-compare \
|
|
||||||
-Wno-implicit-fallthrough \
|
|
||||||
-Wno-unused-variable \
|
|
||||||
-Wno-unused-but-set-variable \
|
|
||||||
-Wno-enum-conversion
|
|
||||||
|
|
||||||
LIBBACKTRACE_SOURCES := \
|
|
||||||
deps/libbacktrace/atomic.c \
|
|
||||||
deps/libbacktrace/backtrace.c \
|
|
||||||
deps/libbacktrace/dwarf.c \
|
|
||||||
deps/libbacktrace/fileline.c \
|
|
||||||
deps/libbacktrace/print.c \
|
|
||||||
deps/libbacktrace/simple.c \
|
|
||||||
deps/libbacktrace/sort.c \
|
|
||||||
deps/libbacktrace/state.c
|
|
||||||
LIBBACKTRACE_SOURCES_unix := \
|
|
||||||
deps/libbacktrace/elf.c \
|
|
||||||
deps/libbacktrace/mmap.c \
|
|
||||||
deps/libbacktrace/mmapio.c \
|
|
||||||
deps/libbacktrace/posix.c
|
|
||||||
LIBBACKTRACE_SOURCES_win := \
|
|
||||||
deps/libbacktrace/alloc.c \
|
|
||||||
deps/libbacktrace/pecoff.c \
|
|
||||||
deps/libbacktrace/posix.c \
|
|
||||||
deps/libbacktrace/read.c
|
|
||||||
LIBBACKTRACE_OBJS := $(call get_objs,LIBBACKTRACE_SOURCES)
|
|
||||||
$(LIBBACKTRACE_OBJS): CFLAGS += \
|
|
||||||
-Ideps/libbacktrace_config \
|
|
||||||
-Wno-unused-but-set-variable \
|
|
||||||
-Wno-maybe-initialized \
|
|
||||||
-Wno-unused-function \
|
|
||||||
-DBACKTRACE_ELF_SIZE=64
|
|
||||||
|
|
||||||
PICOHTTPPARSER_SOURCES := \
|
|
||||||
deps/picohttpparser/picohttpparser.c
|
|
||||||
PICOHTTPPARSER_OBJS := $(call get_objs,PICOHTTPPARSER_SOURCES)
|
|
||||||
# $(PICOHTTPPARSER_OBJS): CFLAGS +=
|
|
||||||
|
|
||||||
LDFLAGS += \
|
|
||||||
-pthread \
|
|
||||||
-lm
|
|
||||||
debug release: LDFLAGS += \
|
|
||||||
-ldl \
|
|
||||||
-lssl \
|
|
||||||
-lcrypto
|
|
||||||
windebug winrelease: LDFLAGS += \
|
|
||||||
-lwsock32 \
|
|
||||||
-lws2_32 \
|
|
||||||
-lkernel32 \
|
|
||||||
-liphlpapi \
|
|
||||||
-luserenv \
|
|
||||||
-lssl \
|
|
||||||
-lcrypto \
|
|
||||||
-lws2_32 \
|
|
||||||
-lcrypt32
|
|
||||||
androiddebug androidrelease: LDFLAGS += \
|
|
||||||
-ldl \
|
|
||||||
-lssl \
|
|
||||||
-lcrypto
|
|
||||||
|
|
||||||
unix: debug release
|
|
||||||
win: windebug winrelease
|
|
||||||
all: $(BUILD_TYPES)
|
|
||||||
.PHONY: all win unix
|
|
||||||
|
|
||||||
ALL_APP_OBJS := \
|
|
||||||
$(APP_OBJS) \
|
|
||||||
$(BLOWFISH_OBJS) \
|
|
||||||
$(LIBBACKTRACE_OBJS) \
|
|
||||||
$(PICOHTTPPARSER_OBJS) \
|
|
||||||
$(QUICKJS_OBJS) \
|
|
||||||
$(SODIUM_OBJS) \
|
|
||||||
$(SQLITE_OBJS) \
|
|
||||||
$(UV_OBJS) \
|
|
||||||
$(XOPT_OBJS)
|
|
||||||
|
|
||||||
DEPS = $(ALL_APP_OBJS:.o=.d)
|
|
||||||
-include $(DEPS)
|
|
||||||
|
|
||||||
define build_rules
|
|
||||||
$(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
|
|
||||||
.PHONY: $(1)
|
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
|
|
||||||
@echo [link] $$@
|
|
||||||
@$$(CC) -o $$@ $$^ $$(LDFLAGS)
|
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/%.o: %.c
|
|
||||||
@mkdir -p $$(dir $$@)
|
|
||||||
@echo [c] $$@
|
|
||||||
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/%.o: %.S
|
|
||||||
@mkdir -p $$(dir $$@)
|
|
||||||
@echo [as] $$@
|
|
||||||
@$$(AS) -c $$< -o $$@
|
|
||||||
endef
|
|
||||||
|
|
||||||
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf $(BUILD_DIR)
|
|
||||||
.PHONY: clean
|
|
31
README.md
31
README.md
@ -1,36 +1,49 @@
|
|||||||
# Tilde Friends
|
# Tilde Friends
|
||||||
|
|
||||||
Tilde Friends is a tool for making and sharing.
|
Tilde Friends is a tool for making and sharing.
|
||||||
|
|
||||||
|
A public instance lives at https://www.tildefriends.net/.
|
||||||
|
|
||||||
It is both a peer-to-peer social network client, participating in Secure
|
It is both a peer-to-peer social network client, participating in Secure
|
||||||
Scuttlebutt, as well as a platform for writing and running web applications.
|
Scuttlebutt, as well as a platform for writing and running web applications.
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
1. Make it easy and fun to run all sorts of web applications.
|
1. Make it easy and fun to run all sorts of web applications.
|
||||||
2. Provide security that is easy to understand and protects your data.
|
2. Provide security that is easy to understand and protects your data.
|
||||||
3. Make creating and sharing web applications accessible to anyone with a
|
3. Make creating and sharing web applications accessible to anyone with a
|
||||||
browser.
|
browser.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
|
|
||||||
|
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
|
||||||
|
all of those host platforms plus mingw64, iOS, and android.
|
||||||
|
|
||||||
|
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
|
||||||
are kept up to date in the tree.
|
are kept up to date in the tree.
|
||||||
2. To build, run `make debug` or `make release`. An executable will be
|
2. To build, run `make debug` or `make release`. An executable will be
|
||||||
generated in a subdirectory of `out/`.
|
generated in a subdirectory of `out/`.
|
||||||
3. `make windebug` or `make winrelease` will generate a windows executable
|
3. It's possible to build for Android, iOS, and Windows on Linux, if you have
|
||||||
which might work.
|
the right dependencies in the right places. `make windebug winrelease
|
||||||
|
iosdebug-ipa iosrelease-ipa release-apk`.
|
||||||
4. To build in docker, `docker build .`.
|
4. To build in docker, `docker build .`.
|
||||||
|
5. `make format` will normalize formatting to the coding standard.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
By default, running the built `tildefriends` executable will start a web server
|
By default, running the built `tildefriends` executable will start a web server
|
||||||
at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
||||||
|
|
||||||
The first user to create an account and log in will be granted administrative
|
The first user to create an account and log in will be granted administrative
|
||||||
privileges. Further administration can be done at
|
privileges. Further administration can be done at
|
||||||
<http://localhost:12345/~core/admin/`>.
|
<http://localhost:12345/~core/admin/>.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
There are the very beginnings of developer documentation in `apps/cory/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,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
}
|
"emoji": "🎛"
|
||||||
|
}
|
||||||
|
@ -9,14 +9,22 @@ tfrpc.register(function global_settings_set(key, value) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
let data = {
|
try {
|
||||||
users: {},
|
let data = {
|
||||||
granted: await core.allPermissionsGranted(),
|
users: {},
|
||||||
settings: await core.globalSettingsDescriptions(),
|
granted: await core.allPermissionsGranted(),
|
||||||
};
|
settings: await core.globalSettingsDescriptions(),
|
||||||
for (let user of await core.users()) {
|
};
|
||||||
data.users[user] = await core.permissionsForUser(user);
|
for (let user of await core.users()) {
|
||||||
|
data.users[user] = await core.permissionsForUser(user);
|
||||||
|
}
|
||||||
|
await app.setDocument(
|
||||||
|
utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data))
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
await app.setDocument(
|
||||||
|
'<span style="color: #f00">Only an administrator can modify these settings.</span>'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
|
|
||||||
}
|
}
|
||||||
main();
|
main();
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html style="width: 100%">
|
||||||
<head>
|
<head>
|
||||||
<script>const g_data = $data;</script>
|
<script>
|
||||||
|
const g_data = $data;
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff">
|
<body style="color: #fff; width: 100%">
|
||||||
<h1>Tilde Friends Administration</h1>
|
<h1>Tilde Friends Administration</h1>
|
||||||
</body>
|
</body>
|
||||||
<script type="module" src="script.js"></script>
|
<script type="module" src="script.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
@ -3,76 +3,90 @@ import * as tfrpc from '/static/tfrpc.js';
|
|||||||
|
|
||||||
function delete_user(user) {
|
function delete_user(user) {
|
||||||
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
|
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
|
||||||
tfrpc.rpc.delete_user(user).then(function() {
|
tfrpc.rpc
|
||||||
alert(`User "${user}" deleted successfully.`);
|
.delete_user(user)
|
||||||
}).catch(function(error) {
|
.then(function () {
|
||||||
alert(`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`);
|
alert(`User "${user}" deleted successfully.`);
|
||||||
});
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
alert(
|
||||||
|
`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function global_settings_set(key, value) {
|
function global_settings_set(key, value) {
|
||||||
tfrpc.rpc.global_settings_set(key, value).then(function() {
|
tfrpc.rpc
|
||||||
alert(`Set "${key}" to "${value}".`);
|
.global_settings_set(key, value)
|
||||||
}).catch(function(error) {
|
.then(function () {
|
||||||
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
alert(`Set "${key}" to "${value}".`);
|
||||||
});
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function () {
|
||||||
const permission_template = (permission) =>
|
const permission_template = (permission) => html` <code>${permission}</code>`;
|
||||||
html` <code>${permission}</code>`;
|
|
||||||
function input_template(key, description) {
|
function input_template(key, description) {
|
||||||
if (description.type === 'boolean') {
|
if (description.type === 'boolean') {
|
||||||
return html`
|
return html`
|
||||||
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
<div style="margin-top: 1em">
|
||||||
<input type="checkbox" ?checked=${description.value} ?id=${'gs_' + key} style="grid-column: 2"></input>
|
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||||
<div style="grid-column: 3">
|
<div>
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.checked)}>Set</button>
|
<input type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
|
||||||
<span>${description.description}</span>
|
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
|
||||||
|
<div>${description.description}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (description.type === 'textarea') {
|
} else if (description.type === 'textarea') {
|
||||||
return html`
|
return html`
|
||||||
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
<div style="margin-top: 1em"">
|
||||||
<textarea style="vertical-align: top" rows=20 cols=80 ?id=${'gs_' + key}>${description.value}</textarea>
|
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||||
<div style="grid-column: 3">
|
<div style="width: 100%; padding: 0; margin: 0">
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
|
<div style="width: 90%; padding: 0 margin: 0">
|
||||||
<span>${description.description}</span>
|
<textarea style="vertical-align: top; width: 100%" rows=20 cols=80 id=${'gs_' + key}>${description.value}</textarea>
|
||||||
|
</div>
|
||||||
|
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstElementChild.value)}>Set</button>
|
||||||
|
<div>${description.description}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
|
<div style="margin-top: 1em">
|
||||||
<input type="text" value="${description.value}" ?id=${'gs_' + key}></input>
|
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
||||||
<div style="grid-column: 3">
|
<div>
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
|
<input type="text" value="${description.value}" id=${'gs_' + key}></input>
|
||||||
<span>${description.description}</span>
|
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
||||||
|
<div>${description.description}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const user_template = (user, permissions) => html`
|
const user_template = (user, permissions) => html`
|
||||||
<li>
|
<li>
|
||||||
<button @click=${(e) => delete_user(user)}>
|
<button @click=${(e) => delete_user(user)}>Delete</button>
|
||||||
Delete
|
${user}: ${permissions.map((x) => permission_template(x))}
|
||||||
</button>
|
|
||||||
${user}:
|
|
||||||
${permissions.map(x => permission_template(x))}
|
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
const users_template = (users) =>
|
const users_template = (users) =>
|
||||||
html`<h2>Users</h2>
|
html`<h2>Users</h2>
|
||||||
<ul>
|
<ul>
|
||||||
${Object.entries(users).map(u => user_template(u[0], u[1]))}
|
${Object.entries(users).map((u) => user_template(u[0], u[1]))}
|
||||||
</ul>`;
|
</ul>`;
|
||||||
const page_template = (data) =>
|
const page_template = (data) =>
|
||||||
html`<div>
|
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
|
||||||
<h2>Global Settings</h2>
|
<h2>Global Settings</h2>
|
||||||
<div style="display: grid">
|
<div>
|
||||||
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
|
${Object.keys(data.settings)
|
||||||
</div>
|
.sort()
|
||||||
${users_template(data.users)}
|
.map((x) => html`${input_template(x, data.settings[x])}`)}
|
||||||
</div>`;
|
</div>
|
||||||
|
${users_template(data.users)}
|
||||||
|
</div> `;
|
||||||
render(page_template(g_data), document.body);
|
render(page_template(g_data), document.body);
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
}
|
"emoji": "📜",
|
||||||
|
"previous": "&miGORZ8BwjHg2YO0t4bms6SI28XWPYqnqOZ8u9zsbZc=.sha256"
|
||||||
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
356
apps/api/docs.js
Normal file
356
apps/api/docs.js
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
export const docs = {};
|
||||||
|
|
||||||
|
docs.global = `# Tilde Friends API Documentation
|
||||||
|
|
||||||
|
Welcome to the Tilde Friends API documentation.
|
||||||
|
|
||||||
|
* [App Globals](#App_Globals)
|
||||||
|
* [Database Interface](#Database)
|
||||||
|
* [Remote Procedure Calls](#tfrpc)
|
||||||
|
|
||||||
|
<a id="App_Globals"></a>
|
||||||
|
## <span style="color: #aaf">App Globals</span>
|
||||||
|
The following are functions and values exposed to all apps in their \`app.js\` or \`handler.js\`. Most
|
||||||
|
of these are asynchronous, returning a \`Promise\` that will be resolved when the call completes, unless
|
||||||
|
noted otherwise.
|
||||||
|
|
||||||
|
This is all a work in progess. These are liable to change without warning. Feedback is welcome.
|
||||||
|
|
||||||
|
The exposed functions in this API balance multiple competing needs:
|
||||||
|
* The surface area of the exposed API ought to be fairly minimal. If something can be implemented entirely app-side, that is
|
||||||
|
generally preferred over building it into the core.
|
||||||
|
* Everything is built on this API. Ideally the admin app, the SSB app, and the editor all use standard API exposed to all
|
||||||
|
apps, with appropriate permission guards in place making it so that only trusted apps do potentially destructive operations.
|
||||||
|
There will be some things here that aren't necessarily general use to support what's required.
|
||||||
|
|
||||||
|
If you are looking at the [Tilde Friends source code](https://www.tildefriends.net/~cory/releases/),
|
||||||
|
the vast majority of these are implemented in \`src/*.js.c\` files, and exposed to apps via \`core/core.js\`.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['core.user.credentials.session.name'] = `
|
||||||
|
*String* The name of the authenticated user.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['app.setDocument()'] = `
|
||||||
|
Set the contents of the client <iframe/>.
|
||||||
|
### Parameters
|
||||||
|
* *String* **html** The HTML contents.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['core.apps()'] = `
|
||||||
|
Gets a list of apps owned by the current user.
|
||||||
|
### Returns
|
||||||
|
*Array* An array of string names of the apps owned by the current user.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['core.url'] = `
|
||||||
|
The url by which the running app is being invoked.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['app.localStorageSet()'] = `
|
||||||
|
Set a value in browser local storage.
|
||||||
|
### Parameters
|
||||||
|
*String* **key** The localStorage key to set.
|
||||||
|
*String* **value** The localStorage value to set.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['app.localStorageGet()'] = `
|
||||||
|
Gets a value from browser local storage.
|
||||||
|
### Parameters
|
||||||
|
*String* **key** The key with which the value was set.
|
||||||
|
### Returns
|
||||||
|
*String* The value, or undefined.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['app.print()'] = `
|
||||||
|
Log information for debugging purposes to the server and to the connected browser console.
|
||||||
|
### Parameters
|
||||||
|
* ... Any args to print.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.createIdentity()'] = `
|
||||||
|
Create a new SSB identity.
|
||||||
|
### Returns
|
||||||
|
*String* The created identity public key.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.getIdentities()'] = `
|
||||||
|
Get all SSB identities owned by the current user.
|
||||||
|
### Returns
|
||||||
|
*Array* An array of public key strings.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.sqlAsync()'] = `
|
||||||
|
Run an SQL query against the sqlite database.
|
||||||
|
### Parameters
|
||||||
|
* *String* **query** The sqlite query.
|
||||||
|
* *Array* **args** The query arguments to bind.
|
||||||
|
* *Function* **callback** Callback called for each row result.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.appendMessageWithIdentity()'] = `
|
||||||
|
Signs and stores a message in the SSB database.
|
||||||
|
### Parameters
|
||||||
|
* *String* **id** The public key of an SSB identity owned by the authenticated user.
|
||||||
|
* *Object* **message** The unsigned message.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.storeMessage()'] = `
|
||||||
|
Verifies and stores a signed message in the SSB database.
|
||||||
|
### Parameters
|
||||||
|
* *Object* **message** The valid, signed message to store.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.blobStore()'] = `
|
||||||
|
Store a blob in the SSB database.
|
||||||
|
### Parameters
|
||||||
|
* *String*/*Uint8Array* **blob** The blob contents to store
|
||||||
|
### Returns
|
||||||
|
*String* The stored blob ID.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['ssb.blobGet()'] = `
|
||||||
|
Fetches a blob from the database.
|
||||||
|
### Parameters
|
||||||
|
* *String* **blob_id** The blob identifier to fetch (\`&....sha256\`).
|
||||||
|
### Returns
|
||||||
|
*ArrayBuffer* The blob data.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['print()'] = `
|
||||||
|
Log debug information both to the server's console and to the visiting user's browser console when possible.
|
||||||
|
### Parameters
|
||||||
|
* **...** Whatever you want to log. Will be joined with spaces.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database()'] = `
|
||||||
|
Returns a database instance that is specific to the authenticated user and the given key.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The database key.
|
||||||
|
### Returns
|
||||||
|
*Database* A database.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['my_shared_database()'] = `
|
||||||
|
Returns a database instance that is specific to the authenticated user and the given key.
|
||||||
|
### Parameters
|
||||||
|
* *String* **package_name** The database package name.
|
||||||
|
* *String* **key** The database key.
|
||||||
|
### Returns
|
||||||
|
*Database* A database.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['shared_database()'] = `
|
||||||
|
Returns a database instance that is shared between all users of the app, determined by its owner and app name.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The database key.
|
||||||
|
### Returns
|
||||||
|
*Database* A database.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['base64Decode()'] = `
|
||||||
|
Decode a base64 string to bytes.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *String* value The base64-encoded string.
|
||||||
|
### Returns
|
||||||
|
*Uint8Array* The decoded bytes.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['base64Encode()'] = `
|
||||||
|
Encode bytes to a base64 string.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* The bytes to encode.
|
||||||
|
### Returns
|
||||||
|
*String* The base64-encoded string.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['utf8Decode()'] = `
|
||||||
|
Decode UTF-8 bytes to a string.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* **value** The value to decode.
|
||||||
|
### Returns
|
||||||
|
*String* The value as a string.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['utf8Encode()'] = `
|
||||||
|
Encodes a string to UTF-8 bytes.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *String* **value** The value to encode.
|
||||||
|
### Returns
|
||||||
|
*Uint8Array* The encoded \`value\`.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['setTimeout()'] = `
|
||||||
|
Call a function after some delay.
|
||||||
|
### Parameters
|
||||||
|
* *Function* **callback** The function to call.
|
||||||
|
* *Number* **timeout** Number of milliseconds to wait before calling the callback function.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['parseHttpRequest()'] = `
|
||||||
|
Parses an HTTP request.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* **request** The request data. Maybe be partial or contain extra data. The return value will
|
||||||
|
indicate when and where it is complete.
|
||||||
|
* *Number* **last_length** The length of the data passed on a previous attempt for the same request, or 0 initially.
|
||||||
|
### Returns
|
||||||
|
* *Integer* **-2** if the request is incomplete.
|
||||||
|
* *Integer* **-1** if the request could not be parsed.
|
||||||
|
* *Object* An object with **bytes_parsed**, **minor_version**, **path**, and **headers** fields on successful parse.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['parseHttpResponse()'] = `
|
||||||
|
Parses an HTTP response.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* **response** The response data. Maybe be partial or contain extra data. The return value will
|
||||||
|
indicate when and where it is complete.
|
||||||
|
* *Number* **last_length** The length of the data passed on a previous attempt for the same response, or 0 initially.
|
||||||
|
### Returns
|
||||||
|
* *Integer* **-2** if the response is incomplete.
|
||||||
|
* *Integer* **-1** if the response could not be parsed.
|
||||||
|
* *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['sha1Digest()'] = `
|
||||||
|
Calculates a SHA1 digest.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *String* **value** The value for which to calculate the digest.
|
||||||
|
### Returns
|
||||||
|
*String* The SHA1 digest of UTF-8 encoded \`value\`.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['maskBytes()'] = `
|
||||||
|
Masks bytes for WebSocket communication.
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *Uint8Array* **bytes** The byte array of data to mask.
|
||||||
|
* *Uint32* **mask** The mask to apply.
|
||||||
|
### Returns
|
||||||
|
*Uint32Array* The masked bytes.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['exit()'] = `
|
||||||
|
Exits the app. But why would you want to do that?
|
||||||
|
|
||||||
|
Completes synchronously.
|
||||||
|
### Parameters
|
||||||
|
* *Integer* **exit_code** System exit code.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['version()'] = `
|
||||||
|
Gets version information for the running server.
|
||||||
|
### Returns
|
||||||
|
*Object* Keys are things like \`name\` and \`number\` for the server itself and \`libuv\` and \`openssl\` for
|
||||||
|
dependencies. Values are *String* version numbers.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['platform()'] = `
|
||||||
|
Gets the host operating system platform of the running server.
|
||||||
|
### Returns
|
||||||
|
*String* The platform, one of \`windows\`, \`android\`, \`linux\`, or \`other\`.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['getFile()'] = `
|
||||||
|
Gets a file from the running app.
|
||||||
|
### Parameters
|
||||||
|
* *String* **name** Name of the file to retrieve.
|
||||||
|
### Returns
|
||||||
|
*Uint8Array* The contents of a file from the app with the given name, or *undefined*.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs.database = `
|
||||||
|
# <span style="color: #aaf">Database</span>
|
||||||
|
Local-only storage is provided by a \`Database\` type representing a key-value store.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.get()'] = `
|
||||||
|
Gets a value from the database.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The key.
|
||||||
|
### Returns
|
||||||
|
*String* The value from the database or undefined if not found.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.getAll()'] = `
|
||||||
|
Gets all keys from the database.
|
||||||
|
### Returns
|
||||||
|
*Array* An array of *String* key names for all keys in the given database.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.getLike()'] = `
|
||||||
|
Gets all keys and values from the database matching a pattern.
|
||||||
|
### Parameters
|
||||||
|
* *String* **pattern** An sqlite \`LIKE\` pattern to match keys against.
|
||||||
|
### Returns
|
||||||
|
*Object* An object whose keys are the database keys and values are the database values that match the given pattern.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.set()'] = `
|
||||||
|
Sets a value in the database, creating a new entry or replacing an existing entry.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The key.
|
||||||
|
* *String* **value** The value.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.exchange()'] = `
|
||||||
|
Performs an atomic compare and exchange operation, setting a value in the database only if its current value matches what is expected.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The key.
|
||||||
|
* *String* **expected** The expected value.
|
||||||
|
* *String* **value** The new value.
|
||||||
|
### Returns
|
||||||
|
*Boolean* true if the value is now the given value.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['database.remove()'] = `
|
||||||
|
Removes an entry from the database if it exists.
|
||||||
|
### Parameters
|
||||||
|
* *String* **key** The key.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs.tfrpc = `
|
||||||
|
# <span style="color: #aaf" id="tfrpc">tfrpc</span>
|
||||||
|
\`tfrpc.js\` is a small helper script that is available to be used to facilitate communication between parts of an application.
|
||||||
|
|
||||||
|
\`tfrpc.js\` can be used to asynchronously make calls between the app code running in a sandboxed iframe in the browser
|
||||||
|
and the app process on the server.
|
||||||
|
|
||||||
|
From \`app.js\`:
|
||||||
|
\`\`\`
|
||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
|
||||||
|
From script running in the browser:
|
||||||
|
\`\`\`
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Either side can register or call functions, though they must be registered before they can be called. Arguments and return
|
||||||
|
values are ultimately serialized by means that attempt to preserve most JSON-serializable values as well as functions themselves.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['tfrpc.register()'] = `
|
||||||
|
Register a function, allowing it to be called remotely.
|
||||||
|
### Parameters
|
||||||
|
* *Function* **function** The function to register. Its name will be how it will be called.
|
||||||
|
`;
|
||||||
|
|
||||||
|
docs['tfrpc.rpc.*()'] = `
|
||||||
|
Call a remote function.
|
||||||
|
### Parameters
|
||||||
|
* **...** Parameters to pass to the function.
|
||||||
|
### Returns
|
||||||
|
The return value of the called function.
|
||||||
|
`;
|
@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
}
|
"emoji": "💻",
|
||||||
|
"previous": "&RdVEsVscZm3aWzcMrEZS8mskO5tUmvaEUihex2MMfZQ=.sha256"
|
||||||
|
}
|
||||||
|
207
apps/apps/app.js
207
apps/apps/app.js
@ -1,31 +1,182 @@
|
|||||||
async function main() {
|
/**
|
||||||
var apps = await core.apps();
|
* Fetches information about the applications
|
||||||
var core_apps = await core.apps('core');
|
* @param apps Record<appName, blobId>
|
||||||
var doc = `<!DOCTYPE html>
|
* @returns an object including the apps' name, emoji, and blobs ids
|
||||||
<html>
|
*/
|
||||||
<body style="background: #888">
|
async function fetch_info(apps) {
|
||||||
<h1>Apps</h1>
|
let result = {};
|
||||||
<ul id="apps"></ul>
|
|
||||||
<h1>Core Apps</h1>
|
// For each app
|
||||||
<ul id="core_apps"></ul>
|
for (let [key, value] of Object.entries(apps)) {
|
||||||
</body>
|
// Get it's blob and parse it
|
||||||
<script>
|
let blob = await ssb.blobGet(value);
|
||||||
function populate_apps(id, name, apps) {
|
blob = blob ? utf8Decode(blob) : '{}';
|
||||||
var list = document.getElementById(id);
|
|
||||||
for (let app of Object.keys(apps).sort()) {
|
// Add it to the result object
|
||||||
var li = list.appendChild(document.createElement('li'));
|
result[key] = JSON.parse(blob);
|
||||||
var a = document.createElement('a');
|
|
||||||
a.innerText = app;
|
|
||||||
a.href = '/~' + name + '/' + app + '/';
|
|
||||||
a.target = '_top';
|
|
||||||
li.appendChild(a);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
|
||||||
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
return result;
|
||||||
</script>
|
|
||||||
</html>`
|
|
||||||
app.setDocument(doc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
async function fetch_shared_apps() {
|
||||||
|
let messages = {};
|
||||||
|
|
||||||
|
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') {
|
||||||
|
messages[JSON.stringify([row.author, mention.name])] = {
|
||||||
|
message: row,
|
||||||
|
blob: mention.link,
|
||||||
|
name: mention.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = {};
|
||||||
|
for (let app of Object.values(messages).sort(
|
||||||
|
(x, y) => y.message.timestamp - x.message.timestamp
|
||||||
|
)) {
|
||||||
|
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app.blob)));
|
||||||
|
if (app_object) {
|
||||||
|
app_object.blob_id = app.blob;
|
||||||
|
result[app.name] = app_object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const apps = await fetch_info(await core.apps());
|
||||||
|
const core_apps = await fetch_info(await core.apps('core'));
|
||||||
|
const shared_apps = await fetch_shared_apps();
|
||||||
|
|
||||||
|
const stylesheet = `
|
||||||
|
body {
|
||||||
|
color: whitesmoke;
|
||||||
|
font-family: sans-serif;
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, 64px);
|
||||||
|
gap: 1em;
|
||||||
|
justify-content: space-around;
|
||||||
|
background-color: #ffffff10;
|
||||||
|
border: 2px solid #073642;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
height: 96px;
|
||||||
|
width: 64px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.app > a {
|
||||||
|
text-decoration: none;
|
||||||
|
max-width: 64px;
|
||||||
|
text-overflow: ellipsis ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
color: whitesmoke;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const body = `
|
||||||
|
<h1 style="text-shadow: #808080 0 0 10px;">Welcome to Tilde Friends.</h1>
|
||||||
|
|
||||||
|
<h2>your apps</h2>
|
||||||
|
<div id="apps" class="container"></div>
|
||||||
|
|
||||||
|
<h2>shared apps</h2>
|
||||||
|
<div id="shared_apps" class="container"></div>
|
||||||
|
|
||||||
|
<h2>core apps</h2>
|
||||||
|
<div id="core_apps" class="container"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const script = `
|
||||||
|
/*
|
||||||
|
* Creates a list of apps
|
||||||
|
* @param id the id of the element to populate
|
||||||
|
* @param name (a username, 'core' or undefined)
|
||||||
|
* @param apps Object, a list of apps
|
||||||
|
*/
|
||||||
|
function populate_apps(id, name, apps) {
|
||||||
|
// Our target
|
||||||
|
var list = document.getElementById(id);
|
||||||
|
|
||||||
|
// For each app in the provided list
|
||||||
|
for (let app of Object.keys(apps).sort()) {
|
||||||
|
|
||||||
|
// Create the item
|
||||||
|
let div = list.appendChild(document.createElement('div'));
|
||||||
|
div.classList.add('app');
|
||||||
|
|
||||||
|
// The app's icon
|
||||||
|
let href = name ? '/~' + name + '/' + app + '/' : ('/' + apps[app].blob_id + '/');
|
||||||
|
let icon_a = document.createElement('a');
|
||||||
|
let icon = document.createElement('div');
|
||||||
|
icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
|
||||||
|
icon.style.fontSize = 'xxx-large';
|
||||||
|
icon_a.appendChild(icon);
|
||||||
|
icon_a.href = href;
|
||||||
|
icon_a.target = '_top';
|
||||||
|
div.appendChild(icon_a);
|
||||||
|
|
||||||
|
// The app's name
|
||||||
|
let a = document.createElement('a');
|
||||||
|
a.appendChild(document.createTextNode(app));
|
||||||
|
a.href = href;
|
||||||
|
a.target = '_top';
|
||||||
|
div.appendChild(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
||||||
|
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
||||||
|
populate_apps('shared_apps', undefined, ${JSON.stringify(shared_apps)});
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Build the document
|
||||||
|
const document = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
${stylesheet}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
${body}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
${script}
|
||||||
|
</script>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
// Send it to the browser
|
||||||
|
app.setDocument(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
5
apps/blog.json
Normal file
5
apps/blog.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🪵",
|
||||||
|
"previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256"
|
||||||
|
}
|
8
apps/blog/app.js
Normal file
8
apps/blog/app.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import * as blog from './blog.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let blogs = await blog.get_posts();
|
||||||
|
await app.setDocument(blog.render_html(blogs));
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
207
apps/blog/blog.js
Normal file
207
apps/blog/blog.js
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import * as commonmark from './commonmark.min.js';
|
||||||
|
|
||||||
|
function escape(text) {
|
||||||
|
return (text ?? '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeAttribute(text) {
|
||||||
|
return (text ?? '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_blog_message(id) {
|
||||||
|
let message;
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
'SELECT author, timestamp, content FROM messages WHERE id = ?',
|
||||||
|
[id],
|
||||||
|
function (row) {
|
||||||
|
let content = JSON.parse(row.content);
|
||||||
|
message = {
|
||||||
|
author: row.author,
|
||||||
|
timestamp: row.timestamp,
|
||||||
|
blog: content?.blog,
|
||||||
|
title: content?.title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (message) {
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
|
SELECT json_extract(content, '$.name') AS name
|
||||||
|
FROM messages
|
||||||
|
WHERE author = ?
|
||||||
|
AND json_extract(content, '$.type') = 'about'
|
||||||
|
AND json_extract(content, '$.about') = author
|
||||||
|
AND name IS NOT NULL
|
||||||
|
ORDER BY sequence DESC LIMIT 1
|
||||||
|
`,
|
||||||
|
[message.author],
|
||||||
|
function (row) {
|
||||||
|
message.name = row.name;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markdown(md) {
|
||||||
|
let reader = new commonmark.Parser({safe: true});
|
||||||
|
let writer = new commonmark.HtmlRenderer();
|
||||||
|
let parsed = reader.parse(md || '');
|
||||||
|
let walker = parsed.walker();
|
||||||
|
let event, node;
|
||||||
|
while ((event = walker.next())) {
|
||||||
|
node = event.node;
|
||||||
|
if (event.entering) {
|
||||||
|
if (node.destination?.startsWith('&')) {
|
||||||
|
node.destination =
|
||||||
|
'/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
||||||
|
} else if (
|
||||||
|
node.destination?.startsWith('@') ||
|
||||||
|
node.destination?.startsWith('%')
|
||||||
|
) {
|
||||||
|
node.destination = '/~core/ssb/#' + escape(node.destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writer.render(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function render_blog_post_html(blog_post) {
|
||||||
|
let blob = utf8Decode(await ssb.blobGet(blog_post.blog));
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>🪵Tilde Friends Blog - ${markdown(blog_post.title)}</title>
|
||||||
|
<base target="_top">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1><a href="./">🪵Tilde Friends Blog</a></h1>
|
||||||
|
<div>
|
||||||
|
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
|
||||||
|
<div>${markdown(blob)}</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_blog_post(blog_post) {
|
||||||
|
return `
|
||||||
|
<div>
|
||||||
|
<h2><a href="/~${core.app.owner}/${core.app.name}/${escapeAttribute(blog_post.id)}">${escape(blog_post.title)}</a></h2>
|
||||||
|
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
|
||||||
|
<div>${markdown(blog_post.summary)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render_html(blogs) {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>🪵Tilde Friends Blog</title>
|
||||||
|
<link href="./atom" type="application/atom+xml" rel="alternate" title="🪵Tilde Blog"/>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<base target="_top">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div style="display: flex; flex-direction: row; align-items: center; gap: 1em">
|
||||||
|
<h1>🪵Tilde Friends Blog</h1>
|
||||||
|
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
|
||||||
|
</div>
|
||||||
|
${blogs.map((blog_post) => render_blog_post(blog_post)).join('\n')}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_blog_post_atom(blog_post) {
|
||||||
|
return `<entry>
|
||||||
|
<title>${escape(blog_post.title)}</title>
|
||||||
|
<link href="/~cory/ssb/#${blog_post.id}" />
|
||||||
|
<id>${blog_post.id}</id>
|
||||||
|
<published>${escape(new Date(blog_post.timestamp).toString())}</published>
|
||||||
|
<summary>${escape(blog_post.summary)}</summary>
|
||||||
|
<author>
|
||||||
|
<name>${escape(blog_post.name)}</name>
|
||||||
|
<feed>${escape(blog_post.author)}</feed>
|
||||||
|
</author>
|
||||||
|
</entry>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render_atom(blogs) {
|
||||||
|
return `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<title>🪵Tilde Blog</title>
|
||||||
|
<subtitle>A subtitle.</subtitle>
|
||||||
|
<link href="${core.url}/atom" rel="self"/>
|
||||||
|
<link href="${core.url}"/>
|
||||||
|
<id>${core.url}</id>
|
||||||
|
<updated>${new Date().toString()}</updated>
|
||||||
|
${blogs.map((blog_post) => render_blog_post_atom(blog_post)).join('\n')}
|
||||||
|
</feed>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get_posts() {
|
||||||
|
let blogs = [];
|
||||||
|
let ids = await ssb.getIdentities();
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
|
WITH
|
||||||
|
blogs AS (
|
||||||
|
SELECT
|
||||||
|
messages.author,
|
||||||
|
messages.id,
|
||||||
|
json_extract(messages.content, '$.title') AS title,
|
||||||
|
json_extract(messages.content, '$.summary') AS summary,
|
||||||
|
json_extract(messages.content, '$.blog') AS blog,
|
||||||
|
messages.timestamp
|
||||||
|
FROM messages_fts('blog')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE json_extract(messages.content, '$.type') = 'blog'),
|
||||||
|
public AS (
|
||||||
|
SELECT author FROM (
|
||||||
|
SELECT
|
||||||
|
messages.author,
|
||||||
|
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
|
||||||
|
json_extract(messages.content, '$.publicWebHosting') AS is_public
|
||||||
|
FROM messages_fts('about')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE json_extract(messages.content, '$.type') = 'about' AND is_public IS NOT NULL)
|
||||||
|
WHERE author_rank = 1 AND is_public),
|
||||||
|
names AS (
|
||||||
|
SELECT author, name FROM (
|
||||||
|
SELECT
|
||||||
|
messages.author,
|
||||||
|
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
|
||||||
|
json_extract(messages.content, '$.name') AS name
|
||||||
|
FROM messages_fts('about')
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE json_extract(messages.content, '$.type') = 'about' AND
|
||||||
|
json_extract(messages.content, '$.about') = messages.author AND
|
||||||
|
name IS NOT NULL)
|
||||||
|
WHERE author_rank = 1)
|
||||||
|
SELECT blogs.*, names.name FROM blogs
|
||||||
|
JOIN json_each(?) AS self ON self.value = blogs.author
|
||||||
|
JOIN public ON public.author = blogs.author
|
||||||
|
LEFT OUTER JOIN names ON names.author = blogs.author
|
||||||
|
ORDER BY blogs.timestamp DESC LIMIT 20
|
||||||
|
`,
|
||||||
|
[JSON.stringify(ids)],
|
||||||
|
function (row) {
|
||||||
|
blogs.push(row);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return blogs;
|
||||||
|
}
|
1
apps/blog/commonmark.min.js
vendored
Normal file
1
apps/blog/commonmark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
51
apps/blog/handler.js
Normal file
51
apps/blog/handler.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import * as blog from './blog.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (request.path.startsWith('%') && request.path.endsWith('.sha256')) {
|
||||||
|
let id = request.path.startsWith('%25')
|
||||||
|
? '%' + request.path.substring(3)
|
||||||
|
: request.path;
|
||||||
|
let message = await blog.get_blog_message(id);
|
||||||
|
if (message) {
|
||||||
|
respond({
|
||||||
|
data: await blog.render_blog_post_html(message),
|
||||||
|
content_type: 'text/html; charset=utf-8',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
respond({
|
||||||
|
data: `Message ${id} not found.`,
|
||||||
|
content_type: 'text/html; charset=utf-8',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (request.path == 'atom') {
|
||||||
|
let blogs = await blog.get_posts();
|
||||||
|
respond({
|
||||||
|
data: blog.render_atom(blogs),
|
||||||
|
content_type: 'application/atom+xml',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let blogs = await blog.get_posts();
|
||||||
|
for (let blog_post of blogs) {
|
||||||
|
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
|
||||||
|
if (request.path === title) {
|
||||||
|
respond({
|
||||||
|
data: await blog.render_blog_post_html(blog_post),
|
||||||
|
content_type: 'text/html; charset=utf-8',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
respond({
|
||||||
|
data: blog.render_html(blogs),
|
||||||
|
content_type: 'text/html; charset=utf-8',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(function (error) {
|
||||||
|
respond({
|
||||||
|
data: `<!DOCTYPE html>
|
||||||
|
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`,
|
||||||
|
content_type: 'text/html',
|
||||||
|
});
|
||||||
|
});
|
120
apps/blog/lit-all.min.js
vendored
Normal file
120
apps/blog/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/blog/lit-all.min.js.map
Normal file
1
apps/blog/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
}
|
"emoji": "💽"
|
||||||
|
}
|
||||||
|
@ -20,7 +20,7 @@ async function database_list() {
|
|||||||
}
|
}
|
||||||
populate_dbs('dbs', ${JSON.stringify(dbs)});
|
populate_dbs('dbs', ${JSON.stringify(dbs)});
|
||||||
</script>
|
</script>
|
||||||
</html>`
|
</html>`;
|
||||||
app.setDocument(doc);
|
app.setDocument(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,11 +47,11 @@ async function key_list(db) {
|
|||||||
}
|
}
|
||||||
populate_dbs('keys', ${JSON.stringify(object)});
|
populate_dbs('keys', ${JSON.stringify(object)});
|
||||||
</script>
|
</script>
|
||||||
</html>`
|
</html>`;
|
||||||
app.setDocument(doc);
|
app.setDocument(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
core.register('message', async function(message) {
|
core.register('message', async function (message) {
|
||||||
if (message.event == 'hashChange') {
|
if (message.event == 'hashChange') {
|
||||||
let hash = message.hash.substring(1);
|
let hash = message.hash.substring(1);
|
||||||
if (hash.startsWith(':shared:')) {
|
if (hash.startsWith(':shared:')) {
|
||||||
@ -67,4 +67,4 @@ core.register('message', async function(message) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
database_list();
|
database_list();
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "tildefriends-app"
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
@ -1,16 +0,0 @@
|
|||||||
# Tilde Friends Developer's Guide
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
A Tilde Friends application runs on the server. To make an interesting
|
|
||||||
application that interacts with the client, it's necessary to understand
|
|
||||||
how the parts work together.
|
|
||||||
|
|
||||||
## Hello, world!
|
|
||||||
|
|
||||||
A simple starting point. Presents `Hello, world!` in the browser when
|
|
||||||
visited.
|
|
||||||
|
|
||||||
**app.js**:
|
|
||||||
```
|
|
||||||
app.setDocument('<h1>Hello, world!</h1>');
|
|
||||||
```
|
|
@ -1,12 +0,0 @@
|
|||||||
# Tilde Friends Documentation
|
|
||||||
|
|
||||||
Tilde Friends is a participating member of a greater social
|
|
||||||
network, [Secure Scuttlebutt](https://scuttlebutt.nz/),
|
|
||||||
adding a way to safely and securely write, share,
|
|
||||||
and run code in the form of server-side web applications.
|
|
||||||
|
|
||||||
- [Tilde Friends Vision](#vision)
|
|
||||||
- [Secure Scuttlebutt from Scratch](#ssb)
|
|
||||||
- [Structure](#structure)
|
|
||||||
- [Guide](#guide)
|
|
||||||
- [TODO](#todo)
|
|
@ -1,41 +0,0 @@
|
|||||||
# Secure Scuttlebutt from Scratch
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
This aims to be the missing reference for those who wish to create a Secure
|
|
||||||
Scuttlebutt client from scratch.
|
|
||||||
|
|
||||||
## Discovery
|
|
||||||
A good way to get started is to participate in local network discovery with a known working
|
|
||||||
client on the same network. The
|
|
||||||
[Scuttlebutt Programming Guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#local-network)
|
|
||||||
is a good start, here, with a few things to note:
|
|
||||||
|
|
||||||
1. Some clients advertise multiple addresses separated by semicolons (`;`).
|
|
||||||
2. Some clients advertise alternative protocols than `shs` and use hostnames instead of
|
|
||||||
IPv4 addresses.
|
|
||||||
|
|
||||||
So be prepared to accept variations.
|
|
||||||
|
|
||||||
There also an undocumented "new" style of discovery message.
|
|
||||||
|
|
||||||
## Secret Handshake, Box Stream, and RPC Protocol
|
|
||||||
Now that two clients are aware of eachother, they need to complete a secret handshake.
|
|
||||||
The [programming guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#handshake)
|
|
||||||
is once again a good reference.
|
|
||||||
|
|
||||||
The box stream and RPC protocol can both be implemented from the
|
|
||||||
[same documentation](https://ssbc.github.io/scuttlebutt-protocol-guide/#box-stream)
|
|
||||||
without surprises.
|
|
||||||
|
|
||||||
## Synchronizing Data
|
|
||||||
|
|
||||||
... `ebt.replicate` or `createHistoryStream` ...
|
|
||||||
|
|
||||||
## Rooms
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## References
|
|
||||||
* [https://ssbc.github.io/scuttlebutt-protocol-guide/](https://ssbc.github.io/scuttlebutt-protocol-guide/)
|
|
||||||
* [https://dev.planetary.social/](https://dev.planetary.social/)
|
|
||||||
* [https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints](https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints)
|
|
@ -1,65 +0,0 @@
|
|||||||
# Tilde Friends Structure
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
Tilde Friends is a mostly-self-contained executable written in C.
|
|
||||||
|
|
||||||
In combines the following key components:
|
|
||||||
- A Secure Scuttlebutt (SSB) client/server. This talks with other SSB
|
|
||||||
instances, storing messages and blobs for anyone visible to local
|
|
||||||
users as they are encountered and sharing anything published locally
|
|
||||||
as appropriate.
|
|
||||||
- An sqlite database. This is where the SSB instance stores its data.
|
|
||||||
The general schema involves a `messages` table, storing mostly JSON,
|
|
||||||
a `blobs` table storing arbitrary blob data, and a `properties` table,
|
|
||||||
storing arbitrary state gleaned from `messages` and `blobs`, generally
|
|
||||||
updated on demand and incrementally.
|
|
||||||
- A QuickJS runtime. The core process runs stock scripts and has access
|
|
||||||
and permission to use all resources. All other processes, which
|
|
||||||
includes everything which runs untrusted code created by Tilde Friends
|
|
||||||
users, are strictly sandboxed in ways similar to how web browsers run
|
|
||||||
untrusted code. All attempts to access potentially sensitive resources
|
|
||||||
are mediated through the core process.
|
|
||||||
|
|
||||||
When run with no arguments, it starts a web server on
|
|
||||||
[http://localhost:12345/](http://localhost:12345/) and an SSB node.
|
|
||||||
|
|
||||||
## Web Interface
|
|
||||||
The Tilde Friends web server provides access to Tilde Friends applications,
|
|
||||||
which are arbitrary user-defined web applications.
|
|
||||||
|
|
||||||
At the top left, in addition to some basic navigation links, is an `edit`
|
|
||||||
link. Anyone can view, modify, and run in-place the code to any Tilde
|
|
||||||
Friends application by using the in-browser editor.
|
|
||||||
|
|
||||||
At the top right, one can `login` (to save work in their own space)
|
|
||||||
or `logout` (proceeding as a guest).
|
|
||||||
|
|
||||||
The rest of the page is an iframe belonging to the application.
|
|
||||||
|
|
||||||
## Special Paths
|
|
||||||
|
|
||||||
- `/~user/app/` - Tilde Friends application paths take the form `/~user/app/`, where `user`
|
|
||||||
is a username of a Tilde Friends account, and `app` is an arbitrary name
|
|
||||||
of an application saved by the given user.
|
|
||||||
- `/~user/app/file` - A raw file in an app.
|
|
||||||
- `/&blobid.ed25519` - A raw blob. Content-Type is inferred for at least
|
|
||||||
a few common image types.
|
|
||||||
|
|
||||||
## Communication Channels
|
|
||||||
Web Browser <-> Core <-> Sandbox
|
|
||||||
|
|
||||||
Visiting an application path delivers stock HTML and JavaScript which
|
|
||||||
establishes a WebSocket connection back to the server.
|
|
||||||
|
|
||||||
At this point, a new sandbox process is started in Tilde Friends, much
|
|
||||||
as a new sandboxed process might be started for a new tab in a web
|
|
||||||
browser. This process has a custom RPC connection to the core process
|
|
||||||
which holds the WebSocket connection to the browser.
|
|
||||||
|
|
||||||
The custom RPC communication between the sandbox process and the core
|
|
||||||
process facilitates passing and calling functions remotely. Calling a
|
|
||||||
function in another process returns a `Promise`.
|
|
||||||
|
|
||||||
An application will typically call `app.setDocument()` at startup to
|
|
||||||
populate the app's iframe in the web browser with its own client web
|
|
||||||
application resources.
|
|
@ -1,63 +0,0 @@
|
|||||||
# Tilde Friends TODO
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
## MVP3
|
|
||||||
- Sync status (problem feeds, messages/seconds stats, ...)
|
|
||||||
- app: wiki
|
|
||||||
- app: public blog
|
|
||||||
- Content-Disposition: download
|
|
||||||
- remove SSB credentials
|
|
||||||
- export SSB credentials
|
|
||||||
- initial: better empty news screen
|
|
||||||
- initial: remembered wrong user across login/logout
|
|
||||||
- initial: bad experience when following nobody
|
|
||||||
- make a cool independent app
|
|
||||||
- indicate when workspace differs from installed
|
|
||||||
- / => Something good.
|
|
||||||
- update docs
|
|
||||||
- audit + document API exposed to apps
|
|
||||||
- sqlStream => sqlExec or something
|
|
||||||
- fix weird HTTP warnings
|
|
||||||
- ssb from child process?
|
|
||||||
- channels
|
|
||||||
- placeholder/missing images
|
|
||||||
- no denial of service
|
|
||||||
- package standalone executable
|
|
||||||
- editor without app iframe
|
|
||||||
- sequence_before_author -> flags
|
|
||||||
- linkify ssb: links
|
|
||||||
- perfect rooms support
|
|
||||||
- connections 2.0
|
|
||||||
- make a better connections API
|
|
||||||
|
|
||||||
## Maybe Done
|
|
||||||
- blob_wants 2.0
|
|
||||||
- image downsample
|
|
||||||
- app: todo
|
|
||||||
- app: build archive
|
|
||||||
- update README
|
|
||||||
- administrators config
|
|
||||||
- apps name characters
|
|
||||||
- initial: can't switch to account when there is only one
|
|
||||||
- get tarball under 5MB
|
|
||||||
- rooms
|
|
||||||
- initial: doesn't refresh when create identity
|
|
||||||
- tf account timeout why
|
|
||||||
- ssb don't overflow boxes
|
|
||||||
- jwt for session tokens
|
|
||||||
- linkify https://...
|
|
||||||
- emoji reaction picker
|
|
||||||
- expose loads of stats
|
|
||||||
- confirm posting all new messages
|
|
||||||
- multiple identities per user, in database
|
|
||||||
- auto-populate data on initial launch
|
|
||||||
- make the docker image good / test it / use it
|
|
||||||
- leaking imports / exports
|
|
||||||
- file upload widget
|
|
||||||
- keep working on good error feedback
|
|
||||||
- build for windows
|
|
||||||
- installable apps (bring back an app message?)
|
|
||||||
|
|
||||||
## Done
|
|
||||||
- update LICENSE
|
|
||||||
- logging to browser
|
|
@ -1,62 +0,0 @@
|
|||||||
# Tilde Friends Vision
|
|
||||||
[Back to index](#index)
|
|
||||||
|
|
||||||
Tilde Friends is a tool for making and sharing.
|
|
||||||
|
|
||||||
It is both a peer-to-peer social network client, participating in Secure
|
|
||||||
Scuttlebutt, and an environment for creating and running web applications.
|
|
||||||
|
|
||||||
## Why
|
|
||||||
|
|
||||||
This is a thing that I wanted to exist and wanted to work on. No other reason.
|
|
||||||
There is not a business model. I believe it is interesting and unique.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
1. Make it **easy and fun** to run all sorts of web applications.
|
|
||||||
|
|
||||||
2. Provide **security** that is easy to understand and protects your data.
|
|
||||||
|
|
||||||
3. Make **creating and sharing** web applications accessible to anyone with a
|
|
||||||
browser.
|
|
||||||
|
|
||||||
## Ways to Use Tilde Friends
|
|
||||||
1. **Social Network User**: This is a social network first. You are just here,
|
|
||||||
because your friends are. Or you like how we limit your message length or
|
|
||||||
short videos or whatever the trend is. If you are ambitious, you click links
|
|
||||||
and see interactive experiences (apps) that you wouldn't see elsewhere.
|
|
||||||
|
|
||||||
2. **Web Visitor**: You get links from a friend to meeting invites, polls, games,
|
|
||||||
lists, wiki pages, ..., and you interact with them as though they were
|
|
||||||
cloud-hosted by a megacorporation. They just work, and you don't think twice.
|
|
||||||
|
|
||||||
3. **Group leader**: You host or use a small public instance, installing apps for
|
|
||||||
a group of friends to use as web visitors.
|
|
||||||
|
|
||||||
4. **Developer**: You like to write code and make or improve apps for fun or to
|
|
||||||
solve problems. When you encounter a Tilde Friends app on a strange server,
|
|
||||||
you know you can trivially modify it or download it to your own instance.
|
|
||||||
|
|
||||||
## Future Goals / Endgame
|
|
||||||
1. Mobile apps. This can run on your old phone. Maybe you won't be hosting
|
|
||||||
the web interface publicly, but you can sync, install and edit apps, and
|
|
||||||
otherwise get the full experience from a tiny touch screen.
|
|
||||||
|
|
||||||
2. The universal application runtime. The web browser is the universal
|
|
||||||
platform, but even for the simplest application that you might want to host
|
|
||||||
for your friends, cloud hosting, containers, and complicated dependencies might
|
|
||||||
all enter the mix. Tilde Friends, though it is yet another thing to host,
|
|
||||||
includes everything you need out of the box to run a vast variety of interesting
|
|
||||||
apps.
|
|
||||||
|
|
||||||
Tilde Friends will be built out, gradually providing safe access to host
|
|
||||||
resources and client resources the same way web browsers extended access to
|
|
||||||
resources like GPU, persistent storage, cameras, ... over the years.
|
|
||||||
|
|
||||||
Not much effort has been put forward yet to having a robust, long-lasting API,
|
|
||||||
but since the client side longevity is already handled by web browsers, it
|
|
||||||
seems possible that the server-side API can be managed in a similar way.
|
|
||||||
|
|
||||||
3. An awesome development environment. Right now it runs JavaScript from the
|
|
||||||
first embeddable text editor I could poorly configure enough to edit code,
|
|
||||||
but it could incorporate a debugger, source control integration a la ssb-git,
|
|
||||||
merge tools, and transpiling from all sorts of different languages.
|
|
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
}
|
"emoji": "➡️"
|
||||||
|
}
|
||||||
|
@ -1,107 +1,233 @@
|
|||||||
"use strict";
|
let g_about_cache = {};
|
||||||
|
|
||||||
var g_following_cache = {};
|
async function query(sql, args) {
|
||||||
var g_following_deep_cache = {};
|
let result = [];
|
||||||
var g_about_cache = {};
|
await ssb.sqlAsync(sql, args, function (row) {
|
||||||
|
result.push(row);
|
||||||
async function following(db, id) {
|
});
|
||||||
if (g_following_cache[id]) {
|
return result;
|
||||||
return g_following_cache[id];
|
|
||||||
}
|
|
||||||
var o = await db.get(id + ":following");
|
|
||||||
const k_version = 5;
|
|
||||||
var f = o ? JSON.parse(o) : o;
|
|
||||||
if (!f || f.version != k_version) {
|
|
||||||
f = {users: [], sequence: 0, version: k_version};
|
|
||||||
}
|
|
||||||
f.users = new Set(f.users);
|
|
||||||
await ssb.sqlAsync(
|
|
||||||
"SELECT "+
|
|
||||||
" sequence, "+
|
|
||||||
" json_extract(content, '$.contact') AS contact, "+
|
|
||||||
" json_extract(content, '$.following') AS following "+
|
|
||||||
"FROM messages "+
|
|
||||||
"WHERE "+
|
|
||||||
" author = ?1 AND "+
|
|
||||||
" sequence > ?2 AND "+
|
|
||||||
" json_extract(content, '$.type') = 'contact' "+
|
|
||||||
"UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
|
|
||||||
"ORDER BY sequence",
|
|
||||||
[id, f.sequence],
|
|
||||||
function(row) {
|
|
||||||
if (row.following) {
|
|
||||||
f.users.add(row.contact);
|
|
||||||
} else {
|
|
||||||
f.users.delete(row.contact);
|
|
||||||
}
|
|
||||||
f.sequence = row.sequence;
|
|
||||||
});
|
|
||||||
var as_set = f.users;
|
|
||||||
f.users = Array.from(f.users).sort();
|
|
||||||
var j = JSON.stringify(f);
|
|
||||||
if (o != j) {
|
|
||||||
await db.set(id + ":following", j);
|
|
||||||
}
|
|
||||||
f.users = as_set;
|
|
||||||
g_following_cache[id] = f.users;
|
|
||||||
return f.users;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function followingDeep(db, seed_ids, depth) {
|
async function contacts_internal(id, last_row_id, following, max_row_id) {
|
||||||
if (depth <= 0) {
|
let result = Object.assign({}, following[id] || {});
|
||||||
return seed_ids;
|
result.following = result.following || {};
|
||||||
|
result.blocking = result.blocking || {};
|
||||||
|
let contacts = await query(
|
||||||
|
`
|
||||||
|
SELECT content FROM messages
|
||||||
|
WHERE author = ? AND
|
||||||
|
rowid > ? AND
|
||||||
|
rowid <= ? AND
|
||||||
|
json_extract(content, '$.type') = 'contact'
|
||||||
|
ORDER BY sequence
|
||||||
|
`,
|
||||||
|
[id, last_row_id, max_row_id]
|
||||||
|
);
|
||||||
|
for (let row of contacts) {
|
||||||
|
let contact = JSON.parse(row.content);
|
||||||
|
if (contact.following === true) {
|
||||||
|
result.following[contact.contact] = true;
|
||||||
|
} else if (contact.following === false) {
|
||||||
|
delete result.following[contact.contact];
|
||||||
|
} else if (contact.blocking === true) {
|
||||||
|
result.blocking[contact.contact] = true;
|
||||||
|
} else if (contact.blocking === false) {
|
||||||
|
delete result.blocking[contact.contact];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var key = JSON.stringify([seed_ids, depth]);
|
following[id] = result;
|
||||||
if (g_following_deep_cache[key]) {
|
return result;
|
||||||
return g_following_deep_cache[key];
|
}
|
||||||
|
|
||||||
|
async function contact(id, last_row_id, following, max_row_id) {
|
||||||
|
return await contacts_internal(id, last_row_id, following, max_row_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function following_deep_internal(
|
||||||
|
ids,
|
||||||
|
depth,
|
||||||
|
blocking,
|
||||||
|
last_row_id,
|
||||||
|
following,
|
||||||
|
max_row_id
|
||||||
|
) {
|
||||||
|
let contacts = await Promise.all(
|
||||||
|
[...new Set(ids)].map((x) => contact(x, last_row_id, following, max_row_id))
|
||||||
|
);
|
||||||
|
let result = {};
|
||||||
|
for (let i = 0; i < ids.length; i++) {
|
||||||
|
let id = ids[i];
|
||||||
|
let contact = contacts[i];
|
||||||
|
let all_blocking = Object.assign({}, contact.blocking, blocking);
|
||||||
|
let found = Object.keys(contact.following).filter((y) => !all_blocking[y]);
|
||||||
|
let deeper =
|
||||||
|
depth > 1
|
||||||
|
? await following_deep_internal(
|
||||||
|
found,
|
||||||
|
depth - 1,
|
||||||
|
all_blocking,
|
||||||
|
last_row_id,
|
||||||
|
following,
|
||||||
|
max_row_id
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
result[id] = [id, ...found, ...deeper];
|
||||||
}
|
}
|
||||||
var f = await Promise.all(seed_ids.map(x => following(db, x).then(x => [...x])));
|
return [...new Set(Object.values(result).flat())];
|
||||||
var ids = [].concat(...f);
|
}
|
||||||
var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1);
|
|
||||||
x = [...new Set([].concat(...x, ...seed_ids))].sort();
|
async function following_deep(ids, depth, blocking) {
|
||||||
g_following_deep_cache[key] = x;
|
let db = await database('cache');
|
||||||
return x;
|
const k_cache_version = 5;
|
||||||
|
let cache = await db.get('following');
|
||||||
|
cache = cache ? JSON.parse(cache) : {};
|
||||||
|
if (cache.version !== k_cache_version) {
|
||||||
|
cache = {
|
||||||
|
version: k_cache_version,
|
||||||
|
following: {},
|
||||||
|
last_row_id: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let max_row_id = (
|
||||||
|
await query(
|
||||||
|
`
|
||||||
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
|
`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
)[0].max_row_id;
|
||||||
|
let result = await following_deep_internal(
|
||||||
|
ids,
|
||||||
|
depth,
|
||||||
|
blocking,
|
||||||
|
cache.last_row_id,
|
||||||
|
cache.following,
|
||||||
|
max_row_id
|
||||||
|
);
|
||||||
|
cache.last_row_id = max_row_id;
|
||||||
|
let store = JSON.stringify(cache);
|
||||||
|
await db.set('following', store);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetch_about(db, ids, users) {
|
||||||
|
const k_cache_version = 1;
|
||||||
|
let cache = await db.get('about');
|
||||||
|
cache = cache ? JSON.parse(cache) : {};
|
||||||
|
if (cache.version !== k_cache_version) {
|
||||||
|
cache = {
|
||||||
|
version: k_cache_version,
|
||||||
|
about: {},
|
||||||
|
last_row_id: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let max_row_id = 0;
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
|
`,
|
||||||
|
[],
|
||||||
|
function (row) {
|
||||||
|
max_row_id = row.max_row_id;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
for (let id of Object.keys(cache.about)) {
|
||||||
|
if (ids.indexOf(id) == -1) {
|
||||||
|
delete cache.about[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let abouts = [];
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
messages.*
|
||||||
|
FROM
|
||||||
|
messages,
|
||||||
|
json_each(?1) AS following
|
||||||
|
WHERE
|
||||||
|
messages.author = following.value AND
|
||||||
|
messages.rowid > ?3 AND
|
||||||
|
messages.rowid <= ?4 AND
|
||||||
|
json_extract(messages.content, '$.type') = 'about'
|
||||||
|
UNION
|
||||||
|
SELECT
|
||||||
|
messages.*
|
||||||
|
FROM
|
||||||
|
messages,
|
||||||
|
json_each(?2) AS following
|
||||||
|
WHERE
|
||||||
|
messages.author = following.value AND
|
||||||
|
messages.rowid <= ?4 AND
|
||||||
|
json_extract(messages.content, '$.type') = 'about'
|
||||||
|
ORDER BY messages.author, messages.sequence
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
JSON.stringify(ids.filter((id) => cache.about[id])),
|
||||||
|
JSON.stringify(ids.filter((id) => !cache.about[id])),
|
||||||
|
cache.last_row_id,
|
||||||
|
max_row_id,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
for (let about of abouts) {
|
||||||
|
let content = JSON.parse(about.content);
|
||||||
|
if (content.about === about.author) {
|
||||||
|
delete content.type;
|
||||||
|
delete content.about;
|
||||||
|
cache.about[about.author] = Object.assign(
|
||||||
|
cache.about[about.author] || {},
|
||||||
|
content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cache.last_row_id = max_row_id;
|
||||||
|
await db.set('about', JSON.stringify(cache));
|
||||||
|
users = users || {};
|
||||||
|
for (let id of Object.keys(cache.about)) {
|
||||||
|
users[id] = Object.assign(users[id] || {}, cache.about[id]);
|
||||||
|
}
|
||||||
|
return Object.assign({}, users);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAbout(db, id) {
|
async function getAbout(db, id) {
|
||||||
if (g_about_cache[id]) {
|
if (g_about_cache[id]) {
|
||||||
return g_about_cache[id];
|
return g_about_cache[id];
|
||||||
}
|
}
|
||||||
var o = await db.get(id + ":about");
|
let o = await db.get(id + ':about');
|
||||||
const k_version = 4;
|
const k_version = 4;
|
||||||
var f = o ? JSON.parse(o) : o;
|
let f = o ? JSON.parse(o) : o;
|
||||||
if (!f || f.version != k_version) {
|
if (!f || f.version != k_version) {
|
||||||
f = {about: {}, sequence: 0, version: k_version};
|
f = {about: {}, sequence: 0, version: k_version};
|
||||||
}
|
}
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
"SELECT "+
|
'SELECT ' +
|
||||||
" sequence, "+
|
' sequence, ' +
|
||||||
" content "+
|
' content ' +
|
||||||
"FROM messages "+
|
'FROM messages ' +
|
||||||
"WHERE "+
|
'WHERE ' +
|
||||||
" author = ?1 AND "+
|
' author = ?1 AND ' +
|
||||||
" sequence > ?2 AND "+
|
' sequence > ?2 AND ' +
|
||||||
" json_extract(content, '$.type') = 'about' AND "+
|
" json_extract(content, '$.type') = 'about' AND " +
|
||||||
" json_extract(content, '$.about') = ?1 "+
|
" json_extract(content, '$.about') = ?1 " +
|
||||||
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
|
'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
|
||||||
"ORDER BY sequence",
|
'ORDER BY sequence',
|
||||||
[id, f.sequence],
|
[id, f.sequence],
|
||||||
function(row) {
|
function (row) {
|
||||||
f.sequence = row.sequence;
|
f.sequence = row.sequence;
|
||||||
if (row.content) {
|
if (row.content) {
|
||||||
var about = {};
|
let about = {};
|
||||||
try {
|
try {
|
||||||
about = JSON.parse(row.content);
|
about = JSON.parse(row.content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
delete about.about;
|
delete about.about;
|
||||||
delete about.type;
|
delete about.type;
|
||||||
f.about = Object.assign(f.about, about);
|
f.about = Object.assign(f.about, about);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
var j = JSON.stringify(f);
|
);
|
||||||
|
let j = JSON.stringify(f);
|
||||||
if (o != j) {
|
if (o != j) {
|
||||||
await db.set(id + ":about", j);
|
await db.set(id + ':about', j);
|
||||||
}
|
}
|
||||||
g_about_cache[id] = f.about;
|
g_about_cache[id] = f.about;
|
||||||
return f.about;
|
return f.about;
|
||||||
@ -110,14 +236,34 @@ async function getAbout(db, id) {
|
|||||||
async function getSize(db, id) {
|
async function getSize(db, id) {
|
||||||
let size = 0;
|
let size = 0;
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
"SELECT (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
|
'SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1',
|
||||||
[id],
|
[id],
|
||||||
function (row) {
|
function (row) {
|
||||||
size += row.size;
|
size += row.size;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getSizes(ids) {
|
||||||
|
let sizes = {};
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
author,
|
||||||
|
(SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(messages.id))) AS size
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?) AS ids ON author = ids.value
|
||||||
|
GROUP BY author
|
||||||
|
`,
|
||||||
|
[JSON.stringify(ids)],
|
||||||
|
function (row) {
|
||||||
|
sizes[row.author] = row.size;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
|
||||||
function niceSize(bytes) {
|
function niceSize(bytes) {
|
||||||
let value = bytes;
|
let value = bytes;
|
||||||
let unit = 'B';
|
let unit = 'B';
|
||||||
@ -133,27 +279,39 @@ function niceSize(bytes) {
|
|||||||
return Math.round(value * 10) / 10 + ' ' + unit;
|
return Math.round(value * 10) / 10 + ' ' + unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildTree(db, root, indent, depth) {
|
function escape(value) {
|
||||||
var f = await following(db, root);
|
return value
|
||||||
var result = indent + '[' + f.size + '] ' + '<a target="_top" href="../index/#' + root + '">' + ((await getAbout(db, root)).name || root) + '</a> ' + niceSize(await getSize(db, root)) + '\n';
|
.replaceAll('&', '&')
|
||||||
if (depth > 0) {
|
.replaceAll('<', '<')
|
||||||
for (let next of f) {
|
.replaceAll('>', '>');
|
||||||
result += await buildTree(db, next, indent + ' ', depth - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
await app.setDocument('<pre style="color: #fff">building...</pre>');
|
await app.setDocument('<pre style="color: #fff">building...</pre>');
|
||||||
var db = await database('ssb');
|
let db = await database('ssb');
|
||||||
var whoami = await ssb.getIdentities();
|
let whoami = await ssb.getIdentities();
|
||||||
var tree = '';
|
let tree = '';
|
||||||
for (let id of whoami) {
|
await app.setDocument(
|
||||||
await app.setDocument(`<pre style="color: #fff">building... ${id}</pre>`);
|
`<pre style="color: #fff">Enumerating followed users...</pre>`
|
||||||
tree += await buildTree(db, id, '', 2);
|
);
|
||||||
|
let following = await following_deep(whoami, 2, {});
|
||||||
|
await app.setDocument(
|
||||||
|
`<pre style="color: #fff">Getting names and sizes...</pre>`
|
||||||
|
);
|
||||||
|
let [about, sizes] = await Promise.all([
|
||||||
|
fetch_about(db, following, {}),
|
||||||
|
getSizes(following),
|
||||||
|
]);
|
||||||
|
await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
|
||||||
|
following.sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0));
|
||||||
|
for (let id of following) {
|
||||||
|
tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
|
||||||
}
|
}
|
||||||
await app.setDocument('<pre style="color: #fff">FOLLOWING:\n' + tree + '</pre>');
|
await app.setDocument(
|
||||||
|
'<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' +
|
||||||
|
tree +
|
||||||
|
'</ul>\n</body>\n</html>'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
5
apps/identity.json
Normal file
5
apps/identity.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🪪",
|
||||||
|
"previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256"
|
||||||
|
}
|
93
apps/identity/app.js
Normal file
93
apps/identity/app.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
|
||||||
|
tfrpc.register(async function get_private_key(id) {
|
||||||
|
return bip39Words(await ssb.getPrivateKey(id));
|
||||||
|
});
|
||||||
|
tfrpc.register(async function create_id(id) {
|
||||||
|
return await ssb.createIdentity();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function add_id(id) {
|
||||||
|
return await ssb.addIdentity(bip39Bytes(id));
|
||||||
|
});
|
||||||
|
tfrpc.register(async function delete_id(id) {
|
||||||
|
return await ssb.deleteIdentity(id);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function reload() {
|
||||||
|
await main();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let ids = await ssb.getIdentities();
|
||||||
|
await app.setDocument(
|
||||||
|
`<body style="color: #fff">
|
||||||
|
<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%; read-only: true';
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.hide_id = function hide_id(event, element) {
|
||||||
|
element.parentNode.removeChild(element);
|
||||||
|
event.srcElement.onclick = handler.export_id;
|
||||||
|
}
|
||||||
|
handler.delete_id = async function delete_id(event) {
|
||||||
|
let id = event.srcElement.dataset.id;
|
||||||
|
try {
|
||||||
|
if (prompt('Are you sure you want to delete "' + id + '"? It cannot be recovered without the exported phrase.\\n\\nEnter the word "DELETE" to confirm you wish to delete it.') === 'DELETE') {
|
||||||
|
if (await tfrpc.rpc.delete_id(id)) {
|
||||||
|
alert('Successfully deleted ID: ' + id);
|
||||||
|
}
|
||||||
|
await tfrpc.rpc.reload();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Error deleting ID: ' + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<h1>SSB Identity Management</h1>
|
||||||
|
<h2>Create a new identity</h2>
|
||||||
|
<button id="create_id" onclick="handler.create_id()">Create Identity</button>
|
||||||
|
<h2>Import an SSB Identity from 12 BIP39 English Words</h2>
|
||||||
|
<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button>
|
||||||
|
<h2>Identities</h2>
|
||||||
|
<ul>` +
|
||||||
|
ids
|
||||||
|
.map(
|
||||||
|
(id) => `<li>
|
||||||
|
<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button>
|
||||||
|
<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button>
|
||||||
|
${id}
|
||||||
|
</li>`
|
||||||
|
)
|
||||||
|
.join('\n') +
|
||||||
|
` </ul>
|
||||||
|
</body>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
5
apps/issues.json
Normal file
5
apps/issues.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🦟",
|
||||||
|
"previous": "&TegdzvFE+im94shygaHkgDYSaSrwY2h0OKUXSRPBQDM=.sha256"
|
||||||
|
}
|
105
apps/issues/app.js
Normal file
105
apps/issues/app.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
|
||||||
|
let g_database;
|
||||||
|
let g_hash;
|
||||||
|
|
||||||
|
tfrpc.register(async function localStorageGet(key) {
|
||||||
|
return app.localStorageGet(key);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function localStorageSet(key, value) {
|
||||||
|
return app.localStorageSet(key, value);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function databaseGet(key) {
|
||||||
|
return g_database ? g_database.get(key) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function databaseSet(key, value) {
|
||||||
|
return g_database ? g_database.set(key, value) : undefined;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function createIdentity() {
|
||||||
|
return ssb.createIdentity();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getIdentities() {
|
||||||
|
return ssb.getIdentities();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getAllIdentities() {
|
||||||
|
return ssb.getAllIdentities();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getBroadcasts() {
|
||||||
|
return ssb.getBroadcasts();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getConnections() {
|
||||||
|
return ssb.connections();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function getStoredConnections() {
|
||||||
|
return ssb.storedConnections();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function forgetStoredConnection(connection) {
|
||||||
|
return ssb.forgetStoredConnection(connection);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function createTunnel(portal, target) {
|
||||||
|
return ssb.createTunnel(portal, target);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function connect(token) {
|
||||||
|
await ssb.connect(token);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function closeConnection(id) {
|
||||||
|
await ssb.closeConnection(id);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function query(sql, args) {
|
||||||
|
let result = [];
|
||||||
|
await ssb.sqlAsync(sql, args, function callback(row) {
|
||||||
|
result.push(row);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
tfrpc.register(async function appendMessage(id, message) {
|
||||||
|
return ssb.appendMessageWithIdentity(id, message);
|
||||||
|
});
|
||||||
|
core.register('message', async function message_handler(message) {
|
||||||
|
if (message.event == 'hashChange') {
|
||||||
|
g_hash = message.hash;
|
||||||
|
await tfrpc.rpc.hashChanged(message.hash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tfrpc.register(function getHash(id, message) {
|
||||||
|
return g_hash;
|
||||||
|
});
|
||||||
|
tfrpc.register(function setHash(hash) {
|
||||||
|
return app.setHash(hash);
|
||||||
|
});
|
||||||
|
ssb.addEventListener('message', async function (id) {
|
||||||
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function store_blob(blob) {
|
||||||
|
if (Array.isArray(blob)) {
|
||||||
|
blob = Uint8Array.from(blob);
|
||||||
|
}
|
||||||
|
return await ssb.blobStore(blob);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function get_blob(id) {
|
||||||
|
return utf8Decode(await ssb.blobGet(id));
|
||||||
|
});
|
||||||
|
tfrpc.register(async function store_message(message) {
|
||||||
|
return await ssb.storeMessage(message);
|
||||||
|
});
|
||||||
|
tfrpc.register(function apps() {
|
||||||
|
return core.apps();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function try_decrypt(id, content) {
|
||||||
|
return await ssb.privateMessageDecrypt(id, content);
|
||||||
|
});
|
||||||
|
ssb.addEventListener('broadcasts', async function () {
|
||||||
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
|
});
|
||||||
|
|
||||||
|
core.register('onConnectionsChanged', async function () {
|
||||||
|
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (typeof database !== 'undefined') {
|
||||||
|
g_database = await database('ssb');
|
||||||
|
}
|
||||||
|
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||||
|
}
|
||||||
|
main();
|
91
apps/issues/commonmark-linkify.js
Normal file
91
apps/issues/commonmark-linkify.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
function textNode(text) {
|
||||||
|
const node = new commonmark.Node("text", undefined);
|
||||||
|
node.literal = text;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkNode(text, url) {
|
||||||
|
const urlNode = new commonmark.Node("link", undefined);
|
||||||
|
urlNode.destination = url;
|
||||||
|
urlNode.appendChild(textNode(text));
|
||||||
|
|
||||||
|
return urlNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMatches(text, regexp) {
|
||||||
|
// Regexp must be sticky.
|
||||||
|
regexp = new RegExp(regexp, "gm");
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
let match = regexp.exec(text);
|
||||||
|
while (match) {
|
||||||
|
const matchText = match[0];
|
||||||
|
|
||||||
|
if (match.index > i) {
|
||||||
|
result.push([text.substring(i, match.index), false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push([matchText, true]);
|
||||||
|
i = match.index + matchText.length;
|
||||||
|
|
||||||
|
match = regexp.exec(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < text.length) {
|
||||||
|
result.push([text.substring(i, text.length), false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlRegexp = new RegExp("https?://[^ ]+[^ .,]");
|
||||||
|
|
||||||
|
function splitURLs(textNodes) {
|
||||||
|
const text = textNodes.map(n => n.literal).join("");
|
||||||
|
const parts = splitMatches(text, urlRegexp);
|
||||||
|
|
||||||
|
return parts.map(part => {
|
||||||
|
if (part[1]) {
|
||||||
|
return linkNode(part[0], part[0]);
|
||||||
|
} else {
|
||||||
|
return textNode(part[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transform(parsed) {
|
||||||
|
const walker = parsed.walker();
|
||||||
|
let event;
|
||||||
|
|
||||||
|
let nodes = [];
|
||||||
|
while ((event = walker.next())) {
|
||||||
|
const node = event.node;
|
||||||
|
if (event.entering && node.type === "text") {
|
||||||
|
nodes.push(node);
|
||||||
|
} else {
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
splitURLs(nodes)
|
||||||
|
.reverse()
|
||||||
|
.forEach(newNode => {
|
||||||
|
nodes[0].insertAfter(newNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodes.forEach(n => n.unlink());
|
||||||
|
nodes = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
splitURLs(nodes)
|
||||||
|
.reverse()
|
||||||
|
.forEach(newNode => {
|
||||||
|
nodes[0].insertAfter(newNode);
|
||||||
|
});
|
||||||
|
nodes.forEach(n => n.unlink());
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
1
apps/issues/commonmark.min.js
vendored
Normal file
1
apps/issues/commonmark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
16
apps/issues/index.html
Normal file
16
apps/issues/index.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html style="color: #fff">
|
||||||
|
<head>
|
||||||
|
<title>Tilde Friends</title>
|
||||||
|
<base target="_top" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<tf-issues-app />
|
||||||
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
|
<script src="commonmark.min.js"></script>
|
||||||
|
<script src="commonmark-linkify.js" type="module"></script>
|
||||||
|
<script src="script.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
120
apps/issues/lit-all.min.js
vendored
Normal file
120
apps/issues/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/issues/lit-all.min.js.map
Normal file
1
apps/issues/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
293
apps/issues/script.js
Normal file
293
apps/issues/script.js
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import * as tfutils from './tf-utils.js';
|
||||||
|
|
||||||
|
const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256';
|
||||||
|
|
||||||
|
class 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 {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
value: {type: String},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
input() {
|
||||||
|
let input = this.renderRoot.getElementById('input');
|
||||||
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
|
if (input && preview) {
|
||||||
|
preview.innerHTML = tfutils.markdown(input.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-submit', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
value: this.renderRoot.getElementById('input').value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.renderRoot.getElementById('input').value = '';
|
||||||
|
this.input();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<textarea id="input" @input=${this.input} style="flex: 1 1">${this.value}</textarea>
|
||||||
|
<div id="preview" style="flex: 1 1"></div>
|
||||||
|
</div>
|
||||||
|
<input type="submit" value="Submit" @click=${this.submit}></input>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-compose', TfComposeElement);
|
||||||
|
|
||||||
|
class TfIssuesAppElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
issues: {type: Array},
|
||||||
|
selected: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.issues = [];
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
let issues = {};
|
||||||
|
let messages = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
WITH issues AS (SELECT messages.* FROM messages_refs JOIN messages ON
|
||||||
|
messages.id = messages_refs.message
|
||||||
|
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
|
||||||
|
edits AS (SELECT messages.* FROM issues JOIN messages_refs ON
|
||||||
|
issues.id = messages_refs.ref JOIN messages ON
|
||||||
|
messages.id = messages_refs.message
|
||||||
|
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
|
||||||
|
SELECT * FROM issues
|
||||||
|
UNION
|
||||||
|
SELECT * FROM edits ORDER BY timestamp
|
||||||
|
`,
|
||||||
|
[k_project]
|
||||||
|
);
|
||||||
|
for (let message of messages) {
|
||||||
|
let content = JSON.parse(message.content);
|
||||||
|
switch (content.type) {
|
||||||
|
case 'issue':
|
||||||
|
issues[message.id] = {
|
||||||
|
id: message.id,
|
||||||
|
author: message.author,
|
||||||
|
text: content.text,
|
||||||
|
updates: [],
|
||||||
|
created: message.timestamp,
|
||||||
|
open: true,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'issue-edit':
|
||||||
|
case 'post':
|
||||||
|
for (let issue of content.issues || []) {
|
||||||
|
if (issues[issue.link]) {
|
||||||
|
if (issue.open !== undefined) {
|
||||||
|
issues[issue.link].open = issue.open;
|
||||||
|
message.open = issue.open;
|
||||||
|
}
|
||||||
|
issues[issue.link].updates.push(message);
|
||||||
|
issues[issue.link].updated = message.timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.issues = Object.values(issues).sort(
|
||||||
|
(x, y) => y.open - x.open || y.created - x.created
|
||||||
|
);
|
||||||
|
if (this.selected) {
|
||||||
|
for (let issue of this.issues) {
|
||||||
|
if (issue.id == this.selected.id) {
|
||||||
|
this.selected = issue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_issue_table_row(issue) {
|
||||||
|
return html`
|
||||||
|
<tr>
|
||||||
|
<td>${issue.open ? '☐ open' : '☑ closed'}</td>
|
||||||
|
<td
|
||||||
|
style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
|
||||||
|
>
|
||||||
|
${issue.author}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer"
|
||||||
|
@click=${() => (this.selected = issue)}
|
||||||
|
>
|
||||||
|
${issue.text.split('\n')?.[0]}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${new Date(issue.updated ?? issue.created).toLocaleDateString()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_update(update) {
|
||||||
|
let content = JSON.parse(update.content);
|
||||||
|
let message;
|
||||||
|
if (content.text) {
|
||||||
|
message = unsafeHTML(tfutils.markdown(content.text));
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div style="border-left: 2px solid #fff; padding-left: 8px">
|
||||||
|
<div>${new Date(update.timestamp).toLocaleString()}</div>
|
||||||
|
<div>${update.author}</div>
|
||||||
|
<div>${message}</div>
|
||||||
|
<div>
|
||||||
|
${update.open !== undefined
|
||||||
|
? update.open
|
||||||
|
? 'issue opened'
|
||||||
|
: 'issue closed'
|
||||||
|
: undefined}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set_open(id, open) {
|
||||||
|
if (
|
||||||
|
confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)
|
||||||
|
) {
|
||||||
|
let whoami = this.shadowRoot.getElementById('picker').selected;
|
||||||
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
|
type: 'issue-edit',
|
||||||
|
issues: [
|
||||||
|
{
|
||||||
|
link: id,
|
||||||
|
open: open,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create_issue(event) {
|
||||||
|
let whoami = this.shadowRoot.getElementById('picker').selected;
|
||||||
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
|
type: 'issue',
|
||||||
|
project: k_project,
|
||||||
|
text: event.detail.value,
|
||||||
|
});
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async reply_to_issue(event) {
|
||||||
|
let whoami = this.shadowRoot.getElementById('picker').selected;
|
||||||
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
|
type: 'post',
|
||||||
|
text: event.detail.value,
|
||||||
|
root: this.selected.id,
|
||||||
|
branch: this.selected.updates.length
|
||||||
|
? this.selected.updates[this.selected.updates.length - 1].id
|
||||||
|
: this.selected.id,
|
||||||
|
issues: [
|
||||||
|
{
|
||||||
|
link: this.selected.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let header = html`
|
||||||
|
<h1>Tilde Friends Issues</h1>
|
||||||
|
<tf-id-picker id="picker"></tf-id-picker>
|
||||||
|
`;
|
||||||
|
if (this.selected) {
|
||||||
|
return html`
|
||||||
|
${header}
|
||||||
|
<div>
|
||||||
|
<input type="button" value="Back" @click=${() => (this.selected = undefined)}></input>
|
||||||
|
${
|
||||||
|
this.selected.open
|
||||||
|
? html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>`
|
||||||
|
: html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>${new Date(this.selected.created).toLocaleString()}</div>
|
||||||
|
<div>${this.selected.author}</div>
|
||||||
|
<div>${this.selected.id}</div>
|
||||||
|
<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
|
||||||
|
${this.selected.updates.map((x) => this.render_update(x))}
|
||||||
|
<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
return html`
|
||||||
|
${header}
|
||||||
|
<h2>New Issue</h2>
|
||||||
|
<tf-compose @tf-submit=${this.create_issue}></tf-compose>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Author</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Date</th>
|
||||||
|
</tr>
|
||||||
|
${this.issues.map((x) => this.render_issue_table_row(x))}
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-issues-app', TfIssuesAppElement);
|
113
apps/issues/tf-utils.js
Normal file
113
apps/issues/tf-utils.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import * as linkify from './commonmark-linkify.js';
|
||||||
|
|
||||||
|
function image(node, entering) {
|
||||||
|
if (
|
||||||
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('video:')
|
||||||
|
) {
|
||||||
|
if (entering) {
|
||||||
|
this.lit(
|
||||||
|
'<video style="max-width: 100%; max-height: 480px" title="' +
|
||||||
|
this.esc(node.firstChild?.literal) +
|
||||||
|
'" controls>'
|
||||||
|
);
|
||||||
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
|
this.disableTags += 1;
|
||||||
|
} else {
|
||||||
|
this.disableTags -= 1;
|
||||||
|
this.lit('</video>');
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('audio:')
|
||||||
|
) {
|
||||||
|
if (entering) {
|
||||||
|
this.lit(
|
||||||
|
'<audio style="height: 32px; max-width: 100%" title="' +
|
||||||
|
this.esc(node.firstChild?.literal) +
|
||||||
|
'" controls>'
|
||||||
|
);
|
||||||
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
|
this.disableTags += 1;
|
||||||
|
} else {
|
||||||
|
this.disableTags -= 1;
|
||||||
|
this.lit('</audio>');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (entering) {
|
||||||
|
if (this.disableTags === 0) {
|
||||||
|
this.lit(
|
||||||
|
'<div class="img_caption">' +
|
||||||
|
this.esc(node.firstChild?.literal || node.destination) +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
|
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
||||||
|
this.lit('<img src="" alt="');
|
||||||
|
} else {
|
||||||
|
this.lit('<img src="' + this.esc(node.destination) + '" alt="');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.disableTags += 1;
|
||||||
|
} else {
|
||||||
|
this.disableTags -= 1;
|
||||||
|
if (this.disableTags === 0) {
|
||||||
|
if (node.title) {
|
||||||
|
this.lit('" title="' + this.esc(node.title));
|
||||||
|
}
|
||||||
|
this.lit('" />');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markdown(md) {
|
||||||
|
var reader = new commonmark.Parser({safe: true});
|
||||||
|
var writer = new commonmark.HtmlRenderer();
|
||||||
|
writer.image = image;
|
||||||
|
var parsed = reader.parse(md || '');
|
||||||
|
parsed = linkify.transform(parsed);
|
||||||
|
var walker = parsed.walker();
|
||||||
|
var event, node;
|
||||||
|
while ((event = walker.next())) {
|
||||||
|
node = event.node;
|
||||||
|
if (event.entering) {
|
||||||
|
if (node.type == 'link') {
|
||||||
|
if (
|
||||||
|
node.destination.startsWith('@') &&
|
||||||
|
node.destination.endsWith('.ed25519')
|
||||||
|
) {
|
||||||
|
node.destination = '#' + node.destination;
|
||||||
|
} else if (
|
||||||
|
node.destination.startsWith('%') &&
|
||||||
|
node.destination.endsWith('.sha256')
|
||||||
|
) {
|
||||||
|
node.destination = '#' + node.destination;
|
||||||
|
} else if (
|
||||||
|
node.destination.startsWith('&') &&
|
||||||
|
node.destination.endsWith('.sha256')
|
||||||
|
) {
|
||||||
|
node.destination = '/' + node.destination + '/view';
|
||||||
|
}
|
||||||
|
} else if (node.type == 'image') {
|
||||||
|
if (node.destination.startsWith('&')) {
|
||||||
|
node.destination = '/' + node.destination + '/view';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writer.render(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function human_readable_size(bytes) {
|
||||||
|
let v = bytes;
|
||||||
|
let u = 'B';
|
||||||
|
for (let unit of ['kB', 'MB', 'GB']) {
|
||||||
|
if (v > 1024) {
|
||||||
|
v /= 1024;
|
||||||
|
u = unit;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${Math.round(v * 10) / 10} ${u}`;
|
||||||
|
}
|
5
apps/journal.json
Normal file
5
apps/journal.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "📝",
|
||||||
|
"previous": "&2hdIDbBrAg63T2X1MzdGSF7yiqHvlnfF0PnInQLp0DA=.sha256"
|
||||||
|
}
|
185
apps/journal/app.js
Normal file
185
apps/journal/app.js
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
|
||||||
|
let g_hash;
|
||||||
|
let g_collection_notifies = {};
|
||||||
|
|
||||||
|
tfrpc.register(async function getOwnerIdentities() {
|
||||||
|
return ssb.getOwnerIdentities();
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function getIdentities() {
|
||||||
|
return ssb.getIdentities();
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function query(sql, args) {
|
||||||
|
let result = [];
|
||||||
|
await ssb.sqlAsync(sql, args, function callback(row) {
|
||||||
|
result.push(row);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function localStorageGet(key) {
|
||||||
|
return app.localStorageGet(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function localStorageSet(key, value) {
|
||||||
|
return app.localStorageSet(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function following(ids, depth) {
|
||||||
|
return ssb.following(ids, depth);
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function appendMessage(id, message) {
|
||||||
|
return ssb.appendMessageWithIdentity(id, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function store_blob(blob) {
|
||||||
|
if (Array.isArray(blob)) {
|
||||||
|
blob = Uint8Array.from(blob);
|
||||||
|
}
|
||||||
|
return await ssb.blobStore(blob);
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function get_blob(id) {
|
||||||
|
return utf8Decode(await ssb.blobGet(id));
|
||||||
|
});
|
||||||
|
|
||||||
|
let g_new_message_resolve;
|
||||||
|
let g_new_message_promise = new Promise(function (resolve, reject) {
|
||||||
|
g_new_message_resolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
function new_message() {
|
||||||
|
return g_new_message_promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssb.addEventListener('message', function (id) {
|
||||||
|
let resolve = g_new_message_resolve;
|
||||||
|
g_new_message_promise = new Promise(function (resolve, reject) {
|
||||||
|
g_new_message_resolve = resolve;
|
||||||
|
});
|
||||||
|
if (resolve) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
core.register('message', async function message_handler(message) {
|
||||||
|
if (message.event == 'hashChange') {
|
||||||
|
print('hash change', message.hash);
|
||||||
|
g_hash = message.hash;
|
||||||
|
await tfrpc.rpc.hash_changed(message.hash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(function set_hash(hash) {
|
||||||
|
if (g_hash != hash) {
|
||||||
|
return app.setHash(hash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(function get_hash(id, message) {
|
||||||
|
return g_hash;
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function try_decrypt(id, content) {
|
||||||
|
return await ssb.privateMessageDecrypt(id, content);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function encrypt(id, recipients, content) {
|
||||||
|
return await ssb.privateMessageEncrypt(id, recipients, content);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function process_message(whoami, collection, message, kind, parent) {
|
||||||
|
let content = JSON.parse(message.content);
|
||||||
|
if (typeof content == 'string') {
|
||||||
|
let x;
|
||||||
|
for (let id of whoami) {
|
||||||
|
x = await ssb.privateMessageDecrypt(id, content);
|
||||||
|
if (x) {
|
||||||
|
content = JSON.parse(x);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!x) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (content.type !== kind || (parent && content.parent !== parent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (content?.key) {
|
||||||
|
if (content?.tombstone) {
|
||||||
|
delete collection[content.key];
|
||||||
|
} else {
|
||||||
|
collection[content.key] = Object.assign(
|
||||||
|
collection[content.key] || {},
|
||||||
|
content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
collection[message.id] = Object.assign(content, {id: message.id});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
|
||||||
|
let whoami = await ssb.getIdentities();
|
||||||
|
data = data ?? {};
|
||||||
|
let rowid = 0;
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
'SELECT MAX(rowid) AS rowid FROM messages',
|
||||||
|
[],
|
||||||
|
function (row) {
|
||||||
|
rowid = row.rowid;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
while (true) {
|
||||||
|
if (rowid == max_rowid) {
|
||||||
|
await new_message();
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
'SELECT MAX(rowid) AS rowid FROM messages',
|
||||||
|
[],
|
||||||
|
function (row) {
|
||||||
|
rowid = row.rowid;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let modified = false;
|
||||||
|
let rows = [];
|
||||||
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
|
SELECT messages.id, author, content, timestamp
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?1) AS id ON messages.author = id.value
|
||||||
|
WHERE
|
||||||
|
messages.rowid > ?2 AND
|
||||||
|
messages.rowid <= ?3 AND
|
||||||
|
((json_extract(messages.content, '$.type') = ?4 AND
|
||||||
|
(?5 IS NULL OR json_extract(messages.content, '$.parent') = ?5)) OR
|
||||||
|
content LIKE '"%')
|
||||||
|
`,
|
||||||
|
[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent],
|
||||||
|
function (row) {
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
max_rowid = rowid;
|
||||||
|
for (let row of rows) {
|
||||||
|
if (await process_message(whoami, data, row, kind, parent)) {
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (modified) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [rowid, data];
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
1
apps/journal/commonmark.min.js
vendored
Normal file
1
apps/journal/commonmark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
16
apps/journal/index.html
Normal file
16
apps/journal/index.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<base target="_top" />
|
||||||
|
</head>
|
||||||
|
<body style="color: #fff">
|
||||||
|
<tf-journal-app></tf-journal-app>
|
||||||
|
<script src="commonmark.min.js"></script>
|
||||||
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
|
<script src="tf-journal-app.js" type="module"></script>
|
||||||
|
<script src="tf-journal-entry.js" type="module"></script>
|
||||||
|
<script src="tf-id-picker.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
120
apps/journal/lit-all.min.js
vendored
Normal file
120
apps/journal/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/journal/lit-all.min.js.map
Normal file
1
apps/journal/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
43
apps/journal/tf-id-picker.js
Normal file
43
apps/journal/tf-id-picker.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Provide a list of IDs, and this lets the user pick one.
|
||||||
|
*/
|
||||||
|
class TfIdentityPickerElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
ids: {type: Array},
|
||||||
|
selected: {type: String},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.ids = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
changed(event) {
|
||||||
|
this.selected = event.srcElement.value;
|
||||||
|
this.dispatchEvent(
|
||||||
|
new Event('change', {
|
||||||
|
srcElement: this,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<select @change=${this.changed} style="max-width: 100%">
|
||||||
|
${(this.ids ?? []).map(
|
||||||
|
(id) =>
|
||||||
|
html`<option ?selected=${id == this.selected} value=${id}>
|
||||||
|
${id}
|
||||||
|
</option>`
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-id-picker', TfIdentityPickerElement);
|
89
apps/journal/tf-journal-app.js
Normal file
89
apps/journal/tf-journal-app.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import {LitElement, html, keyed, live} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
class TfJournalAppElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
ids: {type: Array},
|
||||||
|
owner_ids: {type: Array},
|
||||||
|
whoami: {type: String},
|
||||||
|
journals: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.ids = [];
|
||||||
|
this.owner_ids = [];
|
||||||
|
this.journals = {};
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.ids = await tfrpc.rpc.getIdentities();
|
||||||
|
this.whoami = await tfrpc.rpc.localStorageGet('journal_whoami');
|
||||||
|
await this.read_journals();
|
||||||
|
}
|
||||||
|
|
||||||
|
async read_journals() {
|
||||||
|
let max_rowid;
|
||||||
|
let journals;
|
||||||
|
while (true) {
|
||||||
|
[max_rowid, journals] = await tfrpc.rpc.collection(
|
||||||
|
[this.whoami],
|
||||||
|
'journal-entry',
|
||||||
|
undefined,
|
||||||
|
max_rowid,
|
||||||
|
journals
|
||||||
|
);
|
||||||
|
this.journals = Object.assign({}, journals);
|
||||||
|
console.log('JOURNALS', this.journals);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async on_whoami_changed(event) {
|
||||||
|
let new_id = event.srcElement.selected;
|
||||||
|
await tfrpc.rpc.localStorageSet('journal_whoami', new_id);
|
||||||
|
this.whoami = new_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async on_journal_publish(event) {
|
||||||
|
let key = event.detail.key;
|
||||||
|
let text = event.detail.text;
|
||||||
|
let message = {
|
||||||
|
type: 'journal-entry',
|
||||||
|
key: key,
|
||||||
|
text: text,
|
||||||
|
};
|
||||||
|
message.recps = [this.whoami];
|
||||||
|
print(message);
|
||||||
|
message = await tfrpc.rpc.encrypt(
|
||||||
|
this.whoami,
|
||||||
|
message.recps,
|
||||||
|
JSON.stringify(message)
|
||||||
|
);
|
||||||
|
print(message);
|
||||||
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
console.log('RENDER APP', this.journals);
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<tf-id-picker
|
||||||
|
.ids=${this.ids}
|
||||||
|
selected=${this.whoami}
|
||||||
|
@change=${this.on_whoami_changed}
|
||||||
|
></tf-id-picker>
|
||||||
|
</div>
|
||||||
|
<tf-journal-entry
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.journals=${this.journals}
|
||||||
|
@publish=${this.on_journal_publish}
|
||||||
|
></tf-journal-entry>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-journal-app', TfJournalAppElement);
|
97
apps/journal/tf-journal-entry.js
Normal file
97
apps/journal/tf-journal-entry.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import {LitElement, html, unsafeHTML, range} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
class TfJournalEntryElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
key: {type: String},
|
||||||
|
journals: {type: Object},
|
||||||
|
text: {type: String},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.journals = {};
|
||||||
|
this.key = new Date().toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown(md) {
|
||||||
|
var reader = new commonmark.Parser({safe: true});
|
||||||
|
var writer = new commonmark.HtmlRenderer();
|
||||||
|
var parsed = reader.parse(md || '');
|
||||||
|
return writer.render(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
on_discard(event) {
|
||||||
|
this.text = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async on_publish() {
|
||||||
|
console.log('publish', this.text);
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('publish', {
|
||||||
|
bubbles: true,
|
||||||
|
detail: {
|
||||||
|
key: this.shadowRoot.getElementById('date_picker').value,
|
||||||
|
text: this.text,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
back_dates(count) {
|
||||||
|
let now = new Date();
|
||||||
|
let result = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
let next = new Date(now);
|
||||||
|
next.setDate(now.getDate() - i);
|
||||||
|
result.push(next.toISOString().split('T')[0]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
on_edit(event) {
|
||||||
|
this.text = event.srcElement.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
on_date_change(event) {
|
||||||
|
this.key = event.srcElement.value;
|
||||||
|
this.text = this.journals[this.key]?.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
console.log('RENDER ENTRY', this.key, this.journals?.[this.key]);
|
||||||
|
return html`
|
||||||
|
<select id="date_picker" @change=${this.on_date_change}>
|
||||||
|
${this.back_dates(10).map(
|
||||||
|
(x) => html` <option value=${x}>${x}</option> `
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
<div style="display: inline-flex; flex-direction: row">
|
||||||
|
<button
|
||||||
|
?disabled=${this.text == this.journals?.[this.key]?.text}
|
||||||
|
@click=${this.on_publish}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
|
<button @click=${this.on_discard}>Discard</button>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<textarea
|
||||||
|
style="flex: 1 1; min-height: 10em"
|
||||||
|
@input=${this.on_edit}
|
||||||
|
.value=${this.text ?? this.journals?.[this.key]?.text ?? ''}
|
||||||
|
></textarea>
|
||||||
|
<div style="flex: 1 1">
|
||||||
|
${unsafeHTML(
|
||||||
|
this.markdown(this.text ?? this.journals?.[this.key]?.text)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-journal-entry', TfJournalEntryElement);
|
4
apps/sneaker.json
Normal file
4
apps/sneaker.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "👟"
|
||||||
|
}
|
30
apps/sneaker/app.js
Normal file
30
apps/sneaker/app.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import * as tfrpc from '/tfrpc.js';
|
||||||
|
|
||||||
|
tfrpc.register(async function getAllIdentities() {
|
||||||
|
return ssb.getAllIdentities();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function query(sql, args) {
|
||||||
|
let result = [];
|
||||||
|
await ssb.sqlAsync(sql, args, function callback(row) {
|
||||||
|
result.push(row);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
tfrpc.register(async function store_blob(blob) {
|
||||||
|
if (Array.isArray(blob)) {
|
||||||
|
blob = Uint8Array.from(blob);
|
||||||
|
}
|
||||||
|
return await ssb.blobStore(blob);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function get_blob(id) {
|
||||||
|
return Array.from(new Uint8Array(await ssb.blobGet(id)));
|
||||||
|
});
|
||||||
|
tfrpc.register(async function store_message(message) {
|
||||||
|
return await ssb.storeMessage(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||||
|
}
|
||||||
|
main();
|
3
apps/sneaker/filesaver.min.js
vendored
Normal file
3
apps/sneaker/filesaver.min.js
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
|
||||||
|
|
||||||
|
//# sourceMappingURL=FileSaver.min.js.map
|
16
apps/sneaker/index.html
Normal file
16
apps/sneaker/index.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html style="color: #fff">
|
||||||
|
<head>
|
||||||
|
<title>Tilde Friends</title>
|
||||||
|
<base target="_top" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<tf-sneaker-app />
|
||||||
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
|
<script src="filesaver.min.js"></script>
|
||||||
|
<script src="jszip.min.js"></script>
|
||||||
|
<script src="script.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
apps/sneaker/jszip.min.js
vendored
Normal file
13
apps/sneaker/jszip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
120
apps/sneaker/lit-all.min.js
vendored
Normal file
120
apps/sneaker/lit-all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
apps/sneaker/lit-all.min.js.map
Normal file
1
apps/sneaker/lit-all.min.js.map
Normal file
File diff suppressed because one or more lines are too long
344
apps/sneaker/script.js
Normal file
344
apps/sneaker/script.js
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
|
class TfSneakerAppElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
feeds: {type: Object},
|
||||||
|
progress: {type: Object},
|
||||||
|
result: {type: String},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.feeds = [];
|
||||||
|
this.progress = undefined;
|
||||||
|
this.result = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search() {
|
||||||
|
let q = this.renderRoot.getElementById('search').value;
|
||||||
|
let result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name
|
||||||
|
FROM messages_fts(?)
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE
|
||||||
|
json_extract(messages.content, '$.type') = 'about' AND
|
||||||
|
json_extract(messages.content, '$.about') = messages.author AND
|
||||||
|
json_extract(messages.content, '$.name') IS NOT NULL
|
||||||
|
GROUP BY messages.author
|
||||||
|
HAVING MAX(messages.sequence)
|
||||||
|
ORDER BY COUNT(*) DESC
|
||||||
|
`,
|
||||||
|
[`"${q.replaceAll('"', '""')}"`]
|
||||||
|
);
|
||||||
|
this.feeds = Object.fromEntries(result.map((x) => [x.id, x.name]));
|
||||||
|
}
|
||||||
|
|
||||||
|
format_message(message) {
|
||||||
|
let out = {
|
||||||
|
previous: message.previous ?? null,
|
||||||
|
};
|
||||||
|
if (message.sequence_before_author) {
|
||||||
|
out.sequence = message.sequence;
|
||||||
|
out.author = message.author;
|
||||||
|
} else {
|
||||||
|
out.author = message.author;
|
||||||
|
out.sequence = message.sequence;
|
||||||
|
}
|
||||||
|
out.timestamp = message.timestamp;
|
||||||
|
out.hash = message.hash;
|
||||||
|
out.content = JSON.parse(message.content);
|
||||||
|
out.signature = message.signature;
|
||||||
|
return {key: message.id, value: out};
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitize(value) {
|
||||||
|
return value.replaceAll('/', '_').replaceAll('+', '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
guess_ext(data) {
|
||||||
|
function startsWith(prefix) {
|
||||||
|
if (data.length < prefix.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < prefix.length; i++) {
|
||||||
|
if (prefix[i] !== null && data[i] !== prefix[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
|
||||||
|
startsWith(
|
||||||
|
data,
|
||||||
|
[0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]
|
||||||
|
) ||
|
||||||
|
startsWith(data, [0xff, 0xd8, 0xff, 0xee]) ||
|
||||||
|
startsWith(data, [
|
||||||
|
0xff,
|
||||||
|
0xd8,
|
||||||
|
0xff,
|
||||||
|
0xe1,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x45,
|
||||||
|
0x78,
|
||||||
|
0x69,
|
||||||
|
0x66,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
return '.jpg';
|
||||||
|
} else if (
|
||||||
|
startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
||||||
|
) {
|
||||||
|
return '.png';
|
||||||
|
} else if (
|
||||||
|
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
||||||
|
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])
|
||||||
|
) {
|
||||||
|
return '.gif';
|
||||||
|
} else if (
|
||||||
|
startsWith(data, [
|
||||||
|
0x52,
|
||||||
|
0x49,
|
||||||
|
0x46,
|
||||||
|
0x46,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x57,
|
||||||
|
0x45,
|
||||||
|
0x42,
|
||||||
|
0x50,
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
return '.webp';
|
||||||
|
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
|
||||||
|
return '.svg';
|
||||||
|
} else if (
|
||||||
|
startsWith(data, [
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x66,
|
||||||
|
0x74,
|
||||||
|
0x79,
|
||||||
|
0x70,
|
||||||
|
0x6d,
|
||||||
|
0x70,
|
||||||
|
0x34,
|
||||||
|
0x32,
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
return '.mp3';
|
||||||
|
} else if (
|
||||||
|
startsWith(data, [
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x66,
|
||||||
|
0x74,
|
||||||
|
0x79,
|
||||||
|
0x70,
|
||||||
|
0x69,
|
||||||
|
0x73,
|
||||||
|
0x6f,
|
||||||
|
0x6d,
|
||||||
|
]) ||
|
||||||
|
startsWith(data, [
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x66,
|
||||||
|
0x74,
|
||||||
|
0x79,
|
||||||
|
0x70,
|
||||||
|
0x6d,
|
||||||
|
0x70,
|
||||||
|
0x34,
|
||||||
|
0x32,
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
return '.mp4';
|
||||||
|
} else {
|
||||||
|
return '.bin';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async export(id) {
|
||||||
|
let all_messages = '';
|
||||||
|
let sequence = -1;
|
||||||
|
let messages_done = 0;
|
||||||
|
let messages_max = (
|
||||||
|
await tfrpc.rpc.query(
|
||||||
|
'SELECT MAX(sequence) AS total FROM messages WHERE author = ?',
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
)[0].total;
|
||||||
|
while (true) {
|
||||||
|
let messages = await tfrpc.rpc.query(
|
||||||
|
'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100',
|
||||||
|
[id, sequence]
|
||||||
|
);
|
||||||
|
if (messages?.length) {
|
||||||
|
all_messages +=
|
||||||
|
messages
|
||||||
|
.map((x) => JSON.stringify(this.format_message(x)))
|
||||||
|
.join('\n') + '\n';
|
||||||
|
sequence = messages[messages.length - 1].sequence;
|
||||||
|
messages_done += messages.length;
|
||||||
|
this.progress = {
|
||||||
|
name: 'messages',
|
||||||
|
value: messages_done,
|
||||||
|
max: messages_max,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let zip = new JSZip();
|
||||||
|
zip.file(`message/classic/${this.sanitize(id)}.ndjson`, all_messages);
|
||||||
|
|
||||||
|
let blobs = await tfrpc.rpc.query(
|
||||||
|
`SELECT messages_refs.ref AS id
|
||||||
|
FROM messages
|
||||||
|
JOIN messages_refs ON messages.id = messages_refs.message
|
||||||
|
WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
let blobs_done = 0;
|
||||||
|
for (let row of blobs) {
|
||||||
|
this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
|
||||||
|
let blob;
|
||||||
|
try {
|
||||||
|
blob = await tfrpc.rpc.get_blob(row.id);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Failed to get ${row.id}: ${e.message}`);
|
||||||
|
}
|
||||||
|
if (blob) {
|
||||||
|
zip.file(
|
||||||
|
`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`,
|
||||||
|
new Uint8Array(blob)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
blobs_done++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.progress = {name: 'saving'};
|
||||||
|
let blob = await zip.generateAsync({type: 'blob'});
|
||||||
|
saveAs(blob, `${this.sanitize(id)}.zip`);
|
||||||
|
this.progress = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
keypress(event) {
|
||||||
|
if (event.key == 'Enter') {
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async import(event) {
|
||||||
|
let file = event.target.files[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.progress = {name: 'loading'};
|
||||||
|
let zip = new JSZip();
|
||||||
|
file = await zip.loadAsync(file);
|
||||||
|
let messages = [];
|
||||||
|
let blobs = [];
|
||||||
|
file.forEach(function (path, entry) {
|
||||||
|
if (!entry.dir) {
|
||||||
|
if (path.startsWith('message/classic/')) {
|
||||||
|
messages.push(entry);
|
||||||
|
} else {
|
||||||
|
blobs.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let success = {messages: 0, blobs: 0};
|
||||||
|
let progress = 0;
|
||||||
|
let total_messages = 0;
|
||||||
|
for (let entry of messages) {
|
||||||
|
let lines = (await entry.async('string')).split('\n');
|
||||||
|
total_messages += lines.length;
|
||||||
|
for (let line of lines) {
|
||||||
|
if (!line.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let message = JSON.parse(line);
|
||||||
|
this.progress = {
|
||||||
|
name: 'messages',
|
||||||
|
value: progress++,
|
||||||
|
max: total_messages,
|
||||||
|
};
|
||||||
|
if (await tfrpc.rpc.store_message(message.value)) {
|
||||||
|
success.messages++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progress = 0;
|
||||||
|
for (let blob of blobs) {
|
||||||
|
this.progress = {name: 'blobs', value: progress++, max: blobs.length};
|
||||||
|
if (await tfrpc.rpc.store_blob(await blob.async('arraybuffer'))) {
|
||||||
|
success.blobs++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.progress = undefined;
|
||||||
|
this.result = `imported ${success.messages} messages and ${success.blobs} blobs`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let progress;
|
||||||
|
if (this.progress) {
|
||||||
|
if (this.progress.max) {
|
||||||
|
progress = html`<div>
|
||||||
|
<label for="progress">${this.progress.name}</label
|
||||||
|
><progress
|
||||||
|
value=${this.progress.value}
|
||||||
|
max=${this.progress.max}
|
||||||
|
></progress>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
progress = html`<div><span>${this.progress.name}</span></div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html`<h1>SSB 👟net</h1>
|
||||||
|
<code>${this.result}</code>
|
||||||
|
${progress}
|
||||||
|
|
||||||
|
<h2>Import</h2>
|
||||||
|
<input type="file" id="import" @change=${this.import}></input>
|
||||||
|
|
||||||
|
<h2>Export</h2>
|
||||||
|
<input type="text" id="search" @keypress=${this.keypress}></input>
|
||||||
|
<input type="button" value="Search Users" @click=${this.search}></input>
|
||||||
|
<ul>
|
||||||
|
${Object.entries(this.feeds).map(
|
||||||
|
([id, name]) => html`
|
||||||
|
<li>
|
||||||
|
${this.progress
|
||||||
|
? undefined
|
||||||
|
: html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
|
||||||
|
${name}
|
||||||
|
<code style="color: #ccc">${id}</code>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-sneaker-app', TfSneakerAppElement);
|
@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
}
|
"emoji": "🐌",
|
||||||
|
"previous": "&DUxMMCJcuhm6S9jg/eKgEyWodkITu6Tg9g5I5wgLWFU=.sha256"
|
||||||
|
}
|
||||||
|
@ -18,12 +18,21 @@ tfrpc.register(async function databaseSet(key, value) {
|
|||||||
tfrpc.register(async function createIdentity() {
|
tfrpc.register(async function createIdentity() {
|
||||||
return ssb.createIdentity();
|
return ssb.createIdentity();
|
||||||
});
|
});
|
||||||
|
tfrpc.register(async function getServerIdentity() {
|
||||||
|
return ssb.getServerIdentity();
|
||||||
|
});
|
||||||
|
tfrpc.register(async function setServerFollowingMe(id, following) {
|
||||||
|
return ssb.setServerFollowingMe(id, following);
|
||||||
|
});
|
||||||
tfrpc.register(async function getIdentities() {
|
tfrpc.register(async function getIdentities() {
|
||||||
return ssb.getIdentities();
|
return ssb.getIdentities();
|
||||||
});
|
});
|
||||||
tfrpc.register(async function getAllIdentities() {
|
tfrpc.register(async function getAllIdentities() {
|
||||||
return ssb.getAllIdentities();
|
return ssb.getAllIdentities();
|
||||||
});
|
});
|
||||||
|
tfrpc.register(async function following(ids, depth) {
|
||||||
|
return ssb.following(ids, depth);
|
||||||
|
});
|
||||||
tfrpc.register(async function getBroadcasts() {
|
tfrpc.register(async function getBroadcasts() {
|
||||||
return ssb.getBroadcasts();
|
return ssb.getBroadcasts();
|
||||||
});
|
});
|
||||||
@ -67,7 +76,7 @@ tfrpc.register(function getHash(id, message) {
|
|||||||
tfrpc.register(function setHash(hash) {
|
tfrpc.register(function setHash(hash) {
|
||||||
return app.setHash(hash);
|
return app.setHash(hash);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('message', async function(id) {
|
ssb.addEventListener('message', async function (id) {
|
||||||
await tfrpc.rpc.notifyNewMessage(id);
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
});
|
});
|
||||||
tfrpc.register(async function store_blob(blob) {
|
tfrpc.register(async function store_blob(blob) {
|
||||||
@ -79,21 +88,30 @@ tfrpc.register(async function store_blob(blob) {
|
|||||||
tfrpc.register(async function get_blob(id) {
|
tfrpc.register(async function get_blob(id) {
|
||||||
return utf8Decode(await ssb.blobGet(id));
|
return utf8Decode(await ssb.blobGet(id));
|
||||||
});
|
});
|
||||||
|
tfrpc.register(async function store_message(message) {
|
||||||
|
return await ssb.storeMessage(message);
|
||||||
|
});
|
||||||
tfrpc.register(function apps() {
|
tfrpc.register(function apps() {
|
||||||
return core.apps();
|
return core.apps();
|
||||||
});
|
});
|
||||||
ssb.addEventListener('broadcasts', async function() {
|
tfrpc.register(async function try_decrypt(id, content) {
|
||||||
|
return await ssb.privateMessageDecrypt(id, content);
|
||||||
|
});
|
||||||
|
tfrpc.register(async function encrypt(id, recipients, content) {
|
||||||
|
return await ssb.privateMessageEncrypt(id, recipients, content);
|
||||||
|
});
|
||||||
|
ssb.addEventListener('broadcasts', 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')));
|
||||||
}
|
}
|
||||||
main();
|
main();
|
||||||
|
@ -39,7 +39,7 @@ function splitMatches(text, regexp) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const regex = new RegExp("(?<!\w)#[\\w-]+");
|
const regex = new RegExp("(?<!\\w)#[\\w-]+");
|
||||||
|
|
||||||
function split(textNodes) {
|
function split(textNodes) {
|
||||||
const text = textNodes.map(n => n.literal).join("");
|
const text = textNodes.map(n => n.literal).join("");
|
||||||
|
@ -4,14 +4,14 @@ 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) {
|
export function picker(callback, anchor) {
|
||||||
get_emojis().then(function(json) {
|
get_emojis().then(function (json) {
|
||||||
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';
|
||||||
@ -36,7 +36,7 @@ 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();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -54,21 +54,29 @@ export function picker(callback, anchor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chosen(event) {
|
||||||
|
console.log(event.srcElement.innerText);
|
||||||
|
callback(event.srcElement.innerText);
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
while (list.firstChild) {
|
while (list.firstChild) {
|
||||||
list.removeChild(list.firstChild);
|
list.removeChild(list.firstChild);
|
||||||
}
|
}
|
||||||
let search = input.value;
|
let search = input.value.toLowerCase();
|
||||||
let any_at_all = false;
|
let any_at_all = false;
|
||||||
Object.entries(json).forEach(function(row) {
|
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 row[1]) {
|
for (let entry of Object.entries(row[1])) {
|
||||||
if (search &&
|
if (
|
||||||
|
search &&
|
||||||
search.length &&
|
search.length &&
|
||||||
entry.name.indexOf(search) == -1) {
|
entry[0].toLowerCase().indexOf(search) == -1
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let emoji = document.createElement('span');
|
let emoji = document.createElement('span');
|
||||||
@ -76,12 +84,9 @@ export function picker(callback, anchor) {
|
|||||||
emoji.style.display = 'inline-block';
|
emoji.style.display = 'inline-block';
|
||||||
emoji.style.overflow = 'hidden';
|
emoji.style.overflow = 'hidden';
|
||||||
emoji.style.cursor = 'pointer';
|
emoji.style.cursor = 'pointer';
|
||||||
emoji.onclick = function() {
|
emoji.onclick = chosen;
|
||||||
callback(entry);
|
emoji.title = entry[0];
|
||||||
cleanup();
|
emoji.appendChild(document.createTextNode(entry[1]));
|
||||||
}
|
|
||||||
emoji.title = entry.name;
|
|
||||||
emoji.appendChild(document.createTextNode(entry.emoji));
|
|
||||||
list.appendChild(emoji);
|
list.appendChild(emoji);
|
||||||
any = true;
|
any = true;
|
||||||
any_at_all = true;
|
any_at_all = true;
|
||||||
@ -89,7 +94,7 @@ export function picker(callback, anchor) {
|
|||||||
if (!any) {
|
if (!any) {
|
||||||
list.removeChild(header);
|
list.removeChild(header);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
if (!any_at_all) {
|
if (!any_at_all) {
|
||||||
list.appendChild(document.createTextNode('No matches found.'));
|
list.appendChild(document.createTextNode('No matches found.'));
|
||||||
}
|
}
|
||||||
@ -106,4 +111,4 @@ export function picker(callback, anchor) {
|
|||||||
document.body.addEventListener('mousedown', cleanup);
|
document.body.addEventListener('mousedown', cleanup);
|
||||||
window.addEventListener('keydown', key_down);
|
window.addEventListener('keydown', key_down);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
15116
apps/ssb/emojis.json
15116
apps/ssb/emojis.json
File diff suppressed because one or more lines are too long
3
apps/ssb/filesaver.min.js
vendored
Normal file
3
apps/ssb/filesaver.min.js
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
|
||||||
|
|
||||||
|
//# sourceMappingURL=FileSaver.min.js.map
|
1
apps/ssb/filesaver.min.js.map
Normal file
1
apps/ssb/filesaver.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,8 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="color: #fff">
|
<html style="color: #fff">
|
||||||
<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,15 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body style="background-color: #223a5e">
|
||||||
<tf-app/>
|
<tf-app class="w3-deep-purple" />
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
|
<script src="filesaver.min.js"></script>
|
||||||
<script src="commonmark.min.js"></script>
|
<script src="commonmark.min.js"></script>
|
||||||
<script src="commonmark-linkify.js" type="module"></script>
|
<script src="commonmark-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>
|
||||||
</html>
|
</html>
|
||||||
|
56
apps/ssb/lit-all.min.js
vendored
56
apps/ssb/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -8,6 +8,10 @@ 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_tab_mentions from './tf-tab-mentions.js';
|
||||||
import * as tf_tab_news from './tf-tab-news.js';
|
import * as tf_tab_news from './tf-tab-news.js';
|
||||||
|
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
|
||||||
import * as tf_tab_search from './tf-tab-search.js';
|
import * as tf_tab_search from './tf-tab-search.js';
|
||||||
import * as tf_tab_connections from './tf-tab-connections.js';
|
import * as tf_tab_connections from './tf-tab-connections.js';
|
||||||
|
import * as tf_tab_query from './tf-tab-query.js';
|
||||||
|
import * as tf_tag from './tf-tag.js';
|
||||||
|
@ -16,6 +16,7 @@ class TfElement extends LitElement {
|
|||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
ids: {type: Array},
|
ids: {type: Array},
|
||||||
|
tags: {type: Array},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,9 +33,14 @@ class TfElement extends LitElement {
|
|||||||
this.following = [];
|
this.following = [];
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || [] });
|
this.tags = [];
|
||||||
tfrpc.rpc.getConnections().then(c => { self.connections = c || [] });
|
tfrpc.rpc.getBroadcasts().then((b) => {
|
||||||
tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
|
self.broadcasts = b || [];
|
||||||
|
});
|
||||||
|
tfrpc.rpc.getConnections().then((c) => {
|
||||||
|
self.connections = c || [];
|
||||||
|
});
|
||||||
|
tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
|
||||||
tfrpc.register(function hashChanged(hash) {
|
tfrpc.register(function hashChanged(hash) {
|
||||||
self.set_hash(hash);
|
self.set_hash(hash);
|
||||||
});
|
});
|
||||||
@ -64,83 +70,15 @@ class TfElement extends LitElement {
|
|||||||
this.tab = 'search';
|
this.tab = 'search';
|
||||||
} else if (this.hash === '#connections') {
|
} else if (this.hash === '#connections') {
|
||||||
this.tab = 'connections';
|
this.tab = 'connections';
|
||||||
|
} else if (this.hash === '#mentions') {
|
||||||
|
this.tab = 'mentions';
|
||||||
|
} else if (this.hash.startsWith('#sql=')) {
|
||||||
|
this.tab = 'query';
|
||||||
} else {
|
} else {
|
||||||
this.tab = 'news';
|
this.tab = 'news';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async contacts_internal(id, last_row_id, following, max_row_id) {
|
|
||||||
let result = Object.assign({}, following[id] || {});
|
|
||||||
result.following = result.following || {};
|
|
||||||
result.blocking = result.blocking || {};
|
|
||||||
let contacts = await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
SELECT content FROM messages
|
|
||||||
WHERE author = ? AND
|
|
||||||
rowid > ? AND
|
|
||||||
rowid <= ? AND
|
|
||||||
json_extract(content, "$.type") = "contact"
|
|
||||||
ORDER BY sequence
|
|
||||||
`,
|
|
||||||
[id, last_row_id, max_row_id]);
|
|
||||||
for (let row of contacts) {
|
|
||||||
let contact = JSON.parse(row.content);
|
|
||||||
if (contact.following === true) {
|
|
||||||
result.following[contact.contact] = true;
|
|
||||||
} else if (contact.following === false) {
|
|
||||||
delete result.following[contact.contact];
|
|
||||||
} else if (contact.blocking === true) {
|
|
||||||
result.blocking[contact.contact] = true;
|
|
||||||
} else if (contact.blocking === false) {
|
|
||||||
delete result.blocking[contact.contact];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
following[id] = result;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async contact(id, last_row_id, following, max_row_id) {
|
|
||||||
return await this.contacts_internal(id, last_row_id, following, max_row_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) {
|
|
||||||
let contacts = await Promise.all([...new Set(ids)].map(x => this.contact(x, last_row_id, following, max_row_id)));
|
|
||||||
let result = {};
|
|
||||||
for (let i = 0; i < ids.length; i++) {
|
|
||||||
let id = ids[i];
|
|
||||||
let contact = contacts[i];
|
|
||||||
let all_blocking = Object.assign({}, contact.blocking, blocking);
|
|
||||||
let found = Object.keys(contact.following).filter(y => !all_blocking[y]);
|
|
||||||
let deeper = depth > 1 ? await this.following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : [];
|
|
||||||
result[id] = [id, ...found, ...deeper];
|
|
||||||
}
|
|
||||||
return [...new Set(Object.values(result).flat())];
|
|
||||||
}
|
|
||||||
|
|
||||||
async following_deep(ids, depth, blocking) {
|
|
||||||
const k_cache_version = 5;
|
|
||||||
let cache = await tfrpc.rpc.databaseGet('following');
|
|
||||||
cache = cache ? JSON.parse(cache) : {};
|
|
||||||
if (cache.version !== k_cache_version) {
|
|
||||||
cache = {
|
|
||||||
version: k_cache_version,
|
|
||||||
following: {},
|
|
||||||
last_row_id: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let max_row_id = (await tfrpc.rpc.query(`
|
|
||||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
|
||||||
`, []))[0].max_row_id;
|
|
||||||
let result = await this.following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
|
|
||||||
cache.last_row_id = max_row_id;
|
|
||||||
let store = JSON.stringify(cache);
|
|
||||||
/* 2023-02-20: Exceeding message size. */
|
|
||||||
if (store.length < 512 * 1024) {
|
|
||||||
await tfrpc.rpc.databaseSet('following', store);
|
|
||||||
}
|
|
||||||
return [result, cache.following];
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetch_about(ids, users) {
|
async fetch_about(ids, users) {
|
||||||
const k_cache_version = 1;
|
const k_cache_version = 1;
|
||||||
let cache = await tfrpc.rpc.databaseGet('about');
|
let cache = await tfrpc.rpc.databaseGet('about');
|
||||||
@ -152,9 +90,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];
|
||||||
@ -186,17 +129,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;
|
||||||
@ -216,12 +163,11 @@ class TfElement extends LitElement {
|
|||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.id = ?
|
WHERE messages.id = ?
|
||||||
`,
|
`,
|
||||||
[
|
[JSON.stringify(this.following), id]
|
||||||
JSON.stringify(this.following),
|
);
|
||||||
id,
|
|
||||||
]);
|
|
||||||
if (messages && messages.length) {
|
if (messages && messages.length) {
|
||||||
this.unread = [...this.unread, ...messages];
|
this.unread = [...this.unread, ...messages];
|
||||||
|
this.unread = this.unread.slice(this.unread.length - 1024);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,25 +184,81 @@ class TfElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create_identity() {
|
async create_identity() {
|
||||||
if (confirm("Are you sure you want to create a new identity?")) {
|
if (confirm('Are you sure you want to create a new identity?')) {
|
||||||
await tfrpc.rpc.createIdentity();
|
await tfrpc.rpc.createIdentity();
|
||||||
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
|
if (this.ids && !this.whoami) {
|
||||||
|
this.whoami = this.ids[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_id_picker() {
|
render_id_picker() {
|
||||||
return html`
|
return html`
|
||||||
<tf-id-picker id="picker" selected=${this.whoami} .ids=${this.ids} @change=${this._handle_whoami_changed}></tf-id-picker>
|
<div style="display: flex; gap: 8px">
|
||||||
<button @click=${this.create_identity}>Create Identity</button>
|
<tf-id-picker
|
||||||
|
id="picker"
|
||||||
|
style="flex: 1 1 auto"
|
||||||
|
selected=${this.whoami}
|
||||||
|
.ids=${this.ids}
|
||||||
|
.users=${this.users}
|
||||||
|
@change=${this._handle_whoami_changed}
|
||||||
|
></tf-id-picker>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey w3-border"
|
||||||
|
style="flex: 0 0 auto"
|
||||||
|
@click=${this.create_identity}
|
||||||
|
id="create_identity"
|
||||||
|
>
|
||||||
|
Create Identity
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async load_recent_tags() {
|
||||||
|
let start = new Date();
|
||||||
|
this.tags = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
WITH
|
||||||
|
recent AS (SELECT id, content FROM messages
|
||||||
|
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
|
||||||
|
ORDER BY timestamp DESC LIMIT 1024),
|
||||||
|
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
|
||||||
|
FROM recent
|
||||||
|
WHERE json_extract(content, '$.channel') IS NOT NULL),
|
||||||
|
recent_mentions AS (SELECT recent.id, json_extract(mention.value, '$.link') AS tag
|
||||||
|
FROM recent, json_each(recent.content, '$.mentions') AS mention
|
||||||
|
WHERE json_valid(mention.value) AND tag LIKE '#%'),
|
||||||
|
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
|
||||||
|
by_message AS (SELECT DISTINCT id, tag FROM combined)
|
||||||
|
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
|
||||||
|
`,
|
||||||
|
[new Date() - 7 * 24 * 60 * 60 * 1000]
|
||||||
|
);
|
||||||
|
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
|
||||||
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
let whoami = this.whoami;
|
let whoami = this.whoami;
|
||||||
let [following, users] = await this.following_deep([whoami], 2, {});
|
let tags = this.load_recent_tags();
|
||||||
users = await this.fetch_about(following.sort(), users);
|
let following = await tfrpc.rpc.following([whoami], 2);
|
||||||
this.following = following;
|
let users = {};
|
||||||
|
let by_count = [];
|
||||||
|
for (let [id, v] of Object.entries(following)) {
|
||||||
|
users[id] = {
|
||||||
|
following: v.of,
|
||||||
|
blocking: v.ob,
|
||||||
|
followed: v.if,
|
||||||
|
blocked: v.ib,
|
||||||
|
};
|
||||||
|
by_count.push({count: v.of, id: id});
|
||||||
|
}
|
||||||
|
console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
|
||||||
|
users = await this.fetch_about(Object.keys(following).sort(), users);
|
||||||
|
this.following = Object.keys(following);
|
||||||
this.users = users;
|
this.users = users;
|
||||||
|
await tags;
|
||||||
console.log(`load finished ${whoami} => ${this.whoami}`);
|
console.log(`load finished ${whoami} => ${this.whoami}`);
|
||||||
this.whoami = whoami;
|
this.whoami = whoami;
|
||||||
this.loaded = whoami;
|
this.loaded = whoami;
|
||||||
@ -267,35 +269,67 @@ class TfElement extends LitElement {
|
|||||||
let users = this.users;
|
let users = this.users;
|
||||||
if (this.tab === 'news') {
|
if (this.tab === 'news') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-news .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
|
<tf-tab-news
|
||||||
|
id="tf-tab-news"
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
hash=${this.hash}
|
||||||
|
.unread=${this.unread}
|
||||||
|
@refresh=${() => (this.unread = [])}
|
||||||
|
></tf-tab-news>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'connections') {
|
} else if (this.tab === 'connections') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
|
<tf-tab-connections
|
||||||
|
.users=${this.users}
|
||||||
|
.connections=${this.connections}
|
||||||
|
.broadcasts=${this.broadcasts}
|
||||||
|
></tf-tab-connections>
|
||||||
|
`;
|
||||||
|
} else if (this.tab === 'mentions') {
|
||||||
|
return html`
|
||||||
|
<tf-tab-mentions
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users="${this.users}}"
|
||||||
|
></tf-tab-mentions>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'search') {
|
} else if (this.tab === 'search') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
|
<tf-tab-search
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
query=${this.hash?.startsWith('#q=')
|
||||||
|
? decodeURIComponent(this.hash.substring(3))
|
||||||
|
: null}
|
||||||
|
></tf-tab-search>
|
||||||
|
`;
|
||||||
|
} else if (this.tab === 'query') {
|
||||||
|
return html`
|
||||||
|
<tf-tab-query
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
query=${this.hash?.startsWith('#sql=')
|
||||||
|
? decodeURIComponent(this.hash.substring(5))
|
||||||
|
: null}
|
||||||
|
></tf-tab-query>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add_fake_news() {
|
|
||||||
this.unread = [{
|
|
||||||
author: this.whoami,
|
|
||||||
placeholder: true,
|
|
||||||
id: '%fake_id',
|
|
||||||
text: 'text',
|
|
||||||
content: 'hello',
|
|
||||||
}, ...this.unread];
|
|
||||||
}
|
|
||||||
|
|
||||||
async set_tab(tab) {
|
async set_tab(tab) {
|
||||||
this.tab = tab;
|
this.tab = tab;
|
||||||
if (tab === 'news') {
|
if (tab === 'news') {
|
||||||
await tfrpc.rpc.setHash('#');
|
await tfrpc.rpc.setHash('#');
|
||||||
} else if (tab === 'connections') {
|
} else if (tab === 'connections') {
|
||||||
await tfrpc.rpc.setHash('#connections');
|
await tfrpc.rpc.setHash('#connections');
|
||||||
|
} else if (tab === 'mentions') {
|
||||||
|
await tfrpc.rpc.setHash('#mentions');
|
||||||
|
} else if (tab === 'query') {
|
||||||
|
await tfrpc.rpc.setHash('#sql=');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,33 +337,51 @@ class TfElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
|
|
||||||
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
||||||
console.log(`starting loading ${this.whoami} ${this.loaded}`);
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.load().finally(function() {
|
this.load().finally(function () {
|
||||||
self.loading = false;
|
self.loading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const k_tabs = {
|
||||||
|
'📰': 'news',
|
||||||
|
'📡': 'connections',
|
||||||
|
'@': 'mentions',
|
||||||
|
'🔍': 'search',
|
||||||
|
'👩💻': 'query',
|
||||||
|
};
|
||||||
|
|
||||||
let tabs = html`
|
let tabs = html`
|
||||||
<div>
|
<div class="w3-bar w3-black">
|
||||||
<input type="button" class="tab" value="News" ?disabled=${self.tab == 'news'} @click=${() => self.set_tab('news')}></input>
|
${Object.entries(k_tabs).map(
|
||||||
<input type="button" class="tab" value="Connections" ?disabled=${self.tab == 'connections'} @click=${() => self.set_tab('connections')}></input>
|
([k, v]) => html`
|
||||||
<input type="button" class="tab" value="Search" ?disabled=${self.tab == 'search'} @click=${() => self.set_tab('search')}></input>
|
<button
|
||||||
|
title=${v}
|
||||||
|
class="w3-bar-item w3-padding-large w3-hover-gray tab ${self.tab ==
|
||||||
|
v
|
||||||
|
? 'w3-red'
|
||||||
|
: 'w3-black'}"
|
||||||
|
@click=${() => self.set_tab(v)}
|
||||||
|
>
|
||||||
|
${k}
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
let contents =
|
let contents = !this.loaded
|
||||||
!this.loaded ?
|
? this.loading
|
||||||
this.loading ?
|
? html`<div>Loading...</div>`
|
||||||
html`<div>Loading...</div>` :
|
: html`<div>Select or create an identity.</div>`
|
||||||
html`<div>Select or create an identity.</div>` :
|
: this.render_tab();
|
||||||
this.render_tab();
|
|
||||||
return html`
|
return html`
|
||||||
${this.render_id_picker()}
|
${this.render_id_picker()} ${tabs}
|
||||||
${tabs}
|
${this.tags.map(
|
||||||
<!-- <input type="button" value="Fake News" @click=${this.add_fake_news}></input> -->
|
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
|
||||||
|
)}
|
||||||
${contents}
|
${contents}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-app', TfElement);
|
customElements.define('tf-app', TfElement);
|
||||||
|
@ -13,7 +13,7 @@ class TfComposeElement extends LitElement {
|
|||||||
branch: {type: String},
|
branch: {type: String},
|
||||||
apps: {type: Object},
|
apps: {type: Object},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -56,9 +56,11 @@ class TfComposeElement extends LitElement {
|
|||||||
if (!draft.mentions[link]) {
|
if (!draft.mentions[link]) {
|
||||||
draft.mentions[link] = {
|
draft.mentions[link] = {
|
||||||
link: link,
|
link: link,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
|
draft.mentions[link].name = name.startsWith('@')
|
||||||
|
? name.substring(1)
|
||||||
|
: name;
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
if (updated) {
|
if (updated) {
|
||||||
@ -72,34 +74,39 @@ class TfComposeElement extends LitElement {
|
|||||||
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.value);
|
||||||
let content_warning = this.renderRoot.getElementById('content_warning');
|
let content_warning = this.renderRoot.getElementById('content_warning');
|
||||||
let content_warning_preview = this.renderRoot.getElementById('content_warning_preview');
|
let content_warning_preview = this.renderRoot.getElementById(
|
||||||
|
'content_warning_preview'
|
||||||
|
);
|
||||||
if (content_warning && content_warning_preview) {
|
if (content_warning && content_warning_preview) {
|
||||||
content_warning_preview.innerText = content_warning.value;
|
content_warning_preview.innerText = content_warning.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(draft) {
|
notify(draft) {
|
||||||
this.dispatchEvent(new CustomEvent('tf-draft', {
|
this.dispatchEvent(
|
||||||
bubbles: true,
|
new CustomEvent('tf-draft', {
|
||||||
composed: true,
|
bubbles: true,
|
||||||
detail: {
|
composed: true,
|
||||||
id: this.branch,
|
detail: {
|
||||||
draft: draft
|
id: this.branch,
|
||||||
},
|
draft: draft,
|
||||||
}));
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
change() {
|
change() {
|
||||||
let draft = this.get_draft();
|
let draft = this.get_draft();
|
||||||
draft.text = this.renderRoot.getElementById('edit')?.value;
|
draft.text = this.renderRoot.getElementById('edit')?.value;
|
||||||
draft.content_warning = this.renderRoot.getElementById('content_warning')?.value;
|
draft.content_warning =
|
||||||
|
this.renderRoot.getElementById('content_warning')?.value;
|
||||||
this.notify(draft);
|
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 +116,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 +142,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;
|
||||||
@ -157,7 +172,7 @@ class TfComposeElement extends LitElement {
|
|||||||
edit.value += `\n`;
|
edit.value += `\n`;
|
||||||
self.change();
|
self.change();
|
||||||
self.input();
|
self.input();
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
alert(e?.message);
|
alert(e?.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,7 +191,7 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
async submit() {
|
||||||
let self = this;
|
let self = this;
|
||||||
let draft = this.get_draft();
|
let draft = this.get_draft();
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
@ -195,14 +210,29 @@ class TfComposeElement extends LitElement {
|
|||||||
message.contentWarning = draft.content_warning;
|
message.contentWarning = draft.content_warning;
|
||||||
}
|
}
|
||||||
console.log('Would post:', message);
|
console.log('Would post:', message);
|
||||||
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
if (draft.encrypt_to) {
|
||||||
edit.value = '';
|
let to = new Set(draft.encrypt_to);
|
||||||
self.change();
|
to.add(this.whoami);
|
||||||
self.notify(undefined);
|
to = [...to];
|
||||||
self.requestUpdate();
|
message.recps = to;
|
||||||
}).catch(function(error) {
|
console.log('message is now', message);
|
||||||
|
message = await tfrpc.rpc.encrypt(
|
||||||
|
this.whoami,
|
||||||
|
to,
|
||||||
|
JSON.stringify(message)
|
||||||
|
);
|
||||||
|
console.log('encrypted as', message);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await tfrpc.rpc.appendMessage(this.whoami, message).then(function () {
|
||||||
|
edit.value = '';
|
||||||
|
self.change();
|
||||||
|
self.notify(undefined);
|
||||||
|
self.requestUpdate();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
alert(error.message);
|
alert(error.message);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
discard() {
|
discard() {
|
||||||
@ -219,19 +249,60 @@ class TfComposeElement extends LitElement {
|
|||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
let input = document.createElement('input');
|
let input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.onchange = function(event) {
|
input.onchange = function (event) {
|
||||||
let file = event.target.files[0];
|
let file = event.target.files[0];
|
||||||
self.add_file(file);
|
self.add_file(file);
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async autocomplete(text, callback) {
|
||||||
|
this.last_autocomplete = text;
|
||||||
|
let results = [];
|
||||||
|
try {
|
||||||
|
let rows = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT messages.content FROM messages_fts(?)
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
WHERE messages.content LIKE ?
|
||||||
|
ORDER BY timestamp DESC LIMIT 10
|
||||||
|
`,
|
||||||
|
['"' + text.replace('"', '""') + '"', `%%`]
|
||||||
|
);
|
||||||
|
for (let row of rows) {
|
||||||
|
for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
|
||||||
|
if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
|
||||||
|
results.push({key: match[1], value: match[2]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (this.last_autocomplete === text) {
|
||||||
|
callback(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
let tribute = new Tribute({
|
let tribute = new Tribute({
|
||||||
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
collection: [
|
||||||
selectTemplate: function(item) {
|
{
|
||||||
return `[@${item.original.key}](${item.original.value})`;
|
values: Object.entries(this.users).map((x) => ({
|
||||||
},
|
key: x[1].name,
|
||||||
|
value: x[0],
|
||||||
|
})),
|
||||||
|
selectTemplate: function (item) {
|
||||||
|
return `[@${item.original.key}](${item.original.value})`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: '&',
|
||||||
|
values: this.autocomplete,
|
||||||
|
selectTemplate: function (item) {
|
||||||
|
return ``;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
tribute.attach(this.renderRoot.getElementById('edit'));
|
tribute.attach(this.renderRoot.getElementById('edit'));
|
||||||
}
|
}
|
||||||
@ -244,6 +315,19 @@ class TfComposeElement extends LitElement {
|
|||||||
preview.innerHTML = this.process_text(edit.value);
|
preview.innerHTML = this.process_text(edit.value);
|
||||||
this.last_updated_text = edit.value;
|
this.last_updated_text = edit.value;
|
||||||
}
|
}
|
||||||
|
let encrypt = this.renderRoot.getElementById('encrypt_to');
|
||||||
|
if (encrypt) {
|
||||||
|
let tribute = new Tribute({
|
||||||
|
values: Object.entries(this.users).map((x) => ({
|
||||||
|
key: x[1].name,
|
||||||
|
value: x[0],
|
||||||
|
})),
|
||||||
|
selectTemplate: function (item) {
|
||||||
|
return item.original.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
tribute.attach(encrypt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_mention(id) {
|
remove_mention(id) {
|
||||||
@ -255,11 +339,30 @@ class TfComposeElement extends LitElement {
|
|||||||
|
|
||||||
render_mention(mention) {
|
render_mention(mention) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html` <div style="display: flex; flex-direction: row">
|
||||||
<div>
|
<div style="align-self: center; margin: 0.5em">
|
||||||
<pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>
|
<button
|
||||||
<input type="button" value="x" @click=${() => self.remove_mention(mention.link)}></input>
|
class="w3-button w3-dark-grey"
|
||||||
</div>`;
|
title="Remove ${mention.name} mention"
|
||||||
|
@click=${() => self.remove_mention(mention.link)}
|
||||||
|
>
|
||||||
|
🚮
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; flex-direction: column">
|
||||||
|
<h3>${mention.name}</h3>
|
||||||
|
<div style="padding-left: 1em">
|
||||||
|
${Object.entries(mention)
|
||||||
|
.filter((x) => x[0] != 'name')
|
||||||
|
.map(
|
||||||
|
(x) =>
|
||||||
|
html`<div>
|
||||||
|
<span style="font-weight: bold">${x[0]}</span>: ${x[1]}
|
||||||
|
</div>`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render_attach_app() {
|
render_attach_app() {
|
||||||
@ -283,34 +386,51 @@ class TfComposeElement extends LitElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let draft = this.get_draft();
|
let draft = self.get_draft();
|
||||||
draft.mentions = Object.assign(draft.mentions || {}, mentions);
|
draft.mentions = Object.assign(draft.mentions || {}, mentions);
|
||||||
this.requestUpdate();
|
self.requestUpdate();
|
||||||
this.notify(draft);
|
self.notify(draft);
|
||||||
this.apps = null;
|
self.apps = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.apps) {
|
if (this.apps) {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div class="w3-card-4 w3-margin w3-padding">
|
||||||
<select id="select">
|
<select id="select" class="w3-select w3-dark-grey">
|
||||||
${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-dark-grey" @click=${attach_selected_app}>
|
||||||
<input type="button" value="Cancel" @click=${() => this.apps = null}></input>
|
Attach
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (this.apps = null)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_attach_app_button() {
|
render_attach_app_button() {
|
||||||
|
let self = this;
|
||||||
async function attach_app() {
|
async function attach_app() {
|
||||||
this.apps = await tfrpc.rpc.apps();
|
self.apps = await tfrpc.rpc.apps();
|
||||||
}
|
}
|
||||||
if (!this.apps) {
|
if (!this.apps) {
|
||||||
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`
|
return html`<button class="w3-button w3-dark-grey" @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-dark-grey"
|
||||||
|
@click=${() => (this.apps = null)}
|
||||||
|
>
|
||||||
|
Discard App
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,15 +446,17 @@ class TfComposeElement extends LitElement {
|
|||||||
let draft = this.get_draft();
|
let draft = this.get_draft();
|
||||||
if (draft.content_warning !== undefined) {
|
if (draft.content_warning !== undefined) {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div class="w3-container w3-padding">
|
||||||
<input type="checkbox" id="cw" @change=${() => self.set_content_warning(undefined)} checked></input>
|
<p>
|
||||||
<label for="cw">CW</label>
|
<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
|
||||||
<input type="text" id="content_warning" @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
|
<label for="cw">CW</label>
|
||||||
|
</p>
|
||||||
|
<input type="text" class="w3-input w3-border w3-dark-grey" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<input type="checkbox" id="cw" @change=${() => self.set_content_warning('')}></input>
|
<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning('')}></input>
|
||||||
<label for="cw">CW</label>
|
<label for="cw">CW</label>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -344,31 +466,114 @@ class TfComposeElement extends LitElement {
|
|||||||
return this.drafts[this.branch || ''] || {};
|
return this.drafts[this.branch || ''] || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_encrypt(event) {
|
||||||
|
let input = event.srcElement;
|
||||||
|
let matches = input.value.match(/@.*?\.ed25519/g);
|
||||||
|
if (matches) {
|
||||||
|
let draft = this.get_draft();
|
||||||
|
let to = [...new Set(matches.concat(draft.encrypt_to))];
|
||||||
|
this.set_encrypt(to);
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_encrypt() {
|
||||||
|
let draft = this.get_draft();
|
||||||
|
if (draft.encrypt_to === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div style="display: flex; flex-direction: row; width: 100%">
|
||||||
|
<label for="encrypt_to">🔐 To:</label>
|
||||||
|
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt(undefined)}>🚮</button>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
${draft.encrypt_to.map(
|
||||||
|
(x) => html`
|
||||||
|
<li>
|
||||||
|
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||||
|
<input type="button" class="w3-button w3-dark-grey" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
|
||||||
|
</li>`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_encrypt(encrypt) {
|
||||||
|
let draft = this.get_draft();
|
||||||
|
draft.encrypt_to = encrypt;
|
||||||
|
this.notify(draft);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
let draft = self.get_draft();
|
let draft = self.get_draft();
|
||||||
let content_warning =
|
let content_warning =
|
||||||
draft.content_warning !== undefined ?
|
draft.content_warning !== undefined
|
||||||
html`<div id="content_warning_preview" class="content_warning">${draft.content_warning}</div>` :
|
? html`<div class="w3-panel w3-round-xlarge w3-blue">
|
||||||
undefined;
|
<p id="content_warning_preview">${draft.content_warning}</p>
|
||||||
|
</div>`
|
||||||
|
: undefined;
|
||||||
|
let encrypt =
|
||||||
|
draft.encrypt_to !== undefined
|
||||||
|
? undefined
|
||||||
|
: html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => this.set_encrypt([])}
|
||||||
|
>
|
||||||
|
🔐
|
||||||
|
</button>`;
|
||||||
let result = html`
|
let result = html`
|
||||||
<div style="display: flex; flex-direction: row; width: 100%">
|
<div
|
||||||
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${draft.text}</textarea>
|
class="w3-card-4 w3-blue-grey w3-padding"
|
||||||
<div style="flex: 1 0 50%">
|
style="box-sizing: border-box"
|
||||||
${content_warning}
|
>
|
||||||
<div id="preview"></div>
|
${this.render_encrypt()}
|
||||||
|
<div style="display: flex; flex-direction: row; width: 100%; gap: 4px">
|
||||||
|
<div style="flex: 1 0 50%">
|
||||||
|
<p>
|
||||||
|
<textarea
|
||||||
|
class="w3-input w3-dark-grey w3-border"
|
||||||
|
style="resize: vertical"
|
||||||
|
placeholder="Write a post here."
|
||||||
|
id="edit"
|
||||||
|
@input=${this.input}
|
||||||
|
@change=${this.change}
|
||||||
|
@paste=${this.paste}
|
||||||
|
>
|
||||||
|
${draft.text}</textarea
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1 0 50%">
|
||||||
|
${content_warning}
|
||||||
|
<div id="preview"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${Object.values(draft.mentions || {}).map((x) =>
|
||||||
|
self.render_mention(x)
|
||||||
|
)}
|
||||||
|
${this.render_attach_app()} ${this.render_content_warning()}
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
id="submit"
|
||||||
|
@click=${this.submit}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.attach}>
|
||||||
|
Attach
|
||||||
|
</button>
|
||||||
|
${this.render_attach_app_button()} ${encrypt}
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.discard}>
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
|
|
||||||
${this.render_content_warning()}
|
|
||||||
${this.render_attach_app()}
|
|
||||||
<input type="button" value="Submit" @click=${this.submit}></input>
|
|
||||||
<input type="button" value="Attach" @click=${this.attach}></input>
|
|
||||||
${this.render_attach_app_button()}
|
|
||||||
<input type="button" value="Discard" @click=${this.discard}></input>
|
|
||||||
`;
|
`;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-compose', TfComposeElement);
|
customElements.define('tf-compose', TfComposeElement);
|
||||||
|
@ -1,37 +1,54 @@
|
|||||||
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';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** 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 {
|
||||||
ids: {type: Array},
|
ids: {type: Array},
|
||||||
selected: {type: String},
|
selected: {type: String},
|
||||||
}
|
users: {type: Object},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
let self = this;
|
|
||||||
this.ids = [];
|
this.ids = [];
|
||||||
|
this.users = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
changed(event) {
|
changed(event) {
|
||||||
this.selected = event.srcElement.value;
|
this.selected = event.srcElement.value;
|
||||||
this.dispatchEvent(new Event('change', {
|
this.dispatchEvent(
|
||||||
srcElement: this,
|
new Event('change', {
|
||||||
}));
|
srcElement: this,
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<select @change=${this.changed} style="max-width: 100%">
|
<select
|
||||||
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
class="w3-select w3-dark-grey w3-padding w3-border"
|
||||||
|
@change=${this.changed}
|
||||||
|
style="max-width: 100%; overflow: hidden"
|
||||||
|
>
|
||||||
|
${(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>
|
</select>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-id-picker', TfIdentityPickerElement);
|
customElements.define('tf-id-picker', TfIdentityPickerElement);
|
||||||
|
@ -11,10 +11,10 @@ class TfMessageElement extends LitElement {
|
|||||||
message: {type: Object},
|
message: {type: Object},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
raw: {type: Boolean},
|
format: {type: String},
|
||||||
blog_data: {type: String},
|
blog_data: {type: String},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -26,17 +26,32 @@ class TfMessageElement extends LitElement {
|
|||||||
this.message = {};
|
this.message = {};
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.drafts = {};
|
this.drafts = {};
|
||||||
this.raw = false;
|
this.format = 'message';
|
||||||
this.expanded = {};
|
this.expanded = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
show_reply() {
|
show_reply() {
|
||||||
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: ''}});
|
let event = new CustomEvent('tf-draft', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
id: this.message?.id,
|
||||||
|
draft: {
|
||||||
|
encrypt_to: this.message?.decrypted?.recps,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
discard_reply() {
|
discard_reply() {
|
||||||
this.dispatchEvent(new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.id, draft: undefined}}));
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-draft', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {id: this.id, draft: undefined},
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render_votes() {
|
render_votes() {
|
||||||
@ -51,12 +66,19 @@ class TfMessageElement extends LitElement {
|
|||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return html`<div>${(this.message.votes || []).map(
|
return html`<div>
|
||||||
vote => html`
|
${(this.message.votes || []).map(
|
||||||
<span title="${this.users[vote.author]?.name ?? vote.author} ${new Date(vote.timestamp)}">
|
(vote) => html`
|
||||||
${normalize_expression(vote.content.vote.expression)}
|
<span
|
||||||
</span>
|
title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
|
||||||
`)}</div>`;
|
vote.timestamp
|
||||||
|
)}"
|
||||||
|
>
|
||||||
|
${normalize_expression(vote.content.vote.expression)}
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render_raw() {
|
render_raw() {
|
||||||
@ -69,31 +91,41 @@ class TfMessageElement extends LitElement {
|
|||||||
hash: this.message?.hash,
|
hash: this.message?.hash,
|
||||||
content: this.message?.content,
|
content: this.message?.content,
|
||||||
signature: this.message?.signature,
|
signature: this.message?.signature,
|
||||||
}
|
};
|
||||||
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`
|
return html`<div style="white-space: pre-wrap">
|
||||||
|
${JSON.stringify(raw, null, 2)}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
vote(emoji) {
|
vote(emoji) {
|
||||||
let reaction = emoji.emoji;
|
let reaction = emoji;
|
||||||
let message = this.message.id;
|
let message = this.message.id;
|
||||||
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
|
if (
|
||||||
tfrpc.rpc.appendMessage(
|
confirm(
|
||||||
this.whoami,
|
'Are you sure you want to react with ' +
|
||||||
{
|
reaction +
|
||||||
|
' to ' +
|
||||||
|
message +
|
||||||
|
'?'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
tfrpc.rpc
|
||||||
|
.appendMessage(this.whoami, {
|
||||||
type: 'vote',
|
type: 'vote',
|
||||||
vote: {
|
vote: {
|
||||||
link: message,
|
link: message,
|
||||||
value: 1,
|
value: 1,
|
||||||
expression: reaction,
|
expression: reaction,
|
||||||
},
|
},
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
react(event) {
|
react(event) {
|
||||||
emojis.picker(x => this.vote(x));
|
emojis.picker((x) => this.vote(x));
|
||||||
}
|
}
|
||||||
|
|
||||||
show_image(link) {
|
show_image(link) {
|
||||||
@ -127,57 +159,91 @@ class TfMessageElement extends LitElement {
|
|||||||
body_click(event) {
|
body_click(event) {
|
||||||
if (event.srcElement.tagName == 'IMG') {
|
if (event.srcElement.tagName == 'IMG') {
|
||||||
this.show_image(event.srcElement.src);
|
this.show_image(event.srcElement.src);
|
||||||
|
} else if (
|
||||||
|
event.srcElement.tagName == 'DIV' &&
|
||||||
|
event.srcElement.classList.contains('img_caption')
|
||||||
|
) {
|
||||||
|
let next = event.srcElement.nextSibling;
|
||||||
|
if (next.style.display == 'block') {
|
||||||
|
next.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
next.style.display = 'block';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_mention(mention) {
|
render_mention(mention) {
|
||||||
if (!mention?.link || typeof(mention.link) != 'string') {
|
if (!mention?.link || typeof mention.link != 'string') {
|
||||||
return html` <pre>${JSON.stringify(mention)}</pre>`;
|
return html` <pre>${JSON.stringify(mention)}</pre>`;
|
||||||
} else if (mention?.link?.startsWith('&') &&
|
} else if (
|
||||||
mention?.type?.startsWith('image/')) {
|
mention?.link?.startsWith('&') &&
|
||||||
|
mention?.type?.startsWith('image/')
|
||||||
|
) {
|
||||||
return html`
|
return html`
|
||||||
<img src=${'/' + mention.link + '/view'} style="max-width: 128px; max-height: 128px" title=${mention.name} @click=${() => this.show_image('/' + mention.link + '/view')}>
|
<img
|
||||||
|
src=${'/' + mention.link + '/view'}
|
||||||
|
style="max-width: 128px; max-height: 128px"
|
||||||
|
title=${mention.name}
|
||||||
|
@click=${() => this.show_image('/' + mention.link + '/view')}
|
||||||
|
/>
|
||||||
`;
|
`;
|
||||||
} else if (mention.link?.startsWith('&') &&
|
} else if (
|
||||||
mention.name?.startsWith('audio:')) {
|
mention.link?.startsWith('&') &&
|
||||||
|
mention.name?.startsWith('audio:')
|
||||||
|
) {
|
||||||
return html`
|
return html`
|
||||||
<audio controls style="height: 32px">
|
<audio controls style="height: 32px">
|
||||||
<source src=${'/' + mention.link + '/view'}></source>
|
<source src=${'/' + mention.link + '/view'}></source>
|
||||||
</audio>
|
</audio>
|
||||||
`;
|
`;
|
||||||
} else if (mention.link?.startsWith('&') &&
|
} else if (
|
||||||
mention.name?.startsWith('video:')) {
|
mention.link?.startsWith('&') &&
|
||||||
|
mention.name?.startsWith('video:')
|
||||||
|
) {
|
||||||
return html`
|
return html`
|
||||||
<video controls style="max-height: 240px">
|
<video controls style="max-height: 240px; max-width: 128px">
|
||||||
<source src=${'/' + mention.link + '/view'}></source>
|
<source src=${'/' + mention.link + '/view'}></source>
|
||||||
</video>
|
</video>
|
||||||
`;
|
`;
|
||||||
} else if (mention.link?.startsWith('&') &&
|
} else if (
|
||||||
mention?.type === 'application/tildefriends') {
|
mention.link?.startsWith('&') &&
|
||||||
|
mention?.type === 'application/tildefriends'
|
||||||
|
) {
|
||||||
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
|
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
|
||||||
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
|
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
|
||||||
return html` <a href=${'#' + encodeURIComponent(mention.link)}>${mention.name}</a>`;
|
return html` <a href=${'#' + encodeURIComponent(mention.link)}
|
||||||
|
>${mention.name}</a
|
||||||
|
>`;
|
||||||
} else if (mention.link?.startsWith('#')) {
|
} else if (mention.link?.startsWith('#')) {
|
||||||
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}>${mention.link}</a>`;
|
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}
|
||||||
} else if (Object.keys(mention).length == 2 && mention.link && mention.name) {
|
>${mention.link}</a
|
||||||
|
>`;
|
||||||
|
} else if (
|
||||||
|
Object.keys(mention).length == 2 &&
|
||||||
|
mention.link &&
|
||||||
|
mention.name
|
||||||
|
) {
|
||||||
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
|
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
|
||||||
} else {
|
} else {
|
||||||
return html` <pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>`;
|
return html` <pre style="white-space: pre-wrap">
|
||||||
|
${JSON.stringify(mention, null, 2)}</pre
|
||||||
|
>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_mentions() {
|
render_mentions() {
|
||||||
let mentions = this.message?.content?.mentions || [];
|
let mentions = this.message?.content?.mentions || [];
|
||||||
mentions = mentions.filter(x =>
|
mentions = mentions.filter(
|
||||||
x.name?.startsWith('audio:') ||
|
(x) => this.message?.content?.text?.indexOf(x.link) === -1
|
||||||
x.name?.startsWith('video:') ||
|
);
|
||||||
this.message?.content?.text?.indexOf(x.link) === -1);
|
|
||||||
if (mentions.length) {
|
if (mentions.length) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<fieldset style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black">
|
<fieldset
|
||||||
|
style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black"
|
||||||
|
>
|
||||||
<legend>Mentions</legend>
|
<legend>Mentions</legend>
|
||||||
${mentions.map(x => self.render_mention(x))}
|
${mentions.map((x) => self.render_mention(x))}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -188,59 +254,192 @@ 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-dark-grey"
|
||||||
|
@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-dark-grey"
|
||||||
|
@click=${() => self.set_expanded(false)}
|
||||||
|
>
|
||||||
|
Collapse</button
|
||||||
|
>${(this.message.child_messages || []).map(
|
||||||
|
(x) =>
|
||||||
|
html`<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-message>`
|
||||||
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render_channels() {
|
||||||
|
let content = this.message?.content;
|
||||||
|
if (this?.messsage?.decrypted?.type == 'post') {
|
||||||
|
content = this.message.decrypted;
|
||||||
|
}
|
||||||
|
let channels = [];
|
||||||
|
if (typeof content.channel === 'string') {
|
||||||
|
channels.push(`#${content.channel}`);
|
||||||
|
}
|
||||||
|
if (Array.isArray(content.mentions)) {
|
||||||
|
for (let mention of content.mentions) {
|
||||||
|
if (typeof mention?.link === 'string' && mention.link.startsWith('#')) {
|
||||||
|
channels.push(mention.link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let content = this.message?.content;
|
let content = this.message?.content;
|
||||||
|
if (this.message?.decrypted?.type == 'post') {
|
||||||
|
content = this.message.decrypted;
|
||||||
|
}
|
||||||
let self = this;
|
let self = this;
|
||||||
let raw_button = this.raw ?
|
let raw_button;
|
||||||
html`<input type="button" value="Message" @click=${() => self.raw = false}></input>` :
|
switch (this.format) {
|
||||||
html`<input type="button" value="Raw" @click=${() => self.raw = true}></input>`;
|
case 'raw':
|
||||||
|
if (content?.type == 'post' || content?.type == 'blog') {
|
||||||
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (self.format = 'md')}
|
||||||
|
>
|
||||||
|
Markdown
|
||||||
|
</button>`;
|
||||||
|
} else {
|
||||||
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (self.format = 'message')}
|
||||||
|
>
|
||||||
|
Message
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'md':
|
||||||
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (self.format = 'message')}
|
||||||
|
>
|
||||||
|
Message
|
||||||
|
</button>`;
|
||||||
|
break;
|
||||||
|
case 'decrypted':
|
||||||
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (self.format = 'raw')}
|
||||||
|
>
|
||||||
|
Raw
|
||||||
|
</button>`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (this.message.decrypted) {
|
||||||
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (self.format = 'decrypted')}
|
||||||
|
>
|
||||||
|
Decrypted
|
||||||
|
</button>`;
|
||||||
|
} else {
|
||||||
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (self.format = 'raw')}
|
||||||
|
>
|
||||||
|
Raw
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
function small_frame(inner) {
|
function small_frame(inner) {
|
||||||
|
let body;
|
||||||
return html`
|
return html`
|
||||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block">
|
<div
|
||||||
|
class="w3-card-4"
|
||||||
|
style="background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
|
||||||
|
>
|
||||||
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
||||||
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
|
<span style="padding-right: 8px"
|
||||||
${raw_button}
|
><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(
|
||||||
${self.raw ? self.render_raw() : inner}
|
self.message.timestamp
|
||||||
|
).toLocaleString()}</span
|
||||||
|
>
|
||||||
|
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
|
||||||
${self.render_votes()}
|
${self.render_votes()}
|
||||||
</div>
|
</div>
|
||||||
`
|
`;
|
||||||
}
|
}
|
||||||
if (this.message.placeholder) {
|
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"
|
||||||
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
|
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||||
<div>${this.render_votes()}</div>
|
>
|
||||||
${(this.message.child_messages || []).map(x => html`
|
${this.message.messages.map(
|
||||||
<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>
|
(x) =>
|
||||||
`)}
|
html`<tf-message
|
||||||
</div>`;
|
.message=${x}
|
||||||
} else if (typeof(content?.type === 'string')) {
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-message>`
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
|
} else if (this.message.placeholder) {
|
||||||
|
return html` <div
|
||||||
|
class="w3-card-4"
|
||||||
|
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||||
|
>
|
||||||
|
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
|
||||||
|
(placeholder)
|
||||||
|
<div>${this.render_votes()}</div>
|
||||||
|
${(this.message.child_messages || []).map(
|
||||||
|
(x) => html`
|
||||||
|
<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-message>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
|
} else if (typeof (content?.type === 'string')) {
|
||||||
if (content.type == 'about') {
|
if (content.type == 'about') {
|
||||||
let name;
|
let name;
|
||||||
let image;
|
let image;
|
||||||
@ -250,7 +449,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) {
|
||||||
@ -258,64 +457,105 @@ class TfMessageElement extends LitElement {
|
|||||||
<div style="flex: 1 0 50%; overflow-wrap: anywhere">
|
<div style="flex: 1 0 50%; overflow-wrap: anywhere">
|
||||||
<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
|
<div>${unsafeHTML(tfutils.markdown(content.description))}</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`;
|
||||||
}
|
}
|
||||||
let update = content.about == this.message.author ?
|
let update =
|
||||||
html`<div style="font-weight: bold">Updated profile.</div>` :
|
content.about == this.message.author
|
||||||
html`<div style="font-weight: bold">Updated profile for <tf-user id=${content.about} .users=${this.users}></tf-user>.</div>`;
|
? html`<div style="font-weight: bold">Updated profile.</div>`
|
||||||
return small_frame(html`
|
: html`<div style="font-weight: bold">
|
||||||
${update}
|
Updated profile for
|
||||||
${name}
|
<tf-user id=${content.about} .users=${this.users}></tf-user>.
|
||||||
${image}
|
</div>`;
|
||||||
${description}
|
return small_frame(html` ${update} ${name} ${image} ${description} `);
|
||||||
`);
|
|
||||||
} else if (content.type == 'contact') {
|
} else if (content.type == 'contact') {
|
||||||
return small_frame(html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
|
<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') {
|
|
||||||
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
|
|
||||||
<tf-compose
|
|
||||||
whoami=${this.whoami}
|
|
||||||
.users=${this.users}
|
|
||||||
root=${this.message.content.root || this.message.id}
|
|
||||||
branch=${this.message.id}
|
|
||||||
.drafts=${this.drafts}
|
|
||||||
@tf-discard=${this.discard_reply}></tf-compose>
|
|
||||||
` : html`
|
|
||||||
<input type="button" value="Reply" @click=${this.show_reply}></input>
|
|
||||||
`;
|
`;
|
||||||
|
} else if (content.type == 'post') {
|
||||||
|
let reply =
|
||||||
|
this.drafts[this.message?.id] !== undefined
|
||||||
|
? html`
|
||||||
|
<tf-compose
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
root=${this.message.content.root || this.message.id}
|
||||||
|
branch=${this.message.id}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
@tf-discard=${this.discard_reply}
|
||||||
|
></tf-compose>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${this.show_reply}
|
||||||
|
>
|
||||||
|
Reply
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
let self = this;
|
let self = this;
|
||||||
let body = this.raw ?
|
let body;
|
||||||
this.render_raw() :
|
switch (this.format) {
|
||||||
unsafeHTML(tfutils.markdown(content.text));
|
case 'raw':
|
||||||
|
body = this.render_raw();
|
||||||
|
break;
|
||||||
|
case 'md':
|
||||||
|
body = html`<code
|
||||||
|
style="white-space: pre-wrap; overflow-wrap: anywhere"
|
||||||
|
>${content.text}</code
|
||||||
|
>`;
|
||||||
|
break;
|
||||||
|
case 'message':
|
||||||
|
body = unsafeHTML(tfutils.markdown(content.text));
|
||||||
|
break;
|
||||||
|
case 'decrypted':
|
||||||
|
body = html`<pre
|
||||||
|
style="white-space: pre-wrap; overflow-wrap: anywhere"
|
||||||
|
>
|
||||||
|
${JSON.stringify(content, null, 2)}</pre
|
||||||
|
>`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
let content_warning = html`
|
let content_warning = html`
|
||||||
<div class="content_warning" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
|
<div
|
||||||
`;
|
class="w3-panel w3-round-xlarge w3-blue"
|
||||||
let content_html =
|
style="cursor: pointer"
|
||||||
html`
|
@click=${(x) => this.toggle_expanded(':cw')}
|
||||||
<div @click=${this.body_click}>${body}</div>
|
>
|
||||||
${this.render_mentions()}
|
<p>${content.contentWarning}</p>
|
||||||
`;
|
</div>
|
||||||
let payload =
|
`;
|
||||||
content.contentWarning ?
|
let content_html = html`
|
||||||
self.expanded[(this.message.id || '') + ':cw'] ?
|
${this.render_channels()}
|
||||||
html`
|
<div @click=${this.body_click}>${body}</div>
|
||||||
${content_warning}
|
${this.render_mentions()}
|
||||||
${content_html}
|
`;
|
||||||
` :
|
let payload = content.contentWarning
|
||||||
content_warning :
|
? self.expanded[(this.message.id || '') + ':cw']
|
||||||
content_html;
|
? html` ${content_warning} ${content_html} `
|
||||||
|
: content_warning
|
||||||
|
: content_html;
|
||||||
|
let is_encrypted = this.message?.decrypted
|
||||||
|
? html`<span style="align-self: center">🔓</span>`
|
||||||
|
: undefined;
|
||||||
|
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 {
|
||||||
@ -331,37 +571,100 @@ 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"
|
||||||
|
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
|
||||||
|
>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
|
${is_encrypted}
|
||||||
<span style="flex: 1"></span>
|
<span style="flex: 1"></span>
|
||||||
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
<span style="padding-right: 8px"
|
||||||
|
><a target="_top" href=${'#' + self.message.id}>%</a>
|
||||||
|
${new Date(this.message.timestamp).toLocaleString()}</span
|
||||||
|
>
|
||||||
<span>${raw_button}</span>
|
<span>${raw_button}</span>
|
||||||
</div>
|
</div>
|
||||||
${payload}
|
${payload} ${this.render_votes()}
|
||||||
${this.render_votes()}
|
<p>
|
||||||
<div>
|
|
||||||
${reply}
|
${reply}
|
||||||
<input type="button" value="React" @click=${this.react}></input>
|
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
||||||
|
React
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
${this.render_children()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (content.type === 'issue') {
|
||||||
|
let is_encrypted = this.message?.decrypted
|
||||||
|
? html`<span style="align-self: center">🔓</span>`
|
||||||
|
: undefined;
|
||||||
|
let style_background = this.message?.decrypted
|
||||||
|
? 'rgba(255, 0, 0, 0.2)'
|
||||||
|
: 'rgba(255, 255, 255, 0.1)';
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div
|
||||||
|
class="w3-card-4"
|
||||||
|
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
|
||||||
|
>
|
||||||
|
<div style="display: flex; flex-direction: row">
|
||||||
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
|
${is_encrypted}
|
||||||
|
<span style="flex: 1"></span>
|
||||||
|
<span style="padding-right: 8px"
|
||||||
|
><a target="_top" href=${'#' + self.message.id}>%</a>
|
||||||
|
${new Date(this.message.timestamp).toLocaleString()}</span
|
||||||
|
>
|
||||||
|
<span>${raw_button}</span>
|
||||||
</div>
|
</div>
|
||||||
|
${content.text} ${this.render_votes()}
|
||||||
|
<p>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
||||||
|
React
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
${this.render_children()}
|
${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type === 'blog') {
|
} else if (content.type === 'blog') {
|
||||||
let self = this;
|
let self = this;
|
||||||
tfrpc.rpc.get_blob(content.blog).then(function(data) {
|
tfrpc.rpc.get_blob(content.blog).then(function (data) {
|
||||||
self.blog_data = data;
|
self.blog_data = data;
|
||||||
});
|
});
|
||||||
let payload =
|
let payload = this.expanded[(this.message.id || '') + ':blog']
|
||||||
this.expanded[(this.message.id || '') + ':blog'] ?
|
? html`<div>
|
||||||
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
|
${this.blog_data
|
||||||
undefined;
|
? unsafeHTML(tfutils.markdown(this.blog_data))
|
||||||
let body = this.raw ?
|
: 'Loading...'}
|
||||||
this.render_raw() :
|
</div>`
|
||||||
html`
|
: undefined;
|
||||||
|
let body;
|
||||||
|
switch (this.format) {
|
||||||
|
case 'raw':
|
||||||
|
body = this.render_raw();
|
||||||
|
break;
|
||||||
|
case 'md':
|
||||||
|
body = content.summary;
|
||||||
|
break;
|
||||||
|
case 'message':
|
||||||
|
body = html`
|
||||||
<div
|
<div
|
||||||
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
||||||
@click=${x => self.toggle_expanded(':blog')}>
|
@click=${(x) => self.toggle_expanded(':blog')}>
|
||||||
<h2>${content.title}</h2>
|
<h2>${content.title}</h2>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<img src=/${content.thumbnail}/view></img>
|
<img src=/${content.thumbnail}/view></img>
|
||||||
@ -370,6 +673,28 @@ class TfMessageElement extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
${payload}
|
${payload}
|
||||||
`;
|
`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let reply =
|
||||||
|
this.drafts[this.message?.id] !== undefined
|
||||||
|
? html`
|
||||||
|
<tf-compose
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
root=${this.message.content.root || this.message.id}
|
||||||
|
branch=${this.message.id}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
@tf-discard=${this.discard_reply}
|
||||||
|
></tf-compose>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${this.show_reply}
|
||||||
|
>
|
||||||
|
Reply
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
code {
|
code {
|
||||||
@ -385,40 +710,74 @@ class TfMessageElement extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
|
<div
|
||||||
|
class="w3-card-4"
|
||||||
|
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); 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-dark-grey" @click=${this.react}>
|
||||||
|
React
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${this.render_votes()} ${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type === 'pub') {
|
} else if (content.type === 'pub') {
|
||||||
return small_frame(html`
|
return small_frame(
|
||||||
<style>
|
html` <style>
|
||||||
span {
|
span {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<span>
|
<span>
|
||||||
<div>
|
<div>
|
||||||
🍻 <tf-user .users=${this.users} id=${content.address.key}></tf-user>
|
🍻
|
||||||
</div>
|
<tf-user
|
||||||
<pre>${content.address.host}:${content.address.port}</pre>
|
.users=${this.users}
|
||||||
</span>`);
|
id=${content.address.key}
|
||||||
|
></tf-user>
|
||||||
|
</div>
|
||||||
|
<pre>${content.address.host}:${content.address.port}</pre>
|
||||||
|
</span>`
|
||||||
|
);
|
||||||
} else if (content.type === 'channel') {
|
} else if (content.type === 'channel') {
|
||||||
return small_frame(html`
|
return small_frame(html`
|
||||||
<div>
|
<div>
|
||||||
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} <a href=${'#q=' + encodeURIComponent('#' + content.channel)}>#${content.channel}</a>
|
${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
|
||||||
|
<a href=${'#q=' + encodeURIComponent('#' + content.channel)}
|
||||||
|
>#${content.channel}</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
} else if (typeof(this.message.content) == 'string') {
|
} else if (typeof this.message.content == 'string') {
|
||||||
return small_frame(html`<span>🔒</span>`);
|
if (this.message?.decrypted) {
|
||||||
|
if (this.format == 'decrypted') {
|
||||||
|
return small_frame(
|
||||||
|
html`<span>🔓</span>
|
||||||
|
<pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return small_frame(
|
||||||
|
html`<span>🔓</span>
|
||||||
|
<div>${this.message.decrypted.type}</div>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return small_frame(html`<span>🔒</span>`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
|
return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
|
||||||
}
|
}
|
||||||
@ -428,4 +787,4 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-message', TfMessageElement);
|
customElements.define('tf-message', TfMessageElement);
|
||||||
|
@ -11,7 +11,7 @@ class TfNewsElement extends LitElement {
|
|||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -61,7 +61,7 @@ class TfNewsElement extends LitElement {
|
|||||||
message.parent_message = message.content.vote.link;
|
message.parent_message = message.content.vote.link;
|
||||||
} else if (message.content.type == 'post') {
|
} else if (message.content.type == 'post') {
|
||||||
if (message.content.root) {
|
if (message.content.root) {
|
||||||
if (typeof(message.content.root) === 'string') {
|
if (typeof message.content.root === 'string') {
|
||||||
let m = ensure_message(message.content.root);
|
let m = ensure_message(message.content.root);
|
||||||
if (!m.child_messages) {
|
if (!m.child_messages) {
|
||||||
m.child_messages = [];
|
m.child_messages = [];
|
||||||
@ -89,8 +89,7 @@ class TfNewsElement extends LitElement {
|
|||||||
for (let message of messages) {
|
for (let message of messages) {
|
||||||
try {
|
try {
|
||||||
message.content = JSON.parse(message.content);
|
message.content = JSON.parse(message.content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
if (!messages_by_id[message.id]) {
|
if (!messages_by_id[message.id]) {
|
||||||
messages_by_id[message.id] = message;
|
messages_by_id[message.id] = message;
|
||||||
link_message(message);
|
link_message(message);
|
||||||
@ -100,8 +99,12 @@ class TfNewsElement extends LitElement {
|
|||||||
message.parent_message = placeholder.parent_message;
|
message.parent_message = placeholder.parent_message;
|
||||||
message.child_messages = placeholder.child_messages;
|
message.child_messages = placeholder.child_messages;
|
||||||
message.votes = placeholder.votes;
|
message.votes = placeholder.votes;
|
||||||
if (placeholder.parent_message && messages_by_id[placeholder.parent_message]) {
|
if (
|
||||||
let children = messages_by_id[placeholder.parent_message].child_messages;
|
placeholder.parent_message &&
|
||||||
|
messages_by_id[placeholder.parent_message]
|
||||||
|
) {
|
||||||
|
let children =
|
||||||
|
messages_by_id[placeholder.parent_message].child_messages;
|
||||||
children.splice(children.indexOf(placeholder), 1);
|
children.splice(children.indexOf(placeholder), 1);
|
||||||
children.push(message);
|
children.push(message);
|
||||||
}
|
}
|
||||||
@ -116,7 +119,10 @@ class TfNewsElement extends LitElement {
|
|||||||
let latest = 0;
|
let latest = 0;
|
||||||
for (let message of messages || []) {
|
for (let message of messages || []) {
|
||||||
if (message.latest_subtree_timestamp === undefined) {
|
if (message.latest_subtree_timestamp === undefined) {
|
||||||
message.latest_subtree_timestamp = Math.max(message.timestamp ?? 0, this.update_latest_subtree_timestamp(message.child_messages));
|
message.latest_subtree_timestamp = Math.max(
|
||||||
|
message.timestamp ?? 0,
|
||||||
|
this.update_latest_subtree_timestamp(message.child_messages)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
latest = Math.max(latest, message.latest_subtree_timestamp);
|
latest = Math.max(latest, message.latest_subtree_timestamp);
|
||||||
}
|
}
|
||||||
@ -127,30 +133,64 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
group_following(messages) {
|
||||||
|
let result = [];
|
||||||
|
let group = [];
|
||||||
|
for (let message of messages) {
|
||||||
|
if (message?.content?.type === 'contact') {
|
||||||
|
group.push(message);
|
||||||
|
} else {
|
||||||
|
if (group.length > 0) {
|
||||||
|
result.push({
|
||||||
|
type: 'contact_group',
|
||||||
|
messages: group,
|
||||||
|
});
|
||||||
|
group = [];
|
||||||
|
}
|
||||||
|
result.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
load_and_render(messages) {
|
load_and_render(messages) {
|
||||||
let messages_by_id = this.process_messages(messages);
|
let messages_by_id = this.process_messages(messages);
|
||||||
let final_messages = 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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -160,4 +200,4 @@ class TfNewsElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-news', TfNewsElement);
|
customElements.define('tf-news', TfNewsElement);
|
||||||
|
@ -11,7 +11,10 @@ class TfProfileElement extends LitElement {
|
|||||||
id: {type: String},
|
id: {type: String},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
size: {type: Number},
|
size: {type: Number},
|
||||||
}
|
server_follows_me: {type: Boolean},
|
||||||
|
following: {type: Boolean},
|
||||||
|
blocking: {type: Boolean},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -24,16 +27,77 @@ class TfProfileElement extends LitElement {
|
|||||||
this.id = null;
|
this.id = null;
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.size = 0;
|
this.size = 0;
|
||||||
|
this.server_follows_me = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
if (this.whoami !== this._follow_whoami) {
|
||||||
|
this._follow_whoami = this.whoami;
|
||||||
|
this.following = undefined;
|
||||||
|
this.blocking = undefined;
|
||||||
|
|
||||||
|
let result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT json_extract(content, '$.following') AS following
|
||||||
|
FROM messages WHERE author = ? AND
|
||||||
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
|
json_extract(content, '$.contact') = ? AND
|
||||||
|
following IS NOT NULL
|
||||||
|
ORDER BY sequence DESC LIMIT 1
|
||||||
|
`,
|
||||||
|
[this.whoami, this.id]
|
||||||
|
);
|
||||||
|
this.following = result?.[0]?.following ?? false;
|
||||||
|
result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT json_extract(content, '$.blocking') AS blocking
|
||||||
|
FROM messages WHERE author = ? AND
|
||||||
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
|
json_extract(content, '$.contact') = ? AND
|
||||||
|
blocking IS NOT NULL
|
||||||
|
ORDER BY sequence DESC LIMIT 1
|
||||||
|
`,
|
||||||
|
[this.whoami, this.id]
|
||||||
|
);
|
||||||
|
this.blocking = result?.[0]?.blocking ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initial_load() {
|
||||||
|
this.server_follows_me = undefined;
|
||||||
|
let server_id = await tfrpc.rpc.getServerIdentity();
|
||||||
|
let followed = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT json_extract(content, '$.following') AS following
|
||||||
|
FROM messages
|
||||||
|
WHERE author = ? AND
|
||||||
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
|
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
|
||||||
|
`,
|
||||||
|
[server_id, this.whoami]
|
||||||
|
);
|
||||||
|
let is_followed = false;
|
||||||
|
for (let row of followed) {
|
||||||
|
is_followed = row.following != 0;
|
||||||
|
}
|
||||||
|
this.server_follows_me = is_followed;
|
||||||
}
|
}
|
||||||
|
|
||||||
modify(change) {
|
modify(change) {
|
||||||
tfrpc.rpc.appendMessage(this.whoami,
|
tfrpc.rpc
|
||||||
Object.assign({
|
.appendMessage(
|
||||||
type: 'contact',
|
this.whoami,
|
||||||
contact: this.id,
|
Object.assign(
|
||||||
}, change)).catch(function(error) {
|
{
|
||||||
|
type: 'contact',
|
||||||
|
contact: this.id,
|
||||||
|
},
|
||||||
|
change
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.catch(function (error) {
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
follow() {
|
follow() {
|
||||||
@ -58,6 +122,7 @@ class TfProfileElement extends LitElement {
|
|||||||
name: original.name,
|
name: original.name,
|
||||||
description: original.description,
|
description: original.description,
|
||||||
image: original.image,
|
image: original.image,
|
||||||
|
publicWebHosting: original.publicWebHosting,
|
||||||
};
|
};
|
||||||
console.log(this.editing);
|
console.log(this.editing);
|
||||||
}
|
}
|
||||||
@ -73,11 +138,14 @@ class TfProfileElement extends LitElement {
|
|||||||
message[key] = this.editing[key];
|
message[key] = this.editing[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
tfrpc.rpc
|
||||||
self.editing = null;
|
.appendMessage(this.whoami, message)
|
||||||
}).catch(function(error) {
|
.then(function () {
|
||||||
alert(error?.message);
|
self.editing = null;
|
||||||
});
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
alert(error?.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
discard_edits() {
|
discard_edits() {
|
||||||
@ -88,27 +156,55 @@ class TfProfileElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
let input = document.createElement('input');
|
let input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.onchange = function(event) {
|
input.onchange = function (event) {
|
||||||
let file = event.target.files[0];
|
let file = event.target.files[0];
|
||||||
file.arrayBuffer().then(function(buffer) {
|
file
|
||||||
let bin = Array.from(new Uint8Array(buffer));
|
.arrayBuffer()
|
||||||
return tfrpc.rpc.store_blob(bin);
|
.then(function (buffer) {
|
||||||
}).then(function(id) {
|
let bin = Array.from(new Uint8Array(buffer));
|
||||||
self.editing = Object.assign({}, self.editing, {image: id});
|
return tfrpc.rpc.store_blob(bin);
|
||||||
console.log(self.editing);
|
})
|
||||||
}).catch(function(e) {
|
.then(function (id) {
|
||||||
alert(e.message);
|
self.editing = Object.assign({}, self.editing, {image: id});
|
||||||
});
|
console.log(self.editing);
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
alert(e.message);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async server_follow_me(follow) {
|
||||||
|
try {
|
||||||
|
await tfrpc.rpc.setServerFollowingMe(this.whoami, follow);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.initial_load();
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (
|
||||||
|
this.id == this.whoami &&
|
||||||
|
this.editing &&
|
||||||
|
this.server_follows_me === undefined
|
||||||
|
) {
|
||||||
|
this.initial_load();
|
||||||
|
}
|
||||||
|
this.load();
|
||||||
let self = this;
|
let self = this;
|
||||||
let profile = this.users[this.id] || {};
|
let profile = this.users[this.id] || {};
|
||||||
tfrpc.rpc.query(
|
tfrpc.rpc
|
||||||
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
|
.query(
|
||||||
[this.id]).then(function(result) {
|
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
|
||||||
|
[this.id]
|
||||||
|
)
|
||||||
|
.then(function (result) {
|
||||||
self.size = result[0].size;
|
self.size = result[0].size;
|
||||||
});
|
});
|
||||||
let edit;
|
let edit;
|
||||||
@ -116,46 +212,82 @@ class TfProfileElement extends LitElement {
|
|||||||
let block;
|
let block;
|
||||||
if (this.id === this.whoami) {
|
if (this.id === this.whoami) {
|
||||||
if (this.editing) {
|
if (this.editing) {
|
||||||
|
let server_follow;
|
||||||
|
if (this.server_follows_me === true) {
|
||||||
|
server_follow = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => this.server_follow_me(false)}
|
||||||
|
>
|
||||||
|
Server, Stop Following Me
|
||||||
|
</button>`;
|
||||||
|
} else if (this.server_follows_me === false) {
|
||||||
|
server_follow = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@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-dark-grey" @click=${this.save_edits}>
|
||||||
<input type="button" value="Discard" @click=${this.discard_edits}></input>
|
Save Profile
|
||||||
|
</button>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
|
${server_follow}
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
edit = html`<input type="button" value="Edit Profile" @click=${this.edit}></input>`;
|
edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>
|
||||||
|
Edit Profile
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami &&
|
if (this.id !== this.whoami && this.following !== undefined) {
|
||||||
this.users[this.whoami]?.following) {
|
follow = this.following
|
||||||
follow =
|
? html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>
|
||||||
this.users[this.whoami].following[this.id] ?
|
Unfollow
|
||||||
html`<input type="button" value="Unfollow" @click=${this.unfollow}></input>` :
|
</button>`
|
||||||
html`<input type="button" value="Follow" @click=${this.follow}></input>`;
|
: html`<button class="w3-button w3-dark-grey" @click=${this.follow}>
|
||||||
|
Follow
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami &&
|
if (this.id !== this.whoami && this.blocking !== undefined) {
|
||||||
this.users[this.whoami]?.blocking) {
|
block = this.blocking
|
||||||
block =
|
? html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>
|
||||||
this.users[this.whoami].blocking[this.id] ?
|
Unblock
|
||||||
html`<input type="button" value="Unblock" @click=${this.unblock}></input>` :
|
</button>`
|
||||||
html`<input type="button" value="Block" @click=${this.block}></input>`;
|
: html`<button class="w3-button w3-dark-grey" @click=${this.block}>
|
||||||
|
Block
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
let edit_profile = this.editing ? html`
|
let edit_profile = this.editing
|
||||||
<div style="flex: 1 0 50%">
|
? html`
|
||||||
<div>
|
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
|
||||||
<label for="name">Name:</label>
|
<div class="w3-container">
|
||||||
<input type="text" id="name" value=${this.editing.name} @input=${event => this.editing = Object.assign({}, this.editing, {name: event.srcElement.value})}></input>
|
<div>
|
||||||
</div>
|
<label for="name">Name:</label>
|
||||||
<div>
|
<input class="w3-input w3-dark-grey" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
|
||||||
|
</div>
|
||||||
<div><label for="description">Description:</label></div>
|
<div><label for="description">Description:</label></div>
|
||||||
<textarea id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
|
<textarea class="w3-input w3-dark-grey" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
|
||||||
|
<div>
|
||||||
|
<label for="public_web_hosting">Public Web Hosting:</label>
|
||||||
|
<input class="w3-check w3-dark-grey" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.attach_image}>Attach Image</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="button" value="Attach Image" @click=${this.attach_image}></input>
|
</div>`
|
||||||
</div>` : null;
|
: null;
|
||||||
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
|
let image =
|
||||||
|
typeof profile.image == 'string' ? profile.image : profile.image?.link;
|
||||||
image = this.editing?.image ?? image;
|
image = this.editing?.image ?? image;
|
||||||
let description = this.editing?.description ?? profile.description;
|
let description = this.editing?.description ?? profile.description;
|
||||||
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
|
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
|
||||||
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
|
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row; gap: 1em">
|
||||||
${edit_profile}
|
${edit_profile}
|
||||||
<div style="flex: 1 0 50%">
|
<div style="flex: 1 0 50%">
|
||||||
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
|
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
|
||||||
@ -163,10 +295,10 @@ class TfProfileElement extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Following ${Object.keys(profile.following || {}).length} identities.
|
Following ${profile.following} identities.
|
||||||
Followed by ${Object.values(self.users).filter(x => (x.following || {})[self.id]).length} identities.
|
Followed by ${profile.followed} identities.
|
||||||
Blocking ${Object.keys(profile.blocking || {}).length} identities.
|
Blocking ${profile.blocking} identities.
|
||||||
Blocked by ${Object.values(self.users).filter(x => (x.blocking || {})[self.id]).length} identities.
|
Blocked by ${profile.blocked} identities.
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
${edit}
|
${edit}
|
||||||
@ -177,4 +309,4 @@ class TfProfileElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-profile', TfProfileElement);
|
customElements.define('tf-profile', TfProfileElement);
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,6 @@
|
|||||||
import {LitElement, html} from './lit-all.min.js';
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
class TfTabConnectionsElement extends LitElement {
|
class TfTabConnectionsElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
@ -9,9 +10,11 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
connections: {type: Array},
|
connections: {type: Array},
|
||||||
stored_connections: {type: Array},
|
stored_connections: {type: Array},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
let self = this;
|
let self = this;
|
||||||
@ -20,10 +23,10 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
this.connections = [];
|
this.connections = [];
|
||||||
this.stored_connections = [];
|
this.stored_connections = [];
|
||||||
this.users = {};
|
this.users = {};
|
||||||
tfrpc.rpc.getAllIdentities().then(function(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 || [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -40,13 +43,12 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
|
|
||||||
render_room_peers(connection) {
|
render_room_peers(connection) {
|
||||||
let self = this;
|
let self = this;
|
||||||
let peers = this.broadcasts.filter(x => x.tunnel?.id == connection);
|
let peers = this.broadcasts.filter((x) => x.tunnel?.id == connection);
|
||||||
if (peers.length) {
|
if (peers.length) {
|
||||||
return html`
|
let connections = this.connections.map((x) => x.id);
|
||||||
<ul>
|
return html`${peers
|
||||||
${peers.map(x => html`${self.render_room_peer(x)}`)}
|
.filter((x) => connections.indexOf(x.pubkey) == -1)
|
||||||
</ul>
|
.map((x) => html`${self.render_room_peer(x)}`)}`;
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,8 +60,13 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<li>
|
<li>
|
||||||
<input type="button" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} value="Connect"></input>
|
<button
|
||||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -67,11 +74,16 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
render_broadcast(connection) {
|
render_broadcast(connection) {
|
||||||
return html`
|
return html`
|
||||||
<li>
|
<li>
|
||||||
<input type="button" @click=${() => tfrpc.rpc.connect(connection)} value="Connect"></input>
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => tfrpc.rpc.connect(connection)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
<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)}
|
||||||
</li>
|
</li>
|
||||||
`
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async forget_stored_connection(connection) {
|
async forget_stored_connection(connection) {
|
||||||
@ -79,44 +91,85 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
this.stored_connections = (await tfrpc.rpc.getStoredConnections()) || [];
|
this.stored_connections = (await tfrpc.rpc.getStoredConnections()) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render_connection(connection) {
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<tf-user id=${connection.id} .users=${this.users}></tf-user>
|
||||||
|
${connection.tunnel !== undefined
|
||||||
|
? '🚇'
|
||||||
|
: html`(${connection.host}:${connection.port})`}
|
||||||
|
<ul>
|
||||||
|
${this.connections
|
||||||
|
.filter((x) => x.tunnel === this.connections.indexOf(connection))
|
||||||
|
.map((x) => html`<li>${this.render_connection(x)}</li>`)}
|
||||||
|
${this.render_room_peers(connection.id)}
|
||||||
|
</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<h2>New Connection</h2>
|
<div class="w3-container">
|
||||||
<div style="display: flex; flex-direction: column">
|
<h2>New Connection</h2>
|
||||||
<textarea id="code"></textarea>
|
<textarea class="w3-input w3-dark-grey" id="code"></textarea>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() =>
|
||||||
|
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
<h2>Broadcasts</h2>
|
||||||
|
<ul>
|
||||||
|
${this.broadcasts
|
||||||
|
.filter((x) => x.address)
|
||||||
|
.map((x) => self.render_broadcast(x))}
|
||||||
|
</ul>
|
||||||
|
<h2>Connections</h2>
|
||||||
|
<ul>
|
||||||
|
${this.connections
|
||||||
|
.filter((x) => x.tunnel === undefined)
|
||||||
|
.map((x) => html` <li>${this.render_connection(x)}</li> `)}
|
||||||
|
</ul>
|
||||||
|
<h2>Stored Connections (WIP)</h2>
|
||||||
|
<ul>
|
||||||
|
${this.stored_connections.map(
|
||||||
|
(x) => html`
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => self.forget_stored_connection(x)}
|
||||||
|
>
|
||||||
|
Forget
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => tfrpc.rpc.connect(x)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
${x.address}:${x.port}
|
||||||
|
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
<h2>Local Accounts</h2>
|
||||||
|
<ul>
|
||||||
|
${this.identities.map(
|
||||||
|
(x) =>
|
||||||
|
html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<input type="button" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)} value="Connect"></input>
|
|
||||||
<h2>Broadcasts</h2>
|
|
||||||
<ul>
|
|
||||||
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
|
|
||||||
</ul>
|
|
||||||
<h2>Connections</h2>
|
|
||||||
<ul>
|
|
||||||
${this.connections.map(x => html`
|
|
||||||
<li>
|
|
||||||
<input type="button" @click=${() => tfrpc.rpc.closeConnection(x)} value="Close"></input>
|
|
||||||
<tf-user id=${x} .users=${this.users}></tf-user>
|
|
||||||
${self.render_room_peers(x)}
|
|
||||||
</li>
|
|
||||||
`)}
|
|
||||||
</ul>
|
|
||||||
<h2>Stored Connections (WIP)</h2>
|
|
||||||
<ul>
|
|
||||||
${this.stored_connections.map(x => html`
|
|
||||||
<li>
|
|
||||||
<input type="button" @click=${() => self.forget_stored_connection(x)} value="Forget"></input>
|
|
||||||
<input type="button" @click=${() => tfrpc.rpc.connect(x)} value="Connect"></input>
|
|
||||||
${x.address}:${x.port} <tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
|
||||||
</li>
|
|
||||||
`)}
|
|
||||||
</ul>
|
|
||||||
<h2>Local Accounts</h2>
|
|
||||||
<ul>
|
|
||||||
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
|
||||||
</ul>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-tab-connections', TfTabConnectionsElement);
|
customElements.define('tf-tab-connections', TfTabConnectionsElement);
|
||||||
|
78
apps/ssb/tf-tab-mentions.js
Normal file
78
apps/ssb/tf-tab-mentions.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfTabMentionsElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
|
following: {type: Array},
|
||||||
|
expanded: {type: Object},
|
||||||
|
messages: {type: Array},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.users = {};
|
||||||
|
this.following = [];
|
||||||
|
this.expanded = {};
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
console.log('Loading...', this.whoami);
|
||||||
|
let results = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT messages.*
|
||||||
|
FROM messages_fts(?)
|
||||||
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
|
WHERE messages.author != ?
|
||||||
|
ORDER BY timestamp DESC limit 20
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
'"' + this.whoami.replace('"', '""') + '"',
|
||||||
|
JSON.stringify(this.following),
|
||||||
|
this.whoami,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
console.log('Done.');
|
||||||
|
this.messages = results;
|
||||||
|
}
|
||||||
|
|
||||||
|
on_expand(event) {
|
||||||
|
if (event.detail.expanded) {
|
||||||
|
let expand = {};
|
||||||
|
expand[event.detail.id] = true;
|
||||||
|
this.expanded = Object.assign({}, this.expanded, expand);
|
||||||
|
} else {
|
||||||
|
delete this.expanded[event.detail.id];
|
||||||
|
this.expanded = Object.assign({}, this.expanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let self = this;
|
||||||
|
if (!this.loading) {
|
||||||
|
this.loading = true;
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<tf-news
|
||||||
|
id="news"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.messages=${this.messages}
|
||||||
|
.users=${this.users}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@tf-expand=${this.on_expand}
|
||||||
|
></tf-news>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('tf-tab-mentions', TfTabMentionsElement);
|
211
apps/ssb/tf-tab-news-feed.js
Normal file
211
apps/ssb/tf-tab-news-feed.js
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfTabNewsFeedElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
|
hash: {type: String},
|
||||||
|
following: {type: Array},
|
||||||
|
messages: {type: Array},
|
||||||
|
drafts: {type: Object},
|
||||||
|
expanded: {type: Object},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.users = {};
|
||||||
|
this.hash = '#';
|
||||||
|
this.following = [];
|
||||||
|
this.drafts = {};
|
||||||
|
this.expanded = {};
|
||||||
|
this.start_time = new Date().valueOf() - 24 * 60 * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch_messages() {
|
||||||
|
if (this.hash.startsWith('#@')) {
|
||||||
|
let r = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
WITH mine AS (SELECT messages.*
|
||||||
|
FROM messages
|
||||||
|
WHERE messages.author = ?
|
||||||
|
ORDER BY sequence DESC
|
||||||
|
LIMIT 20)
|
||||||
|
SELECT messages.*
|
||||||
|
FROM mine
|
||||||
|
JOIN messages_refs ON mine.id = messages_refs.ref
|
||||||
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
|
UNION
|
||||||
|
SELECT * FROM mine
|
||||||
|
`,
|
||||||
|
[this.hash.substring(1)]
|
||||||
|
);
|
||||||
|
return r;
|
||||||
|
} else if (this.hash.startsWith('#%')) {
|
||||||
|
return await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT messages.*
|
||||||
|
FROM messages
|
||||||
|
WHERE id = ?1
|
||||||
|
UNION
|
||||||
|
SELECT messages.*
|
||||||
|
FROM messages JOIN messages_refs
|
||||||
|
ON messages.id = messages_refs.message
|
||||||
|
WHERE messages_refs.ref = ?1
|
||||||
|
`,
|
||||||
|
[this.hash.substring(1)]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let promises = [];
|
||||||
|
const k_following_limit = 256;
|
||||||
|
for (let i = 0; i < this.following.length; i += k_following_limit) {
|
||||||
|
promises.push(
|
||||||
|
tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
WITH news AS (SELECT messages.*
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
|
WHERE messages.timestamp > ? AND messages.timestamp < ?
|
||||||
|
ORDER BY messages.timestamp DESC)
|
||||||
|
SELECT messages.*
|
||||||
|
FROM news
|
||||||
|
JOIN messages_refs ON news.id = messages_refs.ref
|
||||||
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
|
UNION
|
||||||
|
SELECT messages.*
|
||||||
|
FROM news
|
||||||
|
JOIN messages_refs ON news.id = messages_refs.message
|
||||||
|
JOIN messages ON messages_refs.ref = messages.id
|
||||||
|
UNION
|
||||||
|
SELECT news.* FROM news
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
JSON.stringify(this.following.slice(i, i + k_following_limit)),
|
||||||
|
this.start_time,
|
||||||
|
/*
|
||||||
|
** Don't show messages more than a day into the future to prevent
|
||||||
|
** messages with far-future timestamps from staying at the top forever.
|
||||||
|
*/
|
||||||
|
new Date().valueOf() + 24 * 60 * 60 * 1000,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [].concat(...(await Promise.all(promises)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async load_more() {
|
||||||
|
let last_start_time = this.start_time;
|
||||||
|
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
|
||||||
|
let more = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
WITH news AS (SELECT messages.*
|
||||||
|
FROM messages
|
||||||
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
|
WHERE messages.timestamp > ?
|
||||||
|
AND messages.timestamp <= ?
|
||||||
|
ORDER BY messages.timestamp DESC)
|
||||||
|
SELECT messages.*
|
||||||
|
FROM news
|
||||||
|
JOIN messages_refs ON news.id = messages_refs.ref
|
||||||
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
|
UNION
|
||||||
|
SELECT messages.*
|
||||||
|
FROM news
|
||||||
|
JOIN messages_refs ON news.id = messages_refs.message
|
||||||
|
JOIN messages ON messages_refs.ref = messages.id
|
||||||
|
UNION
|
||||||
|
SELECT news.* FROM news
|
||||||
|
`,
|
||||||
|
[JSON.stringify(this.following), this.start_time, last_start_time]
|
||||||
|
);
|
||||||
|
this.messages = await this.decrypt([...more, ...this.messages]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(messages) {
|
||||||
|
console.log('decrypt');
|
||||||
|
let result = [];
|
||||||
|
for (let message of messages) {
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
content = JSON.parse(message?.content);
|
||||||
|
} catch {}
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
let decrypted;
|
||||||
|
try {
|
||||||
|
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
|
||||||
|
} catch {}
|
||||||
|
if (decrypted) {
|
||||||
|
try {
|
||||||
|
message.decrypted = JSON.parse(decrypted);
|
||||||
|
} catch {
|
||||||
|
message.decrypted = decrypted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push(message);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async add_messages(messages) {
|
||||||
|
this.messages = await this.decrypt([...messages, ...this.messages]);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (
|
||||||
|
!this.messages ||
|
||||||
|
this._messages_hash !== this.hash ||
|
||||||
|
this._messages_following !== this.following
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`loading messages for ${this.whoami} (following ${this.following.length})`
|
||||||
|
);
|
||||||
|
let self = this;
|
||||||
|
this.messages = [];
|
||||||
|
this._messages_hash = this.hash;
|
||||||
|
this._messages_following = this.following;
|
||||||
|
this.fetch_messages()
|
||||||
|
.then(this.decrypt.bind(this))
|
||||||
|
.then(function (messages) {
|
||||||
|
self.messages = messages;
|
||||||
|
console.log(`loading mesages done for ${self.whoami}`);
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
alert(JSON.stringify(error, null, 2));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let more;
|
||||||
|
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
||||||
|
more = html`
|
||||||
|
<p>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.load_more}>
|
||||||
|
Load More
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<tf-news
|
||||||
|
id="news"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.messages=${this.messages}
|
||||||
|
.following=${this.following}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-news>
|
||||||
|
${more}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
|
@ -2,114 +2,6 @@ import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
|
|||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
class TfTabNewsFeedElement extends LitElement {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
whoami: {type: String},
|
|
||||||
users: {type: Object},
|
|
||||||
hash: {type: String},
|
|
||||||
following: {type: Array},
|
|
||||||
messages: {type: Array},
|
|
||||||
drafts: {type: Object},
|
|
||||||
expanded: {type: Object},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = styles;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
let self = this;
|
|
||||||
this.whoami = null;
|
|
||||||
this.users = {};
|
|
||||||
this.hash = '#';
|
|
||||||
this.following = [];
|
|
||||||
this.drafts = {};
|
|
||||||
this.expanded = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetch_messages() {
|
|
||||||
if (this.hash.startsWith('#@')) {
|
|
||||||
let r = await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
WITH mine AS (SELECT messages.*
|
|
||||||
FROM messages
|
|
||||||
WHERE messages.author = ?
|
|
||||||
ORDER BY sequence DESC
|
|
||||||
LIMIT 20)
|
|
||||||
SELECT messages.*
|
|
||||||
FROM mine
|
|
||||||
JOIN messages_refs ON mine.id = messages_refs.ref
|
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
|
||||||
UNION
|
|
||||||
SELECT * FROM mine
|
|
||||||
`,
|
|
||||||
[
|
|
||||||
this.hash.substring(1),
|
|
||||||
]);
|
|
||||||
return r;
|
|
||||||
} else if (this.hash.startsWith('#%')) {
|
|
||||||
return await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
SELECT messages.*
|
|
||||||
FROM messages
|
|
||||||
WHERE id = ?1
|
|
||||||
UNION
|
|
||||||
SELECT messages.*
|
|
||||||
FROM messages JOIN messages_refs
|
|
||||||
ON messages.id = messages_refs.message
|
|
||||||
WHERE messages_refs.ref = ?1
|
|
||||||
`,
|
|
||||||
[
|
|
||||||
this.hash.substring(1),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
return await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
WITH news AS (SELECT messages.*
|
|
||||||
FROM messages
|
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
|
||||||
WHERE messages.timestamp > ?
|
|
||||||
ORDER BY messages.timestamp DESC)
|
|
||||||
SELECT messages.*
|
|
||||||
FROM news
|
|
||||||
JOIN messages_refs ON news.id = messages_refs.ref
|
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
|
||||||
UNION
|
|
||||||
SELECT messages.*
|
|
||||||
FROM news
|
|
||||||
JOIN messages_refs ON news.id = messages_refs.message
|
|
||||||
JOIN messages ON messages_refs.ref = messages.id
|
|
||||||
UNION
|
|
||||||
SELECT news.* FROM news
|
|
||||||
`,
|
|
||||||
[
|
|
||||||
JSON.stringify(this.following),
|
|
||||||
new Date().valueOf() - 24 * 60 * 60 * 1000,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.messages ||
|
|
||||||
this._messages_hash !== this.hash ||
|
|
||||||
this._messages_following !== this.following) {
|
|
||||||
console.log(`loading messages for ${this.whoami}`);
|
|
||||||
let self = this;
|
|
||||||
this.messages = [];
|
|
||||||
this._messages_hash = this.hash;
|
|
||||||
this._messages_following = this.following;
|
|
||||||
this.fetch_messages().then(function(messages) {
|
|
||||||
self.messages = messages;
|
|
||||||
console.log(`loading mesages done for ${self.whoami}`);
|
|
||||||
}).catch(function(error) {
|
|
||||||
alert(JSON.stringify(error, null, 2));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return html`<tf-news id="news" whoami=${this.whoami} .users=${this.users} .messages=${this.messages} .following=${this.following} .drafts=${this.drafts} .expanded=${this.expanded}></tf-news>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TfTabNewsElement extends LitElement {
|
class TfTabNewsElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
@ -120,7 +12,7 @@ class TfTabNewsElement extends LitElement {
|
|||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -136,17 +28,29 @@ class TfTabNewsElement extends LitElement {
|
|||||||
this.cache = {};
|
this.cache = {};
|
||||||
this.drafts = {};
|
this.drafts = {};
|
||||||
this.expanded = {};
|
this.expanded = {};
|
||||||
tfrpc.rpc.localStorageGet('drafts').then(function(d) {
|
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
|
||||||
self.drafts = JSON.parse(d || '{}');
|
self.drafts = JSON.parse(d || '{}');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
document.body.addEventListener('keypress', this.on_keypress.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
document.body.removeEventListener('keypress', this.on_keypress.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
show_more() {
|
show_more() {
|
||||||
let unread = this.unread;
|
let unread = this.unread;
|
||||||
let news = this.renderRoot?.getElementById('news');
|
let news = this.shadowRoot?.getElementById('news');
|
||||||
if (news) {
|
if (news) {
|
||||||
console.log('injecting messages', news.messages);
|
console.log('injecting messages', news.messages);
|
||||||
news.messages = Object.values(Object.fromEntries([...this.unread, ...news.messages].map(x => [x.id, x])));
|
news.add_messages(
|
||||||
|
Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x])))
|
||||||
|
);
|
||||||
this.dispatchEvent(new CustomEvent('refresh'));
|
this.dispatchEvent(new CustomEvent('refresh'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,11 +64,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) {
|
||||||
@ -193,19 +102,55 @@ class TfTabNewsElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
on_keypress(event) {
|
||||||
|
if (event.target === document.body && event.key == '.') {
|
||||||
|
this.show_more();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let profile = this.hash.startsWith('#@') ?
|
let profile = this.hash.startsWith('#@')
|
||||||
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
|
? html`<tf-profile
|
||||||
|
id=${this.hash.substring(1)}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
></tf-profile>`
|
||||||
|
: undefined;
|
||||||
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-dark-grey"
|
||||||
<div><tf-compose whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
|
@click=${this.show_more}
|
||||||
|
>
|
||||||
|
${this.new_messages_text()}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<tf-compose
|
||||||
|
id="tf-compose"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
@tf-draft=${this.draft}
|
||||||
|
></tf-compose>
|
||||||
|
</div>
|
||||||
${profile}
|
${profile}
|
||||||
<tf-tab-news-feed id="news" whoami=${this.whoami} .users=${this.users} .following=${this.following} hash=${this.hash} .drafts=${this.drafts} .expanded=${this.expanded} @tf-draft=${this.draft} @tf-expand=${this.on_expand}></tf-tab-news-feed>
|
<tf-tab-news-feed
|
||||||
|
id="news"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.following=${this.following}
|
||||||
|
hash=${this.hash}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@tf-draft=${this.draft}
|
||||||
|
@tf-expand=${this.on_expand}
|
||||||
|
></tf-tab-news-feed>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
|
customElements.define('tf-tab-news', TfTabNewsElement);
|
||||||
customElements.define('tf-tab-news', TfTabNewsElement);
|
|
||||||
|
136
apps/ssb/tf-tab-query.js
Normal file
136
apps/ssb/tf-tab-query.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfTabQueryElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
whoami: {type: String},
|
||||||
|
users: {type: Object},
|
||||||
|
following: {type: Array},
|
||||||
|
query: {type: String},
|
||||||
|
expanded: {type: Object},
|
||||||
|
results: {type: Array},
|
||||||
|
error: {type: Object},
|
||||||
|
duration: {type: Number},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let self = this;
|
||||||
|
this.whoami = null;
|
||||||
|
this.users = {};
|
||||||
|
this.following = [];
|
||||||
|
this.expanded = {};
|
||||||
|
this.duration = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query) {
|
||||||
|
console.log('Searching...', this.whoami, query);
|
||||||
|
this.results = [];
|
||||||
|
this.error = undefined;
|
||||||
|
this.duration = undefined;
|
||||||
|
let search = this.renderRoot.getElementById('search');
|
||||||
|
if (search) {
|
||||||
|
search.value = query;
|
||||||
|
search.focus();
|
||||||
|
}
|
||||||
|
await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query));
|
||||||
|
let start_time = new Date();
|
||||||
|
try {
|
||||||
|
this.results = await tfrpc.rpc.query(query, []);
|
||||||
|
} catch (error) {
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
let end_time = new Date();
|
||||||
|
this.duration = (end_time - start_time).valueOf();
|
||||||
|
console.log('Done.');
|
||||||
|
search = this.renderRoot.getElementById('search');
|
||||||
|
if (search) {
|
||||||
|
search.value = query;
|
||||||
|
search.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
search_keydown(event) {
|
||||||
|
if (event.keyCode == 13 && event.ctrlKey) {
|
||||||
|
this.query = this.renderRoot.getElementById('search').value;
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on_expand(event) {
|
||||||
|
if (event.detail.expanded) {
|
||||||
|
let expand = {};
|
||||||
|
expand[event.detail.id] = true;
|
||||||
|
this.expanded = Object.assign({}, this.expanded, expand);
|
||||||
|
} else {
|
||||||
|
delete this.expanded[event.detail.id];
|
||||||
|
this.expanded = Object.assign({}, this.expanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_results() {
|
||||||
|
if (!this.results?.length) {
|
||||||
|
return html`<div>No results.</div>`;
|
||||||
|
} else {
|
||||||
|
let keys = Object.keys(this.results[0]).sort();
|
||||||
|
return html`<table style="width: 100%; max-width: 100%">
|
||||||
|
<tr>
|
||||||
|
${keys.map((key) => html`<th>${key}</th>`)}
|
||||||
|
</tr>
|
||||||
|
${this.results.map(
|
||||||
|
(row) =>
|
||||||
|
html`<tr>
|
||||||
|
${keys.map((key) => html`<td>${row[key]}</td>`)}
|
||||||
|
</tr>`
|
||||||
|
)}
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_error() {
|
||||||
|
if (this.error) {
|
||||||
|
return html`<h2 style="color: red">${this.error.message}</h2>
|
||||||
|
<pre style="color: red">${this.error.stack}</pre>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.query !== this.last_query) {
|
||||||
|
this.last_query = this.query;
|
||||||
|
this.search(this.query);
|
||||||
|
}
|
||||||
|
let self = this;
|
||||||
|
return html`
|
||||||
|
<div style="display: flex; flex-direction: row; gap: 4px">
|
||||||
|
<textarea
|
||||||
|
id="search"
|
||||||
|
rows="8"
|
||||||
|
class="w3-input w3-dark-grey"
|
||||||
|
style="flex: 1; resize: vertical"
|
||||||
|
@keydown=${this.search_keydown}
|
||||||
|
>
|
||||||
|
${this.query}</textarea
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${(event) =>
|
||||||
|
self.search(self.renderRoot.getElementById('search').value)}
|
||||||
|
>
|
||||||
|
Execute
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div ?hidden=${this.duration === undefined}>
|
||||||
|
Took ${this.duration / 1000.0} seconds.
|
||||||
|
</div>
|
||||||
|
<div ?hidden=${this.duration !== undefined}>Executing...</div>
|
||||||
|
${this.render_error()} ${this.render_results()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-tab-query', TfTabQueryElement);
|
@ -9,7 +9,8 @@ class TfTabSearchElement extends LitElement {
|
|||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
query: {type: String},
|
query: {type: String},
|
||||||
}
|
expanded: {type: Object},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -20,28 +21,31 @@ class TfTabSearchElement extends LitElement {
|
|||||||
this.whoami = null;
|
this.whoami = null;
|
||||||
this.users = {};
|
this.users = {};
|
||||||
this.following = [];
|
this.following = [];
|
||||||
|
this.expanded = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query) {
|
async search(query) {
|
||||||
console.log('Searching...', this.whoami, query);
|
console.log('Searching...', this.whoami, query);
|
||||||
let search = this.renderRoot.getElementById('search');
|
let search = this.renderRoot.getElementById('search');
|
||||||
if (search ) {
|
if (search) {
|
||||||
search.value = query;
|
search.value = query;
|
||||||
search.focus();
|
search.focus();
|
||||||
search.select();
|
search.select();
|
||||||
}
|
}
|
||||||
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
|
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
|
||||||
let results = await tfrpc.rpc.query(`
|
let results = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT messages.*
|
SELECT messages.*
|
||||||
FROM messages_fts(?)
|
FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
ORDER BY timestamp DESC limit 100
|
ORDER BY timestamp DESC limit 100
|
||||||
`,
|
`,
|
||||||
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]);
|
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
|
||||||
|
);
|
||||||
console.log('Done.');
|
console.log('Done.');
|
||||||
search = this.renderRoot.getElementById('search');
|
search = this.renderRoot.getElementById('search');
|
||||||
if (search ) {
|
if (search) {
|
||||||
search.value = query;
|
search.value = query;
|
||||||
search.focus();
|
search.focus();
|
||||||
search.select();
|
search.select();
|
||||||
@ -55,19 +59,31 @@ class TfTabSearchElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
on_expand(event) {
|
||||||
|
if (event.detail.expanded) {
|
||||||
|
let expand = {};
|
||||||
|
expand[event.detail.id] = true;
|
||||||
|
this.expanded = Object.assign({}, this.expanded, expand);
|
||||||
|
} else {
|
||||||
|
delete this.expanded[event.detail.id];
|
||||||
|
this.expanded = Object.assign({}, this.expanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.query !== this.last_query) {
|
if (this.query !== this.last_query) {
|
||||||
|
this.last_query = this.query;
|
||||||
this.search(this.query);
|
this.search(this.query);
|
||||||
}
|
}
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row; gap: 4px">
|
||||||
<input type="text" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
<input type="text" class="w3-input w3-dark-grey" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
||||||
<input type="button" value="Search" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}></input>
|
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
|
||||||
</div>
|
</div>
|
||||||
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users}></tf-news>
|
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-tab-search', TfTabSearchElement);
|
customElements.define('tf-tab-search', TfTabSearchElement);
|
||||||
|
28
apps/ssb/tf-tag.js
Normal file
28
apps/ssb/tf-tag.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfTagElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
tag: {type: String},
|
||||||
|
count: {type: Number},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let number = this.count ? html` (${this.count})` : undefined;
|
||||||
|
return html`<a
|
||||||
|
href="#q=${this.tag}"
|
||||||
|
style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px"
|
||||||
|
>${this.tag}${number}</a
|
||||||
|
>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-tag', TfTagElement);
|
@ -7,7 +7,7 @@ class TfUserElement extends LitElement {
|
|||||||
return {
|
return {
|
||||||
id: {type: String},
|
id: {type: String},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
@ -19,21 +19,29 @@ class TfUserElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
let name = this.users?.[this.id]?.name;
|
||||||
|
name =
|
||||||
|
name !== undefined
|
||||||
|
? html`<a target="_top" href=${'#' + this.id}>${name}</a>`
|
||||||
|
: html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
|
||||||
|
|
||||||
if (this.users[this.id]) {
|
if (this.users[this.id]) {
|
||||||
let image = this.users[this.id].image;
|
let image = this.users[this.id].image;
|
||||||
image = typeof(image) == 'string' ? image : image?.link;
|
image = typeof image == 'string' ? image : image?.link;
|
||||||
return html`
|
return html` <div style="display: inline-block; font-weight: bold">
|
||||||
<div style="display: inline-block; font-weight: bold">
|
<img
|
||||||
<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" ?hidden=${image === undefined} src="${image ? '/' + image + '/view' : undefined}">
|
style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%"
|
||||||
<a target="_top" href=${'#' + this.id}>${this.users[this.id].name ?? this.id}</a>
|
?hidden=${image === undefined}
|
||||||
</div>`;
|
src="${image ? '/' + image + '/view' : undefined}"
|
||||||
|
/>
|
||||||
|
${name}
|
||||||
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html` <div style="display: inline-block; font-weight: bold">
|
||||||
<div style="display: inline-block; font-weight: bold; word-wrap: anywhere">
|
${name}
|
||||||
<a target="_top" href=${'#' + this.id}>${this.id}</a>
|
</div>`;
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-user', TfUserElement);
|
customElements.define('tf-user', TfUserElement);
|
||||||
|
@ -1,26 +1,93 @@
|
|||||||
import * as linkify from './commonmark-linkify.js';
|
import * as linkify from './commonmark-linkify.js';
|
||||||
import * as hashtagify from './commonmark-hashtag.js';
|
import * as hashtagify from './commonmark-hashtag.js';
|
||||||
|
|
||||||
|
function image(node, entering) {
|
||||||
|
if (
|
||||||
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('video:')
|
||||||
|
) {
|
||||||
|
if (entering) {
|
||||||
|
this.lit(
|
||||||
|
'<video style="max-width: 100%; max-height: 480px" title="' +
|
||||||
|
this.esc(node.firstChild?.literal) +
|
||||||
|
'" controls>'
|
||||||
|
);
|
||||||
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
|
this.disableTags += 1;
|
||||||
|
} else {
|
||||||
|
this.disableTags -= 1;
|
||||||
|
this.lit('</video>');
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('audio:')
|
||||||
|
) {
|
||||||
|
if (entering) {
|
||||||
|
this.lit(
|
||||||
|
'<audio style="height: 32px; max-width: 100%" title="' +
|
||||||
|
this.esc(node.firstChild?.literal) +
|
||||||
|
'" controls>'
|
||||||
|
);
|
||||||
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
|
this.disableTags += 1;
|
||||||
|
} else {
|
||||||
|
this.disableTags -= 1;
|
||||||
|
this.lit('</audio>');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (entering) {
|
||||||
|
if (this.disableTags === 0) {
|
||||||
|
this.lit(
|
||||||
|
'<div class="img_caption">' +
|
||||||
|
this.esc(node.firstChild?.literal || node.destination) +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
|
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
||||||
|
this.lit('<img src="" alt="');
|
||||||
|
} else {
|
||||||
|
this.lit('<img src="' + this.esc(node.destination) + '" alt="');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.disableTags += 1;
|
||||||
|
} else {
|
||||||
|
this.disableTags -= 1;
|
||||||
|
if (this.disableTags === 0) {
|
||||||
|
if (node.title) {
|
||||||
|
this.lit('" title="' + this.esc(node.title));
|
||||||
|
}
|
||||||
|
this.lit('" />');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function markdown(md) {
|
export function markdown(md) {
|
||||||
var reader = new commonmark.Parser({safe: true});
|
let reader = new commonmark.Parser({safe: true});
|
||||||
var writer = new commonmark.HtmlRenderer();
|
let writer = new commonmark.HtmlRenderer();
|
||||||
var parsed = reader.parse(md || '');
|
writer.image = image;
|
||||||
parsed = linkify.transform(parsed);
|
let parsed = reader.parse(md || '');
|
||||||
parsed = hashtagify.transform(parsed);
|
parsed = hashtagify.transform(parsed);
|
||||||
var walker = parsed.walker();
|
parsed = linkify.transform(parsed);
|
||||||
var event, node;
|
let walker = parsed.walker();
|
||||||
|
let event, node;
|
||||||
while ((event = walker.next())) {
|
while ((event = walker.next())) {
|
||||||
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') {
|
||||||
@ -45,4 +112,4 @@ export function human_readable_size(bytes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return `${Math.round(v * 10) / 10} ${u}`;
|
return `${Math.round(v * 10) / 10} ${u}`;
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,32 @@
|
|||||||
.tribute-container {
|
.tribute-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: auto;
|
height: auto;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: block;
|
display: block;
|
||||||
z-index: 999999;
|
z-index: 999999;
|
||||||
}
|
}
|
||||||
.tribute-container ul {
|
.tribute-container ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background: #efefef;
|
background: #efefef;
|
||||||
}
|
}
|
||||||
.tribute-container li {
|
.tribute-container li {
|
||||||
padding: 5px 5px;
|
padding: 5px 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.tribute-container li.highlight {
|
.tribute-container li.highlight {
|
||||||
background: #ddd;
|
background: #ddd;
|
||||||
}
|
}
|
||||||
.tribute-container li span {
|
.tribute-container li span {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.tribute-container li.no-match {
|
.tribute-container li.no-match {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
.tribute-container .menu-highlighted {
|
.tribute-container .menu-highlighted {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app"
|
"type": "tildefriends-app",
|
||||||
}
|
"emoji": "☑️"
|
||||||
|
}
|
||||||
|
@ -27,7 +27,8 @@ async function todo_add(list) {
|
|||||||
let set = new Set(names);
|
let set = new Set(names);
|
||||||
set.add(list);
|
set.add(list);
|
||||||
names = JSON.stringify([...set].sort());
|
names = JSON.stringify([...set].sort());
|
||||||
exchanged = original === names || await g_db.exchange('files', original, names);
|
exchanged =
|
||||||
|
original === names || (await g_db.exchange('files', original, names));
|
||||||
}
|
}
|
||||||
return exchanged;
|
return exchanged;
|
||||||
}
|
}
|
||||||
@ -42,7 +43,8 @@ async function todo_remove(list) {
|
|||||||
let set = new Set(names);
|
let set = new Set(names);
|
||||||
set.delete(list);
|
set.delete(list);
|
||||||
names = JSON.stringify([...set].sort());
|
names = JSON.stringify([...set].sort());
|
||||||
exchanged = original === names || await g_db.exchange('files', original, names);
|
exchanged =
|
||||||
|
original === names || (await g_db.exchange('files', original, names));
|
||||||
}
|
}
|
||||||
await g_db.remove('list:' + list);
|
await g_db.remove('list:' + list);
|
||||||
return exchanged;
|
return exchanged;
|
||||||
@ -79,4 +81,4 @@ async function main() {
|
|||||||
await app.setDocument(utf8Decode(getFile('index.html')));
|
await app.setDocument(utf8Decode(getFile('index.html')));
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>TODO</title>
|
<title>TODO</title>
|
||||||
@ -8,4 +8,4 @@
|
|||||||
<tf-todos></tf-todos>
|
<tf-todos></tf-todos>
|
||||||
</body>
|
</body>
|
||||||
<script src="script.js" type="module"></script>
|
<script src="script.js" type="module"></script>
|
||||||
</html>
|
</html>
|
||||||
|
@ -4,7 +4,7 @@ import * as tfrpc from '/static/tfrpc.js';
|
|||||||
class TodosElement extends LitElement {
|
class TodosElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
lists: {type: Array}
|
lists: {type: Array},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12,11 +12,14 @@ class TodosElement extends LitElement {
|
|||||||
super();
|
super();
|
||||||
this.lists = [];
|
this.lists = [];
|
||||||
let self = this;
|
let self = this;
|
||||||
tfrpc.rpc.todo_get_all().then(function(lists) {
|
tfrpc.rpc
|
||||||
self.lists = lists;
|
.todo_get_all()
|
||||||
}).catch(function(error) {
|
.then(function (lists) {
|
||||||
console.log(error);
|
self.lists = lists;
|
||||||
});
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async new_list() {
|
async new_list() {
|
||||||
@ -32,9 +35,15 @@ class TodosElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<div style="display: flex">
|
<div style="display: flex">
|
||||||
${this.lists.map(x => html`
|
${this.lists.map(
|
||||||
<tf-todo-list name=${x.name} .items=${x.items} @change=${this.refresh}></tf-todo-list>
|
(x) => html`
|
||||||
`)}
|
<tf-todo-list
|
||||||
|
name=${x.name}
|
||||||
|
.items=${x.items}
|
||||||
|
@change=${this.refresh}
|
||||||
|
></tf-todo-list>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<input type="button" @click=${this.new_list} value="+ List"></input>
|
<input type="button" @click=${this.new_list} value="+ List"></input>
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -59,16 +68,22 @@ class TodoListElement extends LitElement {
|
|||||||
save() {
|
save() {
|
||||||
let self = this;
|
let self = this;
|
||||||
console.log('saving', self.name, self.items);
|
console.log('saving', self.name, self.items);
|
||||||
tfrpc.rpc.todo_set(self.name, self.items).then(function() {
|
tfrpc.rpc
|
||||||
console.log('saved', self.name, self.items);
|
.todo_set(self.name, self.items)
|
||||||
}).catch(function(error) {
|
.then(function () {
|
||||||
console.log(error);
|
console.log('saved', self.name, self.items);
|
||||||
});
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_item(item) {
|
remove_item(item) {
|
||||||
let index = this.items.indexOf(item);
|
let index = this.items.indexOf(item);
|
||||||
this.items = [].concat(this.items.slice(0, index), this.items.slice(index + 1));
|
this.items = [].concat(
|
||||||
|
this.items.slice(0, index),
|
||||||
|
this.items.slice(index + 1)
|
||||||
|
);
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,21 +121,20 @@ class TodoListElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
if (index === this.editing) {
|
if (index === this.editing) {
|
||||||
return html`
|
return html`
|
||||||
<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input>
|
<div><input type="checkbox" ?checked=${item.x} @change=${(x) => self.handle_check(x, item)}></input>
|
||||||
<input
|
<input
|
||||||
id="edit"
|
id="edit"
|
||||||
type="text"
|
type="text"
|
||||||
value=${item.text}
|
value=${item.text}
|
||||||
@change=${event => self.input_change(event, item)}
|
@change=${(event) => self.input_change(event, item)}
|
||||||
@keydown=${event => self.input_keydown(event, item)}
|
@keydown=${(event) => self.input_keydown(event, item)}
|
||||||
@blur=${x => self.input_blur(item)}></input>
|
@blur=${(x) => self.input_blur(item)}></input>
|
||||||
<span @click=${x => self.remove_item(item)}>x</span></div>
|
<span @click=${(x) => self.remove_item(item)} style="cursor: pointer">❎</span></div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input>
|
<div><input type="checkbox" ?checked=${item.x} @change=${(x) => self.handle_check(x, item)}></input>
|
||||||
<span @click=${x => self.editing = index}>${item.text}</span>
|
<span @click=${(x) => (self.editing = index)}>${item.text || '(empty)'}</span>
|
||||||
<span @click=${x => self.remove_item(item)} style="cursor: pointer">❎</span></div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,14 +154,17 @@ class TodoListElement extends LitElement {
|
|||||||
|
|
||||||
rename(new_name) {
|
rename(new_name) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return tfrpc.rpc.todo_rename(this.name, new_name).then(function() {
|
return tfrpc.rpc
|
||||||
self.dispatchEvent(new Event('change'));
|
.todo_rename(this.name, new_name)
|
||||||
self.editing_name = false;
|
.then(function () {
|
||||||
}).catch(function(error) {
|
self.dispatchEvent(new Event('change'));
|
||||||
console.log(error);
|
self.editing_name = false;
|
||||||
alert(error.message);
|
})
|
||||||
self.editing_name = false;
|
.catch(function (error) {
|
||||||
});
|
console.log(error);
|
||||||
|
alert(error.message);
|
||||||
|
self.editing_name = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
name_blur(new_name) {
|
name_blur(new_name) {
|
||||||
@ -164,18 +181,25 @@ class TodoListElement extends LitElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
let name = this.editing_name ?
|
let name = this.editing_name
|
||||||
html`<input
|
? html`<input
|
||||||
type="text"
|
type="text"
|
||||||
id="edit"
|
id="edit"
|
||||||
@keydown=${event => self.name_keydown(event)}
|
@keydown=${(event) => self.name_keydown(event)}
|
||||||
@blur=${event => self.name_blur(event.srcElement.value)}
|
@blur=${(event) => self.name_blur(event.srcElement.value)}
|
||||||
value=${this.name}></input>` :
|
value=${this.name}></input>`
|
||||||
html`<h2 @click=${x => this.editing_name = true}>${this.name}</h2>`;
|
: html`<h2 @click=${(x) => (this.editing_name = true)}>${this.name}</h2>`;
|
||||||
return html`
|
return html`
|
||||||
<div style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444">
|
<div
|
||||||
|
style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444"
|
||||||
|
>
|
||||||
${name}
|
${name}
|
||||||
${(this.items || []).map(x => self.render_item(x))}
|
${(this.items || [])
|
||||||
|
.filter((item) => !item.x)
|
||||||
|
.map((x) => self.render_item(x))}
|
||||||
|
${(this.items || [])
|
||||||
|
.filter((item) => item.x)
|
||||||
|
.map((x) => self.render_item(x))}
|
||||||
<button @click=${self.add_item}>+ Item</button>
|
<button @click=${self.add_item}>+ Item</button>
|
||||||
<button @click=${self.remove_list}>- List</button>
|
<button @click=${self.remove_list}>- List</button>
|
||||||
</div>
|
</div>
|
||||||
@ -184,4 +208,4 @@ class TodoListElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-todo-list', TodoListElement);
|
customElements.define('tf-todo-list', TodoListElement);
|
||||||
customElements.define('tf-todos', TodosElement);
|
customElements.define('tf-todos', TodosElement);
|
||||||
|
5
apps/welcome.json
Normal file
5
apps/welcome.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "👋",
|
||||||
|
"previous": "&zFISmRDAv+SXFonfZ9/sHNhrmMe+poTU22gwZzuSkT4=.sha256"
|
||||||
|
}
|
5
apps/welcome/app.js
Normal file
5
apps/welcome/app.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
async function main() {
|
||||||
|
await app.setDocument(utf8Decode(getFile('index.html')));
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
6
apps/welcome/brands.min.css
vendored
Normal file
6
apps/welcome/brands.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user