Compare commits
510 Commits
Author | SHA1 | Date | |
---|---|---|---|
e5f58c2898 | |||
f83863ef01 | |||
837f069cf5 | |||
9f057dc29a | |||
c4904f176c | |||
d3a5aba703 | |||
9e283e427c | |||
133ba31d66 | |||
241a65a92a | |||
0b54795bab | |||
6208193de5 | |||
c53321532f | |||
34f25e3e06 | |||
c46244366e | |||
6518af04fc | |||
bf137ff1f7 | |||
1877955b62 | |||
50d0875de2 | |||
bf151e6b7d | |||
82893402d0 | |||
8049102787 | |||
f42cc3d9fd | |||
5f9a5208db | |||
6df506d238 | |||
2bd3354256 | |||
b55aaa1d18 | |||
34e19505bd | |||
6e06ec0904 | |||
a5814074fe | |||
d7479df5a2 | |||
34508aa0ae | |||
ae096b2c9c | |||
95d036e34a | |||
4af5e8ec42 | |||
2a5f71bd5d | |||
97fb63dda1 | |||
87d42e3b3b | |||
0394129a4c | |||
3c499c834b | |||
17d6cc7d46 | |||
646bd7dc38 | |||
56e483782d | |||
e1b9066b26 | |||
7114ce2516 | |||
9240c6570a | |||
f80a44ccd7 | |||
e6f5eb244e | |||
ab62e83110 | |||
aeefb9e536 | |||
ee0efa536a | |||
2523130fdc | |||
c024777184 | |||
5951d7cd2d | |||
011670c70b | |||
6cebd6c769 | |||
546ae5cbf1 | |||
f543cc642e | |||
8ac3c5ea22 | |||
63918f0680 | |||
bfb3d8b8a2 | |||
e38ff99607 | |||
b0e3d922c8 | |||
a15bb8e994 | |||
6f487100cd | |||
0693a2315f | |||
f360e886ff | |||
6ea08cc5dc | |||
347c706d6f | |||
5f5e6616c7 | |||
657bcadc7e | |||
107666cc60 | |||
b37669184a | |||
163a01f224 | |||
3d58094199 | |||
463951a4f1 | |||
34804d5162 | |||
3895c33915 | |||
17f4eb1a56 | |||
0abdffdea6 | |||
d32999f178 | |||
f621feb843 | |||
8d277f029d | |||
1788a02338 | |||
ba0800d16c | |||
4008c7d8f6 | |||
610a2e2afc | |||
6f3715d1eb | |||
b78ecaa814 | |||
e6f5399d53 | |||
0e5806cadd | |||
68c9d4afa7 | |||
f0ea38fe49 | |||
b0332f923e | |||
8a76c25394 | |||
fd96126e3e | |||
ff3fbedc18 | |||
8791419f8e | |||
5447b247a0 | |||
aabbb10564 | |||
3ccd6c9a3e | |||
c290240de7 | |||
8e799b174b | |||
a9c3a93989 | |||
3ef8698f42 | |||
fa4e843c30 | |||
9a4d11f4d9 | |||
eed2b8d618 | |||
13f02c2aca | |||
d50f8fbc8b | |||
155238a516 | |||
427fcdbdca | |||
ca05d402a7 | |||
c5a80b68ca | |||
c1fb15b135 | |||
4b2c131836 | |||
9ca1e69b3c | |||
082d041d44 | |||
221f276c4b | |||
24cec21465 | |||
9f71ec6194 | |||
bb36afc390 | |||
b53bf0ff64 | |||
3ebc6f2436 | |||
2eef6778a6 | |||
81fabec810 | |||
dc6e7924b5 | |||
48dec5a2c8 | |||
9b500e1da9 | |||
a038820112 | |||
70a15973b6 | |||
09b6a00731 | |||
883c3cf0e9 | |||
a46bb8183c | |||
d5d5a7b012 | |||
a120efdc91 | |||
d48f4b06eb | |||
f078912736 | |||
63b0f0dedd | |||
84c22dbf5f | |||
b8cd1232be | |||
a518ab07f4 | |||
9e5a1ee975 | |||
95bf3f0316 | |||
d69dd513bc | |||
525cdf571a | |||
9cfe0a8804 | |||
50b54599ef | |||
ed6bef6d24 | |||
71268636df | |||
568729ecd6 | |||
9139725be6 | |||
969a8da6bf | |||
2338b26329 | |||
d4df206740 | |||
8a93cdd33c | |||
92b31de4a9 | |||
5452f3f623 | |||
256614dbaf | |||
049449b213 | |||
85b46336b1 | |||
590afa7b01 | |||
574292b798 | |||
21cf503a59 | |||
3630cdbfe0 | |||
0f3be229e6 | |||
8e5a024d3d | |||
410bb7c09d | |||
9de8b0f449 | |||
d47c3a1222 | |||
df99b3aa90 | |||
0090850e10 | |||
9efd64bd18 | |||
b16c37e48b | |||
3ee2c00726 | |||
d5a7e19f1a | |||
9b52415b35 | |||
dbe24494d9 | |||
3eab5a5f70 | |||
548febfb22 | |||
b40f72443a | |||
2c03496373 | |||
b6a937c954 | |||
63776d40bd | |||
cb3c7afade | |||
991022adfc | |||
2bc71a18a6 | |||
57ca864fbb | |||
a09edfb612 | |||
7997a739ab | |||
248b258413 | |||
0423ed7fb4 | |||
c29378c2f8 | |||
163fbd85e7 | |||
58bb86ebe1 | |||
c5140ee8e8 | |||
6270fd8118 | |||
3fff706848 | |||
c259defab5 | |||
e5fee5c306 | |||
9d35b4bdfb | |||
9497d7cf64 | |||
c7d3e602cb | |||
0076eb4ed4 | |||
6070bde413 | |||
c7a6d426f0 | |||
f66cf0f802 | |||
e4b6c81024 | |||
44d784cd04 | |||
0394201113 | |||
e270c16516 | |||
4c10538632 | |||
71329c5532 | |||
feb4bf9e87 | |||
5d5567e94c | |||
684e6fb9cb | |||
ee21fa6d03 | |||
7a2974e54f | |||
f4dfc1dd98 | |||
2eebfa9a7a | |||
10097ffeb8 | |||
cbe1f54a2a | |||
4d8f081a59 | |||
29e79c9484 | |||
ba35869b0a | |||
580688381e | |||
e63d69a440 | |||
be64fe04fb | |||
801ab20723 | |||
d974a5e044 | |||
1be94ae0be | |||
b883e6a485 | |||
a0210379ae | |||
e56dc207d1 | |||
523c9c9ad2 | |||
74bb2151c1 | |||
f79d7b35a4 | |||
3b36496dac | |||
4ebd6c24a9 | |||
05451d98b3 | |||
22a4bce3c8 | |||
76d499f00b | |||
f0772f9b99 | |||
46e711f0a5 | |||
abffac3f82 | |||
27b275548e | |||
93ce253d1e | |||
a5af312b39 | |||
4b5e8e8a43 | |||
443dd4d168 | |||
907479df84 | |||
9887a78e98 | |||
f669371349 | |||
24c720c79a | |||
4485234980 | |||
b6871c0b1f | |||
47838d5e48 | |||
69fccd56d3 | |||
ca00c4fb5d | |||
427ca3f265 | |||
c1a80e50e7 | |||
52962f3a5e | |||
b3f095b61f | |||
a5004c8ba9 | |||
7d9b1b508b | |||
5e265dfc83 | |||
3a43d6f8ac | |||
11a6649847 | |||
7caf4a0173 | |||
385524352c | |||
5ca5323782 | |||
ba6da856bb | |||
c0e72246cc | |||
c7ab5447ea | |||
5fdd461159 | |||
421955f2a0 | |||
a28f6985ed | |||
8244dddab7 | |||
a5ca436eaa | |||
d7fc1c2c88 | |||
382627ef8d | |||
17667b4cf8 | |||
5231ec22e7 | |||
929ae1b709 | |||
f01f7a5ab9 | |||
a2dce833f8 | |||
de6c7a4fd4 | |||
4edee0f7f6 | |||
988a807fa4 | |||
5258e4253d | |||
09ba86dec5 | |||
78d8a1aa23 | |||
22def15209 | |||
4cbda7a849 | |||
be85a620ef | |||
0b07b678b4 | |||
4733ce9287 | |||
48d6bf4c15 | |||
8c759bcbac | |||
b5ed7014f6 | |||
6cd9dea186 | |||
202b416acf | |||
93d46f5610 | |||
c5ddf3ac99 | |||
a9cb913a47 | |||
b7b5d4f1a5 | |||
a947396bad | |||
d528bc808e | |||
c6fd05c2cf | |||
d6bb9d311a | |||
53b4cbbf8c | |||
628716ec28 | |||
bd14168627 | |||
96037d4da6 | |||
5448e773d8 | |||
848ef21c7c | |||
2ecae7da93 | |||
d9ce569eb9 | |||
eacaf392b1 | |||
ce16592b6a | |||
295d76d354 | |||
23b3c998bd | |||
b5e966c9a1 | |||
96cb6f4b12 | |||
e2c0f82ec0 | |||
dbf28c03e6 | |||
26165e30de | |||
c52331a23a | |||
8007e71e1d | |||
28d08e013f | |||
64bbd383de | |||
8a9f53102b | |||
0412b97170 | |||
c8b8a8fc03 | |||
95d3090b9b | |||
49129ee6dd | |||
6a7ecb0d4a | |||
1ceeed1007 | |||
a7922ff44e | |||
a421604ed5 | |||
7d182db32f | |||
c5cb9979d3 | |||
b9a73106ed | |||
c674cca482 | |||
81d1228b92 | |||
6ae61d5b81 | |||
9cb872eec2 | |||
68e8c010b7 | |||
9671413906 | |||
4c8d24c319 | |||
e50144bd34 | |||
9f3171e3f1 | |||
cc92748747 | |||
0a0b0c1adb | |||
92a74026a6 | |||
3fa1c6c420 | |||
b04eccdbda | |||
9ce30dee70 | |||
3c0b680b8e | |||
895356897b | |||
9164be2f37 | |||
5385264f94 | |||
610e756c07 | |||
15c9f8f458 | |||
fb704a5b83 | |||
fdda628be8 | |||
2b45d8aa05 | |||
0e2fc65301 | |||
e8ef7e74de | |||
c32e1b9583 | |||
9d0f6ec155 | |||
855d603795 | |||
af25782185 | |||
e5ba51b80a | |||
5e240de677 | |||
418cfac0e3 | |||
9d09607013 | |||
eddf25b622 | |||
537a8654fa | |||
9de33d06d2 | |||
0e5f320664 | |||
88d8e60511 | |||
439f07162e | |||
efe2b6cbd9 | |||
0aa1ed9464 | |||
cb94ed6a2a | |||
cf187ee46b | |||
3e71fc20fd | |||
f3601321f7 | |||
540059368c | |||
7ce89123f7 | |||
e3c7c86212 | |||
794804e27f | |||
6d89c1da6e | |||
d059554464 | |||
3a392d4a9f | |||
e3071b372a | |||
18bd279b0c | |||
5b93db7463 | |||
5b7e5eb91b | |||
78ca383e3c | |||
c1eed9ada3 | |||
8d6feb5394 | |||
42994f8977 | |||
f0a871e1f8 | |||
a710c30572 | |||
c991763b00 | |||
72dae14f87 | |||
5800340762 | |||
c5f5adcac6 | |||
591642efb3 | |||
6182ffa1d4 | |||
402a898d96 | |||
13d43d8319 | |||
7bcdbd3813 | |||
60ada22674 | |||
637119d46d | |||
40f3da6a65 | |||
f4697fe7f7 | |||
3bc18b9021 | |||
c21581aefa | |||
165f25db69 | |||
9aa0617aa1 | |||
ddce88dce6 | |||
6aa2bce2be | |||
a43c1d3d1e | |||
1ed0e817e8 | |||
709ca55e65 | |||
8c13f5dbba | |||
4cb82d81b7 | |||
0c42921387 | |||
70a3e7fc7d | |||
d5267be38c | |||
8e7e0ed490 | |||
8cf2837725 | |||
63ae186c76 | |||
dbf5c7b832 | |||
bfbfc01e99 | |||
8fa9d0e843 | |||
2d3e108fd9 | |||
7822b30dcb | |||
2701b7d04e | |||
e361c3f975 | |||
260706c172 | |||
390668ec34 | |||
1d5cdf9607 | |||
a4bf3542e0 | |||
df82cfe66b | |||
f23414adaf | |||
41024ddb79 | |||
53f9547cc5 | |||
4bfd9de100 | |||
c01e00d77d | |||
825191c08f | |||
9dc6670795 | |||
1db8eee9f7 | |||
1bc50cb62c | |||
450b07fd08 | |||
12c7515ee8 | |||
ed65da4340 | |||
d9d2917cf5 | |||
ce5ca1875b | |||
4f869252a2 | |||
17b92126de | |||
6e88c44229 | |||
6c3d338c12 | |||
4ebd44cb4e | |||
75cb9f7fd2 | |||
eacca9d2ab | |||
d0e11bc68b | |||
1958623a7a | |||
498d8b6520 | |||
a12f2fec5a | |||
22bf046643 | |||
dca48fae36 | |||
9af4068bb6 | |||
2992d8ec12 | |||
33dd2560e0 | |||
aeb5c6ee25 | |||
08a2436b8f | |||
fbc3cfeda4 | |||
c8812b1add | |||
8d82e80639 | |||
ed741d53d7 | |||
685754895b | |||
e7791d38ff | |||
9f14653001 | |||
6c5a7b0751 | |||
51a327c52d | |||
5a978bb30d | |||
6801758cb3 | |||
14de3dd9e5 | |||
ed2d57fb4b | |||
e87acc6286 | |||
0de932bc9e | |||
d021d9f757 | |||
eb5da26004 | |||
6765254f43 | |||
e98802f5b2 | |||
af54b6483e | |||
96167c3167 | |||
eecfdf482f | |||
7ceb865206 | |||
b919670706 | |||
72120b8842 | |||
1e53c08d9d | |||
2d1b6a09e9 | |||
7f661d9af9 | |||
81c66bdddd | |||
4bd46a1657 | |||
244a752ae1 |
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
|
35
.gitea/workflows/build.yaml
Normal file
35
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
name: Build Tilde Friends
|
||||||
|
run-name: ${{ gitea.actor }} running 🚀
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Build-All:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
valid_volumes: ['/opt/keys']
|
||||||
|
volumes:
|
||||||
|
- /opt/keys:/opt/keys
|
||||||
|
steps:
|
||||||
|
- name: check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- run: ln -s /opt/keys .keys
|
||||||
|
- name: Setup JDK
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
with:
|
||||||
|
packages: 'tools platform-tools build-tools;34.0.0 platforms;android-34 ndk;26.3.11579264'
|
||||||
|
- run: sudo apt update && sudo apt install -y doxygen graphviz mingw-w64
|
||||||
|
- run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all docs
|
||||||
|
- run: docker build .
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
path: out/TildeFriends-release.fdroid.apk
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
path: out/winrelease/tildefriends.exe
|
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
build/
|
||||||
|
*.core
|
||||||
|
db.*
|
||||||
|
deps/ios_toolchain/
|
||||||
|
deps/openssl/
|
||||||
|
dist/
|
||||||
|
.keys
|
||||||
|
logs/
|
||||||
|
**/node_modules
|
||||||
|
out
|
||||||
|
repo/
|
||||||
|
result
|
||||||
|
*.swo
|
||||||
|
*.swp
|
||||||
|
tmp/
|
||||||
|
unsigned/
|
||||||
|
.zsign_cache/
|
28
.gitmodules
vendored
Normal file
28
.gitmodules
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
[submodule "deps/zlib"]
|
||||||
|
path = deps/zlib
|
||||||
|
url = https://github.com/madler/zlib.git
|
||||||
|
[submodule "deps/libsodium"]
|
||||||
|
path = deps/libsodium
|
||||||
|
url = https://github.com/jedisct1/libsodium.git
|
||||||
|
[submodule "deps/quickjs"]
|
||||||
|
path = deps/quickjs
|
||||||
|
url = https://github.com/bellard/quickjs.git
|
||||||
|
[submodule "deps/crypt_blowfish"]
|
||||||
|
path = deps/crypt_blowfish
|
||||||
|
url = https://github.com/openwall/crypt_blowfish.git
|
||||||
|
[submodule "deps/libbacktrace"]
|
||||||
|
path = deps/libbacktrace
|
||||||
|
url = https://github.com/ianlancetaylor/libbacktrace.git
|
||||||
|
[submodule "deps/libuv"]
|
||||||
|
path = deps/libuv
|
||||||
|
url = https://github.com/libuv/libuv.git
|
||||||
|
[submodule "deps/picohttpparser"]
|
||||||
|
path = deps/picohttpparser
|
||||||
|
url = https://github.com/h2o/picohttpparser.git
|
||||||
|
[submodule "deps/openssl_src"]
|
||||||
|
path = deps/openssl_src
|
||||||
|
url = https://github.com/openssl/openssl.git
|
||||||
|
shallow = true
|
||||||
|
[submodule "deps/c-ares"]
|
||||||
|
path = deps/c-ares
|
||||||
|
url = https://github.com/c-ares/c-ares.git
|
15
.prettierignore
Normal file
15
.prettierignore
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
node_modules
|
||||||
|
src
|
||||||
|
deps
|
||||||
|
.clang-format
|
||||||
|
flake.lock
|
||||||
|
|
||||||
|
# Minified files
|
||||||
|
**/*.min.css
|
||||||
|
**/*.min.js
|
||||||
|
**/leaflet.*
|
||||||
|
**/commonmark*
|
||||||
|
**/w3.css
|
||||||
|
apps/ssb/tribute.esm.js
|
||||||
|
apps/api/app.js
|
||||||
|
**/emojis.json
|
5
.prettierrc.yaml
Normal file
5
.prettierrc.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
trailingComma: 'es5'
|
||||||
|
useTabs: true
|
||||||
|
semi: true
|
||||||
|
singleQuote: true
|
||||||
|
bracketSpacing: false
|
37
CONTRIBUTING.md
Normal file
37
CONTRIBUTING.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Contributing to Tilde Friends
|
||||||
|
|
||||||
|
Thank you for your interest in Tilde Friends.
|
||||||
|
|
||||||
|
Above all, Tilde Friends aims to be a fun, safe place to play. When that is at
|
||||||
|
odds with the course of development, we will work through it with respectful
|
||||||
|
communication.
|
||||||
|
|
||||||
|
## How can I contribute?
|
||||||
|
|
||||||
|
The nature of Tilde Friends makes for a wide range of ways to contribute
|
||||||
|
|
||||||
|
- Just use it. Really, just kicking the tires will probably shake out issues
|
||||||
|
in useful ways at this point.
|
||||||
|
- Report and comment on bugs: https://dev.tildefriends.net/issues.
|
||||||
|
- Make apps. You don't need my permission to make and share apps with Tilde
|
||||||
|
Friends. I hope that an ecosystem of good apps grows outside of this
|
||||||
|
repository. If you want to recreate better versions of the stock apps, just
|
||||||
|
do it. If you make a better ssb app or whatever and drop me a line however
|
||||||
|
is most convenient for you, I will probably take a look and consider
|
||||||
|
replacing the stock one with it.
|
||||||
|
- Write about it. Docs in the git repository, blog posts, private messages to
|
||||||
|
me with ideas...really there is no wrong answer. Just make some noise, and
|
||||||
|
I'll do my best to incorporate or otherwise link your feedback and make the
|
||||||
|
most of it.
|
||||||
|
- Write C code in the git repository. I'm really striving for it to be the
|
||||||
|
case that other people don't really need to meddle in there, but if you can
|
||||||
|
help out, I will gladly review your pull requests via
|
||||||
|
https://dev.tildefriends.net/pulls.
|
||||||
|
|
||||||
|
## Best practices
|
||||||
|
|
||||||
|
- The C code is formatted with clang-format. Run `make format`.
|
||||||
|
- The rest is formatted with prettier. Run `npm run prettier`.
|
||||||
|
- We strive to have code compile on all platforms with no warnings and run with
|
||||||
|
no sanitizer issues.
|
||||||
|
- There are tests. Run `out/debug/tildefriends test`.
|
464
GNUmakefile
464
GNUmakefile
@ -3,9 +3,12 @@
|
|||||||
MAKEFLAGS += --warn-undefined-variables
|
MAKEFLAGS += --warn-undefined-variables
|
||||||
MAKEFLAGS += --no-builtin-rules
|
MAKEFLAGS += --no-builtin-rules
|
||||||
|
|
||||||
VERSION_CODE := 15
|
VERSION_CODE := 27
|
||||||
VERSION_NUMBER := 0.0.15
|
VERSION_NUMBER := 0.0.23-wip
|
||||||
VERSION_NAME := Medium English breakfast tea.
|
VERSION_NAME := Me upon my pony on my boat.
|
||||||
|
|
||||||
|
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3460100.zip
|
||||||
|
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
|
||||||
|
|
||||||
PROJECT = tildefriends
|
PROJECT = tildefriends
|
||||||
BUILD_DIR ?= out
|
BUILD_DIR ?= out
|
||||||
@ -13,6 +16,12 @@ UNAME_S := $(shell uname -s)
|
|||||||
UNAME_M := $(shell uname -m)
|
UNAME_M := $(shell uname -m)
|
||||||
|
|
||||||
ANDROID_SDK ?= ~/Android/Sdk
|
ANDROID_SDK ?= ~/Android/Sdk
|
||||||
|
BUNDLETOOL = out/bundletool.jar
|
||||||
|
|
||||||
|
HAVE_WIN := 0
|
||||||
|
|
||||||
|
export SOURCE_DATE_EPOCH=1
|
||||||
|
export TZ=UTC
|
||||||
|
|
||||||
ifeq ($(UNAME_S),Darwin)
|
ifeq ($(UNAME_S),Darwin)
|
||||||
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
|
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
|
||||||
@ -26,7 +35,8 @@ BUILD_TYPES := debug release
|
|||||||
CFLAGS += -Dstatic_assert=_Static_assert
|
CFLAGS += -Dstatic_assert=_Static_assert
|
||||||
LDFLAGS += \
|
LDFLAGS += \
|
||||||
-lbsd \
|
-lbsd \
|
||||||
-lnetwork
|
-lnetwork \
|
||||||
|
-Wno-stringop-overflow
|
||||||
else ifeq ($(UNAME_S),OpenBSD)
|
else ifeq ($(UNAME_S),OpenBSD)
|
||||||
BUILD_TYPES := debug release
|
BUILD_TYPES := debug release
|
||||||
CFLAGS += \
|
CFLAGS += \
|
||||||
@ -36,7 +46,6 @@ LDFLAGS += \
|
|||||||
-lc++abi
|
-lc++abi
|
||||||
HAVE_ANDROID := 0
|
HAVE_ANDROID := 0
|
||||||
HAVE_LINUX_IOS := 0
|
HAVE_LINUX_IOS := 0
|
||||||
HAVE_WIN := 0
|
|
||||||
else
|
else
|
||||||
$(error Unexpected host platform $(UNAME_S).)
|
$(error Unexpected host platform $(UNAME_S).)
|
||||||
endif
|
endif
|
||||||
@ -46,17 +55,22 @@ CFLAGS += \
|
|||||||
-Wall \
|
-Wall \
|
||||||
-Wextra \
|
-Wextra \
|
||||||
-Wno-unused-parameter \
|
-Wno-unused-parameter \
|
||||||
|
-Wno-unknown-warning-option \
|
||||||
-MMD \
|
-MMD \
|
||||||
|
-MP \
|
||||||
-ffunction-sections \
|
-ffunction-sections \
|
||||||
-fdata-sections \
|
-fdata-sections \
|
||||||
-fno-exceptions \
|
-fno-exceptions \
|
||||||
-g
|
-g
|
||||||
|
LDFLAGS += \
|
||||||
|
-Wno-attributes \
|
||||||
|
-flto=auto
|
||||||
|
|
||||||
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
|
|
||||||
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-33
|
|
||||||
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.0.10792818
|
|
||||||
ANDROID_MIN_SDK_VERSION := 24
|
ANDROID_MIN_SDK_VERSION := 24
|
||||||
ANDROID_TARGET_SDK_VERSION := 34
|
ANDROID_TARGET_SDK_VERSION := 34
|
||||||
|
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
|
||||||
|
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-$(ANDROID_TARGET_SDK_VERSION)
|
||||||
|
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.3.11579264
|
||||||
|
|
||||||
ANDROID_ARMV7A_TARGETS := \
|
ANDROID_ARMV7A_TARGETS := \
|
||||||
out/androiddebug-armv7a/tildefriends \
|
out/androiddebug-armv7a/tildefriends \
|
||||||
@ -85,7 +99,7 @@ BUILD_TYPES += \
|
|||||||
androidrelease-x86 \
|
androidrelease-x86 \
|
||||||
androiddebug-x86_64 \
|
androiddebug-x86_64 \
|
||||||
androidrelease-x86_64
|
androidrelease-x86_64
|
||||||
all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk
|
all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk out/TildeFriends-release.fdroid.apk
|
||||||
endif
|
endif
|
||||||
|
|
||||||
WINDOWS_TARGETS := \
|
WINDOWS_TARGETS := \
|
||||||
@ -145,19 +159,24 @@ ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
|
|||||||
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
|
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
|
||||||
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(ALL_TARGETS))
|
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(ALL_TARGETS))
|
||||||
NONMACOS_TARGETS := $(filter-out $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS),$(ALL_TARGETS))
|
NONMACOS_TARGETS := $(filter-out $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS),$(ALL_TARGETS))
|
||||||
|
DEADSTRIP_TARGETS := $(filter-out $(ANDROID_TARGETS),$(NONMACOS_TARGETS))
|
||||||
|
$(NONMACOS_TARGETS): LDFLAGS += -static-libgcc
|
||||||
|
|
||||||
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
|
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
|
||||||
$(filter-out $(ANDROID_TARGETS) $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
|
$(filter-out $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
|
||||||
$(ANDROID_TARGETS): CFLAGS += \
|
$(ANDROID_TARGETS): CFLAGS += \
|
||||||
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
|
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
|
||||||
-fPIC \
|
-fPIC \
|
||||||
-fdebug-compilation-dir . \
|
-fdebug-compilation-dir . \
|
||||||
-fomit-frame-pointer \
|
-fomit-frame-pointer \
|
||||||
-fno-asynchronous-unwind-tables \
|
-fno-asynchronous-unwind-tables \
|
||||||
-funwind-tables
|
-funwind-tables \
|
||||||
|
-Wno-unknown-warning-option
|
||||||
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
|
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
|
||||||
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
|
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
|
||||||
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
|
$(RELEASE_TARGETS): CFLAGS += \
|
||||||
|
-DNDEBUG \
|
||||||
|
-flto
|
||||||
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
|
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
|
||||||
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
|
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
|
||||||
$(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32
|
$(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32
|
||||||
@ -200,7 +219,7 @@ $(ANDROID_X86_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86/usr/local/lib
|
|||||||
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
|
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
|
||||||
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
|
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
|
||||||
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
|
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
|
||||||
$(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections
|
$(DEADSTRIP_TARGETS): LDFLAGS += -Wl,--gc-sections
|
||||||
$(IOS_TARGETS): CFLAGS += -mios-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
|
$(IOS_TARGETS): CFLAGS += -mios-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
|
||||||
$(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
|
$(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
|
||||||
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
|
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
|
||||||
@ -208,14 +227,14 @@ $(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/li
|
|||||||
|
|
||||||
ifeq ($(UNAME_M),x86_64)
|
ifeq ($(UNAME_M),x86_64)
|
||||||
ifneq ($(UNAME_S),Haiku)
|
ifneq ($(UNAME_S),Haiku)
|
||||||
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||||
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(UNAME_M),aarch64)
|
ifeq ($(UNAME_M),aarch64)
|
||||||
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
|
||||||
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
|
||||||
endif
|
endif
|
||||||
|
|
||||||
get_objs = \
|
get_objs = \
|
||||||
@ -233,6 +252,8 @@ APP_SOURCES_ios := $(wildcard src/*.m)
|
|||||||
APP_OBJS := $(call get_objs,APP_SOURCES)
|
APP_OBJS := $(call get_objs,APP_SOURCES)
|
||||||
$(APP_OBJS): CFLAGS += \
|
$(APP_OBJS): CFLAGS += \
|
||||||
-Ideps/base64c/include \
|
-Ideps/base64c/include \
|
||||||
|
-Ideps/c-ares/include \
|
||||||
|
-Ideps/c-ares_config \
|
||||||
-Ideps/crypt_blowfish \
|
-Ideps/crypt_blowfish \
|
||||||
-Ideps/libbacktrace \
|
-Ideps/libbacktrace \
|
||||||
-Ideps/libsodium \
|
-Ideps/libsodium \
|
||||||
@ -244,7 +265,6 @@ $(APP_OBJS): CFLAGS += \
|
|||||||
-Ideps/quickjs \
|
-Ideps/quickjs \
|
||||||
-Ideps/sqlite \
|
-Ideps/sqlite \
|
||||||
-Ideps/valgrind \
|
-Ideps/valgrind \
|
||||||
-Ideps/xopt \
|
|
||||||
-Wdouble-promotion \
|
-Wdouble-promotion \
|
||||||
-Werror
|
-Werror
|
||||||
ifeq ($(UNAME_M),x86_64)
|
ifeq ($(UNAME_M),x86_64)
|
||||||
@ -252,6 +272,105 @@ $(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/macos% $(BUILD_DIR)/ios%,$(APP_O
|
|||||||
-fanalyzer
|
-fanalyzer
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
ARES_SOURCES := \
|
||||||
|
deps/c-ares/src/lib/ares_platform.c \
|
||||||
|
deps/c-ares/src/lib/record/ares_dns_mapping.c \
|
||||||
|
deps/c-ares/src/lib/record/ares_dns_parse.c \
|
||||||
|
deps/c-ares/src/lib/record/ares_dns_write.c \
|
||||||
|
deps/c-ares/src/lib/record/ares_dns_name.c \
|
||||||
|
deps/c-ares/src/lib/record/ares_dns_record.c \
|
||||||
|
deps/c-ares/src/lib/record/ares_dns_multistring.c \
|
||||||
|
deps/c-ares/src/lib/ares_destroy.c \
|
||||||
|
deps/c-ares/src/lib/ares_data.c \
|
||||||
|
deps/c-ares/src/lib/ares_sysconfig.c \
|
||||||
|
deps/c-ares/src/lib/ares_cancel.c \
|
||||||
|
deps/c-ares/src/lib/ares_metrics.c \
|
||||||
|
deps/c-ares/src/lib/ares_getnameinfo.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_txt_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_naptr_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_create_query.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_mx_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_srv_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_ptr_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_caa_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_aaaa_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_expand_name.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_uri_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_a_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_expand_string.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_fds.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_ns_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_parse_soa_reply.c \
|
||||||
|
deps/c-ares/src/lib/legacy/ares_getsock.c \
|
||||||
|
deps/c-ares/src/lib/windows_port.c \
|
||||||
|
deps/c-ares/src/lib/ares_qcache.c \
|
||||||
|
deps/c-ares/src/lib/ares_update_servers.c \
|
||||||
|
deps/c-ares/src/lib/ares_process.c \
|
||||||
|
deps/c-ares/src/lib/ares_getenv.c \
|
||||||
|
deps/c-ares/src/lib/ares_gethostbyname.c \
|
||||||
|
deps/c-ares/src/lib/ares_send.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares__slist.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares__htable.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares__llist.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares__htable_szvp.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares__htable_asvp.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares__htable_vpvp.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares__htable_strvp.c \
|
||||||
|
deps/c-ares/src/lib/dsa/ares__array.c \
|
||||||
|
deps/c-ares/src/lib/ares__socket.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_poll.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_thread.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_select.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_kqueue.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_configchg.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_epoll.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_wake_pipe.c \
|
||||||
|
deps/c-ares/src/lib/event/ares_event_win32.c \
|
||||||
|
deps/c-ares/src/lib/ares_search.c \
|
||||||
|
deps/c-ares/src/lib/ares__parse_into_addrinfo.c \
|
||||||
|
deps/c-ares/src/lib/ares__hosts_file.c \
|
||||||
|
deps/c-ares/src/lib/ares_getaddrinfo.c \
|
||||||
|
deps/c-ares/src/lib/ares__addrinfo2hostent.c \
|
||||||
|
deps/c-ares/src/lib/ares_freeaddrinfo.c \
|
||||||
|
deps/c-ares/src/lib/ares_strerror.c \
|
||||||
|
deps/c-ares/src/lib/ares_version.c \
|
||||||
|
deps/c-ares/src/lib/ares_gethostbyaddr.c \
|
||||||
|
deps/c-ares/src/lib/ares__addrinfo_localhost.c \
|
||||||
|
deps/c-ares/src/lib/ares_free_hostent.c \
|
||||||
|
deps/c-ares/src/lib/ares__close_sockets.c \
|
||||||
|
deps/c-ares/src/lib/ares_free_string.c \
|
||||||
|
deps/c-ares/src/lib/ares_init.c \
|
||||||
|
deps/c-ares/src/lib/ares_options.c \
|
||||||
|
deps/c-ares/src/lib/str/ares_strcasecmp.c \
|
||||||
|
deps/c-ares/src/lib/str/ares__buf.c \
|
||||||
|
deps/c-ares/src/lib/str/ares_strsplit.c \
|
||||||
|
deps/c-ares/src/lib/str/ares_str.c \
|
||||||
|
deps/c-ares/src/lib/ares_sysconfig_mac.c \
|
||||||
|
deps/c-ares/src/lib/ares__sortaddrinfo.c \
|
||||||
|
deps/c-ares/src/lib/ares_sysconfig_files.c \
|
||||||
|
deps/c-ares/src/lib/util/ares__iface_ips.c \
|
||||||
|
deps/c-ares/src/lib/util/ares__timeval.c \
|
||||||
|
deps/c-ares/src/lib/util/ares_math.c \
|
||||||
|
deps/c-ares/src/lib/util/ares_rand.c \
|
||||||
|
deps/c-ares/src/lib/util/ares__threads.c \
|
||||||
|
deps/c-ares/src/lib/ares_query.c \
|
||||||
|
deps/c-ares/src/lib/ares_cookie.c \
|
||||||
|
deps/c-ares/src/lib/inet_net_pton.c \
|
||||||
|
deps/c-ares/src/lib/inet_ntop.c \
|
||||||
|
deps/c-ares/src/lib/ares_library_init.c \
|
||||||
|
deps/c-ares/src/lib/ares_android.c \
|
||||||
|
deps/c-ares/src/lib/ares_sysconfig_win.c \
|
||||||
|
deps/c-ares/src/lib/ares_timeout.c
|
||||||
|
ARES_OBJS := $(call get_objs,ARES_SOURCES)
|
||||||
|
$(ARES_OBJS): CFLAGS += \
|
||||||
|
-Ideps/c-ares/include \
|
||||||
|
-Ideps/c-ares/src/lib \
|
||||||
|
-Ideps/c-ares_config/ \
|
||||||
|
-D_GNU_SOURCE \
|
||||||
|
-Wno-unused-function \
|
||||||
|
-Wno-deprecated-declarations \
|
||||||
|
-Wno-unused-result
|
||||||
|
|
||||||
BLOWFISH_SOURCES := \
|
BLOWFISH_SOURCES := \
|
||||||
deps/crypt_blowfish/crypt_blowfish.c \
|
deps/crypt_blowfish/crypt_blowfish.c \
|
||||||
deps/crypt_blowfish/crypt_gensalt.c \
|
deps/crypt_blowfish/crypt_gensalt.c \
|
||||||
@ -383,6 +502,8 @@ $(UV_OBJS): CFLAGS += \
|
|||||||
-Wno-unused-but-set-variable \
|
-Wno-unused-but-set-variable \
|
||||||
-Wno-unused-result \
|
-Wno-unused-result \
|
||||||
-Wno-unused-variable
|
-Wno-unused-variable
|
||||||
|
$(UV_OBJS): CFLAGS += -fno-lto
|
||||||
|
$(filter out/win%,$(UV_OBJS)): CFLAGS += -Wno-cast-function-type
|
||||||
ifeq ($(UNAME_S),Linux)
|
ifeq ($(UNAME_S),Linux)
|
||||||
$(UV_OBJS): CFLAGS += \
|
$(UV_OBJS): CFLAGS += \
|
||||||
-D_GNU_SOURCE
|
-D_GNU_SOURCE
|
||||||
@ -458,6 +579,7 @@ ifneq ($(UNAME_S),OpenBSD)
|
|||||||
$(filter-out $(BUILD_DIR)/win%,$(SODIUM_OBJS)): CFLAGS += \
|
$(filter-out $(BUILD_DIR)/win%,$(SODIUM_OBJS)): CFLAGS += \
|
||||||
-DHAVE_ALLOCA_H
|
-DHAVE_ALLOCA_H
|
||||||
endif
|
endif
|
||||||
|
$(SODIUM_OBJS): CFLAGS := $(filter-out -flto,$(CFLAGS))
|
||||||
|
|
||||||
SQLITE_SOURCES := deps/sqlite/sqlite3.c
|
SQLITE_SOURCES := deps/sqlite/sqlite3.c
|
||||||
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
|
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
|
||||||
@ -496,18 +618,6 @@ $(SQLITE_OBJS): CFLAGS += \
|
|||||||
-Wno-unused-function \
|
-Wno-unused-function \
|
||||||
-Wno-unused-variable
|
-Wno-unused-variable
|
||||||
|
|
||||||
XOPT_SOURCES := deps/xopt/xopt.c
|
|
||||||
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
|
|
||||||
$(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
|
|
||||||
-DHAVE_SNPRINTF \
|
|
||||||
-DHAVE_VSNPRINTF \
|
|
||||||
-DHAVE_VASNPRINTF \
|
|
||||||
-DHAVE_VASPRINTF \
|
|
||||||
-Dvsnprintf=rpl_vsnprintf
|
|
||||||
$(XOPT_OBJS): CFLAGS += \
|
|
||||||
-Wno-implicit-const-int-float-conversion \
|
|
||||||
-Wno-pointer-to-int-cast
|
|
||||||
|
|
||||||
QUICKJS_SOURCES := \
|
QUICKJS_SOURCES := \
|
||||||
deps/quickjs/cutils.c \
|
deps/quickjs/cutils.c \
|
||||||
deps/quickjs/libbf.c \
|
deps/quickjs/libbf.c \
|
||||||
@ -587,7 +697,7 @@ $(MINIUNZIP_OBJS): CFLAGS += \
|
|||||||
LDFLAGS += \
|
LDFLAGS += \
|
||||||
-pthread \
|
-pthread \
|
||||||
-lm
|
-lm
|
||||||
debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
$(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
|
||||||
-lssl \
|
-lssl \
|
||||||
-lcrypto
|
-lcrypto
|
||||||
ifneq ($(UNAME_S),Haiku)
|
ifneq ($(UNAME_S),Haiku)
|
||||||
@ -629,6 +739,7 @@ all: $(BUILD_TYPES)
|
|||||||
|
|
||||||
ALL_APP_OBJS := \
|
ALL_APP_OBJS := \
|
||||||
$(APP_OBJS) \
|
$(APP_OBJS) \
|
||||||
|
$(ARES_OBJS) \
|
||||||
$(BLOWFISH_OBJS) \
|
$(BLOWFISH_OBJS) \
|
||||||
$(LIBBACKTRACE_OBJS) \
|
$(LIBBACKTRACE_OBJS) \
|
||||||
$(MINIUNZIP_OBJS) \
|
$(MINIUNZIP_OBJS) \
|
||||||
@ -636,8 +747,7 @@ ALL_APP_OBJS := \
|
|||||||
$(QUICKJS_OBJS) \
|
$(QUICKJS_OBJS) \
|
||||||
$(SODIUM_OBJS) \
|
$(SODIUM_OBJS) \
|
||||||
$(SQLITE_OBJS) \
|
$(SQLITE_OBJS) \
|
||||||
$(UV_OBJS) \
|
$(UV_OBJS)
|
||||||
$(XOPT_OBJS)
|
|
||||||
|
|
||||||
DEPS = $(ALL_APP_OBJS:.o=.d)
|
DEPS = $(ALL_APP_OBJS:.o=.d)
|
||||||
-include $(DEPS)
|
-include $(DEPS)
|
||||||
@ -647,34 +757,34 @@ $(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
|
|||||||
.PHONY: $(1)
|
.PHONY: $(1)
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
|
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
|
||||||
@echo [link] $$@
|
@echo "[link] $$@"
|
||||||
@$$(CC) -o $$@ $$^ $$(LDFLAGS)
|
@$$(CC) -o $$@ $$^ $$(LDFLAGS)
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/%.o: %.c
|
$(BUILD_DIR)/$(1)/%.o: %.c
|
||||||
@mkdir -p $$(dir $$@)
|
@mkdir -p $$(dir $$@)
|
||||||
@echo [c] $$@
|
@echo "[c] $$@"
|
||||||
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/%.o: %.m
|
$(BUILD_DIR)/$(1)/%.o: %.m
|
||||||
@mkdir -p $$(dir $$@)
|
@mkdir -p $$(dir $$@)
|
||||||
@echo [m] $$@
|
@echo "[m] $$@"
|
||||||
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
@$$(CC) $$(CFLAGS) -c $$< -o $$@
|
||||||
|
|
||||||
$(BUILD_DIR)/$(1)/%.o: %.S
|
$(BUILD_DIR)/$(1)/%.o: %.S
|
||||||
@mkdir -p $$(dir $$@)
|
@mkdir -p $$(dir $$@)
|
||||||
@echo [as] $$@
|
@echo "[as] $$@"
|
||||||
@$$(AS) -c $$< -o $$@
|
@$$(AS) -c $$< -o $$@
|
||||||
endef
|
endef
|
||||||
|
|
||||||
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
|
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
|
||||||
|
|
||||||
src/version.h : $(firstword $(MAKEFILE_LIST))
|
src/version.h : $(firstword $(MAKEFILE_LIST))
|
||||||
@echo [version] $@
|
@echo "[version] $@"
|
||||||
@echo "#define VERSION_NUMBER \"$(VERSION_NUMBER)\"" > $@
|
@echo "#define VERSION_NUMBER \"$(VERSION_NUMBER)\"" > $@
|
||||||
@echo "#define VERSION_NAME \"$(VERSION_NAME)\"" >> $@
|
@echo "#define VERSION_NAME \"$(VERSION_NAME)\"" >> $@
|
||||||
|
|
||||||
src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
|
src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
|
||||||
@echo [android_version] $@
|
@echo "[android_version] $@"
|
||||||
@sed -i \
|
@sed -i \
|
||||||
-e 's/versionCode=".*"/versionCode="$(VERSION_CODE)"/' \
|
-e 's/versionCode=".*"/versionCode="$(VERSION_CODE)"/' \
|
||||||
-e 's/versionName=".*"/versionName="$(VERSION_NUMBER)"/' \
|
-e 's/versionName=".*"/versionName="$(VERSION_NUMBER)"/' \
|
||||||
@ -685,82 +795,188 @@ src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
|
|||||||
# Android support.
|
# Android support.
|
||||||
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
|
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@echo [aapt2] $@
|
@echo "[aapt2] $@"
|
||||||
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
|
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
|
||||||
|
|
||||||
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
|
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@echo [aapt2] $@
|
@echo "[aapt2] $@"
|
||||||
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
|
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
|
||||||
|
|
||||||
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
|
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
|
||||||
@mkdir -p $(dir $@)
|
@echo [aapt2 link] res.apk
|
||||||
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
|
@mkdir -p out/apk/
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat \
|
||||||
|
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
|
||||||
|
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
|
||||||
|
--manifest src/android/AndroidManifest.xml \
|
||||||
|
-o out/apk/res.apk \
|
||||||
|
--java out/gen/
|
||||||
|
|
||||||
|
out/apk/res.fdroid.apk out/gen_fdroid/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
|
||||||
|
@echo [aapt2 link] res.fdroid.apk
|
||||||
|
@mkdir -p out/apk/
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat \
|
||||||
|
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
|
||||||
|
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
|
||||||
|
--rename-manifest-package com.unprompted.tildefriends.fdroid \
|
||||||
|
--manifest src/android/AndroidManifest.xml \
|
||||||
|
-o out/apk/res.fdroid.apk \
|
||||||
|
--java out/gen_fdroid/
|
||||||
|
|
||||||
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
|
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
|
||||||
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
|
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
|
||||||
|
|
||||||
$(CLASS_FILES) &: $(JAVA_FILES)
|
$(CLASS_FILES) &: $(JAVA_FILES)
|
||||||
@echo [javac] $(CLASS_FILES)
|
@echo "[javac] $(CLASS_FILES)"
|
||||||
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
|
@javac --release 8 -encoding UTF-8 -Xlint:deprecation -XDuseUnsharedTable=true -classpath $(ANDROID_PLATFORM)/android.jar:$(ANDROID_BUILD_TOOLS)/core-lambda-stubs.jar -d out/classes $(JAVA_FILES)
|
||||||
|
|
||||||
out/apk/classes.dex: $(CLASS_FILES)
|
out/apk/classes.dex: $(CLASS_FILES)
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@echo [d8] $@
|
@echo "[d8] $@"
|
||||||
@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
|
@$(ANDROID_BUILD_TOOLS)/d8 --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
|
||||||
|
|
||||||
PACKAGE_DIRS := \
|
PACKAGE_DIRS := \
|
||||||
apps/ \
|
apps/ \
|
||||||
core/ \
|
core/ \
|
||||||
deps/codemirror/ \
|
deps/codemirror/ \
|
||||||
|
deps/prettier/ \
|
||||||
deps/lit/
|
deps/lit/
|
||||||
|
|
||||||
RAW_FILES := $(filter-out apps/blog% apps/gg% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
|
RAW_FILES := $(sort $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f -not -name '.*')))
|
||||||
|
|
||||||
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
|
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
|
||||||
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
|
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
|
||||||
out/apk/TildeFriends-x86-debug.unsigned.apk: BUILD_TYPE := debug
|
out/apk/TildeFriends-x86-debug.unsigned.apk: BUILD_TYPE := debug
|
||||||
out/apk/TildeFriends-x86-release.unsigned.apk: BUILD_TYPE := release
|
out/apk/TildeFriends-x86-release.unsigned.apk: BUILD_TYPE := release
|
||||||
|
out/apk/TildeFriends-release.fdroid.unsigned.apk: BUILD_TYPE := release
|
||||||
|
|
||||||
out/apk/TildeFriends-arm-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
out/apk/TildeFriends-arm-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
out/apk/TildeFriends-arm-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
out/apk/TildeFriends-arm-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
out/apk/TildeFriends-x86-debug.unsigned.apk: out/apk/classes.dex out/androiddebug-x86_64/tildefriends out/androiddebug-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
out/apk/TildeFriends-x86-debug.unsigned.apk: out/apk/classes.dex out/androiddebug-x86_64/tildefriends out/androiddebug-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
out/apk/TildeFriends-x86-release.unsigned.apk: out/apk/classes.dex out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
out/apk/TildeFriends-x86-release.unsigned.apk: out/apk/classes.dex out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.apk
|
||||||
|
out/apk/TildeFriends-release.fdroid.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.fdroid.apk
|
||||||
|
|
||||||
|
$(BUNDLETOOL):
|
||||||
|
@echo [curl] $(BUNDLETOOL_URL) TO $@
|
||||||
|
@curl -q -L --create-dirs -o $@ $(BUNDLETOOL_URL)
|
||||||
|
|
||||||
|
out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGETS)) $(RAW_FILES) out/apk/res.apk src/android/AndroidManifest.xml $(BUNDLETOOL)
|
||||||
|
@rm -rf out/aab/staging/
|
||||||
|
@mkdir -p out/aab/staging
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/aapt2 link --proto-format -o out/aab/temporary.apk \
|
||||||
|
-I $(ANDROID_PLATFORM)/android.jar \
|
||||||
|
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
|
||||||
|
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
|
||||||
|
--manifest src/android/AndroidManifest.xml \
|
||||||
|
-R out/res/layout_activity_main.xml.flat \
|
||||||
|
-R out/res/drawable_icon.xml.flat \
|
||||||
|
--auto-add-overlay
|
||||||
|
@unzip out/aab/temporary.apk -d out/aab/staging/
|
||||||
|
@mkdir -p out/aab/staging/root/deps
|
||||||
|
@mkdir -p out/aab/staging/classes
|
||||||
|
@mkdir -p out/aab/staging/dex
|
||||||
|
@mkdir -p out/aab/staging/manifest
|
||||||
|
@mv out/aab/staging/AndroidManifest.xml out/aab/staging/manifest/AndroidManifest.xml
|
||||||
|
@cp out/apk/classes.dex out/aab/staging/dex/
|
||||||
|
@rm -fv out/base.zip
|
||||||
|
@mkdir -p out/aab/staging/lib/arm64-v8a out/aab/staging/lib/armeabi-v7a out/aab/staging/lib/x86_64 out/aab/staging/lib/x86
|
||||||
|
@cp out/androidrelease/tildefriends out/aab/staging/lib/arm64-v8a/libtildefriends.so
|
||||||
|
@cp out/androidrelease-armv7a/tildefriends out/aab/staging/lib/armeabi-v7a/libtildefriends.so
|
||||||
|
@cp out/androidrelease-x86_64/tildefriends out/aab/staging/lib/x86_64/libtildefriends.so
|
||||||
|
@cp out/androidrelease-x86/tildefriends out/aab/staging/lib/x86/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/arm64-v8a/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/armeabi-v7a/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86_64/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86/libtildefriends.so
|
||||||
|
@cp -r apps/ out/aab/staging/root/
|
||||||
|
@rm -rf out/aab/staging/root/apps/welcome*
|
||||||
|
@cp -r core/ out/aab/staging/root/
|
||||||
|
@cp -r deps/prettier/ out/aab/staging/root/deps/
|
||||||
|
@cp -r deps/lit/ out/aab/staging/root/deps/
|
||||||
|
@cp -r deps/codemirror/ out/aab/staging/root/deps/
|
||||||
|
@cd out/aab/staging/; zip -r ../base.zip *; cd ../../../
|
||||||
|
@java -jar $(BUNDLETOOL) build-bundle --overwrite --config=src/android/BundleConfig.json --modules=out/aab/base.zip --output=$@
|
||||||
|
@jarsigner -keystore .keys/android.jks $@ androidKey -storepass android
|
||||||
|
|
||||||
|
aab: out/TildeFriends.aab
|
||||||
|
.PHONY: aab
|
||||||
|
|
||||||
|
out/TildeFriends.apks: out/TildeFriends.aab $(BUNDLETOOL)
|
||||||
|
@java -jar $(BUNDLETOOL) build-apks --bundle out/TildeFriends.aab --overwrite --output $@ --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android
|
||||||
|
|
||||||
|
aabgo: out/TildeFriends.apks $(BUNDLETOOL)
|
||||||
|
@java -jar $(BUNDLETOOL) install-apks --apks out/TildeFriends.apks
|
||||||
|
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
|
||||||
|
|
||||||
out/apk/TildeFriends-arm-%.unsigned.apk:
|
out/apk/TildeFriends-arm-%.unsigned.apk:
|
||||||
@mkdir -p $(dir $@) out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/
|
@mkdir -p $(dir $@) out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/
|
||||||
@echo [aapt] $@
|
@echo "[aapt] $@"
|
||||||
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
|
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
|
||||||
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
|
||||||
@cp out/apk/res.apk $@
|
@cp out/apk/res.apk $@.zip
|
||||||
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
|
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
|
||||||
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
|
||||||
@zip -u $@ -q -9 $(RAW_FILES)
|
@zip -u $@.zip -q -9 $(RAW_FILES)
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
|
||||||
|
|
||||||
out/apk/TildeFriends-x86-%.unsigned.apk:
|
out/apk/TildeFriends-x86-%.unsigned.apk:
|
||||||
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
|
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
|
||||||
@echo [aapt] $@
|
@echo "[aapt] $@"
|
||||||
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
|
||||||
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/libtildefriends.so
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
|
||||||
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/libtildefriends.so
|
||||||
@cp out/apk/res.apk $@
|
@cp out/apk/res.apk $@.zip
|
||||||
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
|
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
|
||||||
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
|
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
|
||||||
@zip -u $@ -q -9 $(RAW_FILES)
|
@zip -u $@.zip -q -9 $(RAW_FILES)
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
|
||||||
|
|
||||||
|
out/apk/TildeFriends-%.fdroid.unsigned.apk:
|
||||||
|
@rm -rf out/apk-fdroid-$(BUILD_TYPE) out/apk-fdroid-$(BUILD_TYPE)-raw
|
||||||
|
@mkdir -p $(dir $@) out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/ out/apk-fdroid-$(BUILD_TYPE)/lib/x86/ out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/
|
||||||
|
@echo "[aapt] $@"
|
||||||
|
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
|
||||||
|
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/x86/libtildefriends.so
|
||||||
|
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
|
||||||
|
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/x86/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
|
||||||
|
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
|
||||||
|
@cp out/apk/res.fdroid.apk $@.zip
|
||||||
|
@cp out/apk/classes.dex out/apk-fdroid-$(BUILD_TYPE)/classes.dex
|
||||||
|
@touch -d @0 out/apk-fdroid-$(BUILD_TYPE)/classes.dex out/apk-fdroid-$(BUILD_TYPE)/lib/*/libtildefriends.so
|
||||||
|
@chmod 755 out/apk-fdroid-$(BUILD_TYPE)/classes.dex out/apk-fdroid-$(BUILD_TYPE)/lib/*/libtildefriends.so
|
||||||
|
@cd out/apk-fdroid-$(BUILD_TYPE) && zip -X -u ../../$@.zip -q classes.dex lib/*/libtildefriends.so && cd ../../
|
||||||
|
@mkdir out/apk-fdroid-$(BUILD_TYPE)-raw
|
||||||
|
@for i in $(RAW_FILES); do mkdir -p $$(dirname out/apk-fdroid-$(BUILD_TYPE)-raw/$$i) && cp $$i out/apk-fdroid-$(BUILD_TYPE)-raw/$$i && touch -d @0 out/apk-fdroid-$(BUILD_TYPE)-raw/$$i && chmod 644 out/apk-fdroid-$(BUILD_TYPE)-raw/$$i; done
|
||||||
|
@cd out/apk-fdroid-$(BUILD_TYPE)-raw && zip -X -u ../../$@.zip -q $(RAW_FILES) && cd ../../
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
|
||||||
|
|
||||||
out/%.apk: out/apk/%.unsigned.apk
|
out/%.apk: out/apk/%.unsigned.apk
|
||||||
@echo [apksigner] $(notdir $@)
|
@echo "[apksigner] $(notdir $@)"
|
||||||
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
|
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $<
|
||||||
|
|
||||||
release-apk: out/TildeFriends-arm-release.apk out/TildeFriends-x86-release.apk
|
out/%.zopfli.apk: out/%.apk
|
||||||
|
@echo "[zopfli] $(notdir $@)"
|
||||||
|
$(ANDROID_BUILD_TOOLS)/zipalign -f -z 4 $< $@.zopfli
|
||||||
|
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $@.zopfli
|
||||||
|
|
||||||
|
release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk
|
||||||
.PHONY: release-apk
|
.PHONY: release-apk
|
||||||
|
|
||||||
|
apkgo: out/TildeFriends-arm-debug.apk
|
||||||
|
@adb install -r $<
|
||||||
|
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
|
||||||
|
.PHONY: apkgo
|
||||||
|
|
||||||
releaseapkgo: out/TildeFriends-arm-release.apk
|
releaseapkgo: out/TildeFriends-arm-release.apk
|
||||||
@adb install -r $<
|
@adb install -r $<
|
||||||
@adb shell am start com.unprompted.tildefriends/.MainActivity
|
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
|
||||||
.PHONY: releaseapkgo
|
.PHONY: releaseapkgo
|
||||||
|
|
||||||
# iOS Support
|
# iOS Support
|
||||||
@ -771,10 +987,10 @@ out/%.app/tildefriends.png: src/ios/tildefriends.png
|
|||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@cp -v $< $@
|
@cp -v $< $@
|
||||||
|
|
||||||
out/%/data.zip: $(RAW_FILES)
|
out/data.zip: $(RAW_FILES)
|
||||||
@zip -u $@ -q -9 $(RAW_FILES)
|
@zip -u $@ -q -9 $(RAW_FILES)
|
||||||
|
|
||||||
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/tildefriends-%.app/data.zip
|
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
@cp -v $< $@
|
@cp -v $< $@
|
||||||
ifeq ($(HAVE_LINUX_IOS),1)
|
ifeq ($(HAVE_LINUX_IOS),1)
|
||||||
@ -782,13 +998,23 @@ ifeq ($(HAVE_LINUX_IOS),1)
|
|||||||
endif
|
endif
|
||||||
.SECONDARY:
|
.SECONDARY:
|
||||||
out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
|
out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
|
||||||
@echo [ipa] $@
|
@echo "[ipa] $@"
|
||||||
@rm -rf $@.tmp $@
|
@rm -rf $@.tmp $@
|
||||||
@mkdir -p $@.tmp/Payload/tildefriends.app/
|
@mkdir -p $@.tmp/Payload/tildefriends.app/
|
||||||
@cp -R $(dir $<)/* $@.tmp/Payload/tildefriends.app/
|
@cp -R $(dir $<)/* $@.tmp/Payload/tildefriends.app/
|
||||||
@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
|
@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
|
||||||
@rm -rf $@.tmp/
|
@rm -rf $@.tmp/
|
||||||
|
|
||||||
|
|
||||||
|
out/%/tildefriends.standalone: out/%/tildefriends out/data.zip
|
||||||
|
@echo "[standalone] $@"
|
||||||
|
@cat $< out/data.zip > $@
|
||||||
|
@chmod +x $@
|
||||||
|
out/%/tildefriends.standalone.exe: out/%/tildefriends.exe out/data.zip
|
||||||
|
@echo "[standalone] $@"
|
||||||
|
@cat $< out/data.zip > $@
|
||||||
|
@chmod +x $@
|
||||||
|
|
||||||
iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends
|
iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends
|
||||||
iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends
|
iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends
|
||||||
iosdebug-app: out/tildefriends-iosdebug.app/tildefriends
|
iosdebug-app: out/tildefriends-iosdebug.app/tildefriends
|
||||||
@ -810,18 +1036,60 @@ apklog:
|
|||||||
@adb logcat *:S tildefriends
|
@adb logcat *:S tildefriends
|
||||||
.PHONY: apklog
|
.PHONY: apklog
|
||||||
|
|
||||||
|
fetchdeps:
|
||||||
|
@echo "[fetch] sqlite"
|
||||||
|
@test -f out/deps/sqlite.zip && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p out/deps/ && curl -q $(SQLITE_URL) -o out/deps/sqlite.zip)
|
||||||
|
@test -d deps/sqlite/ && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p deps/sqlite/ && unzip -qDjo -d deps/sqlite/ out/deps/sqlite.zip)
|
||||||
|
@echo -n $(SQLITE_URL) > out/deps/sqlite.txt
|
||||||
|
@echo "[fetch] prettier"
|
||||||
|
@test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs
|
||||||
|
@test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs
|
||||||
|
@test -f deps/prettier/babel.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/babel.mjs
|
||||||
|
@test -f deps/prettier/estree.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/estree.mjs
|
||||||
|
.PHONY: fetchdeps
|
||||||
|
|
||||||
|
ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a
|
||||||
|
$(ANDROID_DEPS):
|
||||||
|
+@ANDROID_NDK_ROOT=$(ANDROID_NDK) tools/ssl-android
|
||||||
|
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
|
||||||
|
|
||||||
|
ifeq ($(HAVE_WIN),1)
|
||||||
|
WINDOWS_DEPS := deps/openssl/mingw64/usr/local/lib/libssl.a
|
||||||
|
$(WINDOWS_DEPS):
|
||||||
|
+@tools/ssl-mingw64
|
||||||
|
$(filter $(BUILD_DIR)/win%,$(APP_OBJS)): | $(WINDOWS_DEPS)
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifeq ($(UNAME_S),Darwin)
|
||||||
|
IOS_DEPS := deps/openssl/ios/ios64-xcrun/usr/local/lib/libssl.a
|
||||||
|
$(IOS_DEPS):
|
||||||
|
+@tools/ssl-ios
|
||||||
|
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
|
||||||
|
endif
|
||||||
|
|
||||||
|
out/Tilde_Friends-x86_64.AppImage: out/release/tildefriends out/data.zip
|
||||||
|
@mkdir -p out/AppDir/usr/bin
|
||||||
|
@mkdir -p out/AppDir/usr/share/applications
|
||||||
|
@mkdir -p out/AppDir/usr/share/icons/hicolor/scalable/apps
|
||||||
|
@echo "[Desktop Entry]\nName=Tilde Friends\nExec=tildefriends\nIcon=tildefriends\nType=Application\nCategories=Network" > out/AppDir/usr/share/applications/tildefriends.desktop
|
||||||
|
@cp src/ios/tildefriends.svg out/AppDir/usr/share/icons/hicolor/scalable/apps/
|
||||||
|
@cat out/release/tildefriends out/data.zip > out/AppDir/usr/bin/tildefriends
|
||||||
|
@chmod +x out/AppDir/usr/bin/tildefriends
|
||||||
|
@unset SOURCE_DATE_EPOCH; cd out; linuxdeploy-x86_64.AppImage --appdir AppDir --output appimage; cd ..
|
||||||
|
|
||||||
|
appimage: out/Tilde_Friends-x86_64.AppImage
|
||||||
|
.PHONY: appimage
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
|
|
||||||
dist: release-apk iosrelease-ipa
|
tarball:
|
||||||
@echo "[export] $$(svn info --show-item url)"
|
@echo [archive] out/tildefriends-$(VERSION_NUMBER).tar.xz
|
||||||
@rm -rf tildefriends-$(VERSION_NUMBER)
|
@rm -rf out/tildefriends-$(VERSION_NUMBER)
|
||||||
@svn export -q . tildefriends-$(VERSION_NUMBER)
|
@mkdir -p out/tildefriends-$(VERSION_NUMBER)
|
||||||
@echo "tildefriends-$(VERSION_NUMBER): $(VERSION_NAME)" > tildefriends-$(VERSION_NUMBER)/VERSION
|
@git ls-files --recurse-submodules | tar -c -T- | tar -x -C out/tildefriends-$(VERSION_NUMBER)
|
||||||
@echo "[tar] tildefriends-$(VERSION_NUMBER).tar.xz"
|
|
||||||
@tar \
|
@tar \
|
||||||
--exclude=apps/gg* \
|
|
||||||
--exclude=apps/welcome* \
|
--exclude=apps/welcome* \
|
||||||
--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
|
--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
|
||||||
--exclude=deps/libsodium/builds/msvc/vs* \
|
--exclude=deps/libsodium/builds/msvc/vs* \
|
||||||
@ -836,14 +1104,29 @@ dist: release-apk iosrelease-ipa
|
|||||||
--exclude=deps/sqlite/shell.c \
|
--exclude=deps/sqlite/shell.c \
|
||||||
--exclude=deps/zlib/contrib/vstudio \
|
--exclude=deps/zlib/contrib/vstudio \
|
||||||
--exclude=deps/zlib/doc \
|
--exclude=deps/zlib/doc \
|
||||||
-caf tildefriends-$(VERSION_NUMBER).tar.xz tildefriends-$(VERSION_NUMBER)
|
-caf out/tildefriends-$(VERSION_NUMBER).tar.xz \
|
||||||
@rm -rf tildefriends-$(VERSION_NUMBER)
|
-C out/ \
|
||||||
|
tildefriends-$(VERSION_NUMBER)
|
||||||
|
.PHONY: tarball
|
||||||
|
|
||||||
|
dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) out/TildeFriends-release.fdroid.apk appimage
|
||||||
|
@mkdir -p dist/
|
||||||
|
@echo "[cp] tildefriends-$(VERSION_NUMBER).tar.xz"
|
||||||
|
@cp out/tildefriends-$(VERSION_NUMBER).tar.xz dist/tildefriends-$(VERSION_NUMBER).tar.xz
|
||||||
@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk"
|
@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk"
|
||||||
@cp out/TildeFriends-x86-release.apk TildeFriends-x86-$(VERSION_NUMBER).apk
|
@cp out/TildeFriends-x86-release.zopfli.apk dist/TildeFriends-x86-$(VERSION_NUMBER).apk
|
||||||
@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk"
|
@echo "[cp] TildeFriends-arm-$(VERSION_NUMBER).apk"
|
||||||
@cp out/TildeFriends-arm-release.apk TildeFriends-arm-$(VERSION_NUMBER).apk
|
@cp out/TildeFriends-arm-release.zopfli.apk dist/TildeFriends-arm-$(VERSION_NUMBER).apk
|
||||||
@echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
|
@echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
|
||||||
@cp out/tildefriends-release.ipa TildeFriends-$(VERSION_NUMBER).ipa
|
@cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
|
||||||
|
@test $(HAVE_WIN) && echo "[cp] tildefriends-$(VERSION_NUMBER).exe"
|
||||||
|
@test $(HAVE_WIN) && cp out/winrelease/tildefriends.standalone.exe dist/tildefriends-$(VERSION_NUMBER).exe
|
||||||
|
@echo "[cp] TildeFriends-$(VERSION_NUMBER).aab"
|
||||||
|
@cp out/TildeFriends.aab dist/TildeFriends-$(VERSION_NUMBER).aab
|
||||||
|
@echo "[cp] TildeFriends-$(VERSION_NUMBER).fdroid.apk"
|
||||||
|
@cp out/TildeFriends-release.fdroid.apk dist/TildeFriends-$(VERSION_NUMBER).fdroid.apk
|
||||||
|
@echo "[cp] TildeFriends-x86_64-$(VERSION_NUMBER).AppImage"
|
||||||
|
@cp out/Tilde_Friends-x86_64.AppImage dist/TildeFriends-x86_64-$(VERSION_NUMBER).AppImage
|
||||||
.PHONY: dist
|
.PHONY: dist
|
||||||
|
|
||||||
dist-test: dist
|
dist-test: dist
|
||||||
@ -852,3 +1135,18 @@ dist-test: dist
|
|||||||
@docker build tildefriends-$(VERSION_NUMBER)/
|
@docker build tildefriends-$(VERSION_NUMBER)/
|
||||||
@rm -rf tildefriends-$(VERSION_NUMBER)
|
@rm -rf tildefriends-$(VERSION_NUMBER)
|
||||||
.PHONY: dist-test
|
.PHONY: dist-test
|
||||||
|
|
||||||
|
format:
|
||||||
|
@clang-format -i $(wildcard src/*.c src/*.h src/*.m)
|
||||||
|
.PHONY: format
|
||||||
|
|
||||||
|
prettier:
|
||||||
|
@npm run prettier
|
||||||
|
.PHONY: prettier
|
||||||
|
|
||||||
|
docs:
|
||||||
|
@doxygen
|
||||||
|
.PHONY: docs
|
||||||
|
|
||||||
|
fdroid: out/apk/TildeFriends-release.fdroid.unsigned.apk
|
||||||
|
.PHONY: fdroid
|
||||||
|
18
README.md
18
README.md
@ -1,16 +1,24 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
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
|
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
|
||||||
@ -19,19 +27,23 @@ Scuttlebutt, as well as a platform for writing and running web applications.
|
|||||||
the right dependencies in the right places. `make windebug winrelease
|
the right dependencies in the right places. `make windebug winrelease
|
||||||
iosdebug-ipa iosrelease-ipa release-apk`.
|
iosdebug-ipa iosrelease-ipa release-apk`.
|
||||||
4. To build in docker, `docker build .`.
|
4. To build in docker, `docker build .`.
|
||||||
|
5. `make format` will normalize formatting to the coding standard.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
By default, running the built `tildefriends` executable will start a web server
|
By default, running the built `tildefriends` executable will start a web server
|
||||||
at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
||||||
|
|
||||||
The first user to create an account and log in will be granted administrative
|
The first user to create an account and log in will be granted administrative
|
||||||
privileges. Further administration can be done at
|
privileges. Further administration can be done at
|
||||||
<http://localhost:12345/~core/admin/`>.
|
<http://localhost:12345/~core/admin/>.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
There are the very beginnings of developer documentation in `apps/docs/`
|
|
||||||
that can be read in-place or at <http://localhost:12345/~core/docs/>.
|
Docs are a work in progress:
|
||||||
|
<https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
All code unless otherwise noted in is provided under the
|
All code unless otherwise noted in is provided under the
|
||||||
[MIT](https://opensource.org/licenses/MIT) license.
|
[MIT](https://opensource.org/licenses/MIT) license.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🎛"
|
"emoji": "🎛",
|
||||||
|
"previous": "&R49FywYF8CXPhoSEydLbSCgvCddeyTiBwGuDU/gqY+M=.sha256"
|
||||||
}
|
}
|
@ -18,9 +18,13 @@ async function main() {
|
|||||||
for (let user of await core.users()) {
|
for (let user of await core.users()) {
|
||||||
data.users[user] = await core.permissionsForUser(user);
|
data.users[user] = await core.permissionsForUser(user);
|
||||||
}
|
}
|
||||||
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
|
await app.setDocument(
|
||||||
|
utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data))
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
await app.setDocument('<span style="color: #f00">Only an administrator can modify these settings.</span>');
|
await app.setDocument(
|
||||||
|
'<span style="color: #f00">Only an administrator can modify these settings.</span>'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
main();
|
main();
|
@ -1,10 +1,41 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="width: 100%">
|
<html style="width: 100%">
|
||||||
<head>
|
<head>
|
||||||
<script>const g_data = $data;</script>
|
<script>
|
||||||
|
const g_data = $data;
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="w3.css" />
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<style>
|
||||||
|
/* 2018 Valiant Poppy */
|
||||||
|
.w3-theme-l5 {color:#000 !important; background-color:#fbf3f3 !important}
|
||||||
|
.w3-theme-l4 {color:#000 !important; background-color:#f3d7d6 !important}
|
||||||
|
.w3-theme-l3 {color:#000 !important; background-color:#e6afae !important}
|
||||||
|
.w3-theme-l2 {color:#fff !important; background-color:#da8785 !important}
|
||||||
|
.w3-theme-l1 {color:#fff !important; background-color:#cd5f5d !important}
|
||||||
|
.w3-theme-d1 {color:#fff !important; background-color:#a93634 !important}
|
||||||
|
.w3-theme-d2 {color:#fff !important; background-color:#96302e !important}
|
||||||
|
.w3-theme-d3 {color:#fff !important; background-color:#832a28 !important}
|
||||||
|
.w3-theme-d4 {color:#fff !important; background-color:#702423 !important}
|
||||||
|
.w3-theme-d5 {color:#fff !important; background-color:#5e1e1d !important}
|
||||||
|
|
||||||
|
.w3-theme-light {color:#000 !important; background-color:#fbf3f3 !important}
|
||||||
|
.w3-theme-dark {color:#fff !important; background-color:#5e1e1d !important}
|
||||||
|
.w3-theme-action {color:#fff !important; background-color:#5e1e1d !important}
|
||||||
|
|
||||||
|
.w3-theme {color:#fff !important; background-color:#bd3d3a !important}
|
||||||
|
.w3-text-theme {color:#bd3d3a !important}
|
||||||
|
.w3-border-theme {border-color:#bd3d3a !important}
|
||||||
|
|
||||||
|
.w3-hover-theme:hover {color:#fff !important; background-color:#bd3d3a !important}
|
||||||
|
.w3-hover-text-theme:hover {color:#bd3d3a !important}
|
||||||
|
.w3-hover-border-theme:hover {border-color:#bd3d3a !important}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff; width: 100%">
|
<body class="w3-theme-l4">
|
||||||
|
<header class="w3-row w3-padding w3-header w3-theme-l1">
|
||||||
<h1>Tilde Friends Administration</h1>
|
<h1>Tilde Friends Administration</h1>
|
||||||
|
</header>
|
||||||
</body>
|
</body>
|
||||||
<script type="module" src="script.js"></script>
|
<script type="module" src="script.js"></script>
|
||||||
</html>
|
</html>
|
@ -3,85 +3,113 @@ import * as tfrpc from '/static/tfrpc.js';
|
|||||||
|
|
||||||
function delete_user(user) {
|
function delete_user(user) {
|
||||||
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
|
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
|
||||||
tfrpc.rpc.delete_user(user).then(function() {
|
tfrpc.rpc
|
||||||
|
.delete_user(user)
|
||||||
|
.then(function () {
|
||||||
alert(`User "${user}" deleted successfully.`);
|
alert(`User "${user}" deleted successfully.`);
|
||||||
}).catch(function(error) {
|
})
|
||||||
alert(`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`);
|
.catch(function (error) {
|
||||||
|
alert(
|
||||||
|
`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function global_settings_set(key, value) {
|
function global_settings_set(key, value) {
|
||||||
tfrpc.rpc.global_settings_set(key, value).then(function() {
|
tfrpc.rpc
|
||||||
|
.global_settings_set(key, value)
|
||||||
|
.then(function () {
|
||||||
alert(`Set "${key}" to "${value}".`);
|
alert(`Set "${key}" to "${value}".`);
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function title_case(name) {
|
||||||
|
return name
|
||||||
|
.split('_')
|
||||||
|
.map((x) => x.charAt(0).toUpperCase() + x.substring(1))
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
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`
|
||||||
<div style="margin-top: 1em">
|
<li class="w3-row">
|
||||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
|
||||||
<div>
|
<div class="w3-quarter w3-padding">${description.description}</div>
|
||||||
<input type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
|
<div class="w3-quarter w3-padding w3-center"><input class="w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input></div>
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
|
<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstChild.checked)}>Set</button>
|
||||||
<div>${description.description}</div>
|
</li>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
} else if (description.type === 'textarea') {
|
} else if (description.type === 'textarea') {
|
||||||
return html`
|
return html`
|
||||||
<div style="margin-top: 1em"">
|
<li class="w3-row">
|
||||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold"
|
||||||
<div style="width: 100%; padding: 0; margin: 0">
|
>${title_case(key)}</label
|
||||||
<div style="width: 90%; padding: 0 margin: 0">
|
>
|
||||||
<textarea style="vertical-align: top; width: 100%" rows=20 cols=80 id=${'gs_' + key}>${description.value}</textarea>
|
<div class="w3-rest w3-padding">${description.description}</div>
|
||||||
</div>
|
<textarea
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstElementChild.value)}>Set</button>
|
class="w3-input"
|
||||||
<div>${description.description}</div>
|
style="vertical-align: top; resize: vertical"
|
||||||
</div>
|
id=${'gs_' + key}
|
||||||
</div>
|
>
|
||||||
|
${description.value}</textarea
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-right w3-quarter w3-theme-action"
|
||||||
|
@click=${(e) =>
|
||||||
|
global_settings_set(
|
||||||
|
key,
|
||||||
|
e.srcElement.previousElementSibling.value
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Set
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<div style="margin-top: 1em">
|
<li class="w3-row">
|
||||||
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
|
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
|
||||||
<div>
|
<div class="w3-quarter w3-padding">${description.description}</div>
|
||||||
<input type="text" value="${description.value}" id=${'gs_' + key}></input>
|
<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input>
|
||||||
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
|
||||||
<div>${description.description}</div>
|
</li>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const user_template = (user, permissions) => html`
|
const user_template = (user, permissions) => html`
|
||||||
<li>
|
<li class="w3-card w3-margin">
|
||||||
<button @click=${(e) => delete_user(user)}>
|
<button
|
||||||
|
class="w3-button w3-theme-action"
|
||||||
|
@click=${(e) => delete_user(user)}
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
${user}:
|
${user}: ${permissions.map((x) => permission_template(x))}
|
||||||
${permissions.map(x => permission_template(x))}
|
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
const users_template = (users) =>
|
const users_template = (users) =>
|
||||||
html`<h2>Users</h2>
|
html` <header class="w3-container w3-theme-l2"><h2>Users</h2></header>
|
||||||
<ul>
|
<ul class="w3-ul">
|
||||||
${Object.entries(users).map(u => user_template(u[0], u[1]))}
|
${Object.entries(users).map((u) => user_template(u[0], u[1]))}
|
||||||
</ul>`;
|
</ul>`;
|
||||||
const page_template = (data) =>
|
const page_template = (data) =>
|
||||||
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
|
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
|
||||||
<h2>Global Settings</h2>
|
<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header>
|
||||||
<div>
|
<div class="w3-container">
|
||||||
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
|
<ul class="w3-ul">
|
||||||
|
${Object.keys(data.settings)
|
||||||
|
.sort()
|
||||||
|
.map((x) => html`${input_template(x, data.settings[x])}`)}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
${users_template(data.users)}
|
${users_template(data.users)}
|
||||||
</div>
|
</div> `;
|
||||||
`;
|
|
||||||
render(page_template(g_data), document.body);
|
render(page_template(g_data), document.body);
|
||||||
});
|
});
|
235
apps/admin/w3.css
Normal file
235
apps/admin/w3.css
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
||||||
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
|
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
|
||||||
|
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
|
||||||
|
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
|
||||||
|
a{background-color:transparent}a:active,a:hover{outline-width:0}
|
||||||
|
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
|
||||||
|
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
|
||||||
|
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||||
|
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
|
||||||
|
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
|
||||||
|
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
|
||||||
|
button,input{overflow:visible}button,select{text-transform:none}
|
||||||
|
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
|
||||||
|
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
|
||||||
|
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
|
||||||
|
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
|
||||||
|
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
|
||||||
|
[type=checkbox],[type=radio]{padding:0}
|
||||||
|
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
|
||||||
|
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
|
||||||
|
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
|
||||||
|
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
|
||||||
|
/* End extract */
|
||||||
|
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
|
||||||
|
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
|
||||||
|
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
|
||||||
|
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
|
||||||
|
hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
||||||
|
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
|
||||||
|
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
|
||||||
|
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
|
||||||
|
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
|
||||||
|
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
|
||||||
|
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
|
||||||
|
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
||||||
|
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
|
||||||
|
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
|
||||||
|
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
|
||||||
|
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
|
||||||
|
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
|
||||||
|
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
|
||||||
|
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
|
||||||
|
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
|
||||||
|
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
|
||||||
|
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
|
||||||
|
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
|
||||||
|
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
|
||||||
|
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
|
||||||
|
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
|
||||||
|
.w3-main,#main{transition:margin-left .4s}
|
||||||
|
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
|
||||||
|
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
|
||||||
|
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
|
||||||
|
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
|
||||||
|
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
|
||||||
|
.w3-bar .w3-button{white-space:normal}
|
||||||
|
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
|
||||||
|
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
|
||||||
|
.w3-responsive{display:block;overflow-x:auto}
|
||||||
|
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
|
||||||
|
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
|
||||||
|
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
|
||||||
|
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
|
||||||
|
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
|
||||||
|
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
|
||||||
|
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
|
||||||
|
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
|
||||||
|
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
|
||||||
|
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
|
||||||
|
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
|
||||||
|
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
|
||||||
|
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
|
||||||
|
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
|
||||||
|
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
|
||||||
|
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
|
||||||
|
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
|
||||||
|
@media (max-width:1205px){.w3-auto{max-width:95%}}
|
||||||
|
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
|
||||||
|
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
|
||||||
|
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
|
||||||
|
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
|
||||||
|
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
|
||||||
|
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
|
||||||
|
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
|
||||||
|
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
|
||||||
|
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
|
||||||
|
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
|
||||||
|
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
|
||||||
|
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
|
||||||
|
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
|
||||||
|
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
|
||||||
|
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
|
||||||
|
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
|
||||||
|
.w3-display-position{position:absolute}
|
||||||
|
.w3-circle{border-radius:50%}
|
||||||
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
|
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
|
||||||
|
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
|
||||||
|
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
|
||||||
|
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
|
||||||
|
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
|
||||||
|
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
|
||||||
|
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
|
||||||
|
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
|
||||||
|
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
|
||||||
|
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
|
||||||
|
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
|
||||||
|
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
|
||||||
|
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
|
||||||
|
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
|
||||||
|
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
|
||||||
|
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
|
||||||
|
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
|
||||||
|
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
|
||||||
|
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
|
||||||
|
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
|
||||||
|
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
|
||||||
|
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
|
||||||
|
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
|
||||||
|
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
|
||||||
|
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
|
||||||
|
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
|
||||||
|
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
|
||||||
|
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
|
||||||
|
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
|
||||||
|
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
|
||||||
|
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
|
||||||
|
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
|
||||||
|
.w3-left{float:left!important}.w3-right{float:right!important}
|
||||||
|
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||||
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
|
/* Colors */
|
||||||
|
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||||
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
|
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
|
||||||
|
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
|
||||||
|
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
|
||||||
|
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
|
||||||
|
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
|
||||||
|
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
|
||||||
|
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
|
||||||
|
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
|
||||||
|
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
|
||||||
|
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
|
||||||
|
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
|
||||||
|
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
|
||||||
|
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
|
||||||
|
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
|
||||||
|
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
|
||||||
|
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
|
||||||
|
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
|
||||||
|
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
|
||||||
|
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
|
||||||
|
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
|
||||||
|
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
|
||||||
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||||
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
|
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
|
||||||
|
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
|
||||||
|
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
|
||||||
|
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
|
||||||
|
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
|
||||||
|
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
|
||||||
|
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
|
||||||
|
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
|
||||||
|
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
|
||||||
|
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
|
||||||
|
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
|
||||||
|
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
|
||||||
|
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
|
||||||
|
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
|
||||||
|
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
|
||||||
|
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
|
||||||
|
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
|
||||||
|
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
|
||||||
|
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
|
||||||
|
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
|
||||||
|
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
|
||||||
|
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
|
||||||
|
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
|
||||||
|
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
|
||||||
|
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
|
||||||
|
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
|
||||||
|
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
|
||||||
|
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
|
||||||
|
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
|
||||||
|
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
|
||||||
|
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
|
||||||
|
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
|
||||||
|
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
|
||||||
|
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
|
||||||
|
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
|
||||||
|
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
|
||||||
|
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
|
||||||
|
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
|
||||||
|
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
|
||||||
|
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
|
||||||
|
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
|
||||||
|
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
|
||||||
|
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
|
||||||
|
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
|
||||||
|
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
|
||||||
|
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
|
||||||
|
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
|
||||||
|
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
|
||||||
|
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
|
||||||
|
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
|
||||||
|
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
|
||||||
|
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
|
||||||
|
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
|
||||||
|
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
|
||||||
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "💻",
|
"emoji": "💻",
|
||||||
"previous": "&33ngNe0YrH3JScss6krlCwddZcXl8C5szonp7DYy4qA=.sha256"
|
"previous": "&icsPplXHgmpkbNWyo/WTvRdT/A8BXxW4lJixOtP4ueQ=.sha256"
|
||||||
}
|
}
|
112
apps/apps/app.js
112
apps/apps/app.js
@ -1,20 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Fetches information about the applications
|
||||||
|
* @param apps Record<appName, blobId>
|
||||||
|
* @returns an object including the apps' name, emoji, and blobs ids
|
||||||
|
*/
|
||||||
async function fetch_info(apps) {
|
async function fetch_info(apps) {
|
||||||
let result = {};
|
let result = {};
|
||||||
|
|
||||||
|
// For each app
|
||||||
for (let [key, value] of Object.entries(apps)) {
|
for (let [key, value] of Object.entries(apps)) {
|
||||||
|
// Get it's blob and parse it
|
||||||
let blob = await ssb.blobGet(value);
|
let blob = await ssb.blobGet(value);
|
||||||
blob = blob ? utf8Decode(blob) : '{}';
|
blob = blob ? utf8Decode(blob) : '{}';
|
||||||
|
|
||||||
|
// Add it to the result object
|
||||||
result[key] = JSON.parse(blob);
|
result[key] = JSON.parse(blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
async function fetch_shared_apps() {
|
async function fetch_shared_apps() {
|
||||||
let messages = {};
|
let messages = {};
|
||||||
await ssb.sqlAsync(`
|
|
||||||
SELECT messages.*
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages_fts('"application/tildefriends"')
|
FROM messages_fts('"application/tildefriends"')
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
ORDER BY timestamp
|
ORDER BY messages.timestamp
|
||||||
`,
|
`,
|
||||||
[],
|
[],
|
||||||
function (row) {
|
function (row) {
|
||||||
@ -28,9 +45,13 @@ async function fetch_shared_apps() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let result = {};
|
let result = {};
|
||||||
for (let app of Object.values(messages).sort((x, y) => y.message.timestamp - x.message.timestamp)) {
|
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)));
|
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app.blob)));
|
||||||
if (app_object) {
|
if (app_object) {
|
||||||
app_object.blob_id = app.blob;
|
app_object.blob_id = app.blob;
|
||||||
@ -41,18 +62,26 @@ async function fetch_shared_apps() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
var apps = await fetch_info(await core.apps());
|
const apps = await fetch_info(await core.apps());
|
||||||
var core_apps = await fetch_info(await core.apps('core'));
|
const core_apps = await fetch_info(await core.apps('core'));
|
||||||
let shared_apps = await fetch_shared_apps();
|
const shared_apps = await fetch_shared_apps();
|
||||||
var doc = `<!DOCTYPE html>
|
|
||||||
<html>
|
const stylesheet = `
|
||||||
<head>
|
body {
|
||||||
<style>
|
color: whitesmoke;
|
||||||
|
font-family: sans-serif;
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
.container {
|
.container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, 64px);
|
grid-template-columns: repeat(auto-fill, 64px);
|
||||||
|
gap: 1em;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
|
background-color: #ffffff10;
|
||||||
|
border: 2px solid #073642;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
height: 96px;
|
height: 96px;
|
||||||
width: 64px;
|
width: 64px;
|
||||||
@ -67,24 +96,42 @@ async function main() {
|
|||||||
max-width: 64px;
|
max-width: 64px;
|
||||||
text-overflow: ellipsis ellipsis;
|
text-overflow: ellipsis ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
color: whitesmoke;
|
||||||
}
|
}
|
||||||
</style>
|
`;
|
||||||
</head>
|
|
||||||
<body style="background: #888">
|
const body = `
|
||||||
<h1 id="apps_title">Apps</h1>
|
<h1 style="text-shadow: #808080 0 0 10px;">Welcome to Tilde Friends.</h1>
|
||||||
|
|
||||||
|
<h2>your apps</h2>
|
||||||
<div id="apps" class="container"></div>
|
<div id="apps" class="container"></div>
|
||||||
<h1>Shared Apps</h1>
|
|
||||||
|
<h2>shared apps</h2>
|
||||||
<div id="shared_apps" class="container"></div>
|
<div id="shared_apps" class="container"></div>
|
||||||
<h1>Core Apps</h1>
|
|
||||||
|
<h2>core apps</h2>
|
||||||
<div id="core_apps" class="container"></div>
|
<div id="core_apps" class="container"></div>
|
||||||
</body>
|
`;
|
||||||
<script>
|
|
||||||
|
const script = `
|
||||||
|
/*
|
||||||
|
* Creates a list of apps
|
||||||
|
* @param id the id of the element to populate
|
||||||
|
* @param name (a username, 'core' or undefined)
|
||||||
|
* @param apps Object, a list of apps
|
||||||
|
*/
|
||||||
function populate_apps(id, name, apps) {
|
function populate_apps(id, name, apps) {
|
||||||
|
// Our target
|
||||||
var list = document.getElementById(id);
|
var list = document.getElementById(id);
|
||||||
|
|
||||||
|
// For each app in the provided list
|
||||||
for (let app of Object.keys(apps).sort()) {
|
for (let app of Object.keys(apps).sort()) {
|
||||||
|
|
||||||
|
// Create the item
|
||||||
let div = list.appendChild(document.createElement('div'));
|
let div = list.appendChild(document.createElement('div'));
|
||||||
div.classList.add('app');
|
div.classList.add('app');
|
||||||
|
|
||||||
|
// The app's icon
|
||||||
let href = name ? '/~' + name + '/' + app + '/' : ('/' + apps[app].blob_id + '/');
|
let href = name ? '/~' + name + '/' + app + '/' : ('/' + apps[app].blob_id + '/');
|
||||||
let icon_a = document.createElement('a');
|
let icon_a = document.createElement('a');
|
||||||
let icon = document.createElement('div');
|
let icon = document.createElement('div');
|
||||||
@ -95,6 +142,7 @@ async function main() {
|
|||||||
icon_a.target = '_top';
|
icon_a.target = '_top';
|
||||||
div.appendChild(icon_a);
|
div.appendChild(icon_a);
|
||||||
|
|
||||||
|
// The app's name
|
||||||
let a = document.createElement('a');
|
let a = document.createElement('a');
|
||||||
a.appendChild(document.createTextNode(app));
|
a.appendChild(document.createTextNode(app));
|
||||||
a.href = href;
|
a.href = href;
|
||||||
@ -102,13 +150,33 @@ async function main() {
|
|||||||
div.appendChild(a);
|
div.appendChild(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.getElementById('apps_title').innerText = "~${escape(core.user.credentials?.session?.name || 'guest')}'s Apps";
|
|
||||||
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
|
||||||
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
|
||||||
populate_apps('shared_apps', undefined, ${JSON.stringify(shared_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>
|
</script>
|
||||||
</html>`;
|
</html>`;
|
||||||
app.setDocument(doc);
|
|
||||||
|
// Send it to the browser
|
||||||
|
app.setDocument(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
import * as commonmark from './commonmark.min.js';
|
import * as commonmark from './commonmark.min.js';
|
||||||
|
|
||||||
function escape(text) {
|
function escape(text) {
|
||||||
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
return (text ?? '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeAttribute(text) {
|
function escapeAttribute(text) {
|
||||||
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
return (text ?? '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_blog_message(id) {
|
export async function get_blog_message(id) {
|
||||||
@ -21,7 +29,8 @@ export async function get_blog_message(id) {
|
|||||||
blog: content?.blog,
|
blog: content?.blog,
|
||||||
title: content?.title,
|
title: content?.title,
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
);
|
||||||
if (message) {
|
if (message) {
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
`
|
`
|
||||||
@ -36,7 +45,8 @@ export async function get_blog_message(id) {
|
|||||||
[message.author],
|
[message.author],
|
||||||
function (row) {
|
function (row) {
|
||||||
message.name = row.name;
|
message.name = row.name;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
@ -51,8 +61,12 @@ export function markdown(md) {
|
|||||||
node = event.node;
|
node = event.node;
|
||||||
if (event.entering) {
|
if (event.entering) {
|
||||||
if (node.destination?.startsWith('&')) {
|
if (node.destination?.startsWith('&')) {
|
||||||
node.destination = '/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
node.destination =
|
||||||
} else if (node.destination?.startsWith('@') || node.destination?.startsWith('%')) {
|
'/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
||||||
|
} else if (
|
||||||
|
node.destination?.startsWith('@') ||
|
||||||
|
node.destination?.startsWith('%')
|
||||||
|
) {
|
||||||
node.destination = '/~core/ssb/#' + escape(node.destination);
|
node.destination = '/~core/ssb/#' + escape(node.destination);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +121,7 @@ export function render_html(blogs) {
|
|||||||
<h1>🪵Tilde Friends Blog</h1>
|
<h1>🪵Tilde Friends Blog</h1>
|
||||||
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
|
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
|
||||||
</div>
|
</div>
|
||||||
${blogs.map(blog_post => render_blog_post(blog_post)).join('\n')}
|
${blogs.map((blog_post) => render_blog_post(blog_post)).join('\n')}
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
@ -135,14 +149,15 @@ export function render_atom(blogs) {
|
|||||||
<link href="${core.url}"/>
|
<link href="${core.url}"/>
|
||||||
<id>${core.url}</id>
|
<id>${core.url}</id>
|
||||||
<updated>${new Date().toString()}</updated>
|
<updated>${new Date().toString()}</updated>
|
||||||
${blogs.map(blog_post => render_blog_post_atom(blog_post)).join('\n')}
|
${blogs.map((blog_post) => render_blog_post_atom(blog_post)).join('\n')}
|
||||||
</feed>`;
|
</feed>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_posts() {
|
export async function get_posts() {
|
||||||
let blogs = [];
|
let blogs = [];
|
||||||
let ids = await ssb.getIdentities();
|
let ids = await ssb.getIdentities();
|
||||||
await ssb.sqlAsync(`
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
WITH
|
WITH
|
||||||
blogs AS (
|
blogs AS (
|
||||||
SELECT
|
SELECT
|
||||||
@ -182,8 +197,11 @@ export async function get_posts() {
|
|||||||
JOIN public ON public.author = blogs.author
|
JOIN public ON public.author = blogs.author
|
||||||
LEFT OUTER JOIN names ON names.author = blogs.author
|
LEFT OUTER JOIN names ON names.author = blogs.author
|
||||||
ORDER BY blogs.timestamp DESC LIMIT 20
|
ORDER BY blogs.timestamp DESC LIMIT 20
|
||||||
`, [JSON.stringify(ids)], function(row) {
|
`,
|
||||||
|
[JSON.stringify(ids)],
|
||||||
|
function (row) {
|
||||||
blogs.push(row);
|
blogs.push(row);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return blogs;
|
return blogs;
|
||||||
}
|
}
|
@ -2,30 +2,50 @@ import * as blog from './blog.js';
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (request.path.startsWith('%') && request.path.endsWith('.sha256')) {
|
if (request.path.startsWith('%') && request.path.endsWith('.sha256')) {
|
||||||
let id = request.path.startsWith('%25') ? '%' + request.path.substring(3) : request.path;
|
let id = request.path.startsWith('%25')
|
||||||
|
? '%' + request.path.substring(3)
|
||||||
|
: request.path;
|
||||||
let message = await blog.get_blog_message(id);
|
let message = await blog.get_blog_message(id);
|
||||||
if (message) {
|
if (message) {
|
||||||
respond({data: await blog.render_blog_post_html(message), content_type: 'text/html; charset=utf-8'});
|
respond({
|
||||||
|
data: await blog.render_blog_post_html(message),
|
||||||
|
content_type: 'text/html; charset=utf-8',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
respond({data: `Message ${id} not found.`, content_type: 'text/html; charset=utf-8'});
|
respond({
|
||||||
|
data: `Message ${id} not found.`,
|
||||||
|
content_type: 'text/html; charset=utf-8',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (request.path == 'atom') {
|
} else if (request.path == 'atom') {
|
||||||
let blogs = await blog.get_posts();
|
let blogs = await blog.get_posts();
|
||||||
respond({data: blog.render_atom(blogs), content_type: 'application/atom+xml'});
|
respond({
|
||||||
|
data: blog.render_atom(blogs),
|
||||||
|
content_type: 'application/atom+xml',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
let blogs = await blog.get_posts();
|
let blogs = await blog.get_posts();
|
||||||
for (let blog_post of blogs) {
|
for (let blog_post of blogs) {
|
||||||
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
|
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
|
||||||
if (request.path === title) {
|
if (request.path === title) {
|
||||||
respond({data: await blog.render_blog_post_html(blog_post), content_type: 'text/html; charset=utf-8'});
|
respond({
|
||||||
|
data: await blog.render_blog_post_html(blog_post),
|
||||||
|
content_type: 'text/html; charset=utf-8',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
respond({data: blog.render_html(blogs), content_type: 'text/html; charset=utf-8'});
|
respond({
|
||||||
|
data: blog.render_html(blogs),
|
||||||
|
content_type: 'text/html; charset=utf-8',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(function (error) {
|
main().catch(function (error) {
|
||||||
respond({data: `<!DOCTYPE html>
|
respond({
|
||||||
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`, content_type: 'text/html'});
|
data: `<!DOCTYPE html>
|
||||||
|
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`,
|
||||||
|
content_type: 'text/html',
|
||||||
|
});
|
||||||
});
|
});
|
44
apps/blog/lit-all.min.js
vendored
44
apps/blog/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -21,7 +21,8 @@ async function contacts_internal(id, last_row_id, following, max_row_id) {
|
|||||||
json_extract(content, '$.type') = 'contact'
|
json_extract(content, '$.type') = 'contact'
|
||||||
ORDER BY sequence
|
ORDER BY sequence
|
||||||
`,
|
`,
|
||||||
[id, last_row_id, max_row_id]);
|
[id, last_row_id, max_row_id]
|
||||||
|
);
|
||||||
for (let row of contacts) {
|
for (let row of contacts) {
|
||||||
let contact = JSON.parse(row.content);
|
let contact = JSON.parse(row.content);
|
||||||
if (contact.following === true) {
|
if (contact.following === true) {
|
||||||
@ -42,15 +43,34 @@ async function contact(id, last_row_id, following, max_row_id) {
|
|||||||
return await contacts_internal(id, last_row_id, following, max_row_id);
|
return await contacts_internal(id, last_row_id, following, max_row_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) {
|
async function following_deep_internal(
|
||||||
let contacts = await Promise.all([...new Set(ids)].map(x => contact(x, last_row_id, following, max_row_id)));
|
ids,
|
||||||
|
depth,
|
||||||
|
blocking,
|
||||||
|
last_row_id,
|
||||||
|
following,
|
||||||
|
max_row_id
|
||||||
|
) {
|
||||||
|
let contacts = await Promise.all(
|
||||||
|
[...new Set(ids)].map((x) => contact(x, last_row_id, following, max_row_id))
|
||||||
|
);
|
||||||
let result = {};
|
let result = {};
|
||||||
for (let i = 0; i < ids.length; i++) {
|
for (let i = 0; i < ids.length; i++) {
|
||||||
let id = ids[i];
|
let id = ids[i];
|
||||||
let contact = contacts[i];
|
let contact = contacts[i];
|
||||||
let all_blocking = Object.assign({}, contact.blocking, blocking);
|
let all_blocking = Object.assign({}, contact.blocking, blocking);
|
||||||
let found = Object.keys(contact.following).filter(y => !all_blocking[y]);
|
let found = Object.keys(contact.following).filter((y) => !all_blocking[y]);
|
||||||
let deeper = depth > 1 ? await following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : [];
|
let deeper =
|
||||||
|
depth > 1
|
||||||
|
? await following_deep_internal(
|
||||||
|
found,
|
||||||
|
depth - 1,
|
||||||
|
all_blocking,
|
||||||
|
last_row_id,
|
||||||
|
following,
|
||||||
|
max_row_id
|
||||||
|
)
|
||||||
|
: [];
|
||||||
result[id] = [id, ...found, ...deeper];
|
result[id] = [id, ...found, ...deeper];
|
||||||
}
|
}
|
||||||
return [...new Set(Object.values(result).flat())];
|
return [...new Set(Object.values(result).flat())];
|
||||||
@ -68,10 +88,22 @@ async function following_deep(ids, depth, blocking) {
|
|||||||
last_row_id: 0,
|
last_row_id: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let max_row_id = (await query(`
|
let max_row_id = (
|
||||||
|
await query(
|
||||||
|
`
|
||||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
`, []))[0].max_row_id;
|
`,
|
||||||
let result = await following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
|
[]
|
||||||
|
)
|
||||||
|
)[0].max_row_id;
|
||||||
|
let result = await following_deep_internal(
|
||||||
|
ids,
|
||||||
|
depth,
|
||||||
|
blocking,
|
||||||
|
cache.last_row_id,
|
||||||
|
cache.following,
|
||||||
|
max_row_id
|
||||||
|
);
|
||||||
cache.last_row_id = max_row_id;
|
cache.last_row_id = max_row_id;
|
||||||
let store = JSON.stringify(cache);
|
let store = JSON.stringify(cache);
|
||||||
await db.set('following', store);
|
await db.set('following', store);
|
||||||
@ -90,13 +122,15 @@ async function fetch_about(db, ids, users) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
let max_row_id = 0;
|
let max_row_id = 0;
|
||||||
await ssb.sqlAsync(`
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
`,
|
`,
|
||||||
[],
|
[],
|
||||||
function (row) {
|
function (row) {
|
||||||
max_row_id = row.max_row_id;
|
max_row_id = row.max_row_id;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
for (let id of Object.keys(cache.about)) {
|
for (let id of Object.keys(cache.about)) {
|
||||||
if (ids.indexOf(id) == -1) {
|
if (ids.indexOf(id) == -1) {
|
||||||
delete cache.about[id];
|
delete cache.about[id];
|
||||||
@ -129,17 +163,21 @@ async function fetch_about(db, ids, users) {
|
|||||||
ORDER BY messages.author, messages.sequence
|
ORDER BY messages.author, messages.sequence
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
JSON.stringify(ids.filter(id => cache.about[id])),
|
JSON.stringify(ids.filter((id) => cache.about[id])),
|
||||||
JSON.stringify(ids.filter(id => !cache.about[id])),
|
JSON.stringify(ids.filter((id) => !cache.about[id])),
|
||||||
cache.last_row_id,
|
cache.last_row_id,
|
||||||
max_row_id,
|
max_row_id,
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
for (let about of abouts) {
|
for (let about of abouts) {
|
||||||
let content = JSON.parse(about.content);
|
let content = JSON.parse(about.content);
|
||||||
if (content.about === about.author) {
|
if (content.about === about.author) {
|
||||||
delete content.type;
|
delete content.type;
|
||||||
delete content.about;
|
delete content.about;
|
||||||
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
|
cache.about[about.author] = Object.assign(
|
||||||
|
cache.about[about.author] || {},
|
||||||
|
content
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache.last_row_id = max_row_id;
|
cache.last_row_id = max_row_id;
|
||||||
@ -155,24 +193,24 @@ async function getAbout(db, id) {
|
|||||||
if (g_about_cache[id]) {
|
if (g_about_cache[id]) {
|
||||||
return g_about_cache[id];
|
return g_about_cache[id];
|
||||||
}
|
}
|
||||||
let o = await db.get(id + ":about");
|
let o = await db.get(id + ':about');
|
||||||
const k_version = 4;
|
const k_version = 4;
|
||||||
let f = o ? JSON.parse(o) : o;
|
let f = o ? JSON.parse(o) : o;
|
||||||
if (!f || f.version != k_version) {
|
if (!f || f.version != k_version) {
|
||||||
f = {about: {}, sequence: 0, version: k_version};
|
f = {about: {}, sequence: 0, version: k_version};
|
||||||
}
|
}
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
"SELECT "+
|
'SELECT ' +
|
||||||
" sequence, "+
|
' sequence, ' +
|
||||||
" content "+
|
' content ' +
|
||||||
"FROM messages "+
|
'FROM messages ' +
|
||||||
"WHERE "+
|
'WHERE ' +
|
||||||
" author = ?1 AND "+
|
' author = ?1 AND ' +
|
||||||
" sequence > ?2 AND "+
|
' sequence > ?2 AND ' +
|
||||||
" json_extract(content, '$.type') = 'about' AND " +
|
" json_extract(content, '$.type') = 'about' AND " +
|
||||||
" json_extract(content, '$.about') = ?1 " +
|
" json_extract(content, '$.about') = ?1 " +
|
||||||
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
|
'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
|
||||||
"ORDER BY sequence",
|
'ORDER BY sequence',
|
||||||
[id, f.sequence],
|
[id, f.sequence],
|
||||||
function (row) {
|
function (row) {
|
||||||
f.sequence = row.sequence;
|
f.sequence = row.sequence;
|
||||||
@ -180,16 +218,16 @@ async function getAbout(db, id) {
|
|||||||
let about = {};
|
let about = {};
|
||||||
try {
|
try {
|
||||||
about = JSON.parse(row.content);
|
about = JSON.parse(row.content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
delete about.about;
|
delete about.about;
|
||||||
delete about.type;
|
delete about.type;
|
||||||
f.about = Object.assign(f.about, about);
|
f.about = Object.assign(f.about, about);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
let j = JSON.stringify(f);
|
let j = JSON.stringify(f);
|
||||||
if (o != j) {
|
if (o != j) {
|
||||||
await db.set(id + ":about", j);
|
await db.set(id + ':about', j);
|
||||||
}
|
}
|
||||||
g_about_cache[id] = f.about;
|
g_about_cache[id] = f.about;
|
||||||
return f.about;
|
return f.about;
|
||||||
@ -198,15 +236,15 @@ async function getAbout(db, id) {
|
|||||||
async function getSize(db, id) {
|
async function getSize(db, id) {
|
||||||
let size = 0;
|
let size = 0;
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
"SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
|
'SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1',
|
||||||
[id],
|
[id],
|
||||||
function (row) {
|
function (row) {
|
||||||
size += row.size;
|
size += row.size;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function getSizes(ids) {
|
async function getSizes(ids) {
|
||||||
let sizes = {};
|
let sizes = {};
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
@ -221,7 +259,8 @@ async function getSizes(ids) {
|
|||||||
[JSON.stringify(ids)],
|
[JSON.stringify(ids)],
|
||||||
function (row) {
|
function (row) {
|
||||||
sizes[row.author] = row.size;
|
sizes[row.author] = row.size;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return sizes;
|
return sizes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +280,10 @@ function niceSize(bytes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function escape(value) {
|
function escape(value) {
|
||||||
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
return value
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@ -249,19 +291,27 @@ async function main() {
|
|||||||
let db = await database('ssb');
|
let db = await database('ssb');
|
||||||
let whoami = await ssb.getIdentities();
|
let whoami = await ssb.getIdentities();
|
||||||
let tree = '';
|
let tree = '';
|
||||||
await app.setDocument(`<pre style="color: #fff">Enumerating followed users...</pre>`);
|
await app.setDocument(
|
||||||
|
`<pre style="color: #fff">Enumerating followed users...</pre>`
|
||||||
|
);
|
||||||
let following = await following_deep(whoami, 2, {});
|
let following = await following_deep(whoami, 2, {});
|
||||||
await app.setDocument(`<pre style="color: #fff">Getting names and sizes...</pre>`);
|
await app.setDocument(
|
||||||
|
`<pre style="color: #fff">Getting names and sizes...</pre>`
|
||||||
|
);
|
||||||
let [about, sizes] = await Promise.all([
|
let [about, sizes] = await Promise.all([
|
||||||
fetch_about(db, following, {}),
|
fetch_about(db, following, {}),
|
||||||
getSizes(following),
|
getSizes(following),
|
||||||
]);
|
]);
|
||||||
await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
|
await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
|
||||||
following.sort((a, b) => ((sizes[b] ?? 0) - (sizes[a] ?? 0)));
|
following.sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0));
|
||||||
for (let id of following) {
|
for (let id of following) {
|
||||||
tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
|
tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
|
||||||
}
|
}
|
||||||
await app.setDocument('<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' + tree + '</ul>\n</body>\n</html>');
|
await app.setDocument(
|
||||||
|
'<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' +
|
||||||
|
tree +
|
||||||
|
'</ul>\n</body>\n</html>'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "tildefriends-app",
|
|
||||||
"emoji": "🗺",
|
|
||||||
"previous": "&0XSp+xdQwVtQ88bXzvWdH15Ex63hv5zUKTa4zx7HBGM=.sha256"
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
import * as tfrpc from '/tfrpc.js';
|
|
||||||
import * as strava from './strava.js';
|
|
||||||
|
|
||||||
let g_database;
|
|
||||||
let g_shared_database;
|
|
||||||
|
|
||||||
tfrpc.register(async function createIdentity() {
|
|
||||||
return ssb.createIdentity();
|
|
||||||
});
|
|
||||||
tfrpc.register(async function appendMessage(id, message) {
|
|
||||||
print('APPEND', JSON.stringify(message));
|
|
||||||
return ssb.appendMessageWithIdentity(id, message);
|
|
||||||
});
|
|
||||||
tfrpc.register(function url() {
|
|
||||||
return core.url;
|
|
||||||
});
|
|
||||||
tfrpc.register(async function getUser() {
|
|
||||||
return core.user;
|
|
||||||
});
|
|
||||||
tfrpc.register(function getIdentities() {
|
|
||||||
return ssb.getIdentities();
|
|
||||||
});
|
|
||||||
tfrpc.register(async function databaseGet(key) {
|
|
||||||
return g_database ? g_database.get(key) : undefined;
|
|
||||||
});
|
|
||||||
tfrpc.register(async function databaseSet(key, value) {
|
|
||||||
return g_database ? g_database.set(key, value) : undefined;
|
|
||||||
});
|
|
||||||
tfrpc.register(async function databaseRemove(key, value) {
|
|
||||||
return g_database ? g_database.remove(key, value) : undefined;
|
|
||||||
});
|
|
||||||
tfrpc.register(async function sharedDatabaseGet(key) {
|
|
||||||
return g_shared_database ? g_shared_database.get(key) : undefined;
|
|
||||||
});
|
|
||||||
tfrpc.register(async function sharedDatabaseSet(key, value) {
|
|
||||||
return g_shared_database ? g_shared_database.set(key, value) : undefined;
|
|
||||||
});
|
|
||||||
tfrpc.register(async function sharedDatabaseRemove(key, value) {
|
|
||||||
return g_shared_database ? g_shared_database.remove(key, value) : undefined;
|
|
||||||
});
|
|
||||||
tfrpc.register(async function query(sql, args) {
|
|
||||||
let result = [];
|
|
||||||
await ssb.sqlAsync(sql, args, function callback(row) {
|
|
||||||
result.push(row);
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
tfrpc.register(async function store_blob(blob) {
|
|
||||||
if (typeof(blob) == 'string') {
|
|
||||||
blob = utf8Encode(blob);
|
|
||||||
}
|
|
||||||
if (Array.isArray(blob)) {
|
|
||||||
blob = Uint8Array.from(blob);
|
|
||||||
}
|
|
||||||
return await ssb.blobStore(blob);
|
|
||||||
});
|
|
||||||
|
|
||||||
tfrpc.register(async function get_blob(id) {
|
|
||||||
return utf8Decode(await ssb.blobGet(id));
|
|
||||||
});
|
|
||||||
tfrpc.register(strava.refresh_token);
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
g_shared_database = await shared_database('state');
|
|
||||||
if (core.user.credentials?.session?.name) {
|
|
||||||
g_database = await database('state');
|
|
||||||
}
|
|
||||||
|
|
||||||
let attempt;
|
|
||||||
if (core.user.credentials?.session?.name) {
|
|
||||||
let shared_db = await shared_database('state');
|
|
||||||
attempt = await shared_db.get(core.user.credentials.session.name);
|
|
||||||
}
|
|
||||||
app.setDocument(utf8Decode(getFile('index.html')).replace('${data}', JSON.stringify({
|
|
||||||
attempt: attempt,
|
|
||||||
state: core.user?.credentials?.session?.name,
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
File diff suppressed because one or more lines are too long
@ -1,81 +0,0 @@
|
|||||||
function xml_parse(xml) {
|
|
||||||
let result;
|
|
||||||
let path = [];
|
|
||||||
let tag_begin;
|
|
||||||
let text_begin;
|
|
||||||
for (let i = 0; i < xml.length; i++) {
|
|
||||||
let c = xml.charAt(i);
|
|
||||||
if (!tag_begin && c == '<') {
|
|
||||||
if (i > text_begin && path.length) {
|
|
||||||
let value = xml.substring(text_begin, i);
|
|
||||||
if (!/^\s*$/.test(value)) {
|
|
||||||
path[path.length - 1].value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tag_begin = i + 1;
|
|
||||||
} else if (tag_begin && c == '>') {
|
|
||||||
let tag = xml.substring(tag_begin, i).trim();
|
|
||||||
if (tag.startsWith('?') && tag.endsWith('?')) {
|
|
||||||
/* Ignore directives. */
|
|
||||||
} else if (tag.startsWith('/')) {
|
|
||||||
path.pop();
|
|
||||||
} else {
|
|
||||||
let parts = tag.split(' ');
|
|
||||||
let attributes = {};
|
|
||||||
for (let j = 1; j < parts.length; j++) {
|
|
||||||
let eq = parts[j].indexOf('=');
|
|
||||||
let value = parts[j].substring(eq + 1);
|
|
||||||
if (value.startsWith('"') && value.endsWith('"')) {
|
|
||||||
value = value.substring(1, value.length - 1);
|
|
||||||
}
|
|
||||||
attributes[parts[j].substring(0, eq)] = value;
|
|
||||||
}
|
|
||||||
let next = {name: parts[0], children: [], attributes: attributes};
|
|
||||||
if (path.length) {
|
|
||||||
path[path.length - 1].children.push(next);
|
|
||||||
} else {
|
|
||||||
result = next;
|
|
||||||
}
|
|
||||||
if (!tag.endsWith('/')) {
|
|
||||||
path.push(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tag_begin = undefined;
|
|
||||||
text_begin = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function* xml_each(node, name) {
|
|
||||||
for (let child of node.children) {
|
|
||||||
if (child.name == name) {
|
|
||||||
yield child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function gpx_parse(xml) {
|
|
||||||
let result = {segments: []};
|
|
||||||
let tree = xml_parse(xml);
|
|
||||||
if (tree?.name == 'gpx') {
|
|
||||||
for (let trk of xml_each(tree, 'trk')) {
|
|
||||||
for (let trkseg of xml_each(trk, 'trkseg')) {
|
|
||||||
let segment = [];
|
|
||||||
for (let trkpt of xml_each(trkseg, 'trkpt')) {
|
|
||||||
segment.push({lat: parseFloat(trkpt.attributes.lat), lon: parseFloat(trkpt.attributes.lon)});
|
|
||||||
}
|
|
||||||
result.segments.push(segment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let metadata of xml_each(tree, 'metadata')) {
|
|
||||||
for (let link of xml_each(metadata, 'link')) {
|
|
||||||
result.link = link.attributes.href;
|
|
||||||
}
|
|
||||||
for (let time of xml_each(metadata, 'time')) {
|
|
||||||
result.time = time.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import * as strava from './strava.js';
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
print('handler running');
|
|
||||||
let r = await strava.authorization_code(request.query.code);
|
|
||||||
print('state =', request.query.state);
|
|
||||||
print('body = ', r.body);
|
|
||||||
if (request.query.state && r.body) {
|
|
||||||
let shared_db = await shared_database('state');
|
|
||||||
await shared_db.set(request.query.state, utf8Decode(r.body));
|
|
||||||
}
|
|
||||||
await respond({
|
|
||||||
data: r.body,
|
|
||||||
content_type: 'text/plain',
|
|
||||||
headers: {
|
|
||||||
Location: 'https://tildefriends.net/~cory/gg/',
|
|
||||||
},
|
|
||||||
status_code: 307,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
main();
|
|
@ -1,14 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html style="width: 100%; height: 100%; margin: 0; padding: 0">
|
|
||||||
<head>
|
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
|
||||||
<script>
|
|
||||||
let g_data = ${data};
|
|
||||||
</script>
|
|
||||||
<script src="script.js" type="module"></script>
|
|
||||||
<script src="leaflet.js"></script>
|
|
||||||
</head>
|
|
||||||
<body style="color: #fff; display: flex; flex-flow: column; height: 100%; width: 100%; margin: 0; padding: 0">
|
|
||||||
<gg-app style="width: 100%; height: 100%" id="ggapp"></gg-app>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,661 +0,0 @@
|
|||||||
/* required styles */
|
|
||||||
|
|
||||||
.leaflet-pane,
|
|
||||||
.leaflet-tile,
|
|
||||||
.leaflet-marker-icon,
|
|
||||||
.leaflet-marker-shadow,
|
|
||||||
.leaflet-tile-container,
|
|
||||||
.leaflet-pane > svg,
|
|
||||||
.leaflet-pane > canvas,
|
|
||||||
.leaflet-zoom-box,
|
|
||||||
.leaflet-image-layer,
|
|
||||||
.leaflet-layer {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
.leaflet-container {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.leaflet-tile,
|
|
||||||
.leaflet-marker-icon,
|
|
||||||
.leaflet-marker-shadow {
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-drag: none;
|
|
||||||
}
|
|
||||||
/* Prevents IE11 from highlighting tiles in blue */
|
|
||||||
.leaflet-tile::selection {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
|
||||||
.leaflet-safari .leaflet-tile {
|
|
||||||
image-rendering: -webkit-optimize-contrast;
|
|
||||||
}
|
|
||||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
|
||||||
.leaflet-safari .leaflet-tile-container {
|
|
||||||
width: 1600px;
|
|
||||||
height: 1600px;
|
|
||||||
-webkit-transform-origin: 0 0;
|
|
||||||
}
|
|
||||||
.leaflet-marker-icon,
|
|
||||||
.leaflet-marker-shadow {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
|
||||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
|
||||||
.leaflet-container .leaflet-overlay-pane svg {
|
|
||||||
max-width: none !important;
|
|
||||||
max-height: none !important;
|
|
||||||
}
|
|
||||||
.leaflet-container .leaflet-marker-pane img,
|
|
||||||
.leaflet-container .leaflet-shadow-pane img,
|
|
||||||
.leaflet-container .leaflet-tile-pane img,
|
|
||||||
.leaflet-container img.leaflet-image-layer,
|
|
||||||
.leaflet-container .leaflet-tile {
|
|
||||||
max-width: none !important;
|
|
||||||
max-height: none !important;
|
|
||||||
width: auto;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-container img.leaflet-tile {
|
|
||||||
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
|
||||||
mix-blend-mode: plus-lighter;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-container.leaflet-touch-zoom {
|
|
||||||
-ms-touch-action: pan-x pan-y;
|
|
||||||
touch-action: pan-x pan-y;
|
|
||||||
}
|
|
||||||
.leaflet-container.leaflet-touch-drag {
|
|
||||||
-ms-touch-action: pinch-zoom;
|
|
||||||
/* Fallback for FF which doesn't support pinch-zoom */
|
|
||||||
touch-action: none;
|
|
||||||
touch-action: pinch-zoom;
|
|
||||||
}
|
|
||||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
|
||||||
-ms-touch-action: none;
|
|
||||||
touch-action: none;
|
|
||||||
}
|
|
||||||
.leaflet-container {
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
|
||||||
.leaflet-container a {
|
|
||||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
|
||||||
}
|
|
||||||
.leaflet-tile {
|
|
||||||
filter: inherit;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
.leaflet-tile-loaded {
|
|
||||||
visibility: inherit;
|
|
||||||
}
|
|
||||||
.leaflet-zoom-box {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
z-index: 800;
|
|
||||||
}
|
|
||||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
|
||||||
.leaflet-overlay-pane svg {
|
|
||||||
-moz-user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-pane { z-index: 400; }
|
|
||||||
|
|
||||||
.leaflet-tile-pane { z-index: 200; }
|
|
||||||
.leaflet-overlay-pane { z-index: 400; }
|
|
||||||
.leaflet-shadow-pane { z-index: 500; }
|
|
||||||
.leaflet-marker-pane { z-index: 600; }
|
|
||||||
.leaflet-tooltip-pane { z-index: 650; }
|
|
||||||
.leaflet-popup-pane { z-index: 700; }
|
|
||||||
|
|
||||||
.leaflet-map-pane canvas { z-index: 100; }
|
|
||||||
.leaflet-map-pane svg { z-index: 200; }
|
|
||||||
|
|
||||||
.leaflet-vml-shape {
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
}
|
|
||||||
.lvml {
|
|
||||||
behavior: url(#default#VML);
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* control positioning */
|
|
||||||
|
|
||||||
.leaflet-control {
|
|
||||||
position: relative;
|
|
||||||
z-index: 800;
|
|
||||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
.leaflet-top,
|
|
||||||
.leaflet-bottom {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1000;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.leaflet-top {
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
.leaflet-right {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
.leaflet-bottom {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
.leaflet-left {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
.leaflet-control {
|
|
||||||
float: left;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
.leaflet-right .leaflet-control {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
.leaflet-top .leaflet-control {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
.leaflet-bottom .leaflet-control {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.leaflet-left .leaflet-control {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.leaflet-right .leaflet-control {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* zoom and fade animations */
|
|
||||||
|
|
||||||
.leaflet-fade-anim .leaflet-popup {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transition: opacity 0.2s linear;
|
|
||||||
-moz-transition: opacity 0.2s linear;
|
|
||||||
transition: opacity 0.2s linear;
|
|
||||||
}
|
|
||||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.leaflet-zoom-animated {
|
|
||||||
-webkit-transform-origin: 0 0;
|
|
||||||
-ms-transform-origin: 0 0;
|
|
||||||
transform-origin: 0 0;
|
|
||||||
}
|
|
||||||
svg.leaflet-zoom-animated {
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
|
||||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
|
||||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
|
||||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
|
||||||
}
|
|
||||||
.leaflet-zoom-anim .leaflet-tile,
|
|
||||||
.leaflet-pan-anim .leaflet-tile {
|
|
||||||
-webkit-transition: none;
|
|
||||||
-moz-transition: none;
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* cursors */
|
|
||||||
|
|
||||||
.leaflet-interactive {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.leaflet-grab {
|
|
||||||
cursor: -webkit-grab;
|
|
||||||
cursor: -moz-grab;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
.leaflet-crosshair,
|
|
||||||
.leaflet-crosshair .leaflet-interactive {
|
|
||||||
cursor: crosshair;
|
|
||||||
}
|
|
||||||
.leaflet-popup-pane,
|
|
||||||
.leaflet-control {
|
|
||||||
cursor: auto;
|
|
||||||
}
|
|
||||||
.leaflet-dragging .leaflet-grab,
|
|
||||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
|
||||||
.leaflet-dragging .leaflet-marker-draggable {
|
|
||||||
cursor: move;
|
|
||||||
cursor: -webkit-grabbing;
|
|
||||||
cursor: -moz-grabbing;
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* marker & overlays interactivity */
|
|
||||||
.leaflet-marker-icon,
|
|
||||||
.leaflet-marker-shadow,
|
|
||||||
.leaflet-image-layer,
|
|
||||||
.leaflet-pane > svg path,
|
|
||||||
.leaflet-tile-container {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-marker-icon.leaflet-interactive,
|
|
||||||
.leaflet-image-layer.leaflet-interactive,
|
|
||||||
.leaflet-pane > svg path.leaflet-interactive,
|
|
||||||
svg.leaflet-image-layer.leaflet-interactive path {
|
|
||||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* visual tweaks */
|
|
||||||
|
|
||||||
.leaflet-container {
|
|
||||||
background: #ddd;
|
|
||||||
outline-offset: 1px;
|
|
||||||
}
|
|
||||||
.leaflet-container a {
|
|
||||||
color: #0078A8;
|
|
||||||
}
|
|
||||||
.leaflet-zoom-box {
|
|
||||||
border: 2px dotted #38f;
|
|
||||||
background: rgba(255,255,255,0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* general typography */
|
|
||||||
.leaflet-container {
|
|
||||||
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
|
||||||
font-size: 12px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* general toolbar styles */
|
|
||||||
|
|
||||||
.leaflet-bar {
|
|
||||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.leaflet-bar a {
|
|
||||||
background-color: #fff;
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
line-height: 26px;
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
.leaflet-bar a,
|
|
||||||
.leaflet-control-layers-toggle {
|
|
||||||
background-position: 50% 50%;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.leaflet-bar a:hover,
|
|
||||||
.leaflet-bar a:focus {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
.leaflet-bar a:first-child {
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
}
|
|
||||||
.leaflet-bar a:last-child {
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
border-bottom-right-radius: 4px;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
.leaflet-bar a.leaflet-disabled {
|
|
||||||
cursor: default;
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
color: #bbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-touch .leaflet-bar a {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
.leaflet-touch .leaflet-bar a:first-child {
|
|
||||||
border-top-left-radius: 2px;
|
|
||||||
border-top-right-radius: 2px;
|
|
||||||
}
|
|
||||||
.leaflet-touch .leaflet-bar a:last-child {
|
|
||||||
border-bottom-left-radius: 2px;
|
|
||||||
border-bottom-right-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* zoom control */
|
|
||||||
|
|
||||||
.leaflet-control-zoom-in,
|
|
||||||
.leaflet-control-zoom-out {
|
|
||||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
|
||||||
text-indent: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* layers control */
|
|
||||||
|
|
||||||
.leaflet-control-layers {
|
|
||||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-toggle {
|
|
||||||
background-image: url(images/layers.png);
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
.leaflet-retina .leaflet-control-layers-toggle {
|
|
||||||
background-image: url(images/layers-2x.png);
|
|
||||||
background-size: 26px 26px;
|
|
||||||
}
|
|
||||||
.leaflet-touch .leaflet-control-layers-toggle {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers .leaflet-control-layers-list,
|
|
||||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-expanded {
|
|
||||||
padding: 6px 10px 6px 6px;
|
|
||||||
color: #333;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-scrollbar {
|
|
||||||
overflow-y: scroll;
|
|
||||||
overflow-x: hidden;
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-selector {
|
|
||||||
margin-top: 2px;
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers label {
|
|
||||||
display: block;
|
|
||||||
font-size: 13px;
|
|
||||||
font-size: 1.08333em;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-separator {
|
|
||||||
height: 0;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
margin: 5px -10px 5px -6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Default icon URLs */
|
|
||||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
|
||||||
background-image: url(images/marker-icon.png);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* attribution and scale controls */
|
|
||||||
|
|
||||||
.leaflet-container .leaflet-control-attribution {
|
|
||||||
background: #fff;
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.leaflet-control-attribution,
|
|
||||||
.leaflet-control-scale-line {
|
|
||||||
padding: 0 5px;
|
|
||||||
color: #333;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
.leaflet-control-attribution a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.leaflet-control-attribution a:hover,
|
|
||||||
.leaflet-control-attribution a:focus {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.leaflet-attribution-flag {
|
|
||||||
display: inline !important;
|
|
||||||
vertical-align: baseline !important;
|
|
||||||
width: 1em;
|
|
||||||
height: 0.6669em;
|
|
||||||
}
|
|
||||||
.leaflet-left .leaflet-control-scale {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
.leaflet-bottom .leaflet-control-scale {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.leaflet-control-scale-line {
|
|
||||||
border: 2px solid #777;
|
|
||||||
border-top: none;
|
|
||||||
line-height: 1.1;
|
|
||||||
padding: 2px 5px 1px;
|
|
||||||
white-space: nowrap;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
text-shadow: 1px 1px #fff;
|
|
||||||
}
|
|
||||||
.leaflet-control-scale-line:not(:first-child) {
|
|
||||||
border-top: 2px solid #777;
|
|
||||||
border-bottom: none;
|
|
||||||
margin-top: -2px;
|
|
||||||
}
|
|
||||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
|
||||||
border-bottom: 2px solid #777;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-touch .leaflet-control-attribution,
|
|
||||||
.leaflet-touch .leaflet-control-layers,
|
|
||||||
.leaflet-touch .leaflet-bar {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
.leaflet-touch .leaflet-control-layers,
|
|
||||||
.leaflet-touch .leaflet-bar {
|
|
||||||
border: 2px solid rgba(0,0,0,0.2);
|
|
||||||
background-clip: padding-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* popup */
|
|
||||||
|
|
||||||
.leaflet-popup {
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.leaflet-popup-content-wrapper {
|
|
||||||
padding: 1px;
|
|
||||||
text-align: left;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
.leaflet-popup-content {
|
|
||||||
margin: 13px 24px 13px 20px;
|
|
||||||
line-height: 1.3;
|
|
||||||
font-size: 13px;
|
|
||||||
font-size: 1.08333em;
|
|
||||||
min-height: 1px;
|
|
||||||
}
|
|
||||||
.leaflet-popup-content p {
|
|
||||||
margin: 17px 0;
|
|
||||||
margin: 1.3em 0;
|
|
||||||
}
|
|
||||||
.leaflet-popup-tip-container {
|
|
||||||
width: 40px;
|
|
||||||
height: 20px;
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
margin-top: -1px;
|
|
||||||
margin-left: -20px;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.leaflet-popup-tip {
|
|
||||||
width: 17px;
|
|
||||||
height: 17px;
|
|
||||||
padding: 1px;
|
|
||||||
|
|
||||||
margin: -10px auto 0;
|
|
||||||
pointer-events: auto;
|
|
||||||
|
|
||||||
-webkit-transform: rotate(45deg);
|
|
||||||
-moz-transform: rotate(45deg);
|
|
||||||
-ms-transform: rotate(45deg);
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
.leaflet-popup-content-wrapper,
|
|
||||||
.leaflet-popup-tip {
|
|
||||||
background: white;
|
|
||||||
color: #333;
|
|
||||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
|
||||||
}
|
|
||||||
.leaflet-container a.leaflet-popup-close-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
border: none;
|
|
||||||
text-align: center;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
font: 16px/24px Tahoma, Verdana, sans-serif;
|
|
||||||
color: #757575;
|
|
||||||
text-decoration: none;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.leaflet-container a.leaflet-popup-close-button:hover,
|
|
||||||
.leaflet-container a.leaflet-popup-close-button:focus {
|
|
||||||
color: #585858;
|
|
||||||
}
|
|
||||||
.leaflet-popup-scrolled {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
|
||||||
-ms-zoom: 1;
|
|
||||||
}
|
|
||||||
.leaflet-oldie .leaflet-popup-tip {
|
|
||||||
width: 24px;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
|
||||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-oldie .leaflet-control-zoom,
|
|
||||||
.leaflet-oldie .leaflet-control-layers,
|
|
||||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
|
||||||
.leaflet-oldie .leaflet-popup-tip {
|
|
||||||
border: 1px solid #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* div icon */
|
|
||||||
|
|
||||||
.leaflet-div-icon {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Tooltip */
|
|
||||||
/* Base styles for the element that has a tooltip */
|
|
||||||
.leaflet-tooltip {
|
|
||||||
position: absolute;
|
|
||||||
padding: 6px;
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #fff;
|
|
||||||
border-radius: 3px;
|
|
||||||
color: #222;
|
|
||||||
white-space: nowrap;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
pointer-events: none;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
|
||||||
}
|
|
||||||
.leaflet-tooltip.leaflet-interactive {
|
|
||||||
cursor: pointer;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-top:before,
|
|
||||||
.leaflet-tooltip-bottom:before,
|
|
||||||
.leaflet-tooltip-left:before,
|
|
||||||
.leaflet-tooltip-right:before {
|
|
||||||
position: absolute;
|
|
||||||
pointer-events: none;
|
|
||||||
border: 6px solid transparent;
|
|
||||||
background: transparent;
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Directions */
|
|
||||||
|
|
||||||
.leaflet-tooltip-bottom {
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-top {
|
|
||||||
margin-top: -6px;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-bottom:before,
|
|
||||||
.leaflet-tooltip-top:before {
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -6px;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-top:before {
|
|
||||||
bottom: 0;
|
|
||||||
margin-bottom: -12px;
|
|
||||||
border-top-color: #fff;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-bottom:before {
|
|
||||||
top: 0;
|
|
||||||
margin-top: -12px;
|
|
||||||
margin-left: -6px;
|
|
||||||
border-bottom-color: #fff;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-left {
|
|
||||||
margin-left: -6px;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-right {
|
|
||||||
margin-left: 6px;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-left:before,
|
|
||||||
.leaflet-tooltip-right:before {
|
|
||||||
top: 50%;
|
|
||||||
margin-top: -6px;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-left:before {
|
|
||||||
right: 0;
|
|
||||||
margin-right: -12px;
|
|
||||||
border-left-color: #fff;
|
|
||||||
}
|
|
||||||
.leaflet-tooltip-right:before {
|
|
||||||
left: 0;
|
|
||||||
margin-left: -12px;
|
|
||||||
border-right-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Printing */
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
/* Prevent printers from removing background-images of controls. */
|
|
||||||
.leaflet-control {
|
|
||||||
-webkit-print-color-adjust: exact;
|
|
||||||
print-color-adjust: exact;
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
120
apps/gg/lit-all.min.js
vendored
120
apps/gg/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,158 +0,0 @@
|
|||||||
/**
|
|
||||||
* Based off of [the offical Google document](https://developers.google.com/maps/documentation/utilities/polylinealgorithm)
|
|
||||||
*
|
|
||||||
* Some parts from [this implementation](http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/PolylineEncoder.js)
|
|
||||||
* by [Mark McClure](http://facstaff.unca.edu/mcmcclur/)
|
|
||||||
*
|
|
||||||
* @module polyline
|
|
||||||
*/
|
|
||||||
|
|
||||||
var polyline = {};
|
|
||||||
|
|
||||||
function py2_round(value) {
|
|
||||||
// Google's polyline algorithm uses the same rounding strategy as Python 2, which is different from JS for negative values
|
|
||||||
return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function encode(current, previous, factor) {
|
|
||||||
current = py2_round(current * factor);
|
|
||||||
previous = py2_round(previous * factor);
|
|
||||||
var coordinate = (current - previous) * 2;
|
|
||||||
if (coordinate < 0) {
|
|
||||||
coordinate = -coordinate - 1
|
|
||||||
}
|
|
||||||
var output = '';
|
|
||||||
while (coordinate >= 0x20) {
|
|
||||||
output += String.fromCharCode((0x20 | (coordinate & 0x1f)) + 63);
|
|
||||||
coordinate /= 32;
|
|
||||||
}
|
|
||||||
output += String.fromCharCode((coordinate | 0) + 63);
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes to a [latitude, longitude] coordinates array.
|
|
||||||
*
|
|
||||||
* This is adapted from the implementation in Project-OSRM.
|
|
||||||
*
|
|
||||||
* @param {String} str
|
|
||||||
* @param {Number} precision
|
|
||||||
* @returns {Array}
|
|
||||||
*
|
|
||||||
* @see https://github.com/Project-OSRM/osrm-frontend/blob/master/WebContent/routing/OSRM.RoutingGeometry.js
|
|
||||||
*/
|
|
||||||
polyline.decode = function(str, precision) {
|
|
||||||
var index = 0,
|
|
||||||
lat = 0,
|
|
||||||
lng = 0,
|
|
||||||
coordinates = [],
|
|
||||||
shift = 0,
|
|
||||||
result = 0,
|
|
||||||
byte = null,
|
|
||||||
latitude_change,
|
|
||||||
longitude_change,
|
|
||||||
factor = Math.pow(10, Number.isInteger(precision) ? precision : 5);
|
|
||||||
|
|
||||||
// Coordinates have variable length when encoded, so just keep
|
|
||||||
// track of whether we've hit the end of the string. In each
|
|
||||||
// loop iteration, a single coordinate is decoded.
|
|
||||||
while (index < str.length) {
|
|
||||||
|
|
||||||
// Reset shift, result, and byte
|
|
||||||
byte = null;
|
|
||||||
shift = 1;
|
|
||||||
result = 0;
|
|
||||||
|
|
||||||
do {
|
|
||||||
byte = str.charCodeAt(index++) - 63;
|
|
||||||
result += (byte & 0x1f) * shift;
|
|
||||||
shift *= 32;
|
|
||||||
} while (byte >= 0x20);
|
|
||||||
|
|
||||||
latitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2);
|
|
||||||
|
|
||||||
shift = 1;
|
|
||||||
result = 0;
|
|
||||||
|
|
||||||
do {
|
|
||||||
byte = str.charCodeAt(index++) - 63;
|
|
||||||
result += (byte & 0x1f) * shift;
|
|
||||||
shift *= 32;
|
|
||||||
} while (byte >= 0x20);
|
|
||||||
|
|
||||||
longitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2);
|
|
||||||
|
|
||||||
lat += latitude_change;
|
|
||||||
lng += longitude_change;
|
|
||||||
|
|
||||||
coordinates.push([lat / factor, lng / factor]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return coordinates;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes the given [latitude, longitude] coordinates array.
|
|
||||||
*
|
|
||||||
* @param {Array.<Array.<Number>>} coordinates
|
|
||||||
* @param {Number} precision
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
polyline.encode = function(coordinates, precision) {
|
|
||||||
if (!coordinates.length) { return ''; }
|
|
||||||
|
|
||||||
var factor = Math.pow(10, Number.isInteger(precision) ? precision : 5),
|
|
||||||
output = encode(coordinates[0][0], 0, factor) + encode(coordinates[0][1], 0, factor);
|
|
||||||
|
|
||||||
for (var i = 1; i < coordinates.length; i++) {
|
|
||||||
var a = coordinates[i], b = coordinates[i - 1];
|
|
||||||
output += encode(a[0], b[0], factor);
|
|
||||||
output += encode(a[1], b[1], factor);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
|
|
||||||
function flipped(coords) {
|
|
||||||
var flipped = [];
|
|
||||||
for (var i = 0; i < coords.length; i++) {
|
|
||||||
var coord = coords[i].slice();
|
|
||||||
flipped.push([coord[1], coord[0]]);
|
|
||||||
}
|
|
||||||
return flipped;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a GeoJSON LineString feature/geometry.
|
|
||||||
*
|
|
||||||
* @param {Object} geojson
|
|
||||||
* @param {Number} precision
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
polyline.fromGeoJSON = function(geojson, precision) {
|
|
||||||
if (geojson && geojson.type === 'Feature') {
|
|
||||||
geojson = geojson.geometry;
|
|
||||||
}
|
|
||||||
if (!geojson || geojson.type !== 'LineString') {
|
|
||||||
throw new Error('Input must be a GeoJSON LineString');
|
|
||||||
}
|
|
||||||
return polyline.encode(flipped(geojson.coordinates), precision);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes to a GeoJSON LineString geometry.
|
|
||||||
*
|
|
||||||
* @param {String} str
|
|
||||||
* @param {Number} precision
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
polyline.toGeoJSON = function(str, precision) {
|
|
||||||
var coords = polyline.decode(str, precision);
|
|
||||||
return {
|
|
||||||
type: 'LineString',
|
|
||||||
coordinates: flipped(coords)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
let polyline_decode = polyline.decode;
|
|
||||||
export { polyline_decode as decode };
|
|
@ -1,807 +0,0 @@
|
|||||||
import {LitElement, html, unsafeHTML, css, guard, until} from './lit-all.min.js';
|
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
|
||||||
import * as polyline from './polyline.js';
|
|
||||||
import {gpx_parse} from './gpx.js';
|
|
||||||
|
|
||||||
const k_client_id = '28276';
|
|
||||||
const k_redirect_url = 'https://tildefriends.net/~cory/gg/login';
|
|
||||||
|
|
||||||
const k_color_snow = [128, 128, 255, 255];
|
|
||||||
const k_color_ice = [160, 160, 255, 255];
|
|
||||||
const k_color_water = [0, 0, 255, 255];
|
|
||||||
const k_color_dirt = [128, 129, 130, 255];
|
|
||||||
const k_color_pavement = [32, 32, 32, 255];
|
|
||||||
const k_color_grass = [0, 255, 0, 255];
|
|
||||||
const k_color_default = [128, 128, 128, 255];
|
|
||||||
|
|
||||||
const k_store = {
|
|
||||||
'🦞': 15,
|
|
||||||
'🛶': 10,
|
|
||||||
'🏠': 10,
|
|
||||||
'⛰': 10,
|
|
||||||
'🐠': 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
const k_marker_snap = {x: 5, y: 4};
|
|
||||||
|
|
||||||
class GgAppElement extends LitElement {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
user: {type: Object},
|
|
||||||
strava: {type: Object},
|
|
||||||
activities: {type: Array},
|
|
||||||
activity: {type: Object},
|
|
||||||
world: {type: Object},
|
|
||||||
whoami: {type: String},
|
|
||||||
status: {type: Object},
|
|
||||||
tab: {type: String},
|
|
||||||
url: {type: String},
|
|
||||||
currency: {type: Number},
|
|
||||||
to_build: {type: String},
|
|
||||||
emoji_of_the_day: {type: String},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.activities = [];
|
|
||||||
this.activity = {};
|
|
||||||
this.loaded_activities = [];
|
|
||||||
this.placed_emojis = [];
|
|
||||||
this.strava = {};
|
|
||||||
this.min_lat = Number.MAX_VALUE;
|
|
||||||
this.min_lon = Number.MAX_VALUE;
|
|
||||||
this.max_lat = -Number.MAX_VALUE;
|
|
||||||
this.max_lon = -Number.MAX_VALUE;
|
|
||||||
this.focus = undefined;
|
|
||||||
this.status = undefined;
|
|
||||||
this.tab = 'map';
|
|
||||||
this.load().catch(function(e) {
|
|
||||||
console.log('load error', e);
|
|
||||||
});
|
|
||||||
this.to_build = '🏠';
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
console.log('load');
|
|
||||||
let emojis = await (await fetch('emojis.json')).json();
|
|
||||||
emojis = Object.values(emojis).map(x => Object.values(x)).flat();
|
|
||||||
let today = new Date();
|
|
||||||
let date_index = today.getYear() * 356 + today.getMonth() * 31 + today.getDate();
|
|
||||||
this.emoji_of_the_day = emojis[(date_index * 123457) % emojis.length];
|
|
||||||
this.user = await tfrpc.rpc.getUser();
|
|
||||||
this.url = (await tfrpc.rpc.url()).split('?')[0];
|
|
||||||
try {
|
|
||||||
await this.update_credentials();
|
|
||||||
} catch (e) {
|
|
||||||
console.log('update_credentials failed', e);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await this.update_activities();
|
|
||||||
} catch (e) {
|
|
||||||
console.log('update_activities failed', e);
|
|
||||||
}
|
|
||||||
await this.acquire_ssb_identity();
|
|
||||||
if (this.whoami && this.activities?.length) {
|
|
||||||
await this.sync_activities();
|
|
||||||
}
|
|
||||||
await this.get_activities_from_ssb();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* https://gist.github.com/jcouyang/632709f30e12a7879a73e9e132c0d56b?permalink_comment_id=3591045#gistcomment-3591045 */
|
|
||||||
async promise_all(promises, max_concurrent) {
|
|
||||||
let index = 0;
|
|
||||||
let results = [];
|
|
||||||
async function exec_thread() {
|
|
||||||
while (index < promises.length) {
|
|
||||||
const current = index++;
|
|
||||||
results[current] = await promises[current];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const threads = [];
|
|
||||||
for (let thread = 0; thread < max_concurrent; thread++) {
|
|
||||||
threads.push(exec_thread());
|
|
||||||
}
|
|
||||||
await Promise.all(threads);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get_activities_from_ssb() {
|
|
||||||
this.status = {text: 'loading activities'};
|
|
||||||
this.loaded_activities = [];
|
|
||||||
let rows = await tfrpc.rpc.query(`
|
|
||||||
SELECT messages.author, json_extract(mention.value, '$.link') AS blob_id
|
|
||||||
FROM messages_fts('"gg-activity"')
|
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid,
|
|
||||||
json_each(messages.content, '$.mentions') as mention
|
|
||||||
WHERE json_extract(messages.content, '$.type') = 'gg-activity' AND
|
|
||||||
json_extract(mention.value, '$.name') = 'activity_data'
|
|
||||||
ORDER BY messages.timestamp DESC
|
|
||||||
`, []);
|
|
||||||
this.status = {text: 'loading activity data'};
|
|
||||||
let authors = rows.map(x => x.author);
|
|
||||||
let blobs = await this.promise_all(rows.map(x => tfrpc.rpc.get_blob(x.blob_id)), 8);
|
|
||||||
this.status = {text: 'processing activity data'};
|
|
||||||
for (let [index, blob] of blobs.entries()) {
|
|
||||||
let activity;
|
|
||||||
try {
|
|
||||||
activity = JSON.parse(blob);
|
|
||||||
} catch {
|
|
||||||
activity = gpx_parse(blob);
|
|
||||||
}
|
|
||||||
if (activity) {
|
|
||||||
activity.author = authors[index];
|
|
||||||
this.loaded_activities.push(activity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.status = {text: 'calculating balance'};
|
|
||||||
rows = await tfrpc.rpc.query(`
|
|
||||||
SELECT count(*) AS currency FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-activity'
|
|
||||||
`, [this.whoami]);
|
|
||||||
let currency = rows[0].currency;
|
|
||||||
rows = await tfrpc.rpc.query(`
|
|
||||||
SELECT SUM(json_extract(content, '$.cost')) AS cost FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-place'
|
|
||||||
`, [this.whoami]);
|
|
||||||
let spent = rows[0].cost;
|
|
||||||
this.currency = currency - spent;
|
|
||||||
this.status = {text: 'getting placed emojis'};
|
|
||||||
rows = await tfrpc.rpc.query(`
|
|
||||||
SELECT messages.content
|
|
||||||
FROM messages_fts('"gg-place"')
|
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
|
||||||
WHERE json_extract(messages.content, '$.type') = 'gg-place'
|
|
||||||
ORDER BY messages.timestamp
|
|
||||||
`);
|
|
||||||
for (let row of rows) {
|
|
||||||
console.log(row.content);
|
|
||||||
let content = JSON.parse(row.content);
|
|
||||||
this.placed_emojis.push({
|
|
||||||
position: content.position,
|
|
||||||
emoji: content.emoji,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log(this.placed_emojis);
|
|
||||||
this.status = undefined;
|
|
||||||
this.update_map();
|
|
||||||
}
|
|
||||||
|
|
||||||
async sync_activities() {
|
|
||||||
let ids = this.activities.map(x => `https://www.strava.com/activities/${x.id}`);
|
|
||||||
let missing = await tfrpc.rpc.query(`
|
|
||||||
WITH my_activities AS (
|
|
||||||
SELECT json_extract(mention.value, '$.link') AS url
|
|
||||||
FROM messages, json_each(messages.content, '$.mentions') AS mention
|
|
||||||
WHERE
|
|
||||||
author = ? AND
|
|
||||||
json_extract(messages.content, '$.type') = 'gg-activity' AND
|
|
||||||
json_extract(mention.value, '$.name') = 'activity_url')
|
|
||||||
SELECT from_strava.value FROM json_each(?) AS from_strava
|
|
||||||
LEFT OUTER JOIN my_activities ON from_strava.value = my_activities.url
|
|
||||||
WHERE my_activities.url IS NULL
|
|
||||||
`, [this.whoami, JSON.stringify(ids)]);
|
|
||||||
console.log('missing = ', missing);
|
|
||||||
for (let [index, row] of missing.entries()) {
|
|
||||||
this.status = {text: 'syncing from strava', value: index, max: missing.length};
|
|
||||||
let url = row.value;
|
|
||||||
let id = url.match(/.*\/(\d+)/)[1];
|
|
||||||
let response = await fetch(`https://www.strava.com/api/v3/activities/${id}`, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${this.strava.access_token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
let activity = await response.json();
|
|
||||||
let blob_id = await tfrpc.rpc.store_blob(JSON.stringify(activity));
|
|
||||||
let message = {
|
|
||||||
type: 'gg-activity',
|
|
||||||
mentions: [
|
|
||||||
{
|
|
||||||
link: url,
|
|
||||||
name: 'activity_url',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: blob_id,
|
|
||||||
name: 'activity_data',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
};
|
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message);
|
|
||||||
}
|
|
||||||
this.status = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async acquire_ssb_identity() {
|
|
||||||
let user = await tfrpc.rpc.getUser();
|
|
||||||
if (!user?.credentials?.session?.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let ids = await tfrpc.rpc.getIdentities();
|
|
||||||
let players = ids.length ? (await tfrpc.rpc.query(`
|
|
||||||
SELECT author FROM messages JOIN json_each(?) ON messages.author = json_each.value
|
|
||||||
WHERE
|
|
||||||
json_extract(messages.content, '$.type') = 'gg-player' AND
|
|
||||||
json_extract(messages.content, '$.active')
|
|
||||||
ORDER BY timestamp DESC limit 1
|
|
||||||
`, [JSON.stringify(ids)])).map(row => row.author) : [];
|
|
||||||
if (!players.length) {
|
|
||||||
this.whoami = await tfrpc.rpc.createIdentity();
|
|
||||||
if (this.whoami) {
|
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, {
|
|
||||||
type: 'gg-player',
|
|
||||||
active: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
players.sort();
|
|
||||||
this.whoami = players[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update_credentials() {
|
|
||||||
let name = this.user?.credentials?.session?.name;
|
|
||||||
if (!name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let shared = await tfrpc.rpc.sharedDatabaseGet(name);
|
|
||||||
if (shared) {
|
|
||||||
await tfrpc.rpc.databaseSet('strava', shared);
|
|
||||||
await tfrpc.rpc.sharedDatabaseRemove(name);
|
|
||||||
}
|
|
||||||
this.strava = JSON.parse(await tfrpc.rpc.databaseGet('strava') || '{}');
|
|
||||||
if (new Date().valueOf() / 1000 > this.strava.expires_at) {
|
|
||||||
console.log('this looks expired', new Date().valueOf() / 1000, '>', this.strava.expires_at);
|
|
||||||
let x = await tfrpc.rpc.refresh_token(this.strava);
|
|
||||||
if (x) {
|
|
||||||
this.strava = x;
|
|
||||||
await tfrpc.rpc.databaseSet('strava', JSON.stringify(x));
|
|
||||||
} else {
|
|
||||||
this.strava = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update_activities() {
|
|
||||||
if (this?.strava?.access_token) {
|
|
||||||
let response = await fetch('https://www.strava.com/api/v3/athlete/activities', {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${this.strava.access_token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.activities = await response.json();
|
|
||||||
this.activities.sort((a, b) => (a.id - b.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
color_to_emoji(color) {
|
|
||||||
const k_map = [
|
|
||||||
[k_color_snow, '⬜'],
|
|
||||||
[k_color_ice, '🟦'],
|
|
||||||
[k_color_water, '🟦'],
|
|
||||||
[k_color_dirt, '🟫'],
|
|
||||||
[k_color_pavement, '⬛'],
|
|
||||||
[k_color_grass, '🟩'],
|
|
||||||
[k_color_default, '🟧'],
|
|
||||||
];
|
|
||||||
for (let m of k_map) {
|
|
||||||
if (m[0][0] == color[0] &&
|
|
||||||
m[0][1] == color[1] &&
|
|
||||||
m[0][2] == color[2] &&
|
|
||||||
m[0][3] == color[3]) {
|
|
||||||
return m[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activity_bounds(activity) {
|
|
||||||
let min_lat = Number.MAX_VALUE;
|
|
||||||
let min_lon = Number.MAX_VALUE;
|
|
||||||
let max_lat = -Number.MAX_VALUE;
|
|
||||||
let max_lon = -Number.MAX_VALUE;
|
|
||||||
if (activity?.map?.polyline) {
|
|
||||||
for (let pt of polyline.decode(activity.map.polyline)) {
|
|
||||||
min_lat = Math.min(min_lat, pt[0]);
|
|
||||||
min_lon = Math.min(min_lon, pt[1]);
|
|
||||||
max_lat = Math.max(max_lat, pt[0]);
|
|
||||||
max_lon = Math.max(max_lon, pt[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (activity?.segments) {
|
|
||||||
for (let segment of activity.segments) {
|
|
||||||
for (let pt of segment) {
|
|
||||||
min_lat = Math.min(min_lat, pt.lat);
|
|
||||||
min_lon = Math.min(min_lon, pt.lon);
|
|
||||||
max_lat = Math.max(max_lat, pt.lat);
|
|
||||||
max_lon = Math.max(max_lon, pt.lon);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
min: {
|
|
||||||
lat: min_lat,
|
|
||||||
lng: min_lon,
|
|
||||||
},
|
|
||||||
max: {
|
|
||||||
lat: max_lat,
|
|
||||||
lng: max_lon,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
on_click(event) {
|
|
||||||
let popup = L.popup()
|
|
||||||
.setLatLng(event.latlng)
|
|
||||||
.setContent(`
|
|
||||||
<div><a target="_top" href="https://www.google.com/maps/search/?api=1&query=${event.latlng.lat},${event.latlng.lng}">${event.latlng.lat}, ${event.latlng.lng}</a></div>
|
|
||||||
`)
|
|
||||||
.openOn(this.leaflet);
|
|
||||||
}
|
|
||||||
|
|
||||||
async build() {
|
|
||||||
if (this.popup) {
|
|
||||||
this.popup.remove();
|
|
||||||
}
|
|
||||||
if (!this.marker) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let latlng = this.marker.getLatLng();
|
|
||||||
|
|
||||||
let cost = k_store[this.to_build];
|
|
||||||
if (cost > this.currency) {
|
|
||||||
alert('Insufficient funds.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let message = {
|
|
||||||
type: 'gg-place',
|
|
||||||
position: {lat: latlng.lat, lng: latlng.lng},
|
|
||||||
emoji: this.to_build,
|
|
||||||
cost: cost,
|
|
||||||
};
|
|
||||||
let id = await tfrpc.rpc.appendMessage(this.whoami, message);
|
|
||||||
this.marker.remove();
|
|
||||||
this.placed_emojis.push({
|
|
||||||
position: {lat: latlng.lat, lng: latlng.lng},
|
|
||||||
emoji: this.to_build,
|
|
||||||
});
|
|
||||||
this.currency -= cost;
|
|
||||||
return this.update_map();
|
|
||||||
}
|
|
||||||
|
|
||||||
on_marker_click(event) {
|
|
||||||
this.popup = L.popup()
|
|
||||||
.setLatLng(event.latlng)
|
|
||||||
.setContent(`
|
|
||||||
${this.to_build} (-${k_store[this.to_build]}) <input type="button" value="Build" onclick="document.getElementById('ggapp').build()"></input>
|
|
||||||
`)
|
|
||||||
.openOn(this.leaflet);
|
|
||||||
}
|
|
||||||
|
|
||||||
snap_to_grid(latlng, fudge, zoom) {
|
|
||||||
let position = this.leaflet.options.crs.latLngToPoint(latlng, zoom ?? this.leaflet.getZoom());
|
|
||||||
position.x = Math.round(position.x / 16) * 16 + (fudge?.x ?? 0);
|
|
||||||
position.y = Math.round(position.y / 16) * 16 + (fudge?.y ?? 0);
|
|
||||||
position = this.leaflet.options.crs.pointToLatLng(position, zoom ?? this.leaflet.getZoom());
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
on_marker_move(event) {
|
|
||||||
if (!this.no_snap && this.marker) {
|
|
||||||
this.no_snap = true;
|
|
||||||
this.marker.setLatLng(this.snap_to_grid(this.marker.getLatLng(), k_marker_snap));
|
|
||||||
this.no_snap = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
on_zoom(event) {
|
|
||||||
if (this.marker) {
|
|
||||||
this.marker.setLatLng(this.snap_to_grid(this.marker.getLatLng(), k_marker_snap));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
on_mouse_down(event) {
|
|
||||||
if (this.marker) {
|
|
||||||
this.marker.remove();
|
|
||||||
this.marker = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.to_build) {
|
|
||||||
this.marker = L.marker(this.snap_to_grid(event.latlng, k_marker_snap), {icon: L.divIcon({className: 'build-icon'}), draggable: true}).addTo(this.leaflet);
|
|
||||||
this.marker.on({click: this.on_marker_click.bind(this)});
|
|
||||||
this.marker.on({drag: this.on_marker_move.bind(this)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update_map() {
|
|
||||||
let map = this.shadowRoot.getElementById('map');
|
|
||||||
if (!map || !this.loaded_activities.length) {
|
|
||||||
this.leaflet = undefined;
|
|
||||||
this.grid_layer = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.leaflet) {
|
|
||||||
this.leaflet = L.map(map, {attributionControl: false, maxZoom: 16, bounceAtZoomLimits: false});
|
|
||||||
this.leaflet.on({contextmenu: this.on_click.bind(this)});
|
|
||||||
this.leaflet.on({click: this.on_mouse_down.bind(this)});
|
|
||||||
this.leaflet.on({zoom: this.on_zoom.bind(this)});
|
|
||||||
}
|
|
||||||
let self = this;
|
|
||||||
let grid_layer = L.GridLayer.extend({
|
|
||||||
createTile: function(coords) {
|
|
||||||
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
|
|
||||||
var size = this.getTileSize();
|
|
||||||
tile.width = size.x;
|
|
||||||
tile.height = size.y;
|
|
||||||
var context = tile.getContext('2d');
|
|
||||||
context.font = '10pt sans';
|
|
||||||
let bounds = this._tileCoordsToBounds(coords);
|
|
||||||
let degrees = 360.0 / (2 ** coords.z);
|
|
||||||
let ul = bounds.getNorthWest();
|
|
||||||
let lr = bounds.getSouthEast();
|
|
||||||
|
|
||||||
let mini = document.createElement('canvas');
|
|
||||||
mini.width = Math.floor(size.x / 16.0);
|
|
||||||
mini.height = Math.floor(size.y / 16.0);
|
|
||||||
let mini_context = mini.getContext('2d');
|
|
||||||
let image_data = context.getImageData(0, 0, mini.width, mini.height);
|
|
||||||
for (let activity of self.loaded_activities) {
|
|
||||||
self.draw_activity_to_tile(image_data, mini.width, mini.height, ul, lr, activity);
|
|
||||||
}
|
|
||||||
context.textAlign = 'left';
|
|
||||||
context.textBaseline = 'bottom';
|
|
||||||
for (let x = 0; x < mini.width; x++) {
|
|
||||||
for (let y = 0; y < mini.height; y++) {
|
|
||||||
let start = (y * mini.width + x) * 4;
|
|
||||||
let pixel = self.color_to_emoji(image_data.data.slice(start, start + 4));
|
|
||||||
if (pixel) {
|
|
||||||
//context.fillRect(x * size.x / mini.width, y * size.y / mini.height, size.x / mini.width, size.y / mini.height);
|
|
||||||
context.fillText(pixel, x * size.x / mini.width, y * size.y / mini.height + mini.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let placed of self.placed_emojis) {
|
|
||||||
let position = self.leaflet.options.crs.latLngToPoint(self.snap_to_grid(placed.position, undefined, coords.z), coords.z);
|
|
||||||
let tile_x = Math.floor(position.x / size.x);
|
|
||||||
let tile_y = Math.floor(position.y / size.y);
|
|
||||||
position.x = position.x - tile_x * size.x;
|
|
||||||
position.y = position.y - tile_y * size.y;
|
|
||||||
if (tile_x == coords.x && tile_y == coords.y) {
|
|
||||||
//context.fillRect(position.x, position.y, size.x / mini.width, size.y / mini.height);
|
|
||||||
context.fillText(placed.emoji, position.x, position.y + mini.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tile;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (this.grid_layer) {
|
|
||||||
this.grid_layer.redraw();
|
|
||||||
} else {
|
|
||||||
this.grid_layer = new grid_layer();
|
|
||||||
this.grid_layer.addTo(this.leaflet);
|
|
||||||
}
|
|
||||||
for (let activity of this.loaded_activities) {
|
|
||||||
let bounds = this.activity_bounds(activity);
|
|
||||||
this.min_lat = Math.min(this.min_lat, bounds.min.lat);
|
|
||||||
this.min_lon = Math.min(this.min_lon, bounds.min.lng);
|
|
||||||
this.max_lat = Math.max(this.max_lat, bounds.max.lat);
|
|
||||||
this.max_lon = Math.max(this.max_lon, bounds.max.lng);
|
|
||||||
}
|
|
||||||
if (this.focus) {
|
|
||||||
this.leaflet.fitBounds([
|
|
||||||
this.focus.min,
|
|
||||||
this.focus.max,
|
|
||||||
]);
|
|
||||||
this.focus = undefined;
|
|
||||||
} else {
|
|
||||||
this.leaflet.fitBounds([
|
|
||||||
[this.min_lat, this.min_lon],
|
|
||||||
[this.max_lat, this.max_lon],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activity_to_color(activity) {
|
|
||||||
let color = [0, 0, 0, 255];
|
|
||||||
switch (activity.sport_type) {
|
|
||||||
/* Implies snow. */
|
|
||||||
case 'AlpineSki':
|
|
||||||
case 'BackcountrySki':
|
|
||||||
case 'NordicSki':
|
|
||||||
case 'Snowshoe':
|
|
||||||
case 'Snowboard':
|
|
||||||
color = k_color_snow;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Implies ice. */
|
|
||||||
case 'IceSkate':
|
|
||||||
case 'InlineSkate':
|
|
||||||
color = k_color_ice;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Implies water. */
|
|
||||||
case 'Canoeing':
|
|
||||||
case 'Kayaking':
|
|
||||||
case 'Kitesurf':
|
|
||||||
case 'Rowing':
|
|
||||||
case 'Sail':
|
|
||||||
case 'StandUpPaddling':
|
|
||||||
case 'Surfing':
|
|
||||||
case 'Swim':
|
|
||||||
case 'Windsurf':
|
|
||||||
color = k_color_water;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Implies dirt. */
|
|
||||||
case 'EMountainBikeRide':
|
|
||||||
case 'Hike':
|
|
||||||
case 'MountainBikeRide':
|
|
||||||
case 'RockClimbing':
|
|
||||||
case 'TrailRun':
|
|
||||||
color = k_color_dirt;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Implies pavement. */
|
|
||||||
case 'EBikeRide':
|
|
||||||
case 'GravelRide':
|
|
||||||
case 'Handcycle':
|
|
||||||
case 'Ride':
|
|
||||||
case 'RollerSki':
|
|
||||||
case 'Run':
|
|
||||||
case 'Skateboard':
|
|
||||||
case 'Badminton':
|
|
||||||
case 'Tennis':
|
|
||||||
case 'Velomobile':
|
|
||||||
case 'Walk':
|
|
||||||
case 'Wheelchair':
|
|
||||||
color = k_color_pavement;
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Grass, maybe? */
|
|
||||||
case 'Golf':
|
|
||||||
case 'Soccer':
|
|
||||||
case 'Squash':
|
|
||||||
color = k_color_grass;
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Crossfit,
|
|
||||||
// Elliptical
|
|
||||||
// HighIntensityIntervalTraining
|
|
||||||
// Pickleball
|
|
||||||
// Pilates
|
|
||||||
// Racquetball
|
|
||||||
// StairStepper
|
|
||||||
// TableTennis,
|
|
||||||
// VirtualRide
|
|
||||||
// VirtualRow
|
|
||||||
// VirtualRun
|
|
||||||
// WeightTraining
|
|
||||||
// Workout
|
|
||||||
// Yoga
|
|
||||||
default:
|
|
||||||
color = k_color_default;
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
line(image_data, x0, y0, x1, y1, value) {
|
|
||||||
/* <3 https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm */
|
|
||||||
let dx = Math.abs(x1 - x0);
|
|
||||||
let sx = x0 < x1 ? 1 : -1;
|
|
||||||
let dy = -Math.abs(y1 - y0);
|
|
||||||
let sy = y0 < y1 ? 1 : -1;
|
|
||||||
let error = dx + dy;
|
|
||||||
while (true) {
|
|
||||||
if (x0 >= 0 && y0 >= 0 && x0 < image_data.width && y0 < image_data.height) {
|
|
||||||
let base = (y0 * image_data.width + x0) * 4;
|
|
||||||
image_data.data[base + 0] = value[0];
|
|
||||||
image_data.data[base + 1] = value[1];
|
|
||||||
image_data.data[base + 2] = value[2];
|
|
||||||
image_data.data[base + 3] = value[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x0 == x1 && y0 == y1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let e2 = 2 * error;
|
|
||||||
if (e2 >= dy) {
|
|
||||||
if (x0 == x1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
error += dy;
|
|
||||||
x0 = Math.round(x0 + sx);
|
|
||||||
}
|
|
||||||
if (e2 <= dx) {
|
|
||||||
if (y0 == y1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
error += dx;
|
|
||||||
y0 = Math.round(y0 + sy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
draw_activity_to_tile(image_data, width, height, ul, lr, activity) {
|
|
||||||
let color = this.activity_to_color(activity);
|
|
||||||
if (activity?.map?.polyline) {
|
|
||||||
let last;
|
|
||||||
for (let pt of polyline.decode(activity.map.polyline)) {
|
|
||||||
let px = [
|
|
||||||
Math.floor(width * (pt[1] - ul.lng) / (lr.lng - ul.lng)),
|
|
||||||
Math.floor(height * (pt[0] - ul.lat) / (lr.lat - ul.lat)),
|
|
||||||
];
|
|
||||||
if (last) {
|
|
||||||
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
|
||||||
}
|
|
||||||
last = px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (activity?.segments) {
|
|
||||||
for (let segment of activity.segments) {
|
|
||||||
let last;
|
|
||||||
for (let pt of segment) {
|
|
||||||
let px = [
|
|
||||||
Math.floor(width * (pt.lon - ul.lng) / (lr.lng - ul.lng)),
|
|
||||||
Math.floor(height * (pt.lat - ul.lat) / (lr.lat - ul.lat)),
|
|
||||||
];
|
|
||||||
if (last) {
|
|
||||||
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
|
||||||
}
|
|
||||||
last = px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async on_upload(event) {
|
|
||||||
try {
|
|
||||||
let file = event.srcElement.files[0];
|
|
||||||
let xml = await file.text();
|
|
||||||
let gpx = gpx_parse(xml);
|
|
||||||
let blob_id = await tfrpc.rpc.store_blob(xml);
|
|
||||||
console.log('blob_id = ', blob_id);
|
|
||||||
console.log(gpx);
|
|
||||||
let message = {
|
|
||||||
type: 'gg-activity',
|
|
||||||
mentions: [
|
|
||||||
{
|
|
||||||
link: `https://${gpx.link}/activity/${gpx.time}`,
|
|
||||||
name: 'activity_url',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: blob_id,
|
|
||||||
name: 'activity_data',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
};
|
|
||||||
console.log('id =', this.whoami, 'message = ', message);
|
|
||||||
let id = await tfrpc.rpc.appendMessage(this.whoami, message);
|
|
||||||
console.log('appended message', id);
|
|
||||||
alert('Activity uploaded.');
|
|
||||||
await this.get_activities_from_ssb();
|
|
||||||
} catch (e) {
|
|
||||||
alert(`Error: ${JSON.stringify(e, null, 2)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
upload() {
|
|
||||||
let input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.onchange = (event) => this.on_upload(event);
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
updated() {
|
|
||||||
this.update_map();
|
|
||||||
}
|
|
||||||
|
|
||||||
focus_map(activity) {
|
|
||||||
let bounds = this.activity_bounds(activity);
|
|
||||||
if (bounds.min.lat < bounds.max.lat &&
|
|
||||||
bounds.min.lng < bounds.max.lng) {
|
|
||||||
this.tab = 'map';
|
|
||||||
this.focus = bounds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render_news() {
|
|
||||||
return html`
|
|
||||||
<ul>
|
|
||||||
${this.loaded_activities.map(x => html`
|
|
||||||
<li style="cursor: pointer" @click=${() => this.focus_map(x)}>${x.author} ${x.name ?? x.time}</li>
|
|
||||||
`)}
|
|
||||||
</ul>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render_store_item(item) {
|
|
||||||
let [emoji, cost] = item;
|
|
||||||
return html`
|
|
||||||
<div>
|
|
||||||
<input type="button" value="${emoji}" @click=${() => this.to_build = emoji}></input> ${cost} ${emoji == this.to_build ? '<-- Will be built next' : undefined}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render_store() {
|
|
||||||
let store = Object.assign({}, k_store);
|
|
||||||
store[this.emoji_of_the_day] = 5;
|
|
||||||
return html`
|
|
||||||
<h2>Store</h2>
|
|
||||||
<div><b>Your balance:</b> ${this.currency}</div>
|
|
||||||
${Object.entries(store).map(this.render_store_item.bind(this))}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let header;
|
|
||||||
if (!this.user?.credentials?.session?.name) {
|
|
||||||
header = html`<div style="flex: 1 0">Please <a target="_top" href="/login?return=${this.url}">login</a> to Tilde Friends, first.</div>`;
|
|
||||||
} else if (!this.strava?.access_token) {
|
|
||||||
let strava_url = `https://www.strava.com/oauth/authorize?client_id=${k_client_id}&redirect_uri=${k_redirect_url}&response_type=code&approval_prompt=auto&scope=activity%3Aread&state=${g_data.state}`;
|
|
||||||
header = html`
|
|
||||||
<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
|
|
||||||
<div style="flex: 1 1">Please <a target="_top" href=${strava_url}>login</a> to Strava.</div>
|
|
||||||
<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
|
|
||||||
<input type="button" value="📁" @click=${this.upload}></input>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
header = html`
|
|
||||||
<div>
|
|
||||||
<div style="flex: 1 0; display: flex; flex-direction: row; align-items: center; gap: 1em; width: 100%">
|
|
||||||
<h1>Welcome, ${this.user.credentials.session.name}</h1>
|
|
||||||
<span style="font-size: xx-small; flex: 1 1; word-break: break-all">${this.whoami}</span>
|
|
||||||
<input type="button" value="📁" @click=${this.upload}></input>
|
|
||||||
</div>
|
|
||||||
<h3 ?hidden=${!this.status?.text}>${this.status?.text} <progress ?hidden=${!this.status?.max} value=${this.status?.value} max=${this.status?.max}>${this.status?.value}</progress></h3>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let navigation = html`
|
|
||||||
<style>
|
|
||||||
#navigation input[type="button"] {
|
|
||||||
min-width: 3em;
|
|
||||||
min-height: 3em;
|
|
||||||
flex: 1 0;
|
|
||||||
font-size: large;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div id="navigation" style="display: flex; flex-direction: row">
|
|
||||||
<input type="button" id="button_map" @click=${() => this.tab = 'map'} value="🗺️Map"></input>
|
|
||||||
<input type="button" id="button_news" @click=${() => this.tab = 'news'} value="🏃News"></input>
|
|
||||||
<input type="button" id="button_friends" @click=${() => this.tab = 'friends'} value="👫Friends"></input>
|
|
||||||
<input type="button" id="button_store" @click=${() => this.tab = 'store'} value="🏗️Store"></input>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
let content;
|
|
||||||
switch (this.tab) {
|
|
||||||
case 'map':
|
|
||||||
content = html`<div id="map" style="width: 100%; height: 100%"></div>`;
|
|
||||||
break;
|
|
||||||
case 'news':
|
|
||||||
content = this.render_news();
|
|
||||||
break;
|
|
||||||
case 'friends':
|
|
||||||
content = html`<div>Friends</div>`;
|
|
||||||
break;
|
|
||||||
case 'store':
|
|
||||||
content = this.render_store();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
.build-icon::before {
|
|
||||||
content: '📍';
|
|
||||||
border: 2px solid red;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<link rel="stylesheet" href="leaflet.css"/>
|
|
||||||
<div style="width: 100%; height: 100%; display: flex; flex-direction: column">
|
|
||||||
${header}
|
|
||||||
<div style="flex: 1 0; overflow: scroll">${content}</div>
|
|
||||||
${navigation}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define('gg-app', GgAppElement);
|
|
@ -1,20 +0,0 @@
|
|||||||
const k_client_id = '28276';
|
|
||||||
const k_client_secret = '3123f1f5afe132d9731111066d1d17bdb22ef27e';
|
|
||||||
const k_access_token = 'f753e77764c26252bd2d80e7c5cc17ace51a8864';
|
|
||||||
const k_refresh_token = 'f58d8e1b5a3ec3bf96e681589d5014f9a294f5a4';
|
|
||||||
const k_redirect_url = 'https://tildefriends.net/~cory/gg/login';
|
|
||||||
|
|
||||||
export async function refresh_token(token) {
|
|
||||||
let r = await fetch('https://www.strava.com/api/v3/oauth/token', {
|
|
||||||
method: 'POST',
|
|
||||||
body: `client_id=${k_client_id}&client_secret=${k_client_secret}&refresh_token=${token.refresh_token}&grant_type=refresh_token`,
|
|
||||||
});
|
|
||||||
return r?.body ? JSON.parse(utf8Decode(r.body)) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function authorization_code(code) {
|
|
||||||
return await fetch('https://www.strava.com/api/v3/oauth/token', {
|
|
||||||
method: 'POST',
|
|
||||||
body: `client_id=${k_client_id}&client_secret=${k_client_secret}&code=${code}&grant_type=authorization_code`,
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🪪",
|
"emoji": "🪪",
|
||||||
"previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256"
|
"previous": "&zxsmzdLKsiG/WZt/Gw7JOxepgypoktNNbIyWiyFiJVc=.sha256"
|
||||||
}
|
}
|
@ -18,7 +18,38 @@ tfrpc.register(async function reload() {
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
let ids = await ssb.getIdentities();
|
let ids = await ssb.getIdentities();
|
||||||
await app.setDocument(`<body style="color: #fff">
|
let server_id = await ssb.getServerIdentity();
|
||||||
|
await app.setDocument(
|
||||||
|
`
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="w3.css"></link>
|
||||||
|
<style>
|
||||||
|
/* "2018 Sargasso Sea" */
|
||||||
|
.w3-theme-l5 {color:#000 !important; background-color:#f3f4f7 !important}
|
||||||
|
.w3-theme-l4 {color:#000 !important; background-color:#d7dbe3 !important}
|
||||||
|
.w3-theme-l3 {color:#000 !important; background-color:#b0b6c8 !important}
|
||||||
|
.w3-theme-l2 {color:#fff !important; background-color:#8892ac !important}
|
||||||
|
.w3-theme-l1 {color:#fff !important; background-color:#636f8e !important}
|
||||||
|
.w3-theme-d1 {color:#fff !important; background-color:#40485c !important}
|
||||||
|
.w3-theme-d2 {color:#fff !important; background-color:#394052 !important}
|
||||||
|
.w3-theme-d3 {color:#fff !important; background-color:#323848 !important}
|
||||||
|
.w3-theme-d4 {color:#fff !important; background-color:#2b303d !important}
|
||||||
|
.w3-theme-d5 {color:#fff !important; background-color:#242833 !important}
|
||||||
|
|
||||||
|
.w3-theme-light {color:#000 !important; background-color:#f3f4f7 !important}
|
||||||
|
.w3-theme-dark {color:#fff !important; background-color:#242833 !important}
|
||||||
|
.w3-theme-action {color:#fff !important; background-color:#242833 !important}
|
||||||
|
|
||||||
|
.w3-theme {color:#fff !important; background-color:#485167 !important}
|
||||||
|
.w3-text-theme {color:#485167 !important}
|
||||||
|
.w3-border-theme {border-color:#485167 !important}
|
||||||
|
|
||||||
|
.w3-hover-theme:hover {color:#fff !important; background-color:#485167 !important}
|
||||||
|
.w3-hover-text-theme:hover {color:#485167 !important}
|
||||||
|
.w3-hover-border-theme:hover {border-color:#485167 !important}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="w3-theme-l3">
|
||||||
<script>const handler = {};</script>
|
<script>const handler = {};</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
@ -26,7 +57,8 @@ async function main() {
|
|||||||
let id = event.srcElement.dataset.id;
|
let id = event.srcElement.dataset.id;
|
||||||
let element = document.createElement('textarea');
|
let element = document.createElement('textarea');
|
||||||
element.value = await tfrpc.rpc.get_private_key(id);
|
element.value = await tfrpc.rpc.get_private_key(id);
|
||||||
element.style = 'width: 100%; read-only: true';
|
element.style = 'width: 100%; height: auto; read-only: true; resize: none';
|
||||||
|
element.classList.add('w3-input');
|
||||||
element.readOnly = true;
|
element.readOnly = true;
|
||||||
event.srcElement.parentElement.appendChild(element);
|
event.srcElement.parentElement.appendChild(element);
|
||||||
event.srcElement.onclick = event => handler.hide_id(event, element);
|
event.srcElement.onclick = event => handler.hide_id(event, element);
|
||||||
@ -47,7 +79,7 @@ async function main() {
|
|||||||
alert('Successfully created: ' + id);
|
alert('Successfully created: ' + id);
|
||||||
await tfrpc.rpc.reload();
|
await tfrpc.rpc.reload();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Error creating identity: ' + e);
|
alert('Error creating identity: ' + e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handler.hide_id = function hide_id(event, element) {
|
handler.hide_id = function hide_id(event, element) {
|
||||||
@ -68,20 +100,38 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<h1>SSB Identity Management</h1>
|
<header class="w3-theme w3-padding"><h1>SSB Identity Management</h1></header>
|
||||||
<h2>Create a new identity</h2>
|
<div class="w3-card-4 w3-margin">
|
||||||
<button id="create_id" onclick="handler.create_id()">Create Identity</button>
|
<header class="w3-container w3-theme-l2"><h2>Create a new identity</h2></header>
|
||||||
<h2>Import an SSB Identity from 12 BIP39 English Words</h2>
|
<footer class="w3-padding">
|
||||||
<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button>
|
<button id="create_id" onclick="handler.create_id()" class="w3-button w3-theme">Create Identity</button>
|
||||||
<h2>Identities</h2>
|
</footer>
|
||||||
<ul>`+
|
</div>
|
||||||
ids.map(id => `<li>
|
<div class="w3-card-4 w3-margin">
|
||||||
<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button>
|
<header class="w3-container w3-theme-l2"><h2>Import an SSB Identity from 12 BIP39 English Words</h2></header>
|
||||||
<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button>
|
<textarea id="add_id" style="width: 100%" rows="4" class="w3-input"></textarea>
|
||||||
${id}
|
<footer class="w3-padding">
|
||||||
</li>`).join('\n')+
|
<button id="add" onclick="handler.add_id(event)" class="w3-button w3-theme">Import Identity</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<div class="w3-card-4 w3-margin">
|
||||||
|
<header class="w3-container w3-theme-l2"><h2>Identities</h2></header>
|
||||||
|
<ul class="w3-ul">` +
|
||||||
|
ids
|
||||||
|
.map(
|
||||||
|
(
|
||||||
|
id
|
||||||
|
) => `<li style="overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis">
|
||||||
|
<button onclick="handler.export_id(event)" data-id="${id}" class="w3-button w3-theme">Export Identity</button>
|
||||||
|
<button onclick="handler.delete_id(event)" data-id="${id}" class="w3-button w3-theme">Delete Identity</button>
|
||||||
|
${id}${id == server_id ? ' <div class="w3-tag w3-theme-l4 w3-round">🖥 local server</div>' : ''}
|
||||||
|
</li>`
|
||||||
|
)
|
||||||
|
.join('\n') +
|
||||||
` </ul>
|
` </ul>
|
||||||
</body>`);
|
</div>
|
||||||
|
</body>`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
235
apps/identity/w3.css
Normal file
235
apps/identity/w3.css
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
||||||
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
|
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
|
||||||
|
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
|
||||||
|
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
|
||||||
|
a{background-color:transparent}a:active,a:hover{outline-width:0}
|
||||||
|
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
|
||||||
|
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
|
||||||
|
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||||
|
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
|
||||||
|
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
|
||||||
|
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
|
||||||
|
button,input{overflow:visible}button,select{text-transform:none}
|
||||||
|
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
|
||||||
|
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
|
||||||
|
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
|
||||||
|
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
|
||||||
|
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
|
||||||
|
[type=checkbox],[type=radio]{padding:0}
|
||||||
|
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
|
||||||
|
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
|
||||||
|
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
|
||||||
|
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
|
||||||
|
/* End extract */
|
||||||
|
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
|
||||||
|
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
|
||||||
|
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
|
||||||
|
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
|
||||||
|
hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
||||||
|
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
|
||||||
|
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
|
||||||
|
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
|
||||||
|
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
|
||||||
|
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
|
||||||
|
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
|
||||||
|
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
|
||||||
|
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
||||||
|
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
|
||||||
|
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
|
||||||
|
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
|
||||||
|
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
|
||||||
|
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
|
||||||
|
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
|
||||||
|
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
|
||||||
|
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
|
||||||
|
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
|
||||||
|
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
|
||||||
|
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
|
||||||
|
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
|
||||||
|
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
|
||||||
|
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
|
||||||
|
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
|
||||||
|
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
|
||||||
|
.w3-main,#main{transition:margin-left .4s}
|
||||||
|
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
|
||||||
|
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
|
||||||
|
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
|
||||||
|
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
|
||||||
|
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
|
||||||
|
.w3-bar .w3-button{white-space:normal}
|
||||||
|
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
|
||||||
|
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
|
||||||
|
.w3-responsive{display:block;overflow-x:auto}
|
||||||
|
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
|
||||||
|
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
|
||||||
|
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
|
||||||
|
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
|
||||||
|
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
|
||||||
|
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
|
||||||
|
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
|
||||||
|
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
|
||||||
|
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
|
||||||
|
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
|
||||||
|
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
|
||||||
|
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
|
||||||
|
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
|
||||||
|
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
|
||||||
|
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
|
||||||
|
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
|
||||||
|
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
|
||||||
|
@media (max-width:1205px){.w3-auto{max-width:95%}}
|
||||||
|
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
|
||||||
|
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
|
||||||
|
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
|
||||||
|
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
|
||||||
|
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
|
||||||
|
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
|
||||||
|
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
|
||||||
|
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
|
||||||
|
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
|
||||||
|
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
|
||||||
|
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
|
||||||
|
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
|
||||||
|
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
|
||||||
|
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
|
||||||
|
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
|
||||||
|
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
|
||||||
|
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
|
||||||
|
.w3-display-position{position:absolute}
|
||||||
|
.w3-circle{border-radius:50%}
|
||||||
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
|
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
|
||||||
|
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
|
||||||
|
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
|
||||||
|
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
|
||||||
|
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
|
||||||
|
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
|
||||||
|
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
|
||||||
|
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
|
||||||
|
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
|
||||||
|
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
|
||||||
|
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
|
||||||
|
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
|
||||||
|
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
|
||||||
|
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
|
||||||
|
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
|
||||||
|
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
|
||||||
|
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
|
||||||
|
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
|
||||||
|
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
|
||||||
|
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
|
||||||
|
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
|
||||||
|
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
|
||||||
|
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
|
||||||
|
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
|
||||||
|
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
|
||||||
|
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
|
||||||
|
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
|
||||||
|
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
|
||||||
|
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
|
||||||
|
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
|
||||||
|
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
|
||||||
|
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
|
||||||
|
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
|
||||||
|
.w3-left{float:left!important}.w3-right{float:right!important}
|
||||||
|
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||||
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
|
/* Colors */
|
||||||
|
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||||
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
|
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
|
||||||
|
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
|
||||||
|
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
|
||||||
|
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
|
||||||
|
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
|
||||||
|
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
|
||||||
|
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
|
||||||
|
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
|
||||||
|
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
|
||||||
|
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
|
||||||
|
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
|
||||||
|
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
|
||||||
|
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
|
||||||
|
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
|
||||||
|
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
|
||||||
|
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
|
||||||
|
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
|
||||||
|
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
|
||||||
|
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
|
||||||
|
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
|
||||||
|
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
|
||||||
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||||
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
|
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
|
||||||
|
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
|
||||||
|
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
|
||||||
|
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
|
||||||
|
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
|
||||||
|
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
|
||||||
|
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
|
||||||
|
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
|
||||||
|
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
|
||||||
|
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
|
||||||
|
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
|
||||||
|
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
|
||||||
|
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
|
||||||
|
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
|
||||||
|
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
|
||||||
|
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
|
||||||
|
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
|
||||||
|
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
|
||||||
|
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
|
||||||
|
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
|
||||||
|
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
|
||||||
|
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
|
||||||
|
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
|
||||||
|
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
|
||||||
|
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
|
||||||
|
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
|
||||||
|
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
|
||||||
|
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
|
||||||
|
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
|
||||||
|
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
|
||||||
|
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
|
||||||
|
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
|
||||||
|
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
|
||||||
|
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
|
||||||
|
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
|
||||||
|
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
|
||||||
|
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
|
||||||
|
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
|
||||||
|
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
|
||||||
|
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
|
||||||
|
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
|
||||||
|
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
|
||||||
|
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
|
||||||
|
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
|
||||||
|
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
|
||||||
|
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
|
||||||
|
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
|
||||||
|
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
|
||||||
|
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
|
||||||
|
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
|
||||||
|
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
|
||||||
|
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
|
||||||
|
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
|
||||||
|
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
|
||||||
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🦟",
|
"emoji": "🦟",
|
||||||
"previous": "&TegdzvFE+im94shygaHkgDYSaSrwY2h0OKUXSRPBQDM=.sha256"
|
"previous": "&cUqvSDUls3jn0haD85LPFAGdkc8wFuy347TtATNcJgg=.sha256"
|
||||||
}
|
}
|
@ -67,9 +67,6 @@ tfrpc.register(function getHash(id, message) {
|
|||||||
tfrpc.register(function setHash(hash) {
|
tfrpc.register(function setHash(hash) {
|
||||||
return app.setHash(hash);
|
return app.setHash(hash);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('message', async function(id) {
|
|
||||||
await tfrpc.rpc.notifyNewMessage(id);
|
|
||||||
});
|
|
||||||
tfrpc.register(async function store_blob(blob) {
|
tfrpc.register(async function store_blob(blob) {
|
||||||
if (Array.isArray(blob)) {
|
if (Array.isArray(blob)) {
|
||||||
blob = Uint8Array.from(blob);
|
blob = Uint8Array.from(blob);
|
||||||
@ -85,19 +82,24 @@ tfrpc.register(async function store_message(message) {
|
|||||||
tfrpc.register(function apps() {
|
tfrpc.register(function apps() {
|
||||||
return core.apps();
|
return core.apps();
|
||||||
});
|
});
|
||||||
|
tfrpc.register(function getActiveIdentity() {
|
||||||
|
return ssb.getActiveIdentity();
|
||||||
|
});
|
||||||
tfrpc.register(async function try_decrypt(id, content) {
|
tfrpc.register(async function try_decrypt(id, content) {
|
||||||
return await ssb.privateMessageDecrypt(id, content);
|
return await ssb.privateMessageDecrypt(id, content);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('broadcasts', async function() {
|
core.register('onMessage', async function (id) {
|
||||||
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
|
});
|
||||||
|
core.register('onBroadcastsChanged', async function () {
|
||||||
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
});
|
});
|
||||||
|
|
||||||
core.register('onConnectionsChanged', async function () {
|
core.register('onConnectionsChanged', async function () {
|
||||||
await tfrpc.rpc.set('connections', await ssb.connections());
|
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||||
});
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (typeof(database) !== 'undefined') {
|
if (typeof database !== 'undefined') {
|
||||||
g_database = await database('ssb');
|
g_database = await database('ssb');
|
||||||
}
|
}
|
||||||
await app.setDocument(utf8Decode(await getFile('index.html')));
|
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="color: #fff">
|
<html style="color: #fff">
|
||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends</title>
|
<title>Tilde Friends</title>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<tf-issues-app />
|
<tf-issues-app />
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="commonmark.min.js"></script>
|
<script src="commonmark.min.js"></script>
|
||||||
<script src="commonmark-linkify.js" type="module"></script>
|
<script src="commonmark-linkify.js" type="module"></script>
|
||||||
<script src="script.js" type="module"></script>
|
<script src="script.js" type="module"></script>
|
||||||
|
44
apps/issues/lit-all.min.js
vendored
44
apps/issues/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -4,43 +4,6 @@ import * as tfutils from './tf-utils.js';
|
|||||||
|
|
||||||
const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256';
|
const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256';
|
||||||
|
|
||||||
class TfIdPickerElement extends LitElement {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
ids: {type: Array},
|
|
||||||
selected: {type: String},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
this.selected = await tfrpc.rpc.localStorageGet('whoami');
|
|
||||||
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
changed(event) {
|
|
||||||
this.selected = event.srcElement.value;
|
|
||||||
tfrpc.rpc.localStorageSet('whoami', this.selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.ids) {
|
|
||||||
return html`
|
|
||||||
<select @change=${this.changed} style="max-width: 100%">
|
|
||||||
${(this.ids).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
|
||||||
</select>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
return html`<div>Loading...</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define('tf-id-picker', TfIdPickerElement);
|
|
||||||
|
|
||||||
class TfComposeElement extends LitElement {
|
class TfComposeElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
@ -57,13 +20,15 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
this.dispatchEvent(new CustomEvent('tf-submit', {
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-submit', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
detail: {
|
detail: {
|
||||||
value: this.renderRoot.getElementById('input').value,
|
value: this.renderRoot.getElementById('input').value,
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
this.renderRoot.getElementById('input').value = '';
|
this.renderRoot.getElementById('input').value = '';
|
||||||
this.input();
|
this.input();
|
||||||
}
|
}
|
||||||
@ -96,18 +61,21 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
let issues = {};
|
let issues = {};
|
||||||
let messages = await tfrpc.rpc.query(`
|
let messages = await tfrpc.rpc.query(
|
||||||
WITH issues AS (SELECT messages.* FROM messages_refs JOIN messages ON
|
`
|
||||||
|
WITH issues AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM messages_refs JOIN messages ON
|
||||||
messages.id = messages_refs.message
|
messages.id = messages_refs.message
|
||||||
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
|
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
|
||||||
edits AS (SELECT messages.* FROM issues JOIN messages_refs ON
|
edits AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM issues JOIN messages_refs ON
|
||||||
issues.id = messages_refs.ref JOIN messages ON
|
issues.id = messages_refs.ref JOIN messages ON
|
||||||
messages.id = messages_refs.message
|
messages.id = messages_refs.message
|
||||||
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
|
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
|
||||||
SELECT * FROM issues
|
SELECT * FROM issues
|
||||||
UNION
|
UNION
|
||||||
SELECT * FROM edits ORDER BY timestamp
|
SELECT * FROM edits ORDER BY timestamp
|
||||||
`, [k_project]);
|
`,
|
||||||
|
[k_project]
|
||||||
|
);
|
||||||
for (let message of messages) {
|
for (let message of messages) {
|
||||||
let content = JSON.parse(message.content);
|
let content = JSON.parse(message.content);
|
||||||
switch (content.type) {
|
switch (content.type) {
|
||||||
@ -123,7 +91,7 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
break;
|
break;
|
||||||
case 'issue-edit':
|
case 'issue-edit':
|
||||||
case 'post':
|
case 'post':
|
||||||
for (let issue of (content.issues || [])) {
|
for (let issue of content.issues || []) {
|
||||||
if (issues[issue.link]) {
|
if (issues[issue.link]) {
|
||||||
if (issue.open !== undefined) {
|
if (issue.open !== undefined) {
|
||||||
issues[issue.link].open = issue.open;
|
issues[issue.link].open = issue.open;
|
||||||
@ -136,7 +104,9 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.issues = Object.values(issues).sort((x, y) => (y.open - x.open) || (y.created - x.created));
|
this.issues = Object.values(issues).sort(
|
||||||
|
(x, y) => y.open - x.open || y.created - x.created
|
||||||
|
);
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
for (let issue of this.issues) {
|
for (let issue of this.issues) {
|
||||||
if (issue.id == this.selected.id) {
|
if (issue.id == this.selected.id) {
|
||||||
@ -150,11 +120,20 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<tr>
|
<tr>
|
||||||
<td>${issue.open ? '☐ open' : '☑ closed'}</td>
|
<td>${issue.open ? '☐ open' : '☑ closed'}</td>
|
||||||
<td style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis">${issue.author}</td>
|
<td
|
||||||
<td style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer" @click=${() => this.selected = issue}>
|
style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
|
||||||
|
>
|
||||||
|
${issue.author}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer"
|
||||||
|
@click=${() => (this.selected = issue)}
|
||||||
|
>
|
||||||
${issue.text.split('\n')?.[0]}
|
${issue.text.split('\n')?.[0]}
|
||||||
</td>
|
</td>
|
||||||
<td>${new Date(issue.updated ?? issue.created).toLocaleDateString()}</td>
|
<td>
|
||||||
|
${new Date(issue.updated ?? issue.created).toLocaleDateString()}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -170,14 +149,22 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
<div>${new Date(update.timestamp).toLocaleString()}</div>
|
<div>${new Date(update.timestamp).toLocaleString()}</div>
|
||||||
<div>${update.author}</div>
|
<div>${update.author}</div>
|
||||||
<div>${message}</div>
|
<div>${message}</div>
|
||||||
<div>${update.open !== undefined ? (update.open ? 'issue opened' : 'issue closed') : undefined}</div>
|
<div>
|
||||||
|
${update.open !== undefined
|
||||||
|
? update.open
|
||||||
|
? 'issue opened'
|
||||||
|
: 'issue closed'
|
||||||
|
: undefined}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async set_open(id, open) {
|
async set_open(id, open) {
|
||||||
if (confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)) {
|
if (
|
||||||
let whoami = this.shadowRoot.getElementById('picker').selected;
|
confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)
|
||||||
|
) {
|
||||||
|
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
await tfrpc.rpc.appendMessage(whoami, {
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
type: 'issue-edit',
|
type: 'issue-edit',
|
||||||
issues: [
|
issues: [
|
||||||
@ -192,7 +179,7 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create_issue(event) {
|
async create_issue(event) {
|
||||||
let whoami = this.shadowRoot.getElementById('picker').selected;
|
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
await tfrpc.rpc.appendMessage(whoami, {
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
type: 'issue',
|
type: 'issue',
|
||||||
project: k_project,
|
project: k_project,
|
||||||
@ -202,12 +189,14 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async reply_to_issue(event) {
|
async reply_to_issue(event) {
|
||||||
let whoami = this.shadowRoot.getElementById('picker').selected;
|
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
await tfrpc.rpc.appendMessage(whoami, {
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
type: 'post',
|
type: 'post',
|
||||||
text: event.detail.value,
|
text: event.detail.value,
|
||||||
root: this.selected.id,
|
root: this.selected.id,
|
||||||
branch: this.selected.updates.length ? this.selected.updates[this.selected.updates.length - 1].id : this.selected.id,
|
branch: this.selected.updates.length
|
||||||
|
? this.selected.updates[this.selected.updates.length - 1].id
|
||||||
|
: this.selected.id,
|
||||||
issues: [
|
issues: [
|
||||||
{
|
{
|
||||||
link: this.selected.id,
|
link: this.selected.id,
|
||||||
@ -218,24 +207,23 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let header = html`
|
let header = html` <h1>Tilde Friends Issues</h1> `;
|
||||||
<h1>Tilde Friends Issues</h1>
|
|
||||||
<tf-id-picker id="picker"></tf-id-picker>
|
|
||||||
`;
|
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
return html`
|
return html`
|
||||||
${header}
|
${header}
|
||||||
<div>
|
<div>
|
||||||
<input type="button" value="Back" @click=${() => this.selected = undefined}></input>
|
<input type="button" value="Back" @click=${() => (this.selected = undefined)}></input>
|
||||||
${this.selected.open ?
|
${
|
||||||
html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>` :
|
this.selected.open
|
||||||
html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`}
|
? html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>`
|
||||||
|
: html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div>${new Date(this.selected.created).toLocaleString()}</div>
|
<div>${new Date(this.selected.created).toLocaleString()}</div>
|
||||||
<div>${this.selected.author}</div>
|
<div>${this.selected.author}</div>
|
||||||
<div>${this.selected.id}</div>
|
<div>${this.selected.id}</div>
|
||||||
<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
|
<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
|
||||||
${this.selected.updates.map(x => this.render_update(x))}
|
${this.selected.updates.map((x) => this.render_update(x))}
|
||||||
<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
|
<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
@ -250,7 +238,7 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
</tr>
|
</tr>
|
||||||
${this.issues.map(x => this.render_issue_table_row(x))}
|
${this.issues.map((x) => this.render_issue_table_row(x))}
|
||||||
</table>
|
</table>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,32 @@
|
|||||||
import * as linkify from './commonmark-linkify.js';
|
import * as linkify from './commonmark-linkify.js';
|
||||||
|
|
||||||
function image(node, entering) {
|
function image(node, entering) {
|
||||||
if (node.firstChild?.type === 'text' &&
|
if (
|
||||||
node.firstChild.literal.startsWith('video:')) {
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('video:')
|
||||||
|
) {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
this.lit(
|
||||||
|
'<video style="max-width: 100%; max-height: 480px" title="' +
|
||||||
|
this.esc(node.firstChild?.literal) +
|
||||||
|
'" controls>'
|
||||||
|
);
|
||||||
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
this.disableTags += 1;
|
this.disableTags += 1;
|
||||||
} else {
|
} else {
|
||||||
this.disableTags -= 1;
|
this.disableTags -= 1;
|
||||||
this.lit('</video>');
|
this.lit('</video>');
|
||||||
}
|
}
|
||||||
} else if (node.firstChild?.type === 'text' &&
|
} else if (
|
||||||
node.firstChild.literal.startsWith('audio:')) {
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('audio:')
|
||||||
|
) {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
this.lit(
|
||||||
|
'<audio style="height: 32px; max-width: 100%" title="' +
|
||||||
|
this.esc(node.firstChild?.literal) +
|
||||||
|
'" controls>'
|
||||||
|
);
|
||||||
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
this.disableTags += 1;
|
this.disableTags += 1;
|
||||||
} else {
|
} else {
|
||||||
@ -24,7 +36,11 @@ function image(node, entering) {
|
|||||||
} else {
|
} else {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
if (this.disableTags === 0) {
|
if (this.disableTags === 0) {
|
||||||
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>');
|
this.lit(
|
||||||
|
'<div class="img_caption">' +
|
||||||
|
this.esc(node.firstChild?.literal || node.destination) +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
||||||
this.lit('<img src="" alt="');
|
this.lit('<img src="" alt="');
|
||||||
} else {
|
} else {
|
||||||
@ -56,14 +72,20 @@ export function markdown(md) {
|
|||||||
node = event.node;
|
node = event.node;
|
||||||
if (event.entering) {
|
if (event.entering) {
|
||||||
if (node.type == 'link') {
|
if (node.type == 'link') {
|
||||||
if (node.destination.startsWith('@') &&
|
if (
|
||||||
node.destination.endsWith('.ed25519')) {
|
node.destination.startsWith('@') &&
|
||||||
|
node.destination.endsWith('.ed25519')
|
||||||
|
) {
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (node.destination.startsWith('%') &&
|
} else if (
|
||||||
node.destination.endsWith('.sha256')) {
|
node.destination.startsWith('%') &&
|
||||||
|
node.destination.endsWith('.sha256')
|
||||||
|
) {
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (node.destination.startsWith('&') &&
|
} else if (
|
||||||
node.destination.endsWith('.sha256')) {
|
node.destination.startsWith('&') &&
|
||||||
|
node.destination.endsWith('.sha256')
|
||||||
|
) {
|
||||||
node.destination = '/' + node.destination + '/view';
|
node.destination = '/' + node.destination + '/view';
|
||||||
}
|
}
|
||||||
} else if (node.type == 'image') {
|
} else if (node.type == 'image') {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "📝",
|
"emoji": "📝",
|
||||||
"previous": "&2hdIDbBrAg63T2X1MzdGSF7yiqHvlnfF0PnInQLp0DA=.sha256"
|
"previous": "&b//KqE4Vx6kOSBRODK1p/8wjOLKZJ+CBB5IkaBt5YsM=.sha256"
|
||||||
}
|
}
|
@ -55,7 +55,7 @@ function new_message() {
|
|||||||
return g_new_message_promise;
|
return g_new_message_promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssb.addEventListener('message', function(id) {
|
core.register('onMessage', function (id) {
|
||||||
let resolve = g_new_message_resolve;
|
let resolve = g_new_message_resolve;
|
||||||
g_new_message_promise = new Promise(function (resolve, reject) {
|
g_new_message_promise = new Promise(function (resolve, reject) {
|
||||||
g_new_message_resolve = resolve;
|
g_new_message_resolve = resolve;
|
||||||
@ -104,8 +104,7 @@ async function process_message(whoami, collection, message, kind, parent) {
|
|||||||
if (!x) {
|
if (!x) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (content.type !== kind ||
|
if (content.type !== kind || (parent && content.parent !== parent)) {
|
||||||
(parent && content.parent !== parent)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,7 +112,10 @@ async function process_message(whoami, collection, message, kind, parent) {
|
|||||||
if (content?.tombstone) {
|
if (content?.tombstone) {
|
||||||
delete collection[content.key];
|
delete collection[content.key];
|
||||||
} else {
|
} else {
|
||||||
collection[content.key] = Object.assign(collection[content.key] || {}, content);
|
collection[content.key] = Object.assign(
|
||||||
|
collection[content.key] || {},
|
||||||
|
content
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
collection[message.id] = Object.assign(content, {id: message.id});
|
collection[message.id] = Object.assign(content, {id: message.id});
|
||||||
@ -125,20 +127,29 @@ tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
|
|||||||
let whoami = await ssb.getIdentities();
|
let whoami = await ssb.getIdentities();
|
||||||
data = data ?? {};
|
data = data ?? {};
|
||||||
let rowid = 0;
|
let rowid = 0;
|
||||||
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
await ssb.sqlAsync(
|
||||||
|
'SELECT MAX(rowid) AS rowid FROM messages',
|
||||||
|
[],
|
||||||
|
function (row) {
|
||||||
rowid = row.rowid;
|
rowid = row.rowid;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
while (true) {
|
while (true) {
|
||||||
if (rowid == max_rowid) {
|
if (rowid == max_rowid) {
|
||||||
await new_message();
|
await new_message();
|
||||||
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
await ssb.sqlAsync(
|
||||||
|
'SELECT MAX(rowid) AS rowid FROM messages',
|
||||||
|
[],
|
||||||
|
function (row) {
|
||||||
rowid = row.rowid;
|
rowid = row.rowid;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let modified = false;
|
let modified = false;
|
||||||
let rows = [];
|
let rows = [];
|
||||||
await ssb.sqlAsync(`
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
SELECT messages.id, author, content, timestamp
|
SELECT messages.id, author, content, timestamp
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?1) AS id ON messages.author = id.value
|
JOIN json_each(?1) AS id ON messages.author = id.value
|
||||||
@ -152,7 +163,8 @@ tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
|
|||||||
[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent],
|
[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent],
|
||||||
function (row) {
|
function (row) {
|
||||||
rows.push(row);
|
rows.push(row);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
max_rowid = rowid;
|
max_rowid = rowid;
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
if (await process_message(whoami, data, row, kind, parent)) {
|
if (await process_message(whoami, data, row, kind, parent)) {
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff">
|
<body style="color: #fff">
|
||||||
<tf-journal-app></tf-journal-app>
|
<tf-journal-app></tf-journal-app>
|
||||||
<script src="commonmark.min.js"></script>
|
<script src="commonmark.min.js"></script>
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="tf-journal-app.js" type="module"></script>
|
<script src="tf-journal-app.js" type="module"></script>
|
||||||
<script src="tf-journal-entry.js" type="module"></script>
|
<script src="tf-journal-entry.js" type="module"></script>
|
||||||
<script src="tf-id-picker.js" type="module"></script>
|
<script src="tf-id-picker.js" type="module"></script>
|
||||||
|
44
apps/journal/lit-all.min.js
vendored
44
apps/journal/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -19,15 +19,22 @@ class TfIdentityPickerElement extends LitElement {
|
|||||||
|
|
||||||
changed(event) {
|
changed(event) {
|
||||||
this.selected = event.srcElement.value;
|
this.selected = event.srcElement.value;
|
||||||
this.dispatchEvent(new Event('change', {
|
this.dispatchEvent(
|
||||||
|
new Event('change', {
|
||||||
srcElement: this,
|
srcElement: this,
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<select @change=${this.changed} style="max-width: 100%">
|
<select @change=${this.changed} style="max-width: 100%">
|
||||||
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
${(this.ids ?? []).map(
|
||||||
|
(id) =>
|
||||||
|
html`<option ?selected=${id == this.selected} value=${id}>
|
||||||
|
${id}
|
||||||
|
</option>`
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -28,9 +28,14 @@ class TfJournalAppElement extends LitElement {
|
|||||||
async read_journals() {
|
async read_journals() {
|
||||||
let max_rowid;
|
let max_rowid;
|
||||||
let journals;
|
let journals;
|
||||||
while (true)
|
while (true) {
|
||||||
{
|
[max_rowid, journals] = await tfrpc.rpc.collection(
|
||||||
[max_rowid, journals] = await tfrpc.rpc.collection([this.whoami], 'journal-entry', undefined, max_rowid, journals);
|
[this.whoami],
|
||||||
|
'journal-entry',
|
||||||
|
undefined,
|
||||||
|
max_rowid,
|
||||||
|
journals
|
||||||
|
);
|
||||||
this.journals = Object.assign({}, journals);
|
this.journals = Object.assign({}, journals);
|
||||||
console.log('JOURNALS', this.journals);
|
console.log('JOURNALS', this.journals);
|
||||||
}
|
}
|
||||||
@ -52,7 +57,11 @@ class TfJournalAppElement extends LitElement {
|
|||||||
};
|
};
|
||||||
message.recps = [this.whoami];
|
message.recps = [this.whoami];
|
||||||
print(message);
|
print(message);
|
||||||
message = await tfrpc.rpc.encrypt(this.whoami, message.recps, JSON.stringify(message));
|
message = await tfrpc.rpc.encrypt(
|
||||||
|
this.whoami,
|
||||||
|
message.recps,
|
||||||
|
JSON.stringify(message)
|
||||||
|
);
|
||||||
print(message);
|
print(message);
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message);
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
}
|
}
|
||||||
@ -62,12 +71,17 @@ class TfJournalAppElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed}></tf-id-picker>
|
<tf-id-picker
|
||||||
|
.ids=${this.ids}
|
||||||
|
selected=${this.whoami}
|
||||||
|
@change=${this.on_whoami_changed}
|
||||||
|
></tf-id-picker>
|
||||||
</div>
|
</div>
|
||||||
<tf-journal-entry
|
<tf-journal-entry
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.journals=${this.journals}
|
.journals=${this.journals}
|
||||||
@publish=${this.on_journal_publish}></tf-journal-entry>
|
@publish=${this.on_journal_publish}
|
||||||
|
></tf-journal-entry>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,13 +30,15 @@ class TfJournalEntryElement extends LitElement {
|
|||||||
|
|
||||||
async on_publish() {
|
async on_publish() {
|
||||||
console.log('publish', this.text);
|
console.log('publish', this.text);
|
||||||
this.dispatchEvent(new CustomEvent('publish', {
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('publish', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: {
|
detail: {
|
||||||
key: this.shadowRoot.getElementById('date_picker').value,
|
key: this.shadowRoot.getElementById('date_picker').value,
|
||||||
text: this.text,
|
text: this.text,
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
back_dates(count) {
|
back_dates(count) {
|
||||||
@ -63,19 +65,30 @@ class TfJournalEntryElement extends LitElement {
|
|||||||
console.log('RENDER ENTRY', this.key, this.journals?.[this.key]);
|
console.log('RENDER ENTRY', this.key, this.journals?.[this.key]);
|
||||||
return html`
|
return html`
|
||||||
<select id="date_picker" @change=${this.on_date_change}>
|
<select id="date_picker" @change=${this.on_date_change}>
|
||||||
${this.back_dates(10).map(x => html`
|
${this.back_dates(10).map(
|
||||||
<option value=${x}>${x}</option>
|
(x) => html` <option value=${x}>${x}</option> `
|
||||||
`)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
<div style="display: inline-flex; flex-direction: row">
|
<div style="display: inline-flex; flex-direction: row">
|
||||||
<button ?disabled=${this.text == this.journals?.[this.key]?.text} @click=${this.on_publish}>Publish</button>
|
<button
|
||||||
|
?disabled=${this.text == this.journals?.[this.key]?.text}
|
||||||
|
@click=${this.on_publish}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
<button @click=${this.on_discard}>Discard</button>
|
<button @click=${this.on_discard}>Discard</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<textarea
|
<textarea
|
||||||
style="flex: 1 1; min-height: 10em"
|
style="flex: 1 1; min-height: 10em"
|
||||||
@input=${this.on_edit} .value=${this.text ?? this.journals?.[this.key]?.text ?? ''}></textarea>
|
@input=${this.on_edit}
|
||||||
<div style="flex: 1 1">${unsafeHTML(this.markdown(this.text ?? this.journals?.[this.key]?.text))}</div>
|
.value=${this.text ?? this.journals?.[this.key]?.text ?? ''}
|
||||||
|
></textarea>
|
||||||
|
<div style="flex: 1 1">
|
||||||
|
${unsafeHTML(
|
||||||
|
this.markdown(this.text ?? this.journals?.[this.key]?.text)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
5
apps/room.json
Normal file
5
apps/room.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"type": "tildefriends-app",
|
||||||
|
"emoji": "🚪",
|
||||||
|
"previous": "&HXCdDG8gGYXElTyEFbg85jqa6lDXNL2ENPIA9UoJNbI=.sha256"
|
||||||
|
}
|
13
apps/room/app.js
Normal file
13
apps/room/app.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
async function main() {
|
||||||
|
let host = core.url.match(/.*\/\/(.*?)\//)[1];
|
||||||
|
let id = (await ssb.getServerIdentity()).substring(1);
|
||||||
|
let room = `net:${host}:${ssb.port}~shs:${id}:SSB+Room+SK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
|
||||||
|
await app.setDocument(`
|
||||||
|
<body style="color: #fff">
|
||||||
|
<h1>Server</h1>
|
||||||
|
<div>The local server address is:</div>
|
||||||
|
<div><input type="text" readonly value="${room}" style="width: 100%"></input></div>
|
||||||
|
</body>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
main();
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "👟"
|
"emoji": "👟",
|
||||||
|
"previous": "&lYZRnT2UGQxXxYISbuaZewik9AuxBpcJumakwrePw5c=.sha256"
|
||||||
}
|
}
|
@ -1,12 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="color: #fff">
|
<html style="color: #fff">
|
||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends</title>
|
<title>Tilde Friends</title>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<tf-sneaker-app />
|
<tf-sneaker-app />
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="filesaver.min.js"></script>
|
<script src="filesaver.min.js"></script>
|
||||||
<script src="jszip.min.js"></script>
|
<script src="jszip.min.js"></script>
|
||||||
<script src="script.js" type="module"></script>
|
<script src="script.js" type="module"></script>
|
||||||
|
44
apps/sneaker/lit-all.min.js
vendored
44
apps/sneaker/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -19,7 +19,8 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
|
|
||||||
async search() {
|
async search() {
|
||||||
let q = this.renderRoot.getElementById('search').value;
|
let q = this.renderRoot.getElementById('search').value;
|
||||||
let result = await tfrpc.rpc.query(`
|
let result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name
|
SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name
|
||||||
FROM messages_fts(?)
|
FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
@ -31,15 +32,17 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
HAVING MAX(messages.sequence)
|
HAVING MAX(messages.sequence)
|
||||||
ORDER BY COUNT(*) DESC
|
ORDER BY COUNT(*) DESC
|
||||||
`,
|
`,
|
||||||
[`"${q.replaceAll('"', '""')}"`]);
|
[`"${q.replaceAll('"', '""')}"`]
|
||||||
this.feeds = Object.fromEntries(result.map(x => [x.id, x.name]));
|
);
|
||||||
|
this.feeds = Object.fromEntries(result.map((x) => [x.id, x.name]));
|
||||||
}
|
}
|
||||||
|
|
||||||
format_message(message) {
|
format_message(message) {
|
||||||
|
const k_flag_sequence_before_author = 1;
|
||||||
let out = {
|
let out = {
|
||||||
previous: message.previous ?? null,
|
previous: message.previous ?? null,
|
||||||
};
|
};
|
||||||
if (message.sequence_before_author) {
|
if (message.flags & k_flag_sequence_before_author) {
|
||||||
out.sequence = message.sequence;
|
out.sequence = message.sequence;
|
||||||
out.author = message.author;
|
out.author = message.author;
|
||||||
} else {
|
} else {
|
||||||
@ -70,24 +73,104 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
|
if (
|
||||||
startsWith(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
|
startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
|
||||||
|
startsWith(
|
||||||
|
data,
|
||||||
|
[0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]
|
||||||
|
) ||
|
||||||
startsWith(data, [0xff, 0xd8, 0xff, 0xee]) ||
|
startsWith(data, [0xff, 0xd8, 0xff, 0xee]) ||
|
||||||
startsWith(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
|
startsWith(data, [
|
||||||
|
0xff,
|
||||||
|
0xd8,
|
||||||
|
0xff,
|
||||||
|
0xe1,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x45,
|
||||||
|
0x78,
|
||||||
|
0x69,
|
||||||
|
0x66,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
])
|
||||||
|
) {
|
||||||
return '.jpg';
|
return '.jpg';
|
||||||
} else if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
} else if (
|
||||||
|
startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
||||||
|
) {
|
||||||
return '.png';
|
return '.png';
|
||||||
} else if (startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
} else if (
|
||||||
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
|
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
||||||
|
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])
|
||||||
|
) {
|
||||||
return '.gif';
|
return '.gif';
|
||||||
} else if (startsWith(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) {
|
} else if (
|
||||||
|
startsWith(data, [
|
||||||
|
0x52,
|
||||||
|
0x49,
|
||||||
|
0x46,
|
||||||
|
0x46,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x57,
|
||||||
|
0x45,
|
||||||
|
0x42,
|
||||||
|
0x50,
|
||||||
|
])
|
||||||
|
) {
|
||||||
return '.webp';
|
return '.webp';
|
||||||
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
|
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
|
||||||
return '.svg';
|
return '.svg';
|
||||||
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
} else if (
|
||||||
|
startsWith(data, [
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x66,
|
||||||
|
0x74,
|
||||||
|
0x79,
|
||||||
|
0x70,
|
||||||
|
0x6d,
|
||||||
|
0x70,
|
||||||
|
0x34,
|
||||||
|
0x32,
|
||||||
|
])
|
||||||
|
) {
|
||||||
return '.mp3';
|
return '.mp3';
|
||||||
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) ||
|
} else if (
|
||||||
startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
startsWith(data, [
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x66,
|
||||||
|
0x74,
|
||||||
|
0x79,
|
||||||
|
0x70,
|
||||||
|
0x69,
|
||||||
|
0x73,
|
||||||
|
0x6f,
|
||||||
|
0x6d,
|
||||||
|
]) ||
|
||||||
|
startsWith(data, [
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
0x66,
|
||||||
|
0x74,
|
||||||
|
0x79,
|
||||||
|
0x70,
|
||||||
|
0x6d,
|
||||||
|
0x70,
|
||||||
|
0x34,
|
||||||
|
0x32,
|
||||||
|
])
|
||||||
|
) {
|
||||||
return '.mp4';
|
return '.mp4';
|
||||||
} else {
|
} else {
|
||||||
return '.bin';
|
return '.bin';
|
||||||
@ -98,17 +181,34 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
let all_messages = '';
|
let all_messages = '';
|
||||||
let sequence = -1;
|
let sequence = -1;
|
||||||
let messages_done = 0;
|
let messages_done = 0;
|
||||||
let messages_max = (await tfrpc.rpc.query('SELECT MAX(sequence) AS total FROM messages WHERE author = ?', [id]))[0].total;
|
let messages_max = (
|
||||||
|
await tfrpc.rpc.query(
|
||||||
|
'SELECT MAX(sequence) AS total FROM messages WHERE author = ?',
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
)[0].total;
|
||||||
while (true) {
|
while (true) {
|
||||||
let messages = await tfrpc.rpc.query(
|
let messages = await tfrpc.rpc.query(
|
||||||
'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100',
|
`
|
||||||
|
SELECT author, id, sequence, timestamp, hash, json(content) AS content, signature, flags
|
||||||
|
FROM messages
|
||||||
|
WHERE author = ? AND SEQUENCE > ?
|
||||||
|
ORDER BY sequence LIMIT 100
|
||||||
|
`,
|
||||||
[id, sequence]
|
[id, sequence]
|
||||||
);
|
);
|
||||||
if (messages?.length) {
|
if (messages?.length) {
|
||||||
all_messages += messages.map(x => JSON.stringify(this.format_message(x))).join('\n') + '\n';
|
all_messages +=
|
||||||
|
messages
|
||||||
|
.map((x) => JSON.stringify(this.format_message(x)))
|
||||||
|
.join('\n') + '\n';
|
||||||
sequence = messages[messages.length - 1].sequence;
|
sequence = messages[messages.length - 1].sequence;
|
||||||
messages_done += messages.length;
|
messages_done += messages.length;
|
||||||
this.progress = {name: 'messages', value: messages_done, max: messages_max};
|
this.progress = {
|
||||||
|
name: 'messages',
|
||||||
|
value: messages_done,
|
||||||
|
max: messages_max,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -122,7 +222,8 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
FROM messages
|
FROM messages
|
||||||
JOIN messages_refs ON messages.id = messages_refs.message
|
JOIN messages_refs ON messages.id = messages_refs.message
|
||||||
WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
|
WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
|
||||||
[id]);
|
[id]
|
||||||
|
);
|
||||||
let blobs_done = 0;
|
let blobs_done = 0;
|
||||||
for (let row of blobs) {
|
for (let row of blobs) {
|
||||||
this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
|
this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
|
||||||
@ -133,7 +234,10 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
console.log(`Failed to get ${row.id}: ${e.message}`);
|
console.log(`Failed to get ${row.id}: ${e.message}`);
|
||||||
}
|
}
|
||||||
if (blob) {
|
if (blob) {
|
||||||
zip.file(`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, new Uint8Array(blob));
|
zip.file(
|
||||||
|
`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`,
|
||||||
|
new Uint8Array(blob)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
blobs_done++;
|
blobs_done++;
|
||||||
}
|
}
|
||||||
@ -181,7 +285,11 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let message = JSON.parse(line);
|
let message = JSON.parse(line);
|
||||||
this.progress = {name: 'messages', value: progress++, max: total_messages};
|
this.progress = {
|
||||||
|
name: 'messages',
|
||||||
|
value: progress++,
|
||||||
|
max: total_messages,
|
||||||
|
};
|
||||||
if (await tfrpc.rpc.store_message(message.value)) {
|
if (await tfrpc.rpc.store_message(message.value)) {
|
||||||
success.messages++;
|
success.messages++;
|
||||||
}
|
}
|
||||||
@ -202,7 +310,13 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
let progress;
|
let progress;
|
||||||
if (this.progress) {
|
if (this.progress) {
|
||||||
if (this.progress.max) {
|
if (this.progress.max) {
|
||||||
progress = html`<div><label for="progress">${this.progress.name}</label><progress value=${this.progress.value} max=${this.progress.max}></progress></div>`;
|
progress = html`<div>
|
||||||
|
<label for="progress">${this.progress.name}</label
|
||||||
|
><progress
|
||||||
|
value=${this.progress.value}
|
||||||
|
max=${this.progress.max}
|
||||||
|
></progress>
|
||||||
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
progress = html`<div><span>${this.progress.name}</span></div>`;
|
progress = html`<div><span>${this.progress.name}</span></div>`;
|
||||||
}
|
}
|
||||||
@ -218,13 +332,17 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
<input type="text" id="search" @keypress=${this.keypress}></input>
|
<input type="text" id="search" @keypress=${this.keypress}></input>
|
||||||
<input type="button" value="Search Users" @click=${this.search}></input>
|
<input type="button" value="Search Users" @click=${this.search}></input>
|
||||||
<ul>
|
<ul>
|
||||||
${Object.entries(this.feeds).map(([id, name]) => html`
|
${Object.entries(this.feeds).map(
|
||||||
|
([id, name]) => html`
|
||||||
<li>
|
<li>
|
||||||
${this.progress ? undefined : html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
|
${this.progress
|
||||||
|
? undefined
|
||||||
|
: html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
|
||||||
${name}
|
${name}
|
||||||
<code style="color: #ccc">${id}</code>
|
<code style="color: #ccc">${id}</code>
|
||||||
</li>
|
</li>
|
||||||
`)}
|
`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🐌",
|
"emoji": "🐌",
|
||||||
"previous": "&h+PXCrnUHtHHfKyUaLW+Y1dP/JpWwG9cbRNjxOCVqw0=.sha256"
|
"previous": "&6oHPQCA26v+4nBXv+YUdCT43j2DpXDspxhHSSRydkiw=.sha256"
|
||||||
}
|
}
|
@ -76,7 +76,7 @@ tfrpc.register(function getHash(id, message) {
|
|||||||
tfrpc.register(function setHash(hash) {
|
tfrpc.register(function setHash(hash) {
|
||||||
return app.setHash(hash);
|
return app.setHash(hash);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('message', async function(id) {
|
core.register('onMessage', async function (id) {
|
||||||
await tfrpc.rpc.notifyNewMessage(id);
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
});
|
});
|
||||||
tfrpc.register(async function store_blob(blob) {
|
tfrpc.register(async function store_blob(blob) {
|
||||||
@ -100,16 +100,22 @@ tfrpc.register(async function try_decrypt(id, content) {
|
|||||||
tfrpc.register(async function encrypt(id, recipients, content) {
|
tfrpc.register(async function encrypt(id, recipients, content) {
|
||||||
return await ssb.privateMessageEncrypt(id, recipients, content);
|
return await ssb.privateMessageEncrypt(id, recipients, content);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('broadcasts', async function() {
|
tfrpc.register(async function getActiveIdentity() {
|
||||||
|
return await ssb.getActiveIdentity();
|
||||||
|
});
|
||||||
|
core.register('onBroadcastsChanged', async function () {
|
||||||
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
});
|
});
|
||||||
|
|
||||||
core.register('onConnectionsChanged', async function () {
|
core.register('onConnectionsChanged', async function () {
|
||||||
await tfrpc.rpc.set('connections', await ssb.connections());
|
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||||
});
|
});
|
||||||
|
core.register('setActiveIdentity', async function (id) {
|
||||||
|
await tfrpc.rpc.set('identity', id);
|
||||||
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (typeof(database) !== 'undefined') {
|
if (typeof database !== 'undefined') {
|
||||||
g_database = await database('ssb');
|
g_database = await database('ssb');
|
||||||
}
|
}
|
||||||
await app.setDocument(utf8Decode(await getFile('index.html')));
|
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
function textNode(text) {
|
function textNode(text) {
|
||||||
const node = new commonmark.Node("text", undefined);
|
const node = new commonmark.Node('text', undefined);
|
||||||
node.literal = text;
|
node.literal = text;
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkNode(text, link) {
|
function linkNode(text, link) {
|
||||||
const linkNode = new commonmark.Node("link", undefined);
|
const linkNode = new commonmark.Node('link', undefined);
|
||||||
|
if (link.startsWith('#')) {
|
||||||
linkNode.destination = `#q=${encodeURIComponent(link)}`;
|
linkNode.destination = `#q=${encodeURIComponent(link)}`;
|
||||||
|
} else {
|
||||||
|
linkNode.destination = link;
|
||||||
|
}
|
||||||
linkNode.appendChild(textNode(text));
|
linkNode.appendChild(textNode(text));
|
||||||
return linkNode;
|
return linkNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitMatches(text, regexp) {
|
function splitMatches(text, regexp) {
|
||||||
// Regexp must be sticky.
|
// Regexp must be sticky.
|
||||||
regexp = new RegExp(regexp, "gm");
|
regexp = new RegExp(regexp, 'gm');
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const result = [];
|
const result = [];
|
||||||
@ -39,13 +43,13 @@ function splitMatches(text, regexp) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const regex = new RegExp("(?<!\w)#[\\w-]+");
|
const regex = new RegExp('(?:https?://[^ ]+[^ .,])|(?:(?<!\\w)#[\\w-]+)|(?:@[A-Za-z0-9+/]+=.ed25519)|(?:[%&][A-Za-z0-9+/]+=.sha256)');
|
||||||
|
|
||||||
function split(textNodes) {
|
function split(textNodes) {
|
||||||
const text = textNodes.map(n => n.literal).join("");
|
const text = textNodes.map((n) => n.literal).join('');
|
||||||
const parts = splitMatches(text, regex);
|
const parts = splitMatches(text, regex);
|
||||||
|
|
||||||
return parts.map(part => {
|
return parts.map((part) => {
|
||||||
if (part[1]) {
|
if (part[1]) {
|
||||||
return linkNode(part[0], part[0]);
|
return linkNode(part[0], part[0]);
|
||||||
} else {
|
} else {
|
||||||
@ -61,17 +65,17 @@ export function transform(parsed) {
|
|||||||
let nodes = [];
|
let nodes = [];
|
||||||
while ((event = walker.next())) {
|
while ((event = walker.next())) {
|
||||||
const node = event.node;
|
const node = event.node;
|
||||||
if (event.entering && node.type === "text") {
|
if (event.entering && node.type === 'text') {
|
||||||
nodes.push(node);
|
nodes.push(node);
|
||||||
} else {
|
} else {
|
||||||
if (nodes.length > 0) {
|
if (nodes.length > 0) {
|
||||||
split(nodes)
|
split(nodes)
|
||||||
.reverse()
|
.reverse()
|
||||||
.forEach(newNode => {
|
.forEach((newNode) => {
|
||||||
nodes[0].insertAfter(newNode);
|
nodes[0].insertAfter(newNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
nodes.forEach(n => n.unlink());
|
nodes.forEach((n) => n.unlink());
|
||||||
nodes = [];
|
nodes = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,10 +84,10 @@ export function transform(parsed) {
|
|||||||
if (nodes.length > 0) {
|
if (nodes.length > 0) {
|
||||||
split(nodes)
|
split(nodes)
|
||||||
.reverse()
|
.reverse()
|
||||||
.forEach(newNode => {
|
.forEach((newNode) => {
|
||||||
nodes[0].insertAfter(newNode);
|
nodes[0].insertAfter(newNode);
|
||||||
});
|
});
|
||||||
nodes.forEach(n => n.unlink());
|
nodes.forEach((n) => n.unlink());
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
|
@ -1,91 +0,0 @@
|
|||||||
function textNode(text) {
|
|
||||||
const node = new commonmark.Node("text", undefined);
|
|
||||||
node.literal = text;
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
function linkNode(text, url) {
|
|
||||||
const urlNode = new commonmark.Node("link", undefined);
|
|
||||||
urlNode.destination = url;
|
|
||||||
urlNode.appendChild(textNode(text));
|
|
||||||
|
|
||||||
return urlNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitMatches(text, regexp) {
|
|
||||||
// Regexp must be sticky.
|
|
||||||
regexp = new RegExp(regexp, "gm");
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
let match = regexp.exec(text);
|
|
||||||
while (match) {
|
|
||||||
const matchText = match[0];
|
|
||||||
|
|
||||||
if (match.index > i) {
|
|
||||||
result.push([text.substring(i, match.index), false]);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push([matchText, true]);
|
|
||||||
i = match.index + matchText.length;
|
|
||||||
|
|
||||||
match = regexp.exec(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i < text.length) {
|
|
||||||
result.push([text.substring(i, text.length), false]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlRegexp = new RegExp("https?://[^ ]+[^ .,]");
|
|
||||||
|
|
||||||
function splitURLs(textNodes) {
|
|
||||||
const text = textNodes.map(n => n.literal).join("");
|
|
||||||
const parts = splitMatches(text, urlRegexp);
|
|
||||||
|
|
||||||
return parts.map(part => {
|
|
||||||
if (part[1]) {
|
|
||||||
return linkNode(part[0], part[0]);
|
|
||||||
} else {
|
|
||||||
return textNode(part[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transform(parsed) {
|
|
||||||
const walker = parsed.walker();
|
|
||||||
let event;
|
|
||||||
|
|
||||||
let nodes = [];
|
|
||||||
while ((event = walker.next())) {
|
|
||||||
const node = event.node;
|
|
||||||
if (event.entering && node.type === "text") {
|
|
||||||
nodes.push(node);
|
|
||||||
} else {
|
|
||||||
if (nodes.length > 0) {
|
|
||||||
splitURLs(nodes)
|
|
||||||
.reverse()
|
|
||||||
.forEach(newNode => {
|
|
||||||
nodes[0].insertAfter(newNode);
|
|
||||||
});
|
|
||||||
|
|
||||||
nodes.forEach(n => n.unlink());
|
|
||||||
nodes = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodes.length > 0) {
|
|
||||||
splitURLs(nodes)
|
|
||||||
.reverse()
|
|
||||||
.forEach(newNode => {
|
|
||||||
nodes[0].insertAfter(newNode);
|
|
||||||
});
|
|
||||||
nodes.forEach(n => n.unlink());
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed;
|
|
||||||
}
|
|
@ -1,3 +1,7 @@
|
|||||||
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
import {html, render} from './lit-all.min.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
let g_emojis;
|
let g_emojis;
|
||||||
|
|
||||||
function get_emojis() {
|
function get_emojis() {
|
||||||
@ -10,19 +14,30 @@ function get_emojis() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function picker(callback, anchor) {
|
async function get_recent(author) {
|
||||||
get_emojis().then(function(json) {
|
let recent = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
|
SELECT DISTINCT content ->> '$.vote.expression' AS value
|
||||||
|
FROM messages
|
||||||
|
WHERE author = ? AND
|
||||||
|
content ->> '$.type' = 'vote'
|
||||||
|
ORDER BY timestamp DESC LIMIT 10
|
||||||
|
`,
|
||||||
|
[author]
|
||||||
|
);
|
||||||
|
return recent.map((x) => x.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function picker(callback, anchor, author) {
|
||||||
|
let json = await get_emojis();
|
||||||
|
let recent = await get_recent(author);
|
||||||
|
|
||||||
let div = document.createElement('div');
|
let div = document.createElement('div');
|
||||||
div.id = 'emoji_picker';
|
div.id = 'emoji_picker';
|
||||||
div.style.color = '#000';
|
div.style.color = '#000';
|
||||||
div.style.background = '#fff';
|
div.style.background = '#fff';
|
||||||
div.style.border = '1px solid #000';
|
div.style.border = '1px solid #000';
|
||||||
div.style.display = 'block';
|
div.style.display = 'block';
|
||||||
div.style.position = 'absolute';
|
|
||||||
div.style.minWidth = 'min(16em, 90vw)';
|
|
||||||
div.style.width = 'min(16em, 90vw)';
|
|
||||||
div.style.maxWidth = 'min(16em, 90vw)';
|
|
||||||
div.style.maxHeight = '16em';
|
|
||||||
div.style.overflow = 'scroll';
|
div.style.overflow = 'scroll';
|
||||||
div.style.fontWeight = 'bold';
|
div.style.fontWeight = 'bold';
|
||||||
div.style.fontSize = 'xx-large';
|
div.style.fontSize = 'xx-large';
|
||||||
@ -40,14 +55,6 @@ export function picker(callback, anchor) {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
function cleanup() {
|
|
||||||
console.log('emoji cleanup');
|
|
||||||
div.parentElement.removeChild(div);
|
|
||||||
window.removeEventListener('keydown', key_down);
|
|
||||||
console.log('removing click');
|
|
||||||
document.body.removeEventListener('mousedown', cleanup);
|
|
||||||
}
|
|
||||||
|
|
||||||
function key_down(event) {
|
function key_down(event) {
|
||||||
if (event.key == 'Escape') {
|
if (event.key == 'Escape') {
|
||||||
cleanup();
|
cleanup();
|
||||||
@ -66,15 +73,51 @@ export function picker(callback, anchor) {
|
|||||||
}
|
}
|
||||||
let search = input.value.toLowerCase();
|
let search = input.value.toLowerCase();
|
||||||
let any_at_all = false;
|
let any_at_all = false;
|
||||||
|
if (recent) {
|
||||||
|
let emoji_to_name = {};
|
||||||
|
for (let row of Object.values(json)) {
|
||||||
|
for (let entry of Object.entries(row)) {
|
||||||
|
emoji_to_name[entry[1]] = entry[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let header = document.createElement('div');
|
||||||
|
header.appendChild(document.createTextNode('Recent'));
|
||||||
|
list.appendChild(header);
|
||||||
|
let any = false;
|
||||||
|
for (let entry of recent) {
|
||||||
|
if (
|
||||||
|
search &&
|
||||||
|
search.length &&
|
||||||
|
(emoji_to_name[entry] || '').toLowerCase().indexOf(search) == -1
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let emoji = document.createElement('span');
|
||||||
|
const k_size = '1.25em';
|
||||||
|
emoji.style.display = 'inline-block';
|
||||||
|
emoji.style.overflow = 'hidden';
|
||||||
|
emoji.style.cursor = 'pointer';
|
||||||
|
emoji.onclick = chosen;
|
||||||
|
emoji.title = emoji_to_name[entry] || entry;
|
||||||
|
emoji.appendChild(document.createTextNode(entry));
|
||||||
|
list.appendChild(emoji);
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
list.removeChild(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
for (let row of Object.entries(json)) {
|
for (let row of Object.entries(json)) {
|
||||||
let header = document.createElement('div');
|
let header = document.createElement('div');
|
||||||
header.appendChild(document.createTextNode(row[0]));
|
header.appendChild(document.createTextNode(row[0]));
|
||||||
list.appendChild(header);
|
list.appendChild(header);
|
||||||
let any = false;
|
let any = false;
|
||||||
for (let entry of Object.entries(row[1])) {
|
for (let entry of Object.entries(row[1])) {
|
||||||
if (search &&
|
if (
|
||||||
|
search &&
|
||||||
search.length &&
|
search.length &&
|
||||||
entry[0].toLowerCase().indexOf(search) == -1) {
|
entry[0].toLowerCase().indexOf(search) == -1
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let emoji = document.createElement('span');
|
let emoji = document.createElement('span');
|
||||||
@ -99,14 +142,23 @@ export function picker(callback, anchor) {
|
|||||||
}
|
}
|
||||||
refresh();
|
refresh();
|
||||||
input.oninput = refresh;
|
input.oninput = refresh;
|
||||||
document.body.appendChild(div);
|
let modal = html`
|
||||||
div.style.position = 'fixed';
|
<style>
|
||||||
div.style.top = '50%';
|
${styles}
|
||||||
div.style.left = '50%';
|
</style>
|
||||||
div.style.transform = 'translate(-50%, -50%)';
|
<div class="w3-modal" style="display: block">
|
||||||
|
<div class="w3-modal-content w3-card-4">${div}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
let parent = document.createElement('div');
|
||||||
|
document.body.appendChild(parent);
|
||||||
|
function cleanup() {
|
||||||
|
parent.parentElement.removeChild(parent);
|
||||||
|
window.removeEventListener('keydown', key_down);
|
||||||
|
document.body.removeEventListener('mousedown', cleanup);
|
||||||
|
}
|
||||||
|
render(modal, parent);
|
||||||
input.focus();
|
input.focus();
|
||||||
console.log('adding click');
|
|
||||||
document.body.addEventListener('mousedown', cleanup);
|
document.body.addEventListener('mousedown', cleanup);
|
||||||
window.addEventListener('keydown', key_down);
|
window.addEventListener('keydown', key_down);
|
||||||
});
|
|
||||||
}
|
}
|
@ -1,8 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="color: #fff">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends</title>
|
<title>Tilde Friends</title>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
<link rel="stylesheet" href="tribute.css" />
|
<link rel="stylesheet" href="tribute.css" />
|
||||||
<style>
|
<style>
|
||||||
.tribute-container {
|
.tribute-container {
|
||||||
@ -10,12 +10,14 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body style="background-color: #223a5e">
|
<body style="margin: 0; padding: 0">
|
||||||
<tf-app class="w3-deep-purple"/>
|
<tf-app></tf-app>
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<tf-reactions-modal id="reactions_modal"></tf-reactions-modal>
|
||||||
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="filesaver.min.js"></script>
|
<script src="filesaver.min.js"></script>
|
||||||
<script src="commonmark.min.js"></script>
|
<script src="commonmark.min.js"></script>
|
||||||
<script src="commonmark-linkify.js" type="module"></script>
|
|
||||||
<script src="commonmark-hashtag.js" type="module"></script>
|
<script src="commonmark-hashtag.js" type="module"></script>
|
||||||
<script src="script.js" type="module"></script>
|
<script src="script.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
44
apps/ssb/lit-all.min.js
vendored
44
apps/ssb/lit-all.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,13 +1,13 @@
|
|||||||
import {LitElement, html} from './lit-all.min.js';
|
import {LitElement, html} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
|
|
||||||
import * as tf_id_picker from './tf-id-picker.js';
|
|
||||||
import * as tf_app from './tf-app.js';
|
import * as tf_app from './tf-app.js';
|
||||||
import * as tf_message from './tf-message.js';
|
import * as tf_message from './tf-message.js';
|
||||||
import * as tf_user from './tf-user.js';
|
import * as tf_user from './tf-user.js';
|
||||||
import * as tf_compose from './tf-compose.js';
|
import * as tf_compose from './tf-compose.js';
|
||||||
import * as tf_news from './tf-news.js';
|
import * as tf_news from './tf-news.js';
|
||||||
import * as tf_profile from './tf-profile.js';
|
import * as tf_profile from './tf-profile.js';
|
||||||
|
import * as tf_reactions_modal from './tf-reactions-modal.js';
|
||||||
import * as tf_tab_mentions from './tf-tab-mentions.js';
|
import * as tf_tab_mentions from './tf-tab-mentions.js';
|
||||||
import * as tf_tab_news from './tf-tab-news.js';
|
import * as tf_tab_news from './tf-tab-news.js';
|
||||||
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
|
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
|
||||||
|
@ -34,9 +34,13 @@ class TfElement extends LitElement {
|
|||||||
this.users = {};
|
this.users = {};
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.tags = [];
|
this.tags = [];
|
||||||
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || []; });
|
tfrpc.rpc.getBroadcasts().then((b) => {
|
||||||
tfrpc.rpc.getConnections().then(c => { self.connections = c || []; });
|
self.broadcasts = b || [];
|
||||||
tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
|
});
|
||||||
|
tfrpc.rpc.getConnections().then((c) => {
|
||||||
|
self.connections = c || [];
|
||||||
|
});
|
||||||
|
tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
|
||||||
tfrpc.register(function hashChanged(hash) {
|
tfrpc.register(function hashChanged(hash) {
|
||||||
self.set_hash(hash);
|
self.set_hash(hash);
|
||||||
});
|
});
|
||||||
@ -48,13 +52,15 @@ class TfElement extends LitElement {
|
|||||||
self.broadcasts = value;
|
self.broadcasts = value;
|
||||||
} else if (name === 'connections') {
|
} else if (name === 'connections') {
|
||||||
self.connections = value;
|
self.connections = value;
|
||||||
|
} else if (name === 'identity') {
|
||||||
|
self.whoami = value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.initial_load();
|
this.initial_load();
|
||||||
}
|
}
|
||||||
|
|
||||||
async initial_load() {
|
async initial_load() {
|
||||||
let whoami = await tfrpc.rpc.localStorageGet('whoami');
|
let whoami = await tfrpc.rpc.getActiveIdentity();
|
||||||
let ids = (await tfrpc.rpc.getIdentities()) || [];
|
let ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
|
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
|
||||||
this.ids = ids;
|
this.ids = ids;
|
||||||
@ -86,9 +92,14 @@ class TfElement extends LitElement {
|
|||||||
last_row_id: 0,
|
last_row_id: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let max_row_id = (await tfrpc.rpc.query(`
|
let max_row_id = (
|
||||||
|
await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
`, []))[0].max_row_id;
|
`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
)[0].max_row_id;
|
||||||
for (let id of Object.keys(cache.about)) {
|
for (let id of Object.keys(cache.about)) {
|
||||||
if (ids.indexOf(id) == -1) {
|
if (ids.indexOf(id) == -1) {
|
||||||
delete cache.about[id];
|
delete cache.about[id];
|
||||||
@ -98,7 +109,7 @@ class TfElement extends LitElement {
|
|||||||
let abouts = await tfrpc.rpc.query(
|
let abouts = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
messages.*
|
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM
|
FROM
|
||||||
messages,
|
messages,
|
||||||
json_each(?1) AS following
|
json_each(?1) AS following
|
||||||
@ -109,7 +120,7 @@ class TfElement extends LitElement {
|
|||||||
json_extract(messages.content, '$.type') = 'about'
|
json_extract(messages.content, '$.type') = 'about'
|
||||||
UNION
|
UNION
|
||||||
SELECT
|
SELECT
|
||||||
messages.*
|
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM
|
FROM
|
||||||
messages,
|
messages,
|
||||||
json_each(?2) AS following
|
json_each(?2) AS following
|
||||||
@ -120,17 +131,21 @@ class TfElement extends LitElement {
|
|||||||
ORDER BY messages.author, messages.sequence
|
ORDER BY messages.author, messages.sequence
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
JSON.stringify(ids.filter(id => cache.about[id])),
|
JSON.stringify(ids.filter((id) => cache.about[id])),
|
||||||
JSON.stringify(ids.filter(id => !cache.about[id])),
|
JSON.stringify(ids.filter((id) => !cache.about[id])),
|
||||||
cache.last_row_id,
|
cache.last_row_id,
|
||||||
max_row_id,
|
max_row_id,
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
for (let about of abouts) {
|
for (let about of abouts) {
|
||||||
let content = JSON.parse(about.content);
|
let content = JSON.parse(about.content);
|
||||||
if (content.about === about.author) {
|
if (content.about === about.author) {
|
||||||
delete content.type;
|
delete content.type;
|
||||||
delete content.about;
|
delete content.about;
|
||||||
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
|
cache.about[about.author] = Object.assign(
|
||||||
|
cache.about[about.author] || {},
|
||||||
|
content
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache.last_row_id = max_row_id;
|
cache.last_row_id = max_row_id;
|
||||||
@ -145,15 +160,13 @@ class TfElement extends LitElement {
|
|||||||
async fetch_new_message(id) {
|
async fetch_new_message(id) {
|
||||||
let messages = await tfrpc.rpc.query(
|
let messages = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT messages.*
|
SELECT messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.id = ?
|
WHERE messages.id = ?
|
||||||
`,
|
`,
|
||||||
[
|
[JSON.stringify(this.following), id]
|
||||||
JSON.stringify(this.following),
|
);
|
||||||
id,
|
|
||||||
]);
|
|
||||||
if (messages && messages.length) {
|
if (messages && messages.length) {
|
||||||
this.unread = [...this.unread, ...messages];
|
this.unread = [...this.unread, ...messages];
|
||||||
this.unread = this.unread.slice(this.unread.length - 1024);
|
this.unread = this.unread.slice(this.unread.length - 1024);
|
||||||
@ -173,7 +186,7 @@ class TfElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create_identity() {
|
async create_identity() {
|
||||||
if (confirm("Are you sure you want to create a new identity?")) {
|
if (confirm('Are you sure you want to create a new identity?')) {
|
||||||
await tfrpc.rpc.createIdentity();
|
await tfrpc.rpc.createIdentity();
|
||||||
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
if (this.ids && !this.whoami) {
|
if (this.ids && !this.whoami) {
|
||||||
@ -182,20 +195,12 @@ class TfElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_id_picker() {
|
|
||||||
return html`
|
|
||||||
<div style="display: flex; gap: 8px">
|
|
||||||
<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() {
|
async load_recent_tags() {
|
||||||
let start = new Date();
|
let start = new Date();
|
||||||
this.tags = await tfrpc.rpc.query(`
|
this.tags = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
WITH
|
WITH
|
||||||
recent AS (SELECT id, content FROM messages
|
recent AS (SELECT id, json(content) AS content FROM messages
|
||||||
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
|
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
|
||||||
ORDER BY timestamp DESC LIMIT 1024),
|
ORDER BY timestamp DESC LIMIT 1024),
|
||||||
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
|
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
|
||||||
@ -207,7 +212,9 @@ class TfElement extends LitElement {
|
|||||||
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
|
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
|
||||||
by_message AS (SELECT DISTINCT id, tag FROM combined)
|
by_message AS (SELECT DISTINCT id, tag FROM combined)
|
||||||
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
|
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
|
||||||
`, [new Date() - 7 * 24 * 60 * 60 * 1000]);
|
`,
|
||||||
|
[new Date() - 7 * 24 * 60 * 60 * 1000]
|
||||||
|
);
|
||||||
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
|
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,7 +234,15 @@ class TfElement extends LitElement {
|
|||||||
by_count.push({count: v.of, id: id});
|
by_count.push({count: v.of, id: id});
|
||||||
}
|
}
|
||||||
console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
|
console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
|
||||||
|
let start_time = new Date();
|
||||||
users = await this.fetch_about(Object.keys(following).sort(), users);
|
users = await this.fetch_about(Object.keys(following).sort(), users);
|
||||||
|
console.log(
|
||||||
|
'about took',
|
||||||
|
(new Date() - start_time) / 1000.0,
|
||||||
|
'seconds for',
|
||||||
|
Object.keys(users).length,
|
||||||
|
'users'
|
||||||
|
);
|
||||||
this.following = Object.keys(following);
|
this.following = Object.keys(following);
|
||||||
this.users = users;
|
this.users = users;
|
||||||
await tags;
|
await tags;
|
||||||
@ -241,23 +256,54 @@ class TfElement extends LitElement {
|
|||||||
let users = this.users;
|
let users = this.users;
|
||||||
if (this.tab === 'news') {
|
if (this.tab === 'news') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-news id="tf-tab-news" .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
|
<tf-tab-news
|
||||||
|
id="tf-tab-news"
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
hash=${this.hash}
|
||||||
|
.unread=${this.unread}
|
||||||
|
@refresh=${() => (this.unread = [])}
|
||||||
|
?loading=${this.loading}
|
||||||
|
></tf-tab-news>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'connections') {
|
} else if (this.tab === 'connections') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
|
<tf-tab-connections
|
||||||
|
.users=${this.users}
|
||||||
|
.connections=${this.connections}
|
||||||
|
.broadcasts=${this.broadcasts}
|
||||||
|
></tf-tab-connections>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'mentions') {
|
} else if (this.tab === 'mentions') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-mentions .following=${this.following} whoami=${this.whoami} .users=${this.users}}></tf-tab-mentions>
|
<tf-tab-mentions
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users="${this.users}}"
|
||||||
|
></tf-tab-mentions>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'search') {
|
} else if (this.tab === 'search') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
|
<tf-tab-search
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
query=${this.hash?.startsWith('#q=')
|
||||||
|
? decodeURIComponent(this.hash.substring(3))
|
||||||
|
: null}
|
||||||
|
></tf-tab-search>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'query') {
|
} else if (this.tab === 'query') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-query .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#sql=') ? decodeURIComponent(this.hash.substring(5)) : null}></tf-tab-query>
|
<tf-tab-query
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
query=${this.hash?.startsWith('#sql=')
|
||||||
|
? decodeURIComponent(this.hash.substring(5))
|
||||||
|
: null}
|
||||||
|
></tf-tab-query>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -294,23 +340,48 @@ class TfElement extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let tabs = html`
|
let tabs = html`
|
||||||
<div class="w3-bar w3-black">
|
<div class="w3-bar w3-theme-l1">
|
||||||
${Object.entries(k_tabs).map(([k, v]) => html`
|
${Object.entries(k_tabs).map(
|
||||||
<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>
|
([k, v]) => html`
|
||||||
`)}
|
<button
|
||||||
|
title=${v}
|
||||||
|
class="w3-bar-item w3-padding w3-hover-theme tab ${self.tab == v
|
||||||
|
? 'w3-theme-l2'
|
||||||
|
: 'w3-theme-l1'}"
|
||||||
|
@click=${() => self.set_tab(v)}
|
||||||
|
>
|
||||||
|
${k}
|
||||||
|
<span class=${self.tab == v ? '' : 'w3-hide-small'}
|
||||||
|
>${v.charAt(0).toUpperCase() + v.substring(1)}</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
let contents =
|
let contents = !this.loaded
|
||||||
!this.loaded ?
|
? this.loading
|
||||||
this.loading ?
|
? html`<div
|
||||||
html`<div>Loading...</div>` :
|
class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge"
|
||||||
html`<div>Select or create an identity.</div>` :
|
>
|
||||||
this.render_tab();
|
Loading...
|
||||||
|
</div>
|
||||||
|
${this.render_tab()}`
|
||||||
|
: html`<div>Select or create an identity.</div>`
|
||||||
|
: this.render_tab();
|
||||||
return html`
|
return html`
|
||||||
${this.render_id_picker()}
|
<div
|
||||||
|
style="width: 100vw; min-height: 100vh; height: 100%"
|
||||||
|
class="w3-theme-dark"
|
||||||
|
>
|
||||||
${tabs}
|
${tabs}
|
||||||
${this.tags.map(x => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`)}
|
<div style="padding: 8px">
|
||||||
|
${this.tags.map(
|
||||||
|
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
|
||||||
|
)}
|
||||||
${contents}
|
${contents}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
import {LitElement, html, unsafeHTML, live} from './lit-all.min.js';
|
||||||
import * as tfutils from './tf-utils.js';
|
import * as tfutils from './tf-utils.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import {styles} from './tf-styles.js';
|
import {styles} from './tf-styles.js';
|
||||||
@ -13,6 +13,7 @@ class TfComposeElement extends LitElement {
|
|||||||
branch: {type: String},
|
branch: {type: String},
|
||||||
apps: {type: Object},
|
apps: {type: Object},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
|
author: {type: String},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ class TfComposeElement extends LitElement {
|
|||||||
this.branch = undefined;
|
this.branch = undefined;
|
||||||
this.apps = undefined;
|
this.apps = undefined;
|
||||||
this.drafts = {};
|
this.drafts = {};
|
||||||
|
this.author = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
process_text(text) {
|
process_text(text) {
|
||||||
@ -58,11 +60,13 @@ class TfComposeElement extends LitElement {
|
|||||||
link: link,
|
link: link,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
|
draft.mentions[link].name = name.startsWith('@')
|
||||||
|
? name.substring(1)
|
||||||
|
: name;
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
if (updated) {
|
if (updated) {
|
||||||
this.requestUpdate();
|
setTimeout(() => this.notify(draft), 0);
|
||||||
}
|
}
|
||||||
return tfutils.markdown(text);
|
return tfutils.markdown(text);
|
||||||
}
|
}
|
||||||
@ -70,30 +74,25 @@ class TfComposeElement extends LitElement {
|
|||||||
input(event) {
|
input(event) {
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
let preview = this.renderRoot.getElementById('preview');
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
preview.innerHTML = this.process_text(edit.value);
|
preview.innerHTML = this.process_text(edit.innerText);
|
||||||
let content_warning = this.renderRoot.getElementById('content_warning');
|
let content_warning = this.renderRoot.getElementById('content_warning');
|
||||||
let content_warning_preview = this.renderRoot.getElementById('content_warning_preview');
|
let draft = this.get_draft();
|
||||||
if (content_warning && content_warning_preview) {
|
draft.text = edit.innerText;
|
||||||
content_warning_preview.innerText = content_warning.value;
|
draft.content_warning = content_warning?.value;
|
||||||
}
|
setTimeout(() => this.notify(draft), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(draft) {
|
notify(draft) {
|
||||||
this.dispatchEvent(new CustomEvent('tf-draft', {
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-draft', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
detail: {
|
detail: {
|
||||||
id: this.branch,
|
id: this.branch,
|
||||||
draft: draft
|
draft: draft,
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
}
|
);
|
||||||
|
|
||||||
change() {
|
|
||||||
let draft = this.get_draft();
|
|
||||||
draft.text = this.renderRoot.getElementById('edit')?.value;
|
|
||||||
draft.content_warning = this.renderRoot.getElementById('content_warning')?.value;
|
|
||||||
this.notify(draft);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
convert_to_format(buffer, type, mime_type) {
|
convert_to_format(buffer, type, mime_type) {
|
||||||
@ -109,13 +108,17 @@ class TfComposeElement extends LitElement {
|
|||||||
let context = canvas.getContext('2d');
|
let context = canvas.getContext('2d');
|
||||||
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
let data_url = canvas.toDataURL(mime_type);
|
let data_url = canvas.toDataURL(mime_type);
|
||||||
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
|
let result = atob(data_url.split(',')[1])
|
||||||
|
.split('')
|
||||||
|
.map((x) => x.charCodeAt(0));
|
||||||
resolve(result);
|
resolve(result);
|
||||||
};
|
};
|
||||||
img.onerror = function (event) {
|
img.onerror = function (event) {
|
||||||
reject(new Error('Failed to load image.'));
|
reject(new Error('Failed to load image.'));
|
||||||
};
|
};
|
||||||
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
|
let raw = Array.from(new Uint8Array(buffer))
|
||||||
|
.map((b) => String.fromCharCode(b))
|
||||||
|
.join('');
|
||||||
let original = `data:${type};base64,${btoa(raw)}`;
|
let original = `data:${type};base64,${btoa(raw)}`;
|
||||||
img.src = original;
|
img.src = original;
|
||||||
});
|
});
|
||||||
@ -131,7 +134,11 @@ class TfComposeElement extends LitElement {
|
|||||||
let best_buffer;
|
let best_buffer;
|
||||||
let best_type;
|
let best_type;
|
||||||
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
||||||
let test_buffer = await self.convert_to_format(buffer, file.type, format);
|
let test_buffer = await self.convert_to_format(
|
||||||
|
buffer,
|
||||||
|
file.type,
|
||||||
|
format
|
||||||
|
);
|
||||||
if (!best_buffer || test_buffer.length < best_buffer.length) {
|
if (!best_buffer || test_buffer.length < best_buffer.length) {
|
||||||
best_buffer = test_buffer;
|
best_buffer = test_buffer;
|
||||||
best_type = format;
|
best_type = format;
|
||||||
@ -154,8 +161,7 @@ class TfComposeElement extends LitElement {
|
|||||||
size: buffer.length ?? buffer.byteLength,
|
size: buffer.length ?? buffer.byteLength,
|
||||||
};
|
};
|
||||||
let edit = self.renderRoot.getElementById('edit');
|
let edit = self.renderRoot.getElementById('edit');
|
||||||
edit.value += `\n![${name}](${id})`;
|
edit.innerText += `\n![${name}](${id})`;
|
||||||
self.change();
|
|
||||||
self.input();
|
self.input();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e?.message);
|
alert(e?.message);
|
||||||
@ -182,7 +188,7 @@ class TfComposeElement extends LitElement {
|
|||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
let message = {
|
let message = {
|
||||||
type: 'post',
|
type: 'post',
|
||||||
text: edit.value,
|
text: edit.innerText,
|
||||||
};
|
};
|
||||||
if (this.root || this.branch) {
|
if (this.root || this.branch) {
|
||||||
message.root = this.root;
|
message.root = this.root;
|
||||||
@ -201,33 +207,27 @@ class TfComposeElement extends LitElement {
|
|||||||
to = [...to];
|
to = [...to];
|
||||||
message.recps = to;
|
message.recps = to;
|
||||||
console.log('message is now', message);
|
console.log('message is now', message);
|
||||||
message = await tfrpc.rpc.encrypt(this.whoami, to, JSON.stringify(message));
|
message = await tfrpc.rpc.encrypt(
|
||||||
|
this.whoami,
|
||||||
|
to,
|
||||||
|
JSON.stringify(message)
|
||||||
|
);
|
||||||
console.log('encrypted as', message);
|
console.log('encrypted as', message);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
edit.value = '';
|
|
||||||
self.change();
|
|
||||||
self.notify(undefined);
|
self.notify(undefined);
|
||||||
self.requestUpdate();
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message);
|
alert(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
discard() {
|
discard() {
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
|
||||||
edit.value = '';
|
|
||||||
this.change();
|
|
||||||
let preview = this.renderRoot.getElementById('preview');
|
|
||||||
preview.innerHTML = '';
|
|
||||||
this.notify(undefined);
|
this.notify(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
attach() {
|
attach() {
|
||||||
let self = this;
|
let self = this;
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
|
||||||
let input = document.createElement('input');
|
let input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.onchange = function (event) {
|
input.onchange = function (event) {
|
||||||
@ -241,12 +241,15 @@ class TfComposeElement extends LitElement {
|
|||||||
this.last_autocomplete = text;
|
this.last_autocomplete = text;
|
||||||
let results = [];
|
let results = [];
|
||||||
try {
|
try {
|
||||||
let rows = await tfrpc.rpc.query(`
|
let rows = await tfrpc.rpc.query(
|
||||||
SELECT messages.content FROM messages_fts(?)
|
`
|
||||||
|
SELECT json(messages.content) AS content FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
WHERE messages.content LIKE ?
|
WHERE json(messages.content) LIKE ?
|
||||||
ORDER BY timestamp DESC LIMIT 10
|
ORDER BY timestamp DESC LIMIT 10
|
||||||
`, ['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]);
|
`,
|
||||||
|
['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]
|
||||||
|
);
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
|
for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
|
||||||
if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
|
if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
|
||||||
@ -262,19 +265,39 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
|
let values = Object.entries(this.users).map((x) => ({
|
||||||
|
key: x[1].name ?? x[0],
|
||||||
|
value: x[0],
|
||||||
|
}));
|
||||||
|
if (this.author) {
|
||||||
|
values = [].concat(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
key: this.users[this.author]?.name,
|
||||||
|
value: this.author,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
values
|
||||||
|
);
|
||||||
|
}
|
||||||
let tribute = new Tribute({
|
let tribute = new Tribute({
|
||||||
|
iframe: this.shadowRoot,
|
||||||
collection: [
|
collection: [
|
||||||
{
|
{
|
||||||
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
values: values,
|
||||||
selectTemplate: function (item) {
|
selectTemplate: function (item) {
|
||||||
return `[@${item.original.key}](${item.original.value})`;
|
return item
|
||||||
|
? `[@${item.original.key}](${item.original.value})`
|
||||||
|
: undefined;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
trigger: '&',
|
trigger: '&',
|
||||||
values: this.autocomplete,
|
values: this.autocomplete,
|
||||||
selectTemplate: function (item) {
|
selectTemplate: function (item) {
|
||||||
return `![${item.original.key}](${item.original.value})`;
|
return item
|
||||||
|
? `![${item.original.key}](${item.original.value})`
|
||||||
|
: undefined;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -285,15 +308,19 @@ class TfComposeElement extends LitElement {
|
|||||||
updated() {
|
updated() {
|
||||||
super.updated();
|
super.updated();
|
||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
if (this.last_updated_text !== edit.value) {
|
if (this.last_updated_text !== edit.innerText) {
|
||||||
let preview = this.renderRoot.getElementById('preview');
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
preview.innerHTML = this.process_text(edit.value);
|
preview.innerHTML = this.process_text(edit.innerText);
|
||||||
this.last_updated_text = edit.value;
|
this.last_updated_text = edit.innerText;
|
||||||
}
|
}
|
||||||
let encrypt = this.renderRoot.getElementById('encrypt_to');
|
let encrypt = this.renderRoot.getElementById('encrypt_to');
|
||||||
if (encrypt) {
|
if (encrypt) {
|
||||||
let tribute = new Tribute({
|
let tribute = new Tribute({
|
||||||
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
iframe: this.shadowRoot,
|
||||||
|
values: Object.entries(this.users).map((x) => ({
|
||||||
|
key: x[1].name,
|
||||||
|
value: x[0],
|
||||||
|
})),
|
||||||
selectTemplate: function (item) {
|
selectTemplate: function (item) {
|
||||||
return item.original.value;
|
return item.original.value;
|
||||||
},
|
},
|
||||||
@ -305,23 +332,32 @@ class TfComposeElement extends LitElement {
|
|||||||
remove_mention(id) {
|
remove_mention(id) {
|
||||||
let draft = this.get_draft();
|
let draft = this.get_draft();
|
||||||
delete draft.mentions[id];
|
delete draft.mentions[id];
|
||||||
this.notify(draft);
|
setTimeout(() => this.notify(), 0);
|
||||||
this.requestUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render_mention(mention) {
|
render_mention(mention) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html` <div style="display: flex; flex-direction: row">
|
||||||
<div style="display: flex; flex-direction: row">
|
|
||||||
<div style="align-self: center; margin: 0.5em">
|
<div style="align-self: center; margin: 0.5em">
|
||||||
<button class="w3-button w3-dark-grey" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}>🚮</button>
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
title="Remove ${mention.name} mention"
|
||||||
|
@click=${() => self.remove_mention(mention.link)}
|
||||||
|
>
|
||||||
|
🚮
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column">
|
<div style="display: flex; flex-direction: column">
|
||||||
<h3>${mention.name}</h3>
|
<h3>${mention.name}</h3>
|
||||||
<div style="padding-left: 1em">
|
<div style="padding-left: 1em">
|
||||||
${Object.entries(mention)
|
${Object.entries(mention)
|
||||||
.filter(x => x[0] != 'name')
|
.filter((x) => x[0] != 'name')
|
||||||
.map(x => html`<div><span style="font-weight: bold">${x[0]}</span>: ${x[1]}</div>`)}
|
.map(
|
||||||
|
(x) =>
|
||||||
|
html`<div>
|
||||||
|
<span style="font-weight: bold">${x[0]}</span>: ${x[1]}
|
||||||
|
</div>`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -358,11 +394,20 @@ class TfComposeElement extends LitElement {
|
|||||||
if (this.apps) {
|
if (this.apps) {
|
||||||
return html`
|
return html`
|
||||||
<div class="w3-card-4 w3-margin w3-padding">
|
<div class="w3-card-4 w3-margin w3-padding">
|
||||||
<select id="select" class="w3-select w3-dark-grey">
|
<select id="select" class="w3-select w3-theme-d1">
|
||||||
${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
|
${Object.keys(self.apps).map(
|
||||||
|
(app) => html`<option value=${app}>${app}</option>`
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}>Attach</button>
|
<button class="w3-button w3-theme-d1" @click=${attach_selected_app}>
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Cancel</button>
|
Attach
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (this.apps = null)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -374,9 +419,16 @@ class TfComposeElement extends LitElement {
|
|||||||
self.apps = await tfrpc.rpc.apps();
|
self.apps = await tfrpc.rpc.apps();
|
||||||
}
|
}
|
||||||
if (!this.apps) {
|
if (!this.apps) {
|
||||||
return html`<button class="w3-button w3-dark-grey" @click=${attach_app}>Attach App</button>`;
|
return html`<button class="w3-button w3-theme-d1" @click=${attach_app}>
|
||||||
|
Attach App
|
||||||
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
return html`<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Discard App</button>`;
|
return html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (this.apps = null)}
|
||||||
|
>
|
||||||
|
Discard App
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,15 +446,15 @@ class TfComposeElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div class="w3-container w3-padding">
|
<div class="w3-container w3-padding">
|
||||||
<p>
|
<p>
|
||||||
<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
|
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
|
||||||
<label for="cw">CW</label>
|
<label for="cw">CW</label>
|
||||||
</p>
|
</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>
|
<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${self.input} value=${draft.content_warning}></input>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning('')}></input>
|
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input>
|
||||||
<label for="cw">CW</label>
|
<label for="cw">CW</label>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -432,14 +484,16 @@ class TfComposeElement extends LitElement {
|
|||||||
<div style="display: flex; flex-direction: row; width: 100%">
|
<div style="display: flex; flex-direction: row; width: 100%">
|
||||||
<label for="encrypt_to">🔐 To:</label>
|
<label for="encrypt_to">🔐 To:</label>
|
||||||
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
|
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt(undefined)}>🚮</button>
|
<button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
${draft.encrypt_to.map(x => html`
|
${draft.encrypt_to.map(
|
||||||
|
(x) => html`
|
||||||
<li>
|
<li>
|
||||||
<tf-user id=${x} .users=${this.users}></tf-user>
|
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||||
<input type="button" class="w3-button w3-dark-grey" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter(id => id != x))}></input>
|
<input type="button" class="w3-button w3-theme-d1" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
|
||||||
</li>`)}
|
</li>`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -455,34 +509,58 @@ class TfComposeElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
let draft = self.get_draft();
|
let draft = self.get_draft();
|
||||||
let content_warning =
|
let content_warning =
|
||||||
draft.content_warning !== undefined ?
|
draft.content_warning !== undefined
|
||||||
html`<div class="w3-panel w3-round-xlarge w3-blue">
|
? html`<div class="w3-panel w3-round-xlarge w3-theme-d2">
|
||||||
<p id="content_warning_preview">${draft.content_warning}</p>
|
<p id="content_warning_preview">${draft.content_warning}</p>
|
||||||
</div>` :
|
</div>`
|
||||||
undefined;
|
: undefined;
|
||||||
let encrypt = draft.encrypt_to !== undefined ?
|
let encrypt =
|
||||||
undefined :
|
draft.encrypt_to !== undefined
|
||||||
html`<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt([])}>🔐</button>`;
|
? undefined
|
||||||
|
: html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => this.set_encrypt([])}
|
||||||
|
>
|
||||||
|
🔐
|
||||||
|
</button>`;
|
||||||
let result = html`
|
let result = html`
|
||||||
<div class="w3-card-4 w3-blue-grey w3-padding" style="box-sizing: border-box">
|
<div
|
||||||
|
class="w3-card-4 w3-theme-d4 w3-padding-small"
|
||||||
|
style="box-sizing: border-box"
|
||||||
|
>
|
||||||
${this.render_encrypt()}
|
${this.render_encrypt()}
|
||||||
<div style="display: flex; flex-direction: row; width: 100%; gap: 4px">
|
<div class="w3-container w3-padding-small">
|
||||||
<div style="flex: 1 0 50%">
|
<div class="w3-half">
|
||||||
<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>
|
<span
|
||||||
|
class="w3-input w3-theme-d1 w3-border"
|
||||||
|
style="resize: vertical; width: 100%; overflow: hidden; white-space: pre-wrap"
|
||||||
|
placeholder="Write a post here."
|
||||||
|
id="edit"
|
||||||
|
@input=${this.input}
|
||||||
|
@paste=${this.paste}
|
||||||
|
contenteditable="plaintext-only"
|
||||||
|
.innerText=${live(draft.text ?? '')}
|
||||||
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex: 1 0 50%">
|
<div class="w3-half w3-padding">
|
||||||
${content_warning}
|
${content_warning}
|
||||||
<div id="preview"></div>
|
<div id="preview"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
|
${Object.values(draft.mentions || {}).map((x) =>
|
||||||
${this.render_attach_app()}
|
self.render_mention(x)
|
||||||
${this.render_content_warning()}
|
)}
|
||||||
<button class="w3-button w3-dark-grey" id="submit" @click=${this.submit}>Submit</button>
|
${this.render_attach_app()} ${this.render_content_warning()}
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.attach}>Attach</button>
|
<button class="w3-button w3-theme-d1" id="submit" @click=${this.submit}>
|
||||||
${this.render_attach_app_button()}
|
Submit
|
||||||
${encrypt}
|
</button>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.discard}>Discard</button>
|
<button class="w3-button w3-theme-d1" @click=${this.attach}>
|
||||||
|
Attach
|
||||||
|
</button>
|
||||||
|
${this.render_attach_app_button()} ${encrypt}
|
||||||
|
<button class="w3-button w3-theme-d1" @click=${this.discard}>
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return result;
|
return result;
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
import {LitElement, html} from './lit-all.min.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.
|
|
||||||
*/
|
|
||||||
class TfIdentityPickerElement extends LitElement {
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
ids: {type: Array},
|
|
||||||
selected: {type: String},
|
|
||||||
users: {type: Object},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = styles;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.ids = [];
|
|
||||||
this.users = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
changed(event) {
|
|
||||||
this.selected = event.srcElement.value;
|
|
||||||
this.dispatchEvent(new Event('change', {
|
|
||||||
srcElement: this,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`
|
|
||||||
<select 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>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('tf-id-picker', TfIdentityPickerElement);
|
|
@ -1,4 +1,4 @@
|
|||||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
import {LitElement, html, render, unsafeHTML} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import * as tfutils from './tf-utils.js';
|
import * as tfutils from './tf-utils.js';
|
||||||
import * as emojis from './emojis.js';
|
import * as emojis from './emojis.js';
|
||||||
@ -31,14 +31,33 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show_reply() {
|
show_reply() {
|
||||||
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: {
|
let event = new CustomEvent('tf-draft', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
id: this.message?.id,
|
||||||
|
draft: {
|
||||||
encrypt_to: this.message?.decrypted?.recps,
|
encrypt_to: this.message?.decrypted?.recps,
|
||||||
}}});
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
discard_reply() {
|
discard_reply() {
|
||||||
this.dispatchEvent(new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.id, draft: undefined}}));
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-draft', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {id: this.id, draft: undefined},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
show_reactions() {
|
||||||
|
let modal = document.getElementById('reactions_modal');
|
||||||
|
modal.users = this.users;
|
||||||
|
modal.votes = this.message?.votes || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
render_votes() {
|
render_votes() {
|
||||||
@ -53,12 +72,21 @@ class TfMessageElement extends LitElement {
|
|||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return html`<div>${(this.message.votes || []).map(
|
if (this.message?.votes?.length) {
|
||||||
vote => html`
|
return html`<div class="w3-button" @click=${this.show_reactions}>
|
||||||
<span title="${this.users[vote.author]?.name ?? vote.author} ${new Date(vote.timestamp)}">
|
${(this.message.votes || []).map(
|
||||||
|
(vote) => html`
|
||||||
|
<span
|
||||||
|
title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
|
||||||
|
vote.timestamp
|
||||||
|
)}"
|
||||||
|
>
|
||||||
${normalize_expression(vote.content.vote.expression)}
|
${normalize_expression(vote.content.vote.expression)}
|
||||||
</span>
|
</span>
|
||||||
`)}</div>`;
|
`
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_raw() {
|
render_raw() {
|
||||||
@ -72,30 +100,40 @@ class TfMessageElement extends LitElement {
|
|||||||
content: this.message?.content,
|
content: this.message?.content,
|
||||||
signature: this.message?.signature,
|
signature: this.message?.signature,
|
||||||
};
|
};
|
||||||
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`;
|
return html`<div style="white-space: pre-wrap">
|
||||||
|
${JSON.stringify(raw, null, 2)}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
vote(emoji) {
|
vote(emoji) {
|
||||||
let reaction = emoji;
|
let reaction = emoji;
|
||||||
let message = this.message.id;
|
let message = this.message.id;
|
||||||
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
|
if (
|
||||||
tfrpc.rpc.appendMessage(
|
confirm(
|
||||||
this.whoami,
|
'Are you sure you want to react with ' +
|
||||||
{
|
reaction +
|
||||||
|
' to ' +
|
||||||
|
message +
|
||||||
|
'?'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
tfrpc.rpc
|
||||||
|
.appendMessage(this.whoami, {
|
||||||
type: 'vote',
|
type: 'vote',
|
||||||
vote: {
|
vote: {
|
||||||
link: message,
|
link: message,
|
||||||
value: 1,
|
value: 1,
|
||||||
expression: reaction,
|
expression: reaction,
|
||||||
},
|
},
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
react(event) {
|
react(event) {
|
||||||
emojis.picker(x => this.vote(x));
|
emojis.picker((x) => this.vote(x), null, this.whoami);
|
||||||
}
|
}
|
||||||
|
|
||||||
show_image(link) {
|
show_image(link) {
|
||||||
@ -129,9 +167,12 @@ class TfMessageElement extends LitElement {
|
|||||||
body_click(event) {
|
body_click(event) {
|
||||||
if (event.srcElement.tagName == 'IMG') {
|
if (event.srcElement.tagName == 'IMG') {
|
||||||
this.show_image(event.srcElement.src);
|
this.show_image(event.srcElement.src);
|
||||||
} else if (event.srcElement.tagName == 'DIV' && event.srcElement.classList.contains('img_caption')) {
|
} else if (
|
||||||
|
event.srcElement.tagName == 'DIV' &&
|
||||||
|
event.srcElement.classList.contains('img_caption')
|
||||||
|
) {
|
||||||
let next = event.srcElement.nextSibling;
|
let next = event.srcElement.nextSibling;
|
||||||
if (next.style.display == 'block') {
|
if (next.style.display != 'none') {
|
||||||
next.style.display = 'none';
|
next.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
next.style.display = 'block';
|
next.style.display = 'block';
|
||||||
@ -140,50 +181,75 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_mention(mention) {
|
render_mention(mention) {
|
||||||
if (!mention?.link || typeof(mention.link) != 'string') {
|
if (!mention?.link || typeof mention.link != 'string') {
|
||||||
return html` <pre>${JSON.stringify(mention)}</pre>`;
|
return html` <pre>${JSON.stringify(mention)}</pre>`;
|
||||||
} else if (mention?.link?.startsWith('&') &&
|
} else if (
|
||||||
mention?.type?.startsWith('image/')) {
|
mention?.link?.startsWith('&') &&
|
||||||
|
mention?.type?.startsWith('image/')
|
||||||
|
) {
|
||||||
return html`
|
return html`
|
||||||
<img src=${'/' + mention.link + '/view'} style="max-width: 128px; max-height: 128px" title=${mention.name} @click=${() => this.show_image('/' + mention.link + '/view')}>
|
<img
|
||||||
|
src=${'/' + mention.link + '/view'}
|
||||||
|
style="max-width: 128px; max-height: 128px"
|
||||||
|
title=${mention.name}
|
||||||
|
@click=${() => this.show_image('/' + mention.link + '/view')}
|
||||||
|
/>
|
||||||
`;
|
`;
|
||||||
} else if (mention.link?.startsWith('&') &&
|
} else if (
|
||||||
mention.name?.startsWith('audio:')) {
|
mention.link?.startsWith('&') &&
|
||||||
|
mention.name?.startsWith('audio:')
|
||||||
|
) {
|
||||||
return html`
|
return html`
|
||||||
<audio controls style="height: 32px">
|
<audio controls style="height: 32px">
|
||||||
<source src=${'/' + mention.link + '/view'}></source>
|
<source src=${'/' + mention.link + '/view'}></source>
|
||||||
</audio>
|
</audio>
|
||||||
`;
|
`;
|
||||||
} else if (mention.link?.startsWith('&') &&
|
} else if (
|
||||||
mention.name?.startsWith('video:')) {
|
mention.link?.startsWith('&') &&
|
||||||
|
mention.name?.startsWith('video:')
|
||||||
|
) {
|
||||||
return html`
|
return html`
|
||||||
<video controls style="max-height: 240px; max-width: 128px">
|
<video controls style="max-height: 240px; max-width: 128px">
|
||||||
<source src=${'/' + mention.link + '/view'}></source>
|
<source src=${'/' + mention.link + '/view'}></source>
|
||||||
</video>
|
</video>
|
||||||
`;
|
`;
|
||||||
} else if (mention.link?.startsWith('&') &&
|
} else if (
|
||||||
mention?.type === 'application/tildefriends') {
|
mention.link?.startsWith('&') &&
|
||||||
|
mention?.type === 'application/tildefriends'
|
||||||
|
) {
|
||||||
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
|
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
|
||||||
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
|
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
|
||||||
return html` <a href=${'#' + encodeURIComponent(mention.link)}>${mention.name}</a>`;
|
return html` <a href=${'#' + encodeURIComponent(mention.link)}
|
||||||
|
>${mention.name}</a
|
||||||
|
>`;
|
||||||
} else if (mention.link?.startsWith('#')) {
|
} else if (mention.link?.startsWith('#')) {
|
||||||
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}>${mention.link}</a>`;
|
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}
|
||||||
} else if (Object.keys(mention).length == 2 && mention.link && mention.name) {
|
>${mention.link}</a
|
||||||
|
>`;
|
||||||
|
} else if (
|
||||||
|
Object.keys(mention).length == 2 &&
|
||||||
|
mention.link &&
|
||||||
|
mention.name
|
||||||
|
) {
|
||||||
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
|
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
|
||||||
} else {
|
} else {
|
||||||
return html` <pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>`;
|
return html` <pre style="white-space: pre-wrap">
|
||||||
|
${JSON.stringify(mention, null, 2)}</pre
|
||||||
|
>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_mentions() {
|
render_mentions() {
|
||||||
let mentions = this.message?.content?.mentions || [];
|
let mentions = this.message?.content?.mentions || [];
|
||||||
mentions = mentions.filter(x => this.message?.content?.text?.indexOf(x.link) === -1);
|
mentions = mentions.filter(
|
||||||
|
(x) => this.message?.content?.text?.indexOf(x.link) === -1
|
||||||
|
);
|
||||||
if (mentions.length) {
|
if (mentions.length) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<fieldset style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black">
|
<fieldset style="padding: 0.5em; border: 1px solid black">
|
||||||
<legend>Mentions</legend>
|
<legend>Mentions</legend>
|
||||||
${mentions.map(x => self.render_mention(x))}
|
${mentions.map((x) => self.render_mention(x))}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -194,28 +260,55 @@ class TfMessageElement extends LitElement {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let total = message.child_messages.length;
|
let total = message.child_messages.length;
|
||||||
for (let m of message.child_messages)
|
for (let m of message.child_messages) {
|
||||||
{
|
|
||||||
total += this.total_child_messages(m);
|
total += this.total_child_messages(m);
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_expanded(expanded, tag) {
|
set_expanded(expanded, tag) {
|
||||||
this.dispatchEvent(new CustomEvent('tf-expand', {bubbles: true, composed: true, detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded}}));
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-expand', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded},
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle_expanded(tag) {
|
toggle_expanded(tag) {
|
||||||
this.set_expanded(!this.expanded[(this.message.id || '') + (tag || '')], tag);
|
this.set_expanded(
|
||||||
|
!this.expanded[(this.message.id || '') + (tag || '')],
|
||||||
|
tag
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render_children() {
|
render_children() {
|
||||||
let self = this;
|
let self = this;
|
||||||
if (this.message.child_messages?.length) {
|
if (this.message.child_messages?.length) {
|
||||||
if (!this.expanded[this.message.id]) {
|
if (!this.expanded[this.message.id]) {
|
||||||
return html`<button class="w3-button w3-dark-grey" @click=${() => self.set_expanded(true)}>+ ${this.total_child_messages(this.message) + ' More'}</button>`;
|
return html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => self.set_expanded(true)}
|
||||||
|
>
|
||||||
|
+ ${this.total_child_messages(this.message) + ' More'}
|
||||||
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
return html`<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>`)}`;
|
return html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => self.set_expanded(false)}
|
||||||
|
>
|
||||||
|
Collapse</button
|
||||||
|
>${(this.message.child_messages || []).map(
|
||||||
|
(x) =>
|
||||||
|
html`<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-message>`
|
||||||
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -231,13 +324,12 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
if (Array.isArray(content.mentions)) {
|
if (Array.isArray(content.mentions)) {
|
||||||
for (let mention of content.mentions) {
|
for (let mention of content.mentions) {
|
||||||
if (typeof mention?.link === 'string' &&
|
if (typeof mention?.link === 'string' && mention.link.startsWith('#')) {
|
||||||
mention.link.startsWith('#')) {
|
|
||||||
channels.push(mention.link);
|
channels.push(mention.link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return channels.map(x => html`<tf-tag tag=${x}></tf-tag>`);
|
return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -245,57 +337,127 @@ class TfMessageElement extends LitElement {
|
|||||||
if (this.message?.decrypted?.type == 'post') {
|
if (this.message?.decrypted?.type == 'post') {
|
||||||
content = this.message.decrypted;
|
content = this.message.decrypted;
|
||||||
}
|
}
|
||||||
|
let class_background = this.message?.decrypted
|
||||||
|
? 'w3-pale-red'
|
||||||
|
: 'w3-theme-d4';
|
||||||
let self = this;
|
let self = this;
|
||||||
let raw_button;
|
let raw_button;
|
||||||
switch (this.format) {
|
switch (this.format) {
|
||||||
case 'raw':
|
case 'raw':
|
||||||
if (content?.type == 'post' || content?.type == 'blog') {
|
if (content?.type == 'post' || content?.type == 'blog') {
|
||||||
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'md'}>Markdown</button>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (self.format = 'md')}
|
||||||
|
>
|
||||||
|
Markdown
|
||||||
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (self.format = 'message')}
|
||||||
|
>
|
||||||
|
Message
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'md':
|
case 'md':
|
||||||
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (self.format = 'message')}
|
||||||
|
>
|
||||||
|
Message
|
||||||
|
</button>`;
|
||||||
break;
|
break;
|
||||||
case 'decrypted':
|
case 'decrypted':
|
||||||
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</button>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (self.format = 'raw')}
|
||||||
|
>
|
||||||
|
Raw
|
||||||
|
</button>`;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (this.message.decrypted) {
|
if (this.message.decrypted) {
|
||||||
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'decrypted'}>Decrypted</button>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (self.format = 'decrypted')}
|
||||||
|
>
|
||||||
|
Decrypted
|
||||||
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</button>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => (self.format = 'raw')}
|
||||||
|
>
|
||||||
|
Raw
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
function small_frame(inner) {
|
function small_frame(inner) {
|
||||||
let body;
|
let body;
|
||||||
return html`
|
return html`
|
||||||
<div class="w3-card-4" style="background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere">
|
<div
|
||||||
|
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||||
|
style="margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
|
||||||
|
>
|
||||||
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
|
||||||
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
|
<span style="padding-right: 8px"
|
||||||
${raw_button}
|
><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(
|
||||||
${self.format == 'raw' ? self.render_raw() : inner}
|
self.message.timestamp
|
||||||
|
).toLocaleString()}</span
|
||||||
|
>
|
||||||
|
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
|
||||||
${self.render_votes()}
|
${self.render_votes()}
|
||||||
|
${(self.message.child_messages || []).map(
|
||||||
|
(x) => html`
|
||||||
|
<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${self.whoami}
|
||||||
|
.users=${self.users}
|
||||||
|
.drafts=${self.drafts}
|
||||||
|
.expanded=${self.expanded}
|
||||||
|
></tf-message>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (this.message?.type === 'contact_group') {
|
if (this.message?.type === 'contact_group') {
|
||||||
return html`
|
return html` <div
|
||||||
<div 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">
|
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||||
${this.message.messages.map(x =>
|
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||||
html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`
|
>
|
||||||
|
${this.message.messages.map(
|
||||||
|
(x) =>
|
||||||
|
html`<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-message>`
|
||||||
)}
|
)}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (this.message.placeholder) {
|
} else if (this.message.placeholder) {
|
||||||
return html`
|
return html` <div
|
||||||
<div 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">
|
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||||
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
|
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||||
|
>
|
||||||
|
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
|
||||||
|
(placeholder)
|
||||||
<div>${this.render_votes()}</div>
|
<div>${this.render_votes()}</div>
|
||||||
${(this.message.child_messages || []).map(x => html`
|
${(this.message.child_messages || []).map(
|
||||||
<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>
|
(x) => html`
|
||||||
`)}
|
<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-message>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (typeof (content?.type === 'string')) {
|
} else if (typeof (content?.type === 'string')) {
|
||||||
if (content.type == 'about') {
|
if (content.type == 'about') {
|
||||||
@ -307,7 +469,7 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
if (content.image !== undefined) {
|
if (content.image !== undefined) {
|
||||||
image = html`
|
image = html`
|
||||||
<div><img src=${'/' + (typeof(content.image?.link) == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
|
<div><img src=${'/' + (typeof content.image?.link == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (content.description !== undefined) {
|
if (content.description !== undefined) {
|
||||||
@ -317,41 +479,52 @@ class TfMessageElement extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
let update = content.about == this.message.author ?
|
let update =
|
||||||
html`<div style="font-weight: bold">Updated profile.</div>` :
|
content.about == this.message.author
|
||||||
html`<div style="font-weight: bold">Updated profile for <tf-user id=${content.about} .users=${this.users}></tf-user>.</div>`;
|
? html`<div style="font-weight: bold">Updated profile.</div>`
|
||||||
return small_frame(html`
|
: html`<div style="font-weight: bold">
|
||||||
${update}
|
Updated profile for
|
||||||
${name}
|
<tf-user id=${content.about} .users=${this.users}></tf-user>.
|
||||||
${image}
|
</div>`;
|
||||||
${description}
|
return small_frame(html` ${update} ${name} ${image} ${description} `);
|
||||||
`);
|
|
||||||
} else if (content.type == 'contact') {
|
} else if (content.type == 'contact') {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
is
|
is
|
||||||
${
|
${content.blocking === true
|
||||||
content.blocking === true ? 'blocking' :
|
? 'blocking'
|
||||||
content.blocking === false ? 'no longer blocking' :
|
: content.blocking === false
|
||||||
content.following === true ? 'following' :
|
? 'no longer blocking'
|
||||||
content.following === false ? 'no longer following' :
|
: content.following === true
|
||||||
'?'
|
? 'following'
|
||||||
}
|
: content.following === false
|
||||||
<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user>
|
? 'no longer following'
|
||||||
|
: '?'}
|
||||||
|
<tf-user
|
||||||
|
id=${this.message.content.contact}
|
||||||
|
.users=${this.users}
|
||||||
|
></tf-user>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type == 'post') {
|
} else if (content.type == 'post') {
|
||||||
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
|
let reply =
|
||||||
|
this.drafts[this.message?.id] !== undefined
|
||||||
|
? html`
|
||||||
<tf-compose
|
<tf-compose
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.users=${this.users}
|
.users=${this.users}
|
||||||
root=${this.message.content.root || this.message.id}
|
root=${content.root || this.message.id}
|
||||||
branch=${this.message.id}
|
branch=${this.message.id}
|
||||||
.drafts=${this.drafts}
|
.drafts=${this.drafts}
|
||||||
@tf-discard=${this.discard_reply}></tf-compose>
|
@tf-discard=${this.discard_reply}
|
||||||
` : html`
|
author=${this.message.author}
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button>
|
></tf-compose>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
|
||||||
|
Reply
|
||||||
|
</button>
|
||||||
`;
|
`;
|
||||||
let self = this;
|
let self = this;
|
||||||
let body;
|
let body;
|
||||||
@ -360,32 +533,44 @@ class TfMessageElement extends LitElement {
|
|||||||
body = this.render_raw();
|
body = this.render_raw();
|
||||||
break;
|
break;
|
||||||
case 'md':
|
case 'md':
|
||||||
body = html`<code style="white-space: pre-wrap; overflow-wrap: anywhere">${content.text}</code>`;
|
body = html`<code
|
||||||
|
style="white-space: pre-wrap; overflow-wrap: anywhere"
|
||||||
|
>${content.text}</code
|
||||||
|
>`;
|
||||||
break;
|
break;
|
||||||
case 'message':
|
case 'message':
|
||||||
body = unsafeHTML(tfutils.markdown(content.text));
|
body = unsafeHTML(tfutils.markdown(content.text));
|
||||||
break;
|
break;
|
||||||
|
case 'decrypted':
|
||||||
|
body = html`<pre
|
||||||
|
style="white-space: pre-wrap; overflow-wrap: anywhere"
|
||||||
|
>
|
||||||
|
${JSON.stringify(content, null, 2)}</pre
|
||||||
|
>`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
let content_warning = html`
|
let content_warning = html`
|
||||||
<div class="w3-panel w3-round-xlarge w3-blue" style="cursor: pointer" @click=${x => this.toggle_expanded(':cw')}><p>${content.contentWarning}</p></div>
|
<div
|
||||||
|
class="w3-panel w3-round-xlarge w3-theme-l4"
|
||||||
|
style="cursor: pointer"
|
||||||
|
@click=${(x) => this.toggle_expanded(':cw')}
|
||||||
|
>
|
||||||
|
<p>${content.contentWarning}</p>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
let content_html =
|
let content_html = html`
|
||||||
html`
|
|
||||||
${this.render_channels()}
|
${this.render_channels()}
|
||||||
<div @click=${this.body_click}>${body}</div>
|
<div @click=${this.body_click}>${body}</div>
|
||||||
${this.render_mentions()}
|
${this.render_mentions()}
|
||||||
`;
|
`;
|
||||||
let payload =
|
let payload = content.contentWarning
|
||||||
content.contentWarning ?
|
? self.expanded[(this.message.id || '') + ':cw']
|
||||||
self.expanded[(this.message.id || '') + ':cw'] ?
|
? html` ${content_warning} ${content_html} `
|
||||||
html`
|
: content_warning
|
||||||
${content_warning}
|
: content_html;
|
||||||
${content_html}
|
let is_encrypted = this.message?.decrypted
|
||||||
` :
|
? html`<span style="align-self: center">🔓</span>`
|
||||||
content_warning :
|
: undefined;
|
||||||
content_html;
|
|
||||||
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
|
|
||||||
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
|
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
code {
|
code {
|
||||||
@ -401,26 +586,34 @@ class TfMessageElement extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="w3-card-4" style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
<div
|
||||||
|
class="w3-card-4 ${class_background} w3-border-theme"
|
||||||
|
style="margin-top: 8px; padding: 16px"
|
||||||
|
>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
${is_encrypted}
|
${is_encrypted}
|
||||||
<span style="flex: 1"></span>
|
<span style="flex: 1"></span>
|
||||||
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
<span style="padding-right: 8px"
|
||||||
|
><a target="_top" href=${'#' + self.message.id}>%</a>
|
||||||
|
${new Date(this.message.timestamp).toLocaleString()}</span
|
||||||
|
>
|
||||||
<span>${raw_button}</span>
|
<span>${raw_button}</span>
|
||||||
</div>
|
</div>
|
||||||
${payload}
|
${payload} ${this.render_votes()}
|
||||||
${this.render_votes()}
|
|
||||||
<p>
|
<p>
|
||||||
${reply}
|
${reply}
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
|
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||||
|
React
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
${this.render_children()}
|
${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type === 'issue') {
|
} else if (content.type === 'issue') {
|
||||||
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
|
let is_encrypted = this.message?.decrypted
|
||||||
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
|
? html`<span style="align-self: center">🔓</span>`
|
||||||
|
: undefined;
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
code {
|
code {
|
||||||
@ -436,18 +629,25 @@ class TfMessageElement extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="w3-card-4" style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
<div
|
||||||
|
class="w3-card-4 ${class_background} w3-border-theme"
|
||||||
|
style="margin-top: 8px; padding: 16px"
|
||||||
|
>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
${is_encrypted}
|
${is_encrypted}
|
||||||
<span style="flex: 1"></span>
|
<span style="flex: 1"></span>
|
||||||
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
<span style="padding-right: 8px"
|
||||||
|
><a target="_top" href=${'#' + self.message.id}>%</a>
|
||||||
|
${new Date(this.message.timestamp).toLocaleString()}</span
|
||||||
|
>
|
||||||
<span>${raw_button}</span>
|
<span>${raw_button}</span>
|
||||||
</div>
|
</div>
|
||||||
${content.text}
|
${content.text} ${this.render_votes()}
|
||||||
${this.render_votes()}
|
|
||||||
<p>
|
<p>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
|
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||||
|
React
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
${this.render_children()}
|
${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
@ -457,10 +657,13 @@ class TfMessageElement extends LitElement {
|
|||||||
tfrpc.rpc.get_blob(content.blog).then(function (data) {
|
tfrpc.rpc.get_blob(content.blog).then(function (data) {
|
||||||
self.blog_data = data;
|
self.blog_data = data;
|
||||||
});
|
});
|
||||||
let payload =
|
let payload = this.expanded[(this.message.id || '') + ':blog']
|
||||||
this.expanded[(this.message.id || '') + ':blog'] ?
|
? html`<div>
|
||||||
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
|
${this.blog_data
|
||||||
undefined;
|
? unsafeHTML(tfutils.markdown(this.blog_data))
|
||||||
|
: 'Loading...'}
|
||||||
|
</div>`
|
||||||
|
: undefined;
|
||||||
let body;
|
let body;
|
||||||
switch (this.format) {
|
switch (this.format) {
|
||||||
case 'raw':
|
case 'raw':
|
||||||
@ -473,7 +676,7 @@ class TfMessageElement extends LitElement {
|
|||||||
body = html`
|
body = html`
|
||||||
<div
|
<div
|
||||||
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
||||||
@click=${x => self.toggle_expanded(':blog')}>
|
@click=${(x) => self.toggle_expanded(':blog')}>
|
||||||
<h2>${content.title}</h2>
|
<h2>${content.title}</h2>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<img src=/${content.thumbnail}/view></img>
|
<img src=/${content.thumbnail}/view></img>
|
||||||
@ -484,16 +687,23 @@ class TfMessageElement extends LitElement {
|
|||||||
`;
|
`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
|
let reply =
|
||||||
|
this.drafts[this.message?.id] !== undefined
|
||||||
|
? html`
|
||||||
<tf-compose
|
<tf-compose
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.users=${this.users}
|
.users=${this.users}
|
||||||
root=${this.message.content.root || this.message.id}
|
root=${content.root || this.message.id}
|
||||||
branch=${this.message.id}
|
branch=${this.message.id}
|
||||||
.drafts=${this.drafts}
|
.drafts=${this.drafts}
|
||||||
@tf-discard=${this.discard_reply}></tf-compose>
|
@tf-discard=${this.discard_reply}
|
||||||
` : html`
|
author=${this.message.author}
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button>
|
></tf-compose>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
|
||||||
|
Reply
|
||||||
|
</button>
|
||||||
`;
|
`;
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
@ -510,11 +720,17 @@ class TfMessageElement extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="w3-card-4" style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
|
<div
|
||||||
|
class="w3-card-4 w3-theme-d4 w3-border-theme"
|
||||||
|
style="margin-top: 8px; padding: 16px"
|
||||||
|
>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
<span style="flex: 1"></span>
|
<span style="flex: 1"></span>
|
||||||
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
<span style="padding-right: 8px"
|
||||||
|
><a target="_top" href=${'#' + self.message.id}>%</a>
|
||||||
|
${new Date(this.message.timestamp).toLocaleString()}</span
|
||||||
|
>
|
||||||
<span>${raw_button}</span>
|
<span>${raw_button}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -522,37 +738,52 @@ class TfMessageElement extends LitElement {
|
|||||||
${this.render_mentions()}
|
${this.render_mentions()}
|
||||||
<div>
|
<div>
|
||||||
${reply}
|
${reply}
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
|
<button class="w3-button w3-theme-d1" @click=${this.react}>
|
||||||
|
React
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
${this.render_votes()}
|
${this.render_votes()} ${this.render_children()}
|
||||||
${this.render_children()}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type === 'pub') {
|
} else if (content.type === 'pub') {
|
||||||
return small_frame(html`
|
return small_frame(
|
||||||
<style>
|
html` <style>
|
||||||
span {
|
span {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<span>
|
<span>
|
||||||
<div>
|
<div>
|
||||||
🍻 <tf-user .users=${this.users} id=${content.address.key}></tf-user>
|
🍻
|
||||||
|
<tf-user
|
||||||
|
.users=${this.users}
|
||||||
|
id=${content.address.key}
|
||||||
|
></tf-user>
|
||||||
</div>
|
</div>
|
||||||
<pre>${content.address.host}:${content.address.port}</pre>
|
<pre>${content.address.host}:${content.address.port}</pre>
|
||||||
</span>`);
|
</span>`
|
||||||
|
);
|
||||||
} else if (content.type === 'channel') {
|
} else if (content.type === 'channel') {
|
||||||
return small_frame(html`
|
return small_frame(html`
|
||||||
<div>
|
<div>
|
||||||
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} <a href=${'#q=' + encodeURIComponent('#' + content.channel)}>#${content.channel}</a>
|
${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
|
||||||
|
<a href=${'#q=' + encodeURIComponent('#' + content.channel)}
|
||||||
|
>#${content.channel}</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
} else if (typeof(this.message.content) == 'string') {
|
} else if (typeof this.message.content == 'string') {
|
||||||
if (this.message?.decrypted) {
|
if (this.message?.decrypted) {
|
||||||
if (this.format == 'decrypted') {
|
if (this.format == 'decrypted') {
|
||||||
return small_frame(html`<span>🔓</span><pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`);
|
return small_frame(
|
||||||
|
html`<span>🔓</span>
|
||||||
|
<pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return small_frame(html`<span>🔓</span><div>${this.message.decrypted.type}</div>`);
|
return small_frame(
|
||||||
|
html`<span>🔓</span>
|
||||||
|
<div>${this.message.decrypted.type}</div>`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return small_frame(html`<span>🔒</span>`);
|
return small_frame(html`<span>🔒</span>`);
|
||||||
|
@ -61,7 +61,7 @@ class TfNewsElement extends LitElement {
|
|||||||
message.parent_message = message.content.vote.link;
|
message.parent_message = message.content.vote.link;
|
||||||
} else if (message.content.type == 'post') {
|
} else if (message.content.type == 'post') {
|
||||||
if (message.content.root) {
|
if (message.content.root) {
|
||||||
if (typeof(message.content.root) === 'string') {
|
if (typeof message.content.root === 'string') {
|
||||||
let m = ensure_message(message.content.root);
|
let m = ensure_message(message.content.root);
|
||||||
if (!m.child_messages) {
|
if (!m.child_messages) {
|
||||||
m.child_messages = [];
|
m.child_messages = [];
|
||||||
@ -89,8 +89,7 @@ class TfNewsElement extends LitElement {
|
|||||||
for (let message of messages) {
|
for (let message of messages) {
|
||||||
try {
|
try {
|
||||||
message.content = JSON.parse(message.content);
|
message.content = JSON.parse(message.content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
if (!messages_by_id[message.id]) {
|
if (!messages_by_id[message.id]) {
|
||||||
messages_by_id[message.id] = message;
|
messages_by_id[message.id] = message;
|
||||||
link_message(message);
|
link_message(message);
|
||||||
@ -100,8 +99,12 @@ class TfNewsElement extends LitElement {
|
|||||||
message.parent_message = placeholder.parent_message;
|
message.parent_message = placeholder.parent_message;
|
||||||
message.child_messages = placeholder.child_messages;
|
message.child_messages = placeholder.child_messages;
|
||||||
message.votes = placeholder.votes;
|
message.votes = placeholder.votes;
|
||||||
if (placeholder.parent_message && messages_by_id[placeholder.parent_message]) {
|
if (
|
||||||
let children = messages_by_id[placeholder.parent_message].child_messages;
|
placeholder.parent_message &&
|
||||||
|
messages_by_id[placeholder.parent_message]
|
||||||
|
) {
|
||||||
|
let children =
|
||||||
|
messages_by_id[placeholder.parent_message].child_messages;
|
||||||
children.splice(children.indexOf(placeholder), 1);
|
children.splice(children.indexOf(placeholder), 1);
|
||||||
children.push(message);
|
children.push(message);
|
||||||
}
|
}
|
||||||
@ -116,7 +119,10 @@ class TfNewsElement extends LitElement {
|
|||||||
let latest = 0;
|
let latest = 0;
|
||||||
for (let message of messages || []) {
|
for (let message of messages || []) {
|
||||||
if (message.latest_subtree_timestamp === undefined) {
|
if (message.latest_subtree_timestamp === undefined) {
|
||||||
message.latest_subtree_timestamp = Math.max(message.timestamp ?? 0, this.update_latest_subtree_timestamp(message.child_messages));
|
message.latest_subtree_timestamp = Math.max(
|
||||||
|
message.timestamp ?? 0,
|
||||||
|
this.update_latest_subtree_timestamp(message.child_messages)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
latest = Math.max(latest, message.latest_subtree_timestamp);
|
latest = Math.max(latest, message.latest_subtree_timestamp);
|
||||||
}
|
}
|
||||||
@ -127,20 +133,22 @@ class TfNewsElement extends LitElement {
|
|||||||
function recursive_sort(messages, top) {
|
function recursive_sort(messages, top) {
|
||||||
if (messages) {
|
if (messages) {
|
||||||
if (top) {
|
if (top) {
|
||||||
messages.sort((a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp);
|
messages.sort(
|
||||||
|
(a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
messages.sort((a, b) => a.timestamp - b.timestamp);
|
messages.sort((a, b) => a.timestamp - b.timestamp);
|
||||||
}
|
}
|
||||||
for (let message of messages) {
|
for (let message of messages) {
|
||||||
recursive_sort(message.child_messages, false);
|
recursive_sort(message.child_messages, false);
|
||||||
}
|
}
|
||||||
return messages.map(x => Object.assign({}, x));
|
return messages.map((x) => Object.assign({}, x));
|
||||||
} else {
|
} else {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let roots = Object.values(messages_by_id).filter(x => !x.parent_message);
|
let roots = Object.values(messages_by_id).filter((x) => !x.parent_message);
|
||||||
this.update_latest_subtree_timestamp(roots);
|
this.update_latest_subtree_timestamp(roots);
|
||||||
return recursive_sort(roots, true);
|
return recursive_sort(roots, true);
|
||||||
}
|
}
|
||||||
@ -167,10 +175,22 @@ class TfNewsElement extends LitElement {
|
|||||||
|
|
||||||
load_and_render(messages) {
|
load_and_render(messages) {
|
||||||
let messages_by_id = this.process_messages(messages);
|
let messages_by_id = this.process_messages(messages);
|
||||||
let final_messages = this.group_following(this.finalize_messages(messages_by_id));
|
let final_messages = this.group_following(
|
||||||
|
this.finalize_messages(messages_by_id)
|
||||||
|
);
|
||||||
return html`
|
return html`
|
||||||
<div style="display: flex; flex-direction: column">
|
<div style="display: flex; flex-direction: column">
|
||||||
${final_messages.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded} collapsed=true></tf-message>`)}
|
${final_messages.map(
|
||||||
|
(x) =>
|
||||||
|
html`<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
collapsed="true"
|
||||||
|
></tf-message>`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -36,23 +36,29 @@ class TfProfileElement extends LitElement {
|
|||||||
this.following = undefined;
|
this.following = undefined;
|
||||||
this.blocking = undefined;
|
this.blocking = undefined;
|
||||||
|
|
||||||
let result = await tfrpc.rpc.query(`
|
let result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT json_extract(content, '$.following') AS following
|
SELECT json_extract(content, '$.following') AS following
|
||||||
FROM messages WHERE author = ? AND
|
FROM messages WHERE author = ? AND
|
||||||
json_extract(content, '$.type') = 'contact' AND
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
json_extract(content, '$.contact') = ? AND
|
json_extract(content, '$.contact') = ? AND
|
||||||
following IS NOT NULL
|
following IS NOT NULL
|
||||||
ORDER BY sequence DESC LIMIT 1
|
ORDER BY sequence DESC LIMIT 1
|
||||||
`, [this.whoami, this.id]);
|
`,
|
||||||
|
[this.whoami, this.id]
|
||||||
|
);
|
||||||
this.following = result?.[0]?.following ?? false;
|
this.following = result?.[0]?.following ?? false;
|
||||||
result = await tfrpc.rpc.query(`
|
result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT json_extract(content, '$.blocking') AS blocking
|
SELECT json_extract(content, '$.blocking') AS blocking
|
||||||
FROM messages WHERE author = ? AND
|
FROM messages WHERE author = ? AND
|
||||||
json_extract(content, '$.type') = 'contact' AND
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
json_extract(content, '$.contact') = ? AND
|
json_extract(content, '$.contact') = ? AND
|
||||||
blocking IS NOT NULL
|
blocking IS NOT NULL
|
||||||
ORDER BY sequence DESC LIMIT 1
|
ORDER BY sequence DESC LIMIT 1
|
||||||
`, [this.whoami, this.id]);
|
`,
|
||||||
|
[this.whoami, this.id]
|
||||||
|
);
|
||||||
this.blocking = result?.[0]?.blocking ?? false;
|
this.blocking = result?.[0]?.blocking ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,13 +66,16 @@ class TfProfileElement extends LitElement {
|
|||||||
async initial_load() {
|
async initial_load() {
|
||||||
this.server_follows_me = undefined;
|
this.server_follows_me = undefined;
|
||||||
let server_id = await tfrpc.rpc.getServerIdentity();
|
let server_id = await tfrpc.rpc.getServerIdentity();
|
||||||
let followed = await tfrpc.rpc.query(`
|
let followed = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT json_extract(content, '$.following') AS following
|
SELECT json_extract(content, '$.following') AS following
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE author = ? AND
|
WHERE author = ? AND
|
||||||
json_extract(content, '$.type') = 'contact' AND
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
|
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
|
||||||
`, [server_id, this.whoami]);
|
`,
|
||||||
|
[server_id, this.whoami]
|
||||||
|
);
|
||||||
let is_followed = false;
|
let is_followed = false;
|
||||||
for (let row of followed) {
|
for (let row of followed) {
|
||||||
is_followed = row.following != 0;
|
is_followed = row.following != 0;
|
||||||
@ -75,11 +84,18 @@ class TfProfileElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
modify(change) {
|
modify(change) {
|
||||||
tfrpc.rpc.appendMessage(this.whoami,
|
tfrpc.rpc
|
||||||
Object.assign({
|
.appendMessage(
|
||||||
|
this.whoami,
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
type: 'contact',
|
type: 'contact',
|
||||||
contact: this.id,
|
contact: this.id,
|
||||||
}, change)).catch(function(error) {
|
},
|
||||||
|
change
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.catch(function (error) {
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -122,9 +138,12 @@ class TfProfileElement extends LitElement {
|
|||||||
message[key] = this.editing[key];
|
message[key] = this.editing[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
tfrpc.rpc
|
||||||
|
.appendMessage(this.whoami, message)
|
||||||
|
.then(function () {
|
||||||
self.editing = null;
|
self.editing = null;
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -139,13 +158,17 @@ class TfProfileElement extends LitElement {
|
|||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.onchange = function (event) {
|
input.onchange = function (event) {
|
||||||
let file = event.target.files[0];
|
let file = event.target.files[0];
|
||||||
file.arrayBuffer().then(function(buffer) {
|
file
|
||||||
|
.arrayBuffer()
|
||||||
|
.then(function (buffer) {
|
||||||
let bin = Array.from(new Uint8Array(buffer));
|
let bin = Array.from(new Uint8Array(buffer));
|
||||||
return tfrpc.rpc.store_blob(bin);
|
return tfrpc.rpc.store_blob(bin);
|
||||||
}).then(function(id) {
|
})
|
||||||
|
.then(function (id) {
|
||||||
self.editing = Object.assign({}, self.editing, {image: id});
|
self.editing = Object.assign({}, self.editing, {image: id});
|
||||||
console.log(self.editing);
|
console.log(self.editing);
|
||||||
}).catch(function(e) {
|
})
|
||||||
|
.catch(function (e) {
|
||||||
alert(e.message);
|
alert(e.message);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -165,16 +188,27 @@ class TfProfileElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copy_id() {
|
||||||
|
navigator.clipboard.writeText(this.id);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.id == this.whoami && this.editing && this.server_follows_me === undefined) {
|
if (
|
||||||
|
this.id == this.whoami &&
|
||||||
|
this.editing &&
|
||||||
|
this.server_follows_me === undefined
|
||||||
|
) {
|
||||||
this.initial_load();
|
this.initial_load();
|
||||||
}
|
}
|
||||||
this.load();
|
this.load();
|
||||||
let self = this;
|
let self = this;
|
||||||
let profile = this.users[this.id] || {};
|
let profile = this.users[this.id] || {};
|
||||||
tfrpc.rpc.query(
|
tfrpc.rpc
|
||||||
|
.query(
|
||||||
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
|
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
|
||||||
[this.id]).then(function(result) {
|
[this.id]
|
||||||
|
)
|
||||||
|
.then(function (result) {
|
||||||
self.size = result[0].size;
|
self.size = result[0].size;
|
||||||
});
|
});
|
||||||
let edit;
|
let edit;
|
||||||
@ -184,56 +218,87 @@ class TfProfileElement extends LitElement {
|
|||||||
if (this.editing) {
|
if (this.editing) {
|
||||||
let server_follow;
|
let server_follow;
|
||||||
if (this.server_follows_me === true) {
|
if (this.server_follows_me === true) {
|
||||||
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(false)}>Server, Stop Following Me</button>`;
|
server_follow = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => this.server_follow_me(false)}
|
||||||
|
>
|
||||||
|
Server, Stop Following Me
|
||||||
|
</button>`;
|
||||||
} else if (this.server_follows_me === false) {
|
} else if (this.server_follows_me === false) {
|
||||||
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(true)}>Server, Follow Me</button>`;
|
server_follow = html`<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => this.server_follow_me(true)}
|
||||||
|
>
|
||||||
|
Server, Follow Me
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
edit = html`
|
edit = html`
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.save_edits}>Save Profile</button>
|
<button class="w3-button w3-theme-d1" @click=${this.save_edits}>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>Discard</button>
|
Save Profile
|
||||||
|
</button>
|
||||||
|
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
${server_follow}
|
${server_follow}
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>Edit Profile</button>`;
|
edit = html`<button class="w3-button w3-theme-d1" @click=${this.edit}>
|
||||||
|
Edit Profile
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami &&
|
if (this.id !== this.whoami && this.following !== undefined) {
|
||||||
this.following !== undefined) {
|
follow = this.following
|
||||||
follow =
|
? html`<button class="w3-button w3-theme-d1" @click=${this.unfollow}>
|
||||||
this.following ?
|
Unfollow
|
||||||
html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>Unfollow</button>` :
|
</button>`
|
||||||
html`<button class="w3-button w3-dark-grey" @click=${this.follow}>Follow</button>`;
|
: html`<button class="w3-button w3-theme-d1" @click=${this.follow}>
|
||||||
|
Follow
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami &&
|
if (this.id !== this.whoami && this.blocking !== undefined) {
|
||||||
this.blocking !== undefined) {
|
block = this.blocking
|
||||||
block =
|
? html`<button class="w3-button w3-theme-d1" @click=${this.unblock}>
|
||||||
this.blocking ?
|
Unblock
|
||||||
html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>Unblock</button>` :
|
</button>`
|
||||||
html`<button class="w3-button w3-dark-grey" @click=${this.block}>Block</button>`;
|
: html`<button class="w3-button w3-theme-d1" @click=${this.block}>
|
||||||
|
Block
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
let edit_profile = this.editing ? html`
|
let edit_profile = this.editing
|
||||||
|
? html`
|
||||||
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
|
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
|
||||||
<div class="w3-container">
|
<div class="w3-container">
|
||||||
<div>
|
<div>
|
||||||
<label for="name">Name:</label>
|
<label for="name">Name:</label>
|
||||||
<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>
|
<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
|
||||||
</div>
|
</div>
|
||||||
<div><label for="description">Description:</label></div>
|
<div><label for="description">Description:</label></div>
|
||||||
<textarea 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>
|
<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
|
||||||
<div>
|
<div>
|
||||||
<label for="public_web_hosting">Public Web Hosting:</label>
|
<label for="public_web_hosting">Public Web Hosting:</label>
|
||||||
<input 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>
|
<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.attach_image}>Attach Image</button>
|
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>` : null;
|
</div>`
|
||||||
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
|
: null;
|
||||||
|
let image =
|
||||||
|
typeof profile.image == 'string' ? profile.image : profile.image?.link;
|
||||||
image = this.editing?.image ?? image;
|
image = this.editing?.image ?? image;
|
||||||
let description = this.editing?.description ?? profile.description;
|
let description = this.editing?.description ?? profile.description;
|
||||||
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
|
return html`<div 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 class="w3-row">
|
||||||
|
<div class="w3-col s1 w3-container w3-right">
|
||||||
|
<button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button>
|
||||||
|
</div>
|
||||||
|
<div class="w3-rest w3-container">
|
||||||
|
<input type="text" class="w3-theme-d1" style="width: 100%; vertical-align: middle" readonly value=${this.id}></input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div style="display: flex; flex-direction: row; gap: 1em">
|
<div style="display: flex; flex-direction: row; gap: 1em">
|
||||||
${edit_profile}
|
${edit_profile}
|
||||||
<div style="flex: 1 0 50%">
|
<div style="flex: 1 0 50%">
|
||||||
|
68
apps/ssb/tf-reactions-modal.js
Normal file
68
apps/ssb/tf-reactions-modal.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
|
||||||
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
|
class TfReactionsModalElement extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
users: {type: Object},
|
||||||
|
votes: {type: Array},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = styles;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.votes = [];
|
||||||
|
this.users = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.votes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let self = this;
|
||||||
|
return this.votes?.length
|
||||||
|
? html` <div
|
||||||
|
class="w3-modal w3-animate-opacity"
|
||||||
|
style="display: block; box-sizing: border-box"
|
||||||
|
>
|
||||||
|
<div class="w3-modal-content w3-card-4 w3-theme-d1">
|
||||||
|
<div class="w3-container w3-padding">
|
||||||
|
<header class="w3-container">
|
||||||
|
<h2>Reactions</h2>
|
||||||
|
<span class="w3-button w3-display-topright" @click=${this.clear}
|
||||||
|
>×</span
|
||||||
|
>
|
||||||
|
</header>
|
||||||
|
<ul class="w3-theme-dark w3-container w3-ul">
|
||||||
|
${this.votes.map(
|
||||||
|
(x) => html`
|
||||||
|
<li class="w3-bar">
|
||||||
|
<span class="w3-bar-item"
|
||||||
|
>${x?.content?.vote?.expression}</span
|
||||||
|
>
|
||||||
|
<tf-user
|
||||||
|
class="w3-bar-item"
|
||||||
|
id=${x.author}
|
||||||
|
.users=${this.users}
|
||||||
|
></tf-user>
|
||||||
|
<span class="w3-bar-item w3-right"
|
||||||
|
>${new Date(x?.timestamp).toLocaleString()}</span
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
<footer class="w3-container w3-padding">
|
||||||
|
<button class="w3-button" @click=${this.clear}>Close</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('tf-reactions-modal', TfReactionsModalElement);
|
@ -1,18 +1,6 @@
|
|||||||
import {css} from './lit-all.min.js';
|
import {css} from './lit-all.min.js';
|
||||||
|
|
||||||
const tf = css`
|
const tf = css`
|
||||||
a:link {
|
|
||||||
color: #bbf;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:visited {
|
|
||||||
color: #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #ddf;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: min(640px, 100%);
|
max-width: min(640px, 100%);
|
||||||
max-height: min(480px, auto);
|
max-height: min(480px, auto);
|
||||||
@ -46,22 +34,19 @@ div.img_caption::after {
|
|||||||
content: ' ±';
|
content: ' ±';
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
pre code {
|
||||||
background-color: #444;
|
display: block;
|
||||||
padding-left: 3px;
|
padding: 8px;
|
||||||
padding-right: 3px;
|
|
||||||
border: 1px dotted #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
background-color: #607d8b;
|
|
||||||
border-left: 4px solid #fff;
|
border-left: 4px solid #fff;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
const w3 = css`
|
const w3 = css`
|
||||||
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
||||||
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
@ -300,4 +285,30 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export let styles = [tf, w3];
|
// prettier-ignore
|
||||||
|
const w3_2016_riverside = css`
|
||||||
|
.w3-theme-l5 {color:#000 !important; background-color:#f4f6f9 !important}
|
||||||
|
.w3-theme-l4 {color:#000 !important; background-color:#d9e1ec !important}
|
||||||
|
.w3-theme-l3 {color:#000 !important; background-color:#b4c3d8 !important}
|
||||||
|
.w3-theme-l2 {color:#fff !important; background-color:#8ea6c5 !important}
|
||||||
|
.w3-theme-l1 {color:#fff !important; background-color:#6888b1 !important}
|
||||||
|
.w3-theme-d1 {color:#fff !important; background-color:#456185 !important}
|
||||||
|
.w3-theme-d2 {color:#fff !important; background-color:#3d5676 !important}
|
||||||
|
.w3-theme-d3 {color:#fff !important; background-color:#354b68 !important}
|
||||||
|
.w3-theme-d4 {color:#fff !important; background-color:#2e4059 !important}
|
||||||
|
.w3-theme-d5 {color:#fff !important; background-color:#26364a !important}
|
||||||
|
|
||||||
|
.w3-theme-light {color:#000 !important; background-color:#f4f6f9 !important}
|
||||||
|
.w3-theme-dark {color:#fff !important; background-color:#26364a !important}
|
||||||
|
.w3-theme-action {color:#fff !important; background-color:#26364a !important}
|
||||||
|
|
||||||
|
.w3-theme {color:#fff !important; background-color:#4c6a92 !important}
|
||||||
|
.w3-text-theme {color:#4c6a92 !important}
|
||||||
|
.w3-border-theme {border-color:#4c6a92 !important}
|
||||||
|
|
||||||
|
.w3-hover-theme:hover {color:#fff !important; background-color:#4c6a92 !important}
|
||||||
|
.w3-hover-text-theme:hover {color:#4c6a92 !important}
|
||||||
|
.w3-hover-border-theme:hover {border-color:#4c6a92 !important}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export let styles = [tf, w3, w3_2016_riverside];
|
||||||
|
@ -7,35 +7,52 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
return {
|
return {
|
||||||
broadcasts: {type: Array},
|
broadcasts: {type: Array},
|
||||||
identities: {type: Array},
|
identities: {type: Array},
|
||||||
|
my_identities: {type: Array},
|
||||||
connections: {type: Array},
|
connections: {type: Array},
|
||||||
stored_connections: {type: Array},
|
stored_connections: {type: Array},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
|
server_identity: {type: String},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = styles;
|
static styles = styles;
|
||||||
|
|
||||||
|
static k_broadcast_emojis = {
|
||||||
|
discovery: '🏓',
|
||||||
|
room: '🚪',
|
||||||
|
peer_exchange: '🕸',
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
let self = this;
|
let self = this;
|
||||||
this.broadcasts = [];
|
this.broadcasts = [];
|
||||||
this.identities = [];
|
this.identities = [];
|
||||||
|
this.my_identities = [];
|
||||||
this.connections = [];
|
this.connections = [];
|
||||||
this.stored_connections = [];
|
this.stored_connections = [];
|
||||||
this.users = {};
|
this.users = {};
|
||||||
|
tfrpc.rpc.getIdentities().then(function (identities) {
|
||||||
|
self.my_identities = identities || [];
|
||||||
|
});
|
||||||
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 || [];
|
||||||
});
|
});
|
||||||
|
tfrpc.rpc.getServerIdentity().then(function (identity) {
|
||||||
|
self.server_identity = identity;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render_connection_summary(connection) {
|
render_connection_summary(connection) {
|
||||||
if (connection.address && connection.port) {
|
if (connection.address && connection.port) {
|
||||||
return html`(<small>${connection.address}:${connection.port}</small>)`;
|
return html`<div>
|
||||||
|
<small>${connection.address}:${connection.port}</small>
|
||||||
|
</div>`;
|
||||||
} else if (connection.tunnel) {
|
} else if (connection.tunnel) {
|
||||||
return html`(room peer)`;
|
return html`<div>room peer</div>`;
|
||||||
} else {
|
} else {
|
||||||
return JSON.stringify(connection);
|
return JSON.stringify(connection);
|
||||||
}
|
}
|
||||||
@ -43,10 +60,12 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
|
|
||||||
render_room_peers(connection) {
|
render_room_peers(connection) {
|
||||||
let self = this;
|
let self = this;
|
||||||
let peers = this.broadcasts.filter(x => x.tunnel?.id == connection);
|
let peers = this.broadcasts.filter((x) => x.tunnel?.id == connection);
|
||||||
if (peers.length) {
|
if (peers.length) {
|
||||||
let connections = this.connections.map(x => x.id);
|
let connections = this.connections.map((x) => x.id);
|
||||||
return html`${peers.filter(x => connections.indexOf(x.pubkey) == -1).map(x => html`${self.render_room_peer(x)}`)}`;
|
return html`${peers
|
||||||
|
.filter((x) => connections.indexOf(x.pubkey) == -1)
|
||||||
|
.map((x) => html`${self.render_room_peer(x)}`)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +77,12 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<li>
|
<li>
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}>Connect</button>
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
@ -66,10 +90,18 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
|
|
||||||
render_broadcast(connection) {
|
render_broadcast(connection) {
|
||||||
return html`
|
return html`
|
||||||
<li>
|
<li class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap">
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(connection)}>Connect</button>
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
|
@click=${() => tfrpc.rpc.connect(connection)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
<div class="w3-bar-item">
|
||||||
|
${TfTabConnectionsElement.k_broadcast_emojis[connection.origin]}
|
||||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
||||||
${this.render_connection_summary(connection)}
|
${this.render_connection_summary(connection)}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -80,12 +112,45 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_connection(connection) {
|
render_connection(connection) {
|
||||||
|
let requests = Object.values(
|
||||||
|
connection.requests.reduce(function (accumulator, value) {
|
||||||
|
let key = `${value.name}:${Math.sign(value.request_number)}`;
|
||||||
|
if (!accumulator[key]) {
|
||||||
|
accumulator[key] = Object.assign({count: 0}, value);
|
||||||
|
}
|
||||||
|
accumulator[key].count++;
|
||||||
|
return accumulator;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
return html`
|
return html`
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.closeConnection(connection.id)}>Close</button>
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
<tf-user id=${connection.id} .users=${this.users}></tf-user>
|
<tf-user id=${connection.id} .users=${this.users}></tf-user>
|
||||||
${connection.tunnel !== undefined ? '🚇' : html`(${connection.host}:${connection.port})`}
|
${connection.tunnel !== undefined
|
||||||
|
? '🚇'
|
||||||
|
: html`(${connection.host}:${connection.port})`}
|
||||||
|
<div>
|
||||||
|
${requests.map(
|
||||||
|
(x) => html`
|
||||||
|
<span class="w3-tag w3-small"
|
||||||
|
>${x.request_number > 0 ? '🟩' : '🟥'} ${x.name}
|
||||||
|
<span
|
||||||
|
class="w3-badge w3-white"
|
||||||
|
style=${x.count > 1 ? undefined : 'display: none'}
|
||||||
|
>${x.count}</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
${this.connections.filter(x => x.tunnel === this.connections.indexOf(connection)).map(x => html`<li>${this.render_connection(x)}</li>`)}
|
${this.connections
|
||||||
|
.filter((x) => x.tunnel === this.connections.indexOf(connection))
|
||||||
|
.map((x) => html`<li>${this.render_connection(x)}</li>`)}
|
||||||
${this.render_room_peers(connection.id)}
|
${this.render_room_peers(connection.id)}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
@ -94,33 +159,75 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<div class="w3-container">
|
<div class="w3-container" style="box-sizing: border-box">
|
||||||
<h2>New Connection</h2>
|
<h2>New Connection</h2>
|
||||||
<textarea class="w3-input w3-dark-grey" id="code"></textarea>
|
<textarea class="w3-input w3-theme-d1" id="code"></textarea>
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}>Connect</button>
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${() =>
|
||||||
|
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
<h2>Broadcasts</h2>
|
<h2>Broadcasts</h2>
|
||||||
<ul>
|
<ul class="w3-ul w3-border">
|
||||||
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
|
${this.broadcasts
|
||||||
|
.filter((x) => x.address)
|
||||||
|
.map((x) => self.render_broadcast(x))}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Connections</h2>
|
<h2>Connections</h2>
|
||||||
<ul>
|
<ul class="w3-ul w3-border">
|
||||||
${this.connections.filter(x => x.tunnel === undefined).map(x => html`
|
${this.connections
|
||||||
<li>${this.render_connection(x)}</li>
|
.filter((x) => x.tunnel === undefined)
|
||||||
`)}
|
.map(
|
||||||
|
(x) => html`
|
||||||
|
<li class="w3-bar">${this.render_connection(x)}</li>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Stored Connections (WIP)</h2>
|
<h2>Stored Connections</h2>
|
||||||
<ul>
|
<ul class="w3-ul w3-border">
|
||||||
${this.stored_connections.map(x => html`
|
${this.stored_connections.map(
|
||||||
<li>
|
(x) => html`
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => self.forget_stored_connection(x)}>Forget</button>
|
<li class="w3-bar">
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(x)}>Connect</button>
|
<button
|
||||||
${x.address}:${x.port} <tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
|
@click=${() => self.forget_stored_connection(x)}
|
||||||
|
>
|
||||||
|
Forget
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
|
@click=${() => tfrpc.rpc.connect(x)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
<div class="w3-bar-item">
|
||||||
|
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
||||||
|
<div><small>${x.address}:${x.port}</small></div>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
`)}
|
`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Local Accounts</h2>
|
<h2>Local Accounts</h2>
|
||||||
<ul>
|
<ul class="w3-ul w3-border">
|
||||||
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
${this.identities.map(
|
||||||
|
(x) =>
|
||||||
|
html`<li class="w3-bar">
|
||||||
|
${x == this.server_identity
|
||||||
|
? html`<span class="w3-tag w3-medium w3-round w3-theme-l1"
|
||||||
|
>🖥 local server</span
|
||||||
|
>`
|
||||||
|
: undefined}
|
||||||
|
${this.my_identities.indexOf(x) != -1
|
||||||
|
? html`<span class="w3-tag w3-medium w3-round w3-theme-d1"
|
||||||
|
>😎 you</span
|
||||||
|
>`
|
||||||
|
: undefined}
|
||||||
|
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||||
|
</li>`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -27,15 +27,21 @@ class TfTabMentionsElement extends LitElement {
|
|||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
console.log('Loading...', this.whoami);
|
console.log('Loading...', this.whoami);
|
||||||
let results = await tfrpc.rpc.query(`
|
let results = await tfrpc.rpc.query(
|
||||||
SELECT messages.*
|
`
|
||||||
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages_fts(?)
|
FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.author != ?
|
WHERE messages.author != ?
|
||||||
ORDER BY timestamp DESC limit 20
|
ORDER BY timestamp DESC limit 20
|
||||||
`,
|
`,
|
||||||
['"' + this.whoami.replace('"', '""') + '"', JSON.stringify(this.following), this.whoami]);
|
[
|
||||||
|
'"' + this.whoami.replace('"', '""') + '"',
|
||||||
|
JSON.stringify(this.following),
|
||||||
|
this.whoami,
|
||||||
|
]
|
||||||
|
);
|
||||||
console.log('Done.');
|
console.log('Done.');
|
||||||
this.messages = results;
|
this.messages = results;
|
||||||
}
|
}
|
||||||
@ -58,7 +64,14 @@ class TfTabMentionsElement extends LitElement {
|
|||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
|
<tf-news
|
||||||
|
id="news"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.messages=${this.messages}
|
||||||
|
.users=${this.users}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@tf-expand=${this.on_expand}
|
||||||
|
></tf-news>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,54 +33,53 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
if (this.hash.startsWith('#@')) {
|
if (this.hash.startsWith('#@')) {
|
||||||
let r = await tfrpc.rpc.query(
|
let r = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH mine AS (SELECT messages.*
|
WITH mine AS (SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE messages.author = ?
|
WHERE messages.author = ?
|
||||||
ORDER BY sequence DESC
|
ORDER BY sequence DESC
|
||||||
LIMIT 20)
|
LIMIT 20)
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM mine
|
FROM mine
|
||||||
JOIN messages_refs ON mine.id = messages_refs.ref
|
JOIN messages_refs ON mine.id = messages_refs.ref
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
UNION
|
UNION
|
||||||
SELECT * FROM mine
|
SELECT * FROM mine
|
||||||
`,
|
`,
|
||||||
[
|
[this.hash.substring(1)]
|
||||||
this.hash.substring(1),
|
);
|
||||||
]);
|
|
||||||
return r;
|
return r;
|
||||||
} else if (this.hash.startsWith('#%')) {
|
} else if (this.hash.startsWith('#%')) {
|
||||||
return await tfrpc.rpc.query(
|
return await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
SELECT messages.*
|
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE id = ?1
|
WHERE id = ?1
|
||||||
UNION
|
UNION
|
||||||
SELECT messages.*
|
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
|
||||||
FROM messages JOIN messages_refs
|
FROM messages JOIN messages_refs
|
||||||
ON messages.id = messages_refs.message
|
ON messages.id = messages_refs.message
|
||||||
WHERE messages_refs.ref = ?1
|
WHERE messages_refs.ref = ?1
|
||||||
`,
|
`,
|
||||||
[
|
[this.hash.substring(1)]
|
||||||
this.hash.substring(1),
|
);
|
||||||
]);
|
|
||||||
} else {
|
} else {
|
||||||
let promises = [];
|
let promises = [];
|
||||||
const k_following_limit = 256;
|
const k_following_limit = 256;
|
||||||
for (let i = 0; i < this.following.length; i += k_following_limit) {
|
for (let i = 0; i < this.following.length; i += k_following_limit) {
|
||||||
promises.push(tfrpc.rpc.query(
|
promises.push(
|
||||||
|
tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH news AS (SELECT messages.*
|
WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.timestamp > ? AND messages.timestamp < ?
|
WHERE messages.timestamp > ? AND messages.timestamp < ?
|
||||||
ORDER BY messages.timestamp DESC)
|
ORDER BY messages.timestamp DESC)
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM news
|
FROM news
|
||||||
JOIN messages_refs ON news.id = messages_refs.ref
|
JOIN messages_refs ON news.id = messages_refs.ref
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
UNION
|
UNION
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM news
|
FROM news
|
||||||
JOIN messages_refs ON news.id = messages_refs.message
|
JOIN messages_refs ON news.id = messages_refs.message
|
||||||
JOIN messages ON messages_refs.ref = messages.id
|
JOIN messages ON messages_refs.ref = messages.id
|
||||||
@ -95,7 +94,9 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
** messages with far-future timestamps from staying at the top forever.
|
** messages with far-future timestamps from staying at the top forever.
|
||||||
*/
|
*/
|
||||||
new Date().valueOf() + 24 * 60 * 60 * 1000,
|
new Date().valueOf() + 24 * 60 * 60 * 1000,
|
||||||
]));
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return [].concat(...(await Promise.all(promises)));
|
return [].concat(...(await Promise.all(promises)));
|
||||||
}
|
}
|
||||||
@ -106,29 +107,26 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
|
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
|
||||||
let more = await tfrpc.rpc.query(
|
let more = await tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH news AS (SELECT messages.*
|
WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.timestamp > ?
|
WHERE messages.timestamp > ?
|
||||||
AND messages.timestamp <= ?
|
AND messages.timestamp <= ?
|
||||||
ORDER BY messages.timestamp DESC)
|
ORDER BY messages.timestamp DESC)
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM news
|
FROM news
|
||||||
JOIN messages_refs ON news.id = messages_refs.ref
|
JOIN messages_refs ON news.id = messages_refs.ref
|
||||||
JOIN messages ON messages_refs.message = messages.id
|
JOIN messages ON messages_refs.message = messages.id
|
||||||
UNION
|
UNION
|
||||||
SELECT messages.*
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM news
|
FROM news
|
||||||
JOIN messages_refs ON news.id = messages_refs.message
|
JOIN messages_refs ON news.id = messages_refs.message
|
||||||
JOIN messages ON messages_refs.ref = messages.id
|
JOIN messages ON messages_refs.ref = messages.id
|
||||||
UNION
|
UNION
|
||||||
SELECT news.* FROM news
|
SELECT news.* FROM news
|
||||||
`,
|
`,
|
||||||
[
|
[JSON.stringify(this.following), this.start_time, last_start_time]
|
||||||
JSON.stringify(this.following),
|
);
|
||||||
this.start_time,
|
|
||||||
last_start_time,
|
|
||||||
]);
|
|
||||||
this.messages = await this.decrypt([...more, ...this.messages]);
|
this.messages = await this.decrypt([...more, ...this.messages]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,14 +137,12 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
let content;
|
let content;
|
||||||
try {
|
try {
|
||||||
content = JSON.parse(message?.content);
|
content = JSON.parse(message?.content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
if (typeof content === 'string') {
|
||||||
if (typeof(content) === 'string') {
|
|
||||||
let decrypted;
|
let decrypted;
|
||||||
try {
|
try {
|
||||||
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
|
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
if (decrypted) {
|
if (decrypted) {
|
||||||
try {
|
try {
|
||||||
message.decrypted = JSON.parse(decrypted);
|
message.decrypted = JSON.parse(decrypted);
|
||||||
@ -165,18 +161,25 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.messages ||
|
if (
|
||||||
|
!this.messages ||
|
||||||
this._messages_hash !== this.hash ||
|
this._messages_hash !== this.hash ||
|
||||||
this._messages_following !== this.following) {
|
this._messages_following !== this.following
|
||||||
console.log(`loading messages for ${this.whoami} (following ${this.following.length})`);
|
) {
|
||||||
|
console.log(
|
||||||
|
`loading messages for ${this.whoami} (following ${this.following.length})`
|
||||||
|
);
|
||||||
let self = this;
|
let self = this;
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this._messages_hash = this.hash;
|
this._messages_hash = this.hash;
|
||||||
this._messages_following = this.following;
|
this._messages_following = this.following;
|
||||||
this.fetch_messages().then(this.decrypt.bind(this)).then(function(messages) {
|
this.fetch_messages()
|
||||||
|
.then(this.decrypt.bind(this))
|
||||||
|
.then(function (messages) {
|
||||||
self.messages = messages;
|
self.messages = messages;
|
||||||
console.log(`loading mesages done for ${self.whoami}`);
|
console.log(`loading mesages done for ${self.whoami}`);
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
alert(JSON.stringify(error, null, 2));
|
alert(JSON.stringify(error, null, 2));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -184,12 +187,22 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
||||||
more = html`
|
more = html`
|
||||||
<p>
|
<p>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.load_more}>Load More</button>
|
<button class="w3-button w3-theme-d1" @click=${this.load_more}>
|
||||||
|
Load More
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<tf-news id="news" whoami=${this.whoami} .users=${this.users} .messages=${this.messages} .following=${this.following} .drafts=${this.drafts} .expanded=${this.expanded}></tf-news>
|
<tf-news
|
||||||
|
id="news"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.messages=${this.messages}
|
||||||
|
.following=${this.following}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-news>
|
||||||
${more}
|
${more}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ class TfTabNewsElement extends LitElement {
|
|||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
drafts: {type: Object},
|
drafts: {type: Object},
|
||||||
expanded: {type: Object},
|
expanded: {type: Object},
|
||||||
|
loading: {type: Boolean},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +49,9 @@ class TfTabNewsElement extends LitElement {
|
|||||||
let news = this.shadowRoot?.getElementById('news');
|
let news = this.shadowRoot?.getElementById('news');
|
||||||
if (news) {
|
if (news) {
|
||||||
console.log('injecting messages', news.messages);
|
console.log('injecting messages', news.messages);
|
||||||
news.add_messages(Object.values(Object.fromEntries(this.unread.map(x => [x.id, x]))));
|
news.add_messages(
|
||||||
|
Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x])))
|
||||||
|
);
|
||||||
this.dispatchEvent(new CustomEvent('refresh'));
|
this.dispatchEvent(new CustomEvent('refresh'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,11 +65,16 @@ class TfTabNewsElement extends LitElement {
|
|||||||
let type = 'private';
|
let type = 'private';
|
||||||
try {
|
try {
|
||||||
type = JSON.parse(message.content).type || type;
|
type = JSON.parse(message.content).type || type;
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
counts[type] = (counts[type] || 0) + 1;
|
counts[type] = (counts[type] || 0) + 1;
|
||||||
}
|
}
|
||||||
return '↻ Show New: ' + Object.keys(counts).sort().map(x => (counts[x].toString() + ' ' + x + 's')).join(', ');
|
return (
|
||||||
|
'↻ Show New: ' +
|
||||||
|
Object.keys(counts)
|
||||||
|
.sort()
|
||||||
|
.map((x) => counts[x].toString() + ' ' + x + 's')
|
||||||
|
.join(', ')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
draft(event) {
|
draft(event) {
|
||||||
@ -77,10 +85,7 @@ class TfTabNewsElement extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
delete this.drafts[id];
|
delete this.drafts[id];
|
||||||
}
|
}
|
||||||
/* Only trigger a re-render if we're creating a new draft or discarding an old one. */
|
|
||||||
if ((previous !== undefined) != (event.detail.draft !== undefined)) {
|
|
||||||
this.drafts = Object.assign({}, this.drafts);
|
this.drafts = Object.assign({}, this.drafts);
|
||||||
}
|
|
||||||
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
|
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,23 +101,66 @@ class TfTabNewsElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
on_keypress(event) {
|
on_keypress(event) {
|
||||||
if (event.target === document.body &&
|
if (event.target === document.body && event.key == '.') {
|
||||||
event.key == '.') {
|
|
||||||
this.show_more();
|
this.show_more();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let profile = this.hash.startsWith('#@') ?
|
let profile = this.hash.startsWith('#@')
|
||||||
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
|
? html`<tf-profile
|
||||||
|
id=${this.hash.substring(1)}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
></tf-profile>`
|
||||||
|
: undefined;
|
||||||
|
let edit_profile;
|
||||||
|
if (
|
||||||
|
!this.loading &&
|
||||||
|
this.users[this.whoami]?.name === undefined &&
|
||||||
|
this.hash.substring(1) != this.whoami
|
||||||
|
) {
|
||||||
|
edit_profile = html` <div
|
||||||
|
class="w3-panel w3-padding w3-round w3-card-4 w3-theme-l3"
|
||||||
|
>
|
||||||
|
ℹ️ Follow your identity link ☝️ above to edit your profile and set your
|
||||||
|
name.
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
<p class="w3-bar">
|
<p class="w3-bar">
|
||||||
<button class="w3-bar-item w3-button w3-dark-grey" @click=${this.show_more}>${this.new_messages_text()}</button>
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-theme-d1"
|
||||||
|
@click=${this.show_more}
|
||||||
|
>
|
||||||
|
${this.new_messages_text()}
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div>
|
<div class="w3-bar">
|
||||||
<div><tf-compose id="tf-compose" whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
|
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
|
||||||
|
${edit_profile}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<tf-compose
|
||||||
|
id="tf-compose"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
@tf-draft=${this.draft}
|
||||||
|
></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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ class TfTabQueryElement extends LitElement {
|
|||||||
await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query));
|
await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query));
|
||||||
let start_time = new Date();
|
let start_time = new Date();
|
||||||
try {
|
try {
|
||||||
this.results = await tfrpc.rpc.query(query, [])
|
this.results = await tfrpc.rpc.query(query, []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error = error;
|
this.error = error;
|
||||||
}
|
}
|
||||||
@ -79,8 +79,15 @@ class TfTabQueryElement extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
let keys = Object.keys(this.results[0]).sort();
|
let keys = Object.keys(this.results[0]).sort();
|
||||||
return html`<table style="width: 100%; max-width: 100%">
|
return html`<table style="width: 100%; max-width: 100%">
|
||||||
<tr>${keys.map(key => html`<th>${key}</th>`)}</tr>
|
<tr>
|
||||||
${this.results.map(row => html`<tr>${keys.map(key => html`<td>${row[key]}</td>`)}</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>`;
|
</table>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -100,13 +107,28 @@ class TfTabQueryElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<div style="display: flex; flex-direction: row; gap: 4px">
|
<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>
|
<textarea
|
||||||
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Execute</button>
|
id="search"
|
||||||
|
rows="8"
|
||||||
|
class="w3-input w3-theme-d1"
|
||||||
|
style="flex: 1; resize: vertical"
|
||||||
|
@keydown=${this.search_keydown}
|
||||||
|
>
|
||||||
|
${this.query}</textarea
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-theme-d1"
|
||||||
|
@click=${(event) =>
|
||||||
|
self.search(self.renderRoot.getElementById('search').value)}
|
||||||
|
>
|
||||||
|
Execute
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div ?hidden=${this.duration === undefined}>
|
||||||
|
Took ${this.duration / 1000.0} seconds.
|
||||||
</div>
|
</div>
|
||||||
<div ?hidden=${this.duration === undefined}>Took ${this.duration / 1000.0} seconds.</div>
|
|
||||||
<div ?hidden=${this.duration !== undefined}>Executing...</div>
|
<div ?hidden=${this.duration !== undefined}>Executing...</div>
|
||||||
${this.render_error()}
|
${this.render_error()} ${this.render_results()}
|
||||||
${this.render_results()}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import {styles} from './tf-styles.js';
|
|||||||
class TfTabSearchElement extends LitElement {
|
class TfTabSearchElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
|
drafts: {type: Object},
|
||||||
whoami: {type: String},
|
whoami: {type: String},
|
||||||
users: {type: Object},
|
users: {type: Object},
|
||||||
following: {type: Array},
|
following: {type: Array},
|
||||||
@ -22,6 +23,10 @@ class TfTabSearchElement extends LitElement {
|
|||||||
this.users = {};
|
this.users = {};
|
||||||
this.following = [];
|
this.following = [];
|
||||||
this.expanded = {};
|
this.expanded = {};
|
||||||
|
this.drafts = {};
|
||||||
|
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
|
||||||
|
self.drafts = JSON.parse(d || '{}');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query) {
|
async search(query) {
|
||||||
@ -33,14 +38,16 @@ class TfTabSearchElement extends LitElement {
|
|||||||
search.select();
|
search.select();
|
||||||
}
|
}
|
||||||
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
|
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
|
||||||
let results = await tfrpc.rpc.query(`
|
let results = await tfrpc.rpc.query(
|
||||||
SELECT messages.*
|
`
|
||||||
|
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
|
||||||
FROM messages_fts(?)
|
FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
ORDER BY timestamp DESC limit 100
|
ORDER BY timestamp DESC limit 100
|
||||||
`,
|
`,
|
||||||
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]);
|
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
|
||||||
|
);
|
||||||
console.log('Done.');
|
console.log('Done.');
|
||||||
search = this.renderRoot.getElementById('search');
|
search = this.renderRoot.getElementById('search');
|
||||||
if (search) {
|
if (search) {
|
||||||
@ -68,6 +75,18 @@ class TfTabSearchElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
draft(event) {
|
||||||
|
let id = event.detail.id || '';
|
||||||
|
let previous = this.drafts[id];
|
||||||
|
if (event.detail.draft !== undefined) {
|
||||||
|
this.drafts[id] = event.detail.draft;
|
||||||
|
} else {
|
||||||
|
delete this.drafts[id];
|
||||||
|
}
|
||||||
|
this.drafts = Object.assign({}, this.drafts);
|
||||||
|
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.query !== this.last_query) {
|
if (this.query !== this.last_query) {
|
||||||
this.last_query = this.query;
|
this.last_query = this.query;
|
||||||
@ -76,10 +95,10 @@ class TfTabSearchElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<div style="display: flex; flex-direction: row; gap: 4px">
|
<div style="display: flex; flex-direction: row; gap: 4px">
|
||||||
<input type="text" class="w3-input w3-dark-grey" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
|
||||||
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
|
<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
|
||||||
</div>
|
</div>
|
||||||
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
|
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} .drafts=${this.drafts} @tf-expand=${this.on_expand} @tf-draft=${this.draft}></tf-news>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,11 @@ class TfTagElement extends LitElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let number = this.count ? html` (${this.count})` : undefined;
|
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>`;
|
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
|
||||||
|
>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,26 +19,33 @@ class TfUserElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
let image = html`<span
|
||||||
|
class="w3-theme-light w3-circle"
|
||||||
|
style="display: inline-block; width: 2em; height: 2em; text-align: center; line-height: 2em"
|
||||||
|
>?</span
|
||||||
|
>`;
|
||||||
let name = this.users?.[this.id]?.name;
|
let name = this.users?.[this.id]?.name;
|
||||||
name = name !== undefined ?
|
name =
|
||||||
html`<a target="_top" href=${'#' + this.id}>${name}</a>` :
|
name !== undefined
|
||||||
html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
|
? html`<a target="_top" href=${'#' + this.id}>${name}</a>`
|
||||||
|
: html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
|
||||||
|
|
||||||
if (this.users[this.id]) {
|
if (this.users[this.id]) {
|
||||||
let image = this.users[this.id].image;
|
let image_link = this.users[this.id].image;
|
||||||
image = typeof(image) == 'string' ? image : image?.link;
|
image_link =
|
||||||
return html`
|
typeof image_link == 'string' ? image_link : image_link?.link;
|
||||||
<div style="display: inline-block; font-weight: bold">
|
if (image_link !== undefined) {
|
||||||
<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" ?hidden=${image === undefined} src="${image ? '/' + image + '/view' : undefined}">
|
image = html`<img
|
||||||
${name}
|
class="w3-circle"
|
||||||
</div>`;
|
style="width: 2em; height: 2em; vertical-align: middle"
|
||||||
} else {
|
src="/${image_link}/view"
|
||||||
return html`
|
/>`;
|
||||||
<div style="display: inline-block; font-weight: bold">
|
|
||||||
${name}
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return html` <div style="display: inline-block; font-weight: bold">
|
||||||
|
${image} ${name}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('tf-user', TfUserElement);
|
customElements.define('tf-user', TfUserElement);
|
@ -1,21 +1,34 @@
|
|||||||
import * as linkify from './commonmark-linkify.js';
|
|
||||||
import * as hashtagify from './commonmark-hashtag.js';
|
import * as hashtagify from './commonmark-hashtag.js';
|
||||||
|
|
||||||
|
const k_code_classes = 'w3-theme-l4 w3-theme-border w3-round';
|
||||||
|
|
||||||
function image(node, entering) {
|
function image(node, entering) {
|
||||||
if (node.firstChild?.type === 'text' &&
|
if (
|
||||||
node.firstChild.literal.startsWith('video:')) {
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('video:')
|
||||||
|
) {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
this.lit(
|
||||||
|
'<video style="max-width: 100%; max-height: 480px" title="' +
|
||||||
|
this.esc(node.firstChild?.literal) +
|
||||||
|
'" controls>'
|
||||||
|
);
|
||||||
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
this.disableTags += 1;
|
this.disableTags += 1;
|
||||||
} else {
|
} else {
|
||||||
this.disableTags -= 1;
|
this.disableTags -= 1;
|
||||||
this.lit('</video>');
|
this.lit('</video>');
|
||||||
}
|
}
|
||||||
} else if (node.firstChild?.type === 'text' &&
|
} else if (
|
||||||
node.firstChild.literal.startsWith('audio:')) {
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('audio:')
|
||||||
|
) {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
this.lit(
|
||||||
|
'<audio style="height: 32px; max-width: 100%" title="' +
|
||||||
|
this.esc(node.firstChild?.literal) +
|
||||||
|
'" controls>'
|
||||||
|
);
|
||||||
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
this.disableTags += 1;
|
this.disableTags += 1;
|
||||||
} else {
|
} else {
|
||||||
@ -25,7 +38,11 @@ function image(node, entering) {
|
|||||||
} else {
|
} else {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
if (this.disableTags === 0) {
|
if (this.disableTags === 0) {
|
||||||
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>');
|
this.lit(
|
||||||
|
'<div class="img_caption">' +
|
||||||
|
this.esc(node.firstChild?.literal || node.destination) +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
||||||
this.lit('<img src="" alt="');
|
this.lit('<img src="" alt="');
|
||||||
} else {
|
} else {
|
||||||
@ -45,12 +62,31 @@ function image(node, entering) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function code(node) {
|
||||||
|
let attrs = this.attrs(node);
|
||||||
|
attrs.push(['class', k_code_classes]);
|
||||||
|
this.tag('code', attrs);
|
||||||
|
this.out(node.literal);
|
||||||
|
this.tag('/code');
|
||||||
|
}
|
||||||
|
|
||||||
|
function attrs(node) {
|
||||||
|
let result = commonmark.HtmlRenderer.prototype.attrs.bind(this)(node);
|
||||||
|
if (node.type == 'block_quote') {
|
||||||
|
result.push(['class', 'w3-theme-d1']);
|
||||||
|
} else if (node.type == 'code_block') {
|
||||||
|
result.push(['class', k_code_classes]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export function markdown(md) {
|
export function markdown(md) {
|
||||||
let reader = new commonmark.Parser({safe: true});
|
let reader = new commonmark.Parser({safe: true});
|
||||||
let writer = new commonmark.HtmlRenderer();
|
let writer = new commonmark.HtmlRenderer();
|
||||||
writer.image = image;
|
writer.image = image;
|
||||||
|
writer.code = code;
|
||||||
|
writer.attrs = attrs;
|
||||||
let parsed = reader.parse(md || '');
|
let parsed = reader.parse(md || '');
|
||||||
parsed = linkify.transform(parsed);
|
|
||||||
parsed = hashtagify.transform(parsed);
|
parsed = hashtagify.transform(parsed);
|
||||||
let walker = parsed.walker();
|
let walker = parsed.walker();
|
||||||
let event, node;
|
let event, node;
|
||||||
@ -58,14 +94,20 @@ export function markdown(md) {
|
|||||||
node = event.node;
|
node = event.node;
|
||||||
if (event.entering) {
|
if (event.entering) {
|
||||||
if (node.type == 'link') {
|
if (node.type == 'link') {
|
||||||
if (node.destination.startsWith('@') &&
|
if (
|
||||||
node.destination.endsWith('.ed25519')) {
|
node.destination.startsWith('@') &&
|
||||||
|
node.destination.endsWith('.ed25519')
|
||||||
|
) {
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (node.destination.startsWith('%') &&
|
} else if (
|
||||||
node.destination.endsWith('.sha256')) {
|
node.destination.startsWith('%') &&
|
||||||
|
node.destination.endsWith('.sha256')
|
||||||
|
) {
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (node.destination.startsWith('&') &&
|
} else if (
|
||||||
node.destination.endsWith('.sha256')) {
|
node.destination.startsWith('&') &&
|
||||||
|
node.destination.endsWith('.sha256')
|
||||||
|
) {
|
||||||
node.destination = '/' + node.destination + '/view';
|
node.destination = '/' + node.destination + '/view';
|
||||||
}
|
}
|
||||||
} else if (node.type == 'image') {
|
} else if (node.type == 'image') {
|
||||||
|
@ -482,16 +482,7 @@ class TributeRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDocument() {
|
getDocument() {
|
||||||
let iframe;
|
return document;
|
||||||
if (this.tribute.current.collection) {
|
|
||||||
iframe = this.tribute.current.collection.iframe;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!iframe) {
|
|
||||||
return document
|
|
||||||
}
|
|
||||||
|
|
||||||
return iframe.contentWindow.document
|
|
||||||
}
|
}
|
||||||
|
|
||||||
positionMenuAtCaret(scrollTo) {
|
positionMenuAtCaret(scrollTo) {
|
||||||
@ -653,8 +644,8 @@ class TributeRange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getWindowSelection() {
|
getWindowSelection() {
|
||||||
if (this.tribute.collection.iframe) {
|
if (this.tribute.collection[0].iframe?.getSelection) {
|
||||||
return this.tribute.collection.iframe.contentWindow.getSelection()
|
return this.tribute.collection[0].iframe.getSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.getSelection()
|
return window.getSelection()
|
||||||
|
@ -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;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>TODO</title>
|
<title>TODO</title>
|
||||||
|
@ -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,9 +12,12 @@ 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
|
||||||
|
.todo_get_all()
|
||||||
|
.then(function (lists) {
|
||||||
self.lists = lists;
|
self.lists = lists;
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
.todo_set(self.name, self.items)
|
||||||
|
.then(function () {
|
||||||
console.log('saved', self.name, self.items);
|
console.log('saved', self.name, self.items);
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
console.log(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,20 +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)} style="cursor: pointer">❎</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 || '(empty)'}</span>
|
<span @click=${(x) => (self.editing = index)}>${item.text || '(empty)'}</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,10 +154,13 @@ 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
|
||||||
|
.todo_rename(this.name, new_name)
|
||||||
|
.then(function () {
|
||||||
self.dispatchEvent(new Event('change'));
|
self.dispatchEvent(new Event('change'));
|
||||||
self.editing_name = false;
|
self.editing_name = false;
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
alert(error.message);
|
alert(error.message);
|
||||||
self.editing_name = false;
|
self.editing_name = false;
|
||||||
@ -163,19 +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 || []).filter(item => !item.x).map(x => self.render_item(x))}
|
${(this.items || [])
|
||||||
${(this.items || []).filter(item => item.x).map(x => self.render_item(x))}
|
.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>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "👋",
|
"emoji": "👋",
|
||||||
"previous": "&zFISmRDAv+SXFonfZ9/sHNhrmMe+poTU22gwZzuSkT4=.sha256"
|
"previous": "&7Pqk5nBAcbjzp0etv6WgiyTD3UF++ID0mW6qIbhwt3s=.sha256"
|
||||||
}
|
}
|
124
apps/welcome/f-droid.svg
Normal file
124
apps/welcome/f-droid.svg
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Original Author: Unknown (if you are the original creator of the F-Droid button, please contact laura@ind.ie so I can credit you!) -->
|
||||||
|
<!-- Author: Created by Laura Kalbag and Released with ❤ by ind.ie (laura@ind.ie) -->
|
||||||
|
<!-- License: This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/. (As attribution is included in this file, you needn't include additional attribution on your site.) -->
|
||||||
|
<!-- How to use: The original blog post about this SVG button and how I use SVG on the Ind.ie website is at https://ind.ie/blog/f-droid-button/ -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="button_1_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="1490 188 300 104" enable-background="new 1490 188 300 104" xml:space="preserve">
|
||||||
|
<g id="get_it_on_f-droid_2_">
|
||||||
|
<path id="button" d="M1780,292h-280c-5.5,0-10-4.5-10-10v-84c0-5.5,4.5-10,10-10h280c5.5,0,10,4.5,10,10v84
|
||||||
|
C1790,287.5,1785.5,292,1780,292z"/>
|
||||||
|
<g id="f-droid_2_">
|
||||||
|
<path fill="#FFFFFF" d="M1621.2,236.8v3.6v1.2v5h-0.3h-0.6h-2.2c-0.7,0-1-0.3-1.2-1c-0.1-0.4-0.2-1.6-0.3-3.4h-13.5v11.1h13.2v5.5
|
||||||
|
h-13.2v10.1c0.9,0.2,1.7,0.3,2.6,0.4c0.9,0.2,1.3,0.7,1.3,1.6v3h-3.9h-7.1h-3.9v-3c0-0.9,0.4-1.5,1.3-1.6c0.9-0.2,1.7-0.3,2.6-0.4
|
||||||
|
V242c-0.9-0.2-1.7-0.3-2.6-0.4c-0.9-0.2-1.3-0.7-1.3-1.6v-3h3.9h22.7h1.2L1621.2,236.8L1621.2,236.8z"/>
|
||||||
|
<path fill="#FFFFFF" d="M1637.2,256v5.4h-13.5V256H1637.2z"/>
|
||||||
|
<path fill="#FFFFFF" d="M1676.7,255.6c0,5-1.7,10-5.3,13.5c-3.7,3.6-8.8,5.3-13.9,5.3h-13.9h-3.9v-3c0-0.9,0.4-1.5,1.3-1.6
|
||||||
|
c0.9-0.2,1.7-0.3,2.6-0.4V242c-0.9-0.2-1.7-0.3-2.6-0.4c-0.9-0.2-1.3-0.7-1.3-1.6v-3h3.9h13.9c5.1,0,10.2,1.6,13.9,5.3
|
||||||
|
C1675.1,245.6,1676.7,250.7,1676.7,255.6C1676.7,258.4,1676.7,252.9,1676.7,255.6z M1669.6,255.6c0-3.4-0.8-6.9-3.1-9.5
|
||||||
|
s-5.6-3.7-8.9-3.7h-6.8v26.4h6.8c3.4,0,6.7-1.1,8.9-3.7C1668.8,262.5,1669.6,259,1669.6,255.6z"/>
|
||||||
|
<path fill="#FFFFFF" d="M1699.7,248.1l-0.9,4.8c-0.2,1.1-1.3,0.9-2.1,0.7c-1-0.3-2.2-0.3-3.1,0c-2.2,0.5-3.7,2.3-4.5,4.2v11.4
|
||||||
|
c2.3,0.4,2.3,0.4,2.6,0.4c0.9,0.2,1.3,0.7,1.3,1.6v3h-3.9l0,0h-6.5h-3.9v-3c0-0.9,0.4-1.5,1.3-1.6c0.4-0.1,0.4-0.1,2.6-0.4v-16.3
|
||||||
|
c-2.3-0.4-2.3-0.4-2.6-0.4c-0.9-0.2-1.3-0.7-1.3-1.6v-3h3.9l0,0h3.8c1.1,0,1.7,0.4,1.9,1.6l0.3,3.2c1.1-1.9,2.6-3.7,4.7-4.7
|
||||||
|
C1695.2,247,1697.8,246.9,1699.7,248.1z"/>
|
||||||
|
<path fill="#FFFFFF" d="M1719.4,248.3c5.7,2.3,8,8,7.8,13.7c-0.3,5.6-3.4,10.7-9.1,12.3c-5.4,1.5-11.9,0-15.1-4.9
|
||||||
|
c-3.1-4.6-3.2-11.6-0.3-16.4C1706,247.6,1713.6,246,1719.4,248.3C1721,248.9,1717.8,247.6,1719.4,248.3z M1718.9,267.6
|
||||||
|
c2-2.8,2-7.1,1.2-10.2c-0.3-1.5-1-2.9-2.2-3.9c-1.3-1-3-1.4-4.6-1.2c-3.5,0.2-5.3,2.7-5.8,5.9c-0.5,3.1-0.5,7.5,1.8,10
|
||||||
|
C1711.8,270.7,1716.8,270.5,1718.9,267.6C1720,266.2,1717.9,269.1,1718.9,267.6z"/>
|
||||||
|
<path fill="#FFFFFF" d="M1743.3,271.4v3h-3.9h-6.5h-4v-3c0-0.9,0.4-1.5,1.3-1.6c0.9-0.2,1.7-0.3,2.6-0.4v-16.4
|
||||||
|
c-0.9-0.2-1.7-0.3-2.6-0.4c-0.9-0.2-1.3-0.7-1.3-1.6v-3h3.9h6.5v21.5c0.9,0.2,1.7,0.3,2.6,0.4
|
||||||
|
C1742.9,269.9,1743.3,270.5,1743.3,271.4z M1740,241.6c-1,2-3.4,3-5.4,2.2c-2-0.9-3.1-3.3-2.2-5.3c0.9-2.1,3.3-3,5.4-2.2
|
||||||
|
C1739.9,237.1,1741,239.5,1740,241.6C1739.8,242,1740.3,241,1740,241.6z"/>
|
||||||
|
<path fill="#FFFFFF" d="M1773.4,274.3h-3.7h-3.9c-0.8,0-1.5-0.3-1.7-1.2l-0.5-2.4c-2.4,2.8-5.9,4.5-9.8,4.1
|
||||||
|
c-3.8-0.4-6.5-3.2-7.8-6.6c-2.4-6.6-1.3-16.3,5.6-19.8c3.8-1.9,8.5-1.5,11.6,1.5V241c-0.9-0.2-1.7-0.3-2.6-0.4
|
||||||
|
c-0.9-0.2-1.3-0.7-1.3-1.6v-3h3.9h6.5v33.5c0.8,0.2,1.6,0.3,2.4,0.4c0.9,0.2,1.3,0.7,1.3,1.6V274.3z M1763.3,254.6
|
||||||
|
c-1.7-2-4.2-2.9-6.7-2.3c-2.5,0.6-3.9,2.8-4.4,5.1c-0.5,2.3-0.5,5-0.1,7.4c0.4,2.3,1.7,4.4,4.1,4.9c2.9,0.5,5.4-1,7.2-3.1V254.6z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g id="get_it_on_2_">
|
||||||
|
<path fill="#FFFFFF" d="M1597.5,218.2v-2.9h7.6v6.9c-3.7,3.6-11.2,3.9-14.5-0.3c-3.3-4.2-2.4-11.5,2.6-14c2.2-1.1,5.1-1.1,7.3-0.4
|
||||||
|
s3.8,2.4,4.3,4.7l-3.5,0.7c-0.7-2.5-3.5-3.3-5.8-2.5c-2.2,0.7-3.1,2.8-3.2,4.9c-0.1,2.2,0.3,4.6,2.1,5.9c2.1,1.6,5.1,0.9,7-0.6
|
||||||
|
v-2.3H1597.5z"/>
|
||||||
|
<path fill="#FFFFFF" d="M1608.3,224.7v-17.3h13v2.9h-9.4v3.8h8.8v2.9h-8.8v4.8h9.8v2.9L1608.3,224.7L1608.3,224.7z"/>
|
||||||
|
<path fill="#FFFFFF" d="M1628.6,224.7v-14.4h-5.1v-2.9h13.9v2.9h-5.1v14.5L1628.6,224.7L1628.6,224.7z"/>
|
||||||
|
<path fill="#FFFFFF" d="M1646.3,224.7v-17.3h3.5v17.3H1646.3z"/>
|
||||||
|
<path fill="#FFFFFF" d="M1657.1,224.7v-14.4h-5.1v-2.9h13.9v2.9h-5.1v14.5L1657.1,224.7L1657.1,224.7z"/>
|
||||||
|
<path fill="#FFFFFF" d="M1674.1,216.1c0-1.7,0.3-3.3,0.8-4.4c0.8-1.7,2.2-3.2,3.9-4c2.8-1.1,6.5-1,9,0.9c2.5,1.8,3.4,4.9,3.3,7.8
|
||||||
|
c-0.1,2.9-1.1,5.8-3.8,7.5c-2.5,1.6-6.1,1.6-8.7,0.3C1675.4,222.7,1674.1,219.4,1674.1,216.1z M1677.8,216c0,2.3,0.7,4.8,3.1,5.6
|
||||||
|
c2.2,0.9,4.8,0,5.8-2c1-1.9,1-4.9,0.3-6.8c-0.9-2.1-3.1-3.1-5.3-2.7C1678.7,210.6,1677.8,213.4,1677.8,216z"/>
|
||||||
|
<path fill="#FFFFFF" d="M1693.9,224.7v-17.3h3.4l7.2,11.6v-11.6h3.3v17.3h-3.6l-7.1-11.4v11.4H1693.9z"/>
|
||||||
|
</g>
|
||||||
|
<g id="droid_2_">
|
||||||
|
<g>
|
||||||
|
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="413.9844" y1="440.5436" x2="413.9844" y2="452.6552" gradientTransform="matrix(3.7209 0 0 -3.7209 0 1914.4187)">
|
||||||
|
<stop offset="0" style="stop-color:#2B6099"/>
|
||||||
|
<stop offset="0.1299" style="stop-color:#2F69A1"/>
|
||||||
|
<stop offset="0.3451" style="stop-color:#3B83B6"/>
|
||||||
|
<stop offset="0.5" style="stop-color:#4699C8"/>
|
||||||
|
<stop offset="0.9944" style="stop-color:#479ECB"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path fill="url(#SVGID_1_)" d="M1570.1,275.2h-59.3c-2.9,0-5.2-2.3-5.2-5.2v-34.7c0-2.9,2.4-5.2,5.2-5.2h59.3
|
||||||
|
c2.9,0,5.2,2.3,5.2,5.2V270C1575.3,272.8,1572.9,275.2,1570.1,275.2z"/>
|
||||||
|
|
||||||
|
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="413.9844" y1="440.5436" x2="413.9844" y2="452.6552" gradientTransform="matrix(3.7209 0 0 -3.7209 0 1914.4187)">
|
||||||
|
<stop offset="0" style="stop-color:#2B6099"/>
|
||||||
|
<stop offset="0.5" style="stop-color:#58A4CD"/>
|
||||||
|
<stop offset="0.7865" style="stop-color:#7FB8D9"/>
|
||||||
|
<stop offset="1" style="stop-color:#9EC9E2"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path fill="url(#SVGID_2_)" d="M1570.1,231.9c1.9,0,3.5,1.6,3.5,3.5V270c0,1.9-1.6,3.5-3.5,3.5h-59.3c-1.9,0-3.5-1.6-3.5-3.5
|
||||||
|
v-34.7c0-1.9,1.6-3.5,3.5-3.5H1570.1 M1570.1,230.1h-59.3c-2.9,0-5.2,2.3-5.2,5.2V270c0,2.9,2.4,5.2,5.2,5.2h59.3
|
||||||
|
c2.9,0,5.2-2.3,5.2-5.2v-34.7C1575.3,232.5,1572.9,230.1,1570.1,230.1L1570.1,230.1z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path fill="#295384" d="M1540.4,236.6c8.9,0,16.1,7.2,16.1,16c0,8.8-7.2,16-16.1,16s-16.1-7.2-16.1-16S1531.5,236.6,1540.4,236.6
|
||||||
|
M1540.4,235.3c-9.6,0-17.4,7.8-17.4,17.3s7.8,17.3,17.4,17.3s17.4-7.8,17.4-17.3S1550.1,235.3,1540.4,235.3L1540.4,235.3z"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#295384" d="M1535.4,251.1c1.9-5.7,10.7-3.9,10.2,2.2c-0.5,5.6-8.5,6.2-10.2,1.2c0,0,0,0.1,0,0l0,0
|
||||||
|
c-2.4,0.2-4.7,0.4-7.1,0.6c1.7,10.1,15.3,13.1,21.6,5.3c6.5-8,0.3-20.2-10-19.8c-5.6,0.3-10.5,4.3-11.5,9.8"/>
|
||||||
|
<path fill="#7B952D" d="M1580.3,198.7l-2-1.6c-0.3-0.3-1-0.3-1.2,0.1l-6.8,7.9c-0.3,0.3-0.3,1,0.1,1.2l2,1.6
|
||||||
|
c0.3,0.3,1,0.3,1.2-0.1l6.8-7.9C1580.7,199.6,1580.6,199,1580.3,198.7z"/>
|
||||||
|
<path fill="#7B952D" d="M1500.6,198.1l2-1.6c0.3-0.3,1-0.3,1.2,0.1l6.8,7.9c0.3,0.3,0.3,1-0.1,1.2l-2,1.6c-0.3,0.3-1,0.3-1.2-0.1
|
||||||
|
l-6.8-7.9C1500.2,199,1500.3,198.5,1500.6,198.1z"/>
|
||||||
|
|
||||||
|
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="413.9844" y1="453.354" x2="413.9844" y2="459.4099" gradientTransform="matrix(3.7209 0 0 -3.7209 0 1914.4187)">
|
||||||
|
<stop offset="0" style="stop-color:#BCDB52"/>
|
||||||
|
<stop offset="0.3206" style="stop-color:#C5E358"/>
|
||||||
|
<stop offset="0.5" style="stop-color:#CDEA5C"/>
|
||||||
|
<stop offset="0.9944" style="stop-color:#DCF285"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path fill="url(#SVGID_3_)" stroke="#7B952D" stroke-miterlimit="10" d="M1572.7,227.5h-64.5c-1,0-1.7-0.8-1.7-1.7v-19.1
|
||||||
|
c0-1,0.8-1.7,1.7-1.7h64.5c1,0,1.7,0.8,1.7,1.7v19.1C1574.4,226.7,1573.6,227.5,1572.7,227.5z"/>
|
||||||
|
<g>
|
||||||
|
<ellipse fill="#FFFFFF" cx="1523" cy="215.4" rx="6.1" ry="6.1"/>
|
||||||
|
|
||||||
|
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="409.8439" y1="458.401" x2="408.7539" y2="454.8358" gradientTransform="matrix(3.7209 0 0 -3.7209 0 1914.4187)">
|
||||||
|
<stop offset="0" style="stop-color:#8BA53D"/>
|
||||||
|
<stop offset="8.484717e-02" style="stop-color:#94AE45"/>
|
||||||
|
<stop offset="0.2259" style="stop-color:#AEC65C"/>
|
||||||
|
<stop offset="0.4048" style="stop-color:#D7ED81"/>
|
||||||
|
<stop offset="0.4246" style="stop-color:#DCF285"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path fill="url(#SVGID_4_)" d="M1523,222.3c-3.8,0-7-3.1-7-6.9c0-3.8,3.1-6.9,7-6.9c3.8,0,7,3.1,7,6.9
|
||||||
|
C1529.9,219.2,1526.9,222.3,1523,222.3z M1523,210.2c-2.9,0-5.2,2.3-5.2,5.2s2.4,5.2,5.2,5.2s5.2-2.3,5.2-5.2
|
||||||
|
S1525.9,210.2,1523,210.2z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<ellipse fill="#FFFFFF" cx="1557.8" cy="215.4" rx="6.1" ry="6.1"/>
|
||||||
|
|
||||||
|
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="419.2189" y1="458.401" x2="418.129" y2="454.8359" gradientTransform="matrix(3.7209 0 0 -3.7209 0 1914.4187)">
|
||||||
|
<stop offset="0" style="stop-color:#8BA53D"/>
|
||||||
|
<stop offset="8.484717e-02" style="stop-color:#94AE45"/>
|
||||||
|
<stop offset="0.2259" style="stop-color:#AEC65C"/>
|
||||||
|
<stop offset="0.4048" style="stop-color:#D7ED81"/>
|
||||||
|
<stop offset="0.4246" style="stop-color:#DCF285"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path fill="url(#SVGID_5_)" d="M1557.8,222.3c-3.8,0-7-3.1-7-6.9c0-3.8,3.1-6.9,7-6.9c3.8,0,7,3.1,7,6.9
|
||||||
|
C1564.8,219.2,1561.8,222.3,1557.8,222.3z M1557.8,210.2c-2.9,0-5.2,2.3-5.2,5.2s2.4,5.2,5.2,5.2c2.9,0,5.2-2.3,5.2-5.2
|
||||||
|
S1560.8,210.2,1557.8,210.2z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 9.4 KiB |
@ -1,23 +1,36 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="w3.css">
|
<link rel="stylesheet" href="w3.css" />
|
||||||
<link rel="stylesheet" href="fontawesome.min.css">
|
<link rel="stylesheet" href="fontawesome.min.css" />
|
||||||
<link rel="stylesheet" href="regular.min.css">
|
<link rel="stylesheet" href="regular.min.css" />
|
||||||
<link rel="stylesheet" href="solid.min.css">
|
<link rel="stylesheet" href="solid.min.css" />
|
||||||
<link rel="stylesheet" href="brands.min.css">
|
<link rel="stylesheet" href="brands.min.css" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body,h1,h2,h3,h4,h5 {font-family: "Poppins", sans-serif}
|
body,
|
||||||
body {font-size: 16px;}
|
h1,
|
||||||
img {margin-bottom: -8px;}
|
h2,
|
||||||
.mySlides {display: none;}
|
h3,
|
||||||
|
h4,
|
||||||
|
h5 {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
margin-bottom: -8px;
|
||||||
|
}
|
||||||
|
.mySlides {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
</head>
|
</head>
|
||||||
<body class="w3-content w3-black" style="max-width:1500px;">
|
<body class="w3-content w3-black" style="max-width: 1500px">
|
||||||
<!-- The App Section -->
|
<!-- The App Section -->
|
||||||
<div class="w3-padding-64 w3-white">
|
<div class="w3-padding-64 w3-white">
|
||||||
<div class="w3-row-padding">
|
<div class="w3-row-padding">
|
||||||
@ -25,20 +38,96 @@
|
|||||||
<h1 class="w3-jumbo">
|
<h1 class="w3-jumbo">
|
||||||
<b>😎 Tilde Friends</b>
|
<b>😎 Tilde Friends</b>
|
||||||
</h1>
|
</h1>
|
||||||
<h1 class="w3-xxlarge w3-text-green"><b>Make apps and friends from the comfort of your web browser.</b></h1>
|
<h1 class="w3-xxlarge w3-text-green">
|
||||||
<p>Tilde Friends is a platform for building, running, and sharing web applications.</p>
|
<b>Make apps and friends from the comfort of your web browser.</b>
|
||||||
<p>Available for lots of devices:
|
</h1>
|
||||||
|
<p>
|
||||||
|
Tilde Friends is a platform for building, running, and sharing web
|
||||||
|
applications.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Available for lots of devices:
|
||||||
<i class="fa-brands fa-linux w3-xlarge"></i>
|
<i class="fa-brands fa-linux w3-xlarge"></i>
|
||||||
<i class="fa-brands fa-android w3-xlarge"></i>
|
<i class="fa-brands fa-android w3-xlarge"></i>
|
||||||
<i class="fa-brands fa-apple w3-xlarge"></i>
|
<i class="fa-brands fa-apple w3-xlarge"></i>
|
||||||
<i class="fa fa-mobile-screen w3-xlarge"></i>
|
<i class="fa fa-mobile-screen w3-xlarge"></i>
|
||||||
<i class="fa-brands fa-windows w3-xlarge"></i>
|
<i class="fa-brands fa-windows w3-xlarge"></i>
|
||||||
</p>
|
</p>
|
||||||
<a class="w3-button w3-black w3-padding-large" href="https://www.tildefriends.net/~cory/releases/"><i class="fa fa-download"></i> Download</a>
|
<a
|
||||||
<a class="w3-button w3-black w3-padding-large" href="https://www.tildefriends.net/~cory/apps/"><i class="fa fa-link"></i> Try It</a>
|
class="w3-button w3-black w3-padding-large"
|
||||||
|
href="https://dev.tildefriends.net/cory/tildefriends/releases"
|
||||||
|
><i class="fa fa-download"></i> Download</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="w3-button w3-black w3-padding-large"
|
||||||
|
href="https://www.tildefriends.net/~cory/apps/"
|
||||||
|
><i class="fa fa-link"></i> Try It</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="w3-button w3-black w3-padding-large"
|
||||||
|
href="https://dev.tildefriends.net/"
|
||||||
|
><i class="fa fa-mug-hot"></i> Code</a
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<a href="https://f-droid.org/en/packages/com.unprompted.tildefriends.fdroid/"><img src="f-droid.svg" style="height: 3em"></a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-col l4 m6">
|
<div class="w3-col l4 m6">
|
||||||
<img src="tildefriends.png" class="w3-image w3-right w3-hide-small">
|
<img src="tildefriends.png" class="w3-image w3-right w3-hide-small" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Getting Starting Section -->
|
||||||
|
<div class="w3-indigo w3-center">
|
||||||
|
<div class="w3-row-padding w3-padding-64">
|
||||||
|
<div class="w3-jumbo">
|
||||||
|
<i class="fa fa-rocket"></i> <b>Getting Started</b>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>First-time user checklist:</h2>
|
||||||
|
<ol type="1" style="text-align: left">
|
||||||
|
<li>
|
||||||
|
<a href="https://dev.tildefriends.net/cory/tildefriends/releases"
|
||||||
|
>Download</a
|
||||||
|
>
|
||||||
|
Tilde Friends and run your own instance, or use
|
||||||
|
<a href="https://www.tildefriends.net/"
|
||||||
|
>https://www.tildefriends.net/</a
|
||||||
|
>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Create an account to identify yourself with that instance by
|
||||||
|
username and password.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Create an SSB identity in the <b>ssb</b> app. This will generate a
|
||||||
|
keypair used to identify yourself to other users and sign your
|
||||||
|
messages so that they can be verified as from you.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Describe yourself in your profile in the <b>ssb</b> app. Give
|
||||||
|
yourself a name and an avatar if you like.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Connect to others. You will automatically discover peers on the
|
||||||
|
same instance and same network if there are any. Or use
|
||||||
|
<a href="https://github.com/staltz/ssb-room/blob/master/FAQ.md"
|
||||||
|
>rooms</a
|
||||||
|
>
|
||||||
|
and pubs to reach more distant users.
|
||||||
|
<a href="https://www.tildefriends.net/~cory/room/"
|
||||||
|
>tildefriends.net itself</a
|
||||||
|
>
|
||||||
|
operates as a room, so you can connect and see who else is online
|
||||||
|
and establish a connection.
|
||||||
|
</li>
|
||||||
|
<li>Follow people to grow your network.</li>
|
||||||
|
<li>
|
||||||
|
Use the <b>edit</b> link at the top of any page to start modifying
|
||||||
|
and making apps.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -47,19 +136,28 @@
|
|||||||
<div class="w3-light-grey">
|
<div class="w3-light-grey">
|
||||||
<div class="w3-row-padding w3-padding-64">
|
<div class="w3-row-padding w3-padding-64">
|
||||||
<div class="w3-col l4 m6 s4">
|
<div class="w3-col l4 m6 s4">
|
||||||
<a href="https://scuttlebutt.nz/"><img class="w3-image w3-round-large" src="ssb.png" alt="Secure Scuttlebutt"></a>
|
<a href="https://scuttlebutt.nz/"
|
||||||
|
><img
|
||||||
|
class="w3-image w3-round-large"
|
||||||
|
src="ssb.png"
|
||||||
|
alt="Secure Scuttlebutt"
|
||||||
|
/></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-col l8 m6" style="height: auto">
|
<div class="w3-col l8 m6" style="height: auto">
|
||||||
<h1 class="w3-jumbo"><b>Built for Sharing</b></h1>
|
<h1 class="w3-jumbo"><b>Built for Sharing</b></h1>
|
||||||
<p>
|
<p>
|
||||||
Tilde Friends participates in the <a href="https://scuttlebutt.nz/">Secure Scuttlebutt</a> distributed social network.
|
Tilde Friends participates in the
|
||||||
|
<a href="https://scuttlebutt.nz/">Secure Scuttlebutt</a> distributed
|
||||||
|
social network.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Share apps with friends. Discover new apps made by enemies. Post pictures of your coffee. Or just lurk.
|
Share apps with friends. Discover new apps made by enemies. Post
|
||||||
|
pictures of your coffee. Or just lurk.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The social network integration provides tools for connecting with other people world-wide
|
The social network integration provides tools for connecting with
|
||||||
while still allowing apps and everything to operate offline.
|
other people world-wide while still allowing apps and everything to
|
||||||
|
operate offline.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -70,14 +168,16 @@
|
|||||||
<div class="w3-row-padding">
|
<div class="w3-row-padding">
|
||||||
<div class="w3-col l8 m6">
|
<div class="w3-col l8 m6">
|
||||||
<h1 class="w3-jumbo"><b>Edit Anything</b></h1>
|
<h1 class="w3-jumbo"><b>Edit Anything</b></h1>
|
||||||
<i class="fa fa-pen-to-square w3-left w3-jumbo w3-text-gray" style="padding: 32px"></i>
|
<i
|
||||||
|
class="fa fa-pen-to-square w3-left w3-jumbo w3-text-gray"
|
||||||
|
style="padding: 32px"
|
||||||
|
></i>
|
||||||
<p>
|
<p>
|
||||||
See that <code><b>edit</b></code> link near the top left corner of this page? It's there for
|
See that <code><b>edit</b></code> link near the top left corner of
|
||||||
every Tilde Friends app, so you can modify and see your changes right away.
|
this page? It's there for every Tilde Friends app, so you can modify
|
||||||
</p>
|
and see your changes right away.
|
||||||
<p>
|
|
||||||
It's kind of like a wiki, but for code!
|
|
||||||
</p>
|
</p>
|
||||||
|
<p>It's kind of like a wiki, but for code!</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -86,16 +186,22 @@
|
|||||||
<div class="w3-padding-64 w3-grey">
|
<div class="w3-padding-64 w3-grey">
|
||||||
<div class="w3-row-padding">
|
<div class="w3-row-padding">
|
||||||
<div class="w3-col">
|
<div class="w3-col">
|
||||||
<h1 class="w3-jumbo" style="text-align: right"><b>Sandbox Security</b></h1>
|
<h1 class="w3-jumbo" style="text-align: right">
|
||||||
<i class="fa fa-road-barrier w3-right w3-jumbo w3-text-yellow" style="padding: 32px"></i>
|
<b>Sandbox Security</b>
|
||||||
|
</h1>
|
||||||
|
<i
|
||||||
|
class="fa fa-road-barrier w3-right w3-jumbo w3-text-yellow"
|
||||||
|
style="padding: 32px"
|
||||||
|
></i>
|
||||||
<p>
|
<p>
|
||||||
Tilde Friends tries to make sure apps can be trusted using similar techniques to how web
|
Tilde Friends tries to make sure apps can be trusted using similar
|
||||||
browsers and operating systems do it.
|
techniques to how web browsers and operating systems do it.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This is all a work in progress, and it varies by platform, so don't give it all your
|
This is all a work in progress, and it varies by platform, so don't
|
||||||
innermost secrets yet, but do kick its tires and
|
give it all your innermost secrets yet, but do kick its tires and
|
||||||
<a href="mailto:cory@tildefriends.net">share</a> any surprises you find.
|
<a href="mailto:cory@tildefriends.net">share</a> any surprises you
|
||||||
|
find.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -105,10 +211,16 @@
|
|||||||
<div class="w3-container w3-padding-64 w3-light-grey w3-center">
|
<div class="w3-container w3-padding-64 w3-light-grey w3-center">
|
||||||
<h1 class="w3-jumbo"><b>Trusted Technology</b></h1>
|
<h1 class="w3-jumbo"><b>Trusted Technology</b></h1>
|
||||||
<p>Tilde Friends is built using boring, trusted tech.</p>
|
<p>Tilde Friends is built using boring, trusted tech.</p>
|
||||||
<p>Though of course for building Tilde Friends apps, you are free to use whatever fits.</p>
|
<p>
|
||||||
|
Though of course for building Tilde Friends apps, you are free to use
|
||||||
|
whatever fits.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="w3-row" style="margin-top: 64px">
|
<div class="w3-row" style="margin-top: 64px">
|
||||||
<a href="https://en.wikipedia.org/wiki/C_(programming_language)" class="w3-col s3">
|
<a
|
||||||
|
href="https://en.wikipedia.org/wiki/C_(programming_language)"
|
||||||
|
class="w3-col s3"
|
||||||
|
>
|
||||||
<i class="fa fa-c w3-text-blue w3-jumbo"></i>
|
<i class="fa fa-c w3-text-blue w3-jumbo"></i>
|
||||||
<p>C</p>
|
<p>C</p>
|
||||||
</a>
|
</a>
|
||||||
@ -139,14 +251,17 @@
|
|||||||
<i class="fa fa-shield-halved w3-text-green w3-jumbo"></i>
|
<i class="fa fa-shield-halved w3-text-green w3-jumbo"></i>
|
||||||
<p>OpenSSL</p>
|
<p>OpenSSL</p>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/ianlancetaylor/libbacktrace" class="w3-col s3">
|
<a
|
||||||
|
href="https://github.com/ianlancetaylor/libbacktrace"
|
||||||
|
class="w3-col s3"
|
||||||
|
>
|
||||||
<i class="fa fa-burst w3-text-pink w3-jumbo"></i>
|
<i class="fa fa-burst w3-text-pink w3-jumbo"></i>
|
||||||
<p>libbacktrace</p>
|
<p>libbacktrace</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w3-row" style="margin-top: 64px">
|
<div class="w3-row" style="margin-top: 64px">
|
||||||
<a href="https://codemirror.net/5/" class="w3-col s3">
|
<a href="https://codemirror.net/docs/changelog/" class="w3-col s3">
|
||||||
<i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i>
|
<i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i>
|
||||||
<p>CodeMirror</p>
|
<p>CodeMirror</p>
|
||||||
</a>
|
</a>
|
||||||
@ -158,6 +273,13 @@
|
|||||||
<i class="fa fa-fire w3-text-cyan w3-jumbo"></i>
|
<i class="fa fa-fire w3-text-cyan w3-jumbo"></i>
|
||||||
<p>Lit</p>
|
<p>Lit</p>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/c-ares/c-ares" class="w3-col s3">
|
||||||
|
<i class="fa fa-book-atlas w3-text-purple w3-jumbo"></i>
|
||||||
|
<p>c-ares</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w3-row" style="margin-top: 64px">
|
||||||
<a href="https://www.gnu.org/software/make/" class="w3-col s3">
|
<a href="https://www.gnu.org/software/make/" class="w3-col s3">
|
||||||
<i class="fa fa-hammer w3-text-teal w3-jumbo"></i>
|
<i class="fa fa-hammer w3-text-teal w3-jumbo"></i>
|
||||||
<p>GNU Make</p>
|
<p>GNU Make</p>
|
||||||
@ -167,7 +289,10 @@
|
|||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="w3-container w3-padding-32 w3-blue-grey w3-center w3-xlarge">
|
<footer class="w3-container w3-padding-32 w3-blue-grey w3-center w3-xlarge">
|
||||||
<p class="w3-medium">This page and Tilde Friends itself was made by Cory mostly in coffee shops and a local pizza place.</p>
|
<p class="w3-medium">
|
||||||
|
This page and Tilde Friends itself was made by Cory mostly in coffee
|
||||||
|
shops and a local pizza place.
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "📝",
|
"emoji": "📝",
|
||||||
"previous": "&qrcrBeaWg89ikgql9hXdr68krkg+5NZkmwTbpodEW4U=.sha256"
|
"previous": "&DaYqKHRBKhjFGaOzbKZ1+/pLspJeEkDJYTF2B50tH6k=.sha256"
|
||||||
}
|
}
|
@ -4,6 +4,9 @@ import * as utils from './utils.js';
|
|||||||
let g_hash;
|
let g_hash;
|
||||||
let g_collection_notifies = {};
|
let g_collection_notifies = {};
|
||||||
|
|
||||||
|
tfrpc.register(async function getActiveIdentity() {
|
||||||
|
return ssb.getActiveIdentity();
|
||||||
|
});
|
||||||
tfrpc.register(async function getOwnerIdentities() {
|
tfrpc.register(async function getOwnerIdentities() {
|
||||||
return ssb.getOwnerIdentities();
|
return ssb.getOwnerIdentities();
|
||||||
});
|
});
|
||||||
@ -54,6 +57,9 @@ core.register('message', async function message_handler(message) {
|
|||||||
await tfrpc.rpc.hash_changed(message.hash);
|
await tfrpc.rpc.hash_changed(message.hash);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
core.register('setActiveIdentity', async function setActiveIdentityHandler(id) {
|
||||||
|
await tfrpc.rpc.setActiveIdentity(id);
|
||||||
|
});
|
||||||
|
|
||||||
tfrpc.register(function set_hash(hash) {
|
tfrpc.register(function set_hash(hash) {
|
||||||
if (g_hash != hash) {
|
if (g_hash != hash) {
|
||||||
|
@ -10,9 +10,14 @@ function markdown(md) {
|
|||||||
while ((event = walker.next())) {
|
while ((event = walker.next())) {
|
||||||
let node = event.node;
|
let node = event.node;
|
||||||
if (event.entering) {
|
if (event.entering) {
|
||||||
if (node.type === 'link') {
|
if (node.destination?.startsWith('&')) {
|
||||||
if (node.destination.indexOf(':') == -1 &&
|
node.destination =
|
||||||
node.destination.indexOf('/') == -1) {
|
'/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
||||||
|
} else if (node.type === 'link') {
|
||||||
|
if (
|
||||||
|
node.destination.indexOf(':') == -1 &&
|
||||||
|
node.destination.indexOf('/') == -1
|
||||||
|
) {
|
||||||
node.destination = `${node.destination}`;
|
node.destination = `${node.destination}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -27,7 +32,9 @@ async function main() {
|
|||||||
let wiki_name = request.path.substring(0, slash);
|
let wiki_name = request.path.substring(0, slash);
|
||||||
let wiki_doc_name = request.path.substring(slash + 1);
|
let wiki_doc_name = request.path.substring(slash + 1);
|
||||||
|
|
||||||
let ids = Object.keys(await ssb.following(await ssb.getOwnerIdentities(), 1));
|
let ids = Object.keys(
|
||||||
|
await ssb.following(await ssb.getOwnerIdentities(), 1)
|
||||||
|
);
|
||||||
let [max_row_id, wikis] = await utils.collection(ids, 'wiki', null, -1, {});
|
let [max_row_id, wikis] = await utils.collection(ids, 'wiki', null, -1, {});
|
||||||
let wiki;
|
let wiki;
|
||||||
for (let w of Object.values(wikis)) {
|
for (let w of Object.values(wikis)) {
|
||||||
@ -38,7 +45,13 @@ async function main() {
|
|||||||
}
|
}
|
||||||
let wiki_doc;
|
let wiki_doc;
|
||||||
if (wiki) {
|
if (wiki) {
|
||||||
let [max_row_id, wiki_docs] = await utils.collection(ids, 'wiki-doc', wiki.id, -1, {});
|
let [max_row_id, wiki_docs] = await utils.collection(
|
||||||
|
ids,
|
||||||
|
'wiki-doc',
|
||||||
|
wiki.id,
|
||||||
|
-1,
|
||||||
|
{}
|
||||||
|
);
|
||||||
for (let w of Object.values(wiki_docs)) {
|
for (let w of Object.values(wiki_docs)) {
|
||||||
if (w.name === wiki_doc_name && !w.tombstone) {
|
if (w.name === wiki_doc_name && !w.tombstone) {
|
||||||
wiki_doc = w;
|
wiki_doc = w;
|
||||||
@ -57,7 +70,7 @@ async function main() {
|
|||||||
<h1>${wiki_name}: ${wiki_doc_name}</h1>
|
<h1>${wiki_name}: ${wiki_doc_name}</h1>
|
||||||
<div>${markdown(md)}</div>
|
<div>${markdown(md)}</div>
|
||||||
`,
|
`,
|
||||||
content_type: 'text/html',
|
content_type: 'text/html; charset=utf-8',
|
||||||
status_code: 200,
|
status_code: 200,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
|
<link rel="stylesheet" href="tildefriends.css" />
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff">
|
<body>
|
||||||
<tf-collections-app></tf-collections-app>
|
<tf-collections-app></tf-collections-app>
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="tf-collection.js" type="module"></script>
|
<script src="tf-collection.js" type="module"></script>
|
||||||
<script src="tf-id-picker.js" type="module"></script>
|
|
||||||
<script src="tf-wiki-doc.js" type="module"></script>
|
<script src="tf-wiki-doc.js" type="module"></script>
|
||||||
<script src="tf-wiki-app.js" type="module"></script>
|
<script src="tf-wiki-app.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
44
apps/wiki/lit-all.min.js
vendored
44
apps/wiki/lit-all.min.js
vendored
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
Loading…
Reference in New Issue
Block a user