Compare commits

...

156 Commits

Author SHA1 Message Date
58dbf42a3a theme picker: preselect the saved theme 2024-03-21 22:29:39 +01:00
a1f221879b remove the identity app 2024-03-21 20:45:04 +01:00
2a928dcafc feat-user_settings): add ability to import and delete an identity, formatting 2024-03-21 20:10:35 +01:00
5474c5a101 Merge branch 'user_settings' of https://dev.tildefriends.net/tasiaiso/tildefriends into user_settings 2024-03-21 19:33:08 +01:00
4b7261fa20 Merge branch 'main' into user_settings 2024-03-21 18:28:52 +00:00
4992ff3a2d feat(user_settings): finish ui, add ability to export and create identities, add tildefriends.css to the /static/ folder 2024-03-21 19:27:29 +01:00
88ee0aa6f0 feat(user_settings): finish ui, add ability to export and create identities, add tildefriends.css to the /static/ folder 2024-03-21 19:25:07 +01:00
fdda628be8 Fix paths in the source tarball. 2024-03-20 20:43:41 -04:00
2b45d8aa05 Doh. Never mean to add that. 2024-03-20 20:37:52 -04:00
0e2fc65301 Document run -k flag. 2024-03-20 20:33:23 -04:00
392206c19e Merge branch 'main' into user_settings 2024-03-20 10:51:57 +00:00
e8ef7e74de Fixed a leak in JS blob store. 2024-03-18 12:46:12 -04:00
c32e1b9583 http request cleanup crash fix. 2024-03-18 16:34:07 +00:00
9d0f6ec155 Fix the sneaker app RE: JSONB. 2024-03-18 12:32:40 -04:00
855d603795 docs + prettier 2024-03-17 13:21:33 -04:00
af25782185 More http/request shutdown issues. 2024-03-17 12:38:37 -04:00
e5ba51b80a Chasing a leak that looks like an EBT clock. Deleted some unneeded code and adding a missing JS free. 2024-03-17 13:44:05 +00:00
5e240de677 Fix requesting blobs from blob_wants. ids were trucated. Yikes. 2024-03-17 09:16:06 -04:00
418cfac0e3 Add a stock app with local room connection info. #15 2024-03-14 00:43:11 +00:00
9d09607013 Update CodeMirror. 2024-03-13 20:23:48 -04:00
eddf25b622 Give libuv the same download treatment as sqlite. 2024-03-13 19:53:57 -04:00
537a8654fa Rename sequence_before_author => flags. #29 2024-03-13 19:40:09 -04:00
9de33d06d2 Specifying -fsanitize=... early seems good. 2024-03-13 18:26:24 -04:00
0e5f320664 sqlite 3.45.2. 2024-03-13 12:30:14 -04:00
88d8e60511 Some minor paranoia to appease valgrind. 2024-03-12 21:44:20 -04:00
439f07162e Disable Haiku automation tests until I find a way to automate a browser on Haiku. 2024-03-09 08:44:06 -05:00
efe2b6cbd9 A make target to run prettier. 2024-03-08 21:43:08 -05:00
0aa1ed9464 Fix a failure requesting more blobs. 2024-03-08 21:38:31 -05:00
cb94ed6a2a Some plumbing to expose the actual bound SHS port so that I can make a dynamic room app. 2024-03-07 21:03:14 -05:00
cf187ee46b Reorder things so that we only zipalign -z during a dist build. To slow for make all. 2024-03-07 20:42:08 -05:00
3e71fc20fd Prettier. 2024-03-06 21:14:09 -05:00
f3601321f7 That's all the doxygen warnings. #27 2024-03-06 21:13:16 -05:00
540059368c 11 make docs warnings left, but I'm out of time for tonight. 2024-03-06 20:57:38 -05:00
7ce89123f7 85 make docs warnings remain. 2024-03-06 12:46:27 -05:00
e3c7c86212 All but the two biggest .h files have docs. 2024-03-06 12:31:17 -05:00
794804e27f A few more .h file docs. 2024-03-05 21:17:20 -05:00
6d89c1da6e Format. 2024-03-05 20:49:30 -05:00
d059554464 Some workarounds for Haiku. uv_fs_scandir can't tell if a dirent is a file. setrlimit doesn't do anything productive for us. 2024-03-05 20:49:16 -05:00
3a392d4a9f More .h docs. 2024-03-05 12:47:58 -05:00
e3071b372a Poking at TCP binds from Haiku. 2024-03-04 21:51:27 -05:00
18bd279b0c Some progress on .h docs, and add a preliminary CONTRIBUTING.md. 2024-03-04 12:23:00 -05:00
5b93db7463 A buncha muncha cruncha .h docs. Also add vim temporary files to .gitignore. 2024-03-03 18:12:44 -05:00
5b7e5eb91b Give fts a better chance of working with jsonb messages.content. 2024-03-03 18:55:58 +00:00
78ca383e3c http.h docs. 2024-03-03 12:35:10 -05:00
c1eed9ada3 Fixed a leak in ssb.getServerIdentity(). 2024-03-03 12:20:03 -05:00
8d6feb5394 Set the root of private messages correct so that other clients show them. 2024-03-03 12:09:03 -05:00
42994f8977 Make the SSB network key configurable by command-line argument. 2024-03-02 15:01:09 -05:00
f0a871e1f8 More docs. 2024-03-01 21:18:12 -05:00
a710c30572 Fix apps for jsonb. 2024-02-29 19:26:56 -05:00
c991763b00 tests.h and tlscontext.js.h docs. 2024-02-28 21:18:59 -05:00
72dae14f87 Android NDK update. 2024-02-28 21:04:22 -05:00
5800340762 Fix up some more jsonb references. 2024-02-28 20:41:27 -05:00
c5f5adcac6 Missed some more jsonb messages.content use issues. 2024-02-28 20:31:25 -05:00
591642efb3 Convert messages.content to JSONB. This is a very disruptive change. 2024-02-28 20:01:52 -05:00
6182ffa1d4 Docs for tls.h and trace.h. 2024-02-28 19:12:41 -05:00
402a898d96 Let's start working on 0.0.17. 2024-02-28 18:47:21 -05:00
13d43d8319 Let's release 0.0.16. 2024-02-28 18:24:12 -05:00
7bcdbd3813 Revert "Update commonmark.js to 0.31.0."
This reverts commit 165f25db69.
2024-02-26 12:34:50 -05:00
60ada22674 Add a button to toggle visible whitespace for now. Not yet persisted. 2024-02-25 22:31:31 -05:00
637119d46d ideviceinstaller makes this unnecessary. 2024-02-25 21:41:32 -05:00
40f3da6a65 Fix a leak in returning HTTP responses. 2024-02-25 19:38:00 -05:00
f4697fe7f7 Update CodeMirror. 2024-02-25 18:54:35 -05:00
3bc18b9021 Docs for util.js.h. 2024-02-25 18:52:34 -05:00
c21581aefa Use zipalign w/zopfli for APKs to save a little on size. 2024-02-25 18:29:10 -05:00
165f25db69 Update commonmark.js to 0.31.0. 2024-02-25 16:25:23 -05:00
9aa0617aa1 Fix android argv. 2024-02-25 16:02:56 -05:00
ddce88dce6 Merge branch 'tasiaiso-wiki-improvements' 2024-02-25 15:37:56 -05:00
6aa2bce2be Merge branch 'wiki-improvements' of https://dev.tildefriends.net/tasiaiso/tildefriends into tasiaiso-wiki-improvements 2024-02-25 15:37:13 -05:00
a43c1d3d1e Format. 2024-02-25 15:03:43 -05:00
1ed0e817e8 BSD compile fix. 2024-02-25 14:57:14 -05:00
709ca55e65 Fix overbuild on macos. 2024-02-25 14:52:35 -05:00
8c13f5dbba xopt => getopt_long. I give up on xopt. It didn't help me as much as I had hoped, and I had problems building for mingw with only some versions of GCC. Not worth any further time. 2024-02-25 14:45:31 -05:00
4cb82d81b7 apps/gg doesn't belong here and isn't ready for prime time.. 2024-02-24 11:19:36 -05:00
0c42921387 Appease prettier in index.html. 2024-02-24 11:16:07 -05:00
70a3e7fc7d Make app export append a trailing newline to the app.json files so that we match prettier. 2024-02-24 11:12:35 -05:00
d5267be38c Run prettier. 2024-02-24 11:09:34 -05:00
8e7e0ed490 Merge branch 'tasiaiso-prettier' 2024-02-24 11:03:36 -05:00
8cf2837725 Export app json files indented with tabs. 2024-02-24 10:58:53 -05:00
63ae186c76 Export app json files indented with tabs. 2024-02-24 10:55:09 -05:00
dbf5c7b832 Merge branch 'main' into prettier 2024-02-23 09:50:49 +00:00
bfbfc01e99 keep the new config files 2024-02-23 10:42:26 +01:00
8fa9d0e843 Revert "build: Add prettier to the project"
This reverts commit 41024ddb79.
2024-02-23 10:35:39 +01:00
2d3e108fd9 Reapply "build: Add prettier to the project"
This reverts commit 7822b30dcb.
2024-02-23 10:29:46 +01:00
7822b30dcb Revert "build: Add prettier to the project"
This reverts commit 41024ddb79.
2024-02-23 10:25:51 +01:00
2701b7d04e Address some gcc-13 analyzer warnings. #33 2024-02-22 20:13:51 -05:00
f9e95e5733 test 2024-02-22 23:28:15 +01:00
1444c945de feat: Create the user settings app 2024-02-22 23:18:12 +01:00
e361c3f975 chore(wiki): the button class is now optional for input elements 2024-02-22 22:34:11 +01:00
260706c172 chore: copy .prettierrc.yaml over to client.js 2024-02-22 21:31:15 +01:00
390668ec34 Merge branch 'master' into prettier 2024-02-22 21:23:39 +01:00
1d5cdf9607 feat(wiki): improvements to the wiki's UI 2024-02-22 18:39:53 +01:00
a4bf3542e0 Merge branch 'main' into wiki-improvements 2024-02-22 15:14:49 +00:00
df82cfe66b chore rename core.css to tildefriends.css, remove license from tildefriends.css 2024-02-22 16:11:49 +01:00
f23414adaf prevent previous commits from appearing in git blame 2024-02-22 15:37:40 +01:00
41024ddb79 build: Add prettier to the project 2024-02-22 15:36:45 +01:00
53f9547cc5 style(wiki): use core.js 2024-02-22 13:03:21 +01:00
4bfd9de100 Give iOS the same openssl build treatment as android and mingw. #11 2024-02-21 20:23:35 -05:00
c01e00d77d Get geckodriver.log from 'tildefriends test -t auto' out of the root. 2024-02-21 20:06:25 -05:00
825191c08f Fix 'make dist'. 2024-02-21 19:59:26 -05:00
9dc6670795 Fetch and build OpenSSL as part of the code build. 2024-02-21 19:46:28 -05:00
1db8eee9f7 Merge pull request 'Documentation, build improvements' (#1) from tasiaiso/tildefriends:dev_tasia into main
Reviewed-on: #1
2024-02-21 23:51:01 +00:00
1bc50cb62c Remove prebuilt OpenSSL from source control. #11 2024-02-21 12:31:05 -05:00
450b07fd08 Add a Doxyfile and preliminary module-level docs. 2024-02-20 21:41:37 -05:00
12c7515ee8 chore: revert changes to codemirror's config 2024-02-20 18:45:46 +01:00
ed65da4340 doc: typos 2024-02-19 19:26:15 +01:00
d9d2917cf5 chore: add folders to .gitignore 2024-02-19 19:16:05 +01:00
ce5ca1875b doc: add JSDoc annotations in the core folder
start documenting a bit, mostly inconsequential changes
2024-02-19 19:12:42 +01:00
4f869252a2 codemirror: add build script, edit build settings
Disabled `highlightWhitespace()` and `highlightTrailingWhitespace()`, could be reverted if necessary
2024-02-19 15:15:41 +01:00
17b92126de Fill in some blanks in the readme. 2024-02-18 21:31:07 -05:00
6e88c44229 Fiddle with some deps as I'm building from a clean tree. 2024-02-18 21:22:42 -05:00
6c3d338c12 How long has this extra character been in the readme? 2024-02-18 20:48:10 -05:00
4ebd44cb4e Avoid leaking some console colors.f
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4859 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-18 15:39:32 +00:00
75cb9f7fd2 Make things workable with a reverse proxy in front.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4858 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-18 15:27:44 +00:00
eacca9d2ab Ignore some files in subversion.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4857 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-17 20:45:22 +00:00
d0e11bc68b Add missing .clang-format, and fix some spaces that slipped through.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4856 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-17 19:22:02 +00:00
1958623a7a Package prettier.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4855 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-17 18:59:36 +00:00
498d8b6520 Hook up prettier in the editor.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4854 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-17 18:53:21 +00:00
a12f2fec5a Grarrgh. Build.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4853 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-17 16:36:40 +00:00
22bf046643 Fix fix fix tests.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4852 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-17 15:33:08 +00:00
dca48fae36 Some test fixes, and introduce some pledge and unveil for OpenBSD.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4851 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-17 14:55:39 +00:00
9af4068bb6 This apparently works around errors if I remove a header.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4850 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-16 03:27:41 +00:00
2992d8ec12 Fix showing decrypted messages.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4849 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-16 01:09:08 +00:00
33dd2560e0 Format.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4848 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-16 00:48:49 +00:00
aeb5c6ee25 Run the selenium automation tests from C, so that they all run in once place, and because I get better errors for some reason. Fix more issues along the way.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4847 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-16 00:47:54 +00:00
08a2436b8f Updates to 'apps' from Tasia.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4846 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-16 00:13:39 +00:00
fbc3cfeda4 clang-format the source. Not exactly how I want it, but automated is better than perfect.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4845 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-15 23:35:01 +00:00
c8812b1add Make the tests not do the imports all the time.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4844 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-15 03:00:34 +00:00
8d82e80639 Nope. JS_EVAL_FLAG_STRIP loses line numbers and other debug information. Need those.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4843 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-14 17:45:28 +00:00
ed741d53d7 Enable top-level async and JS_EVAL_FLAG_STRIP.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4842 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-14 17:39:27 +00:00
685754895b Have we achieved clean shutdown?
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4841 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-13 23:07:36 +00:00
e7791d38ff Continuing the fight against http lifetime issues.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4840 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-10 22:09:52 +00:00
9f14653001 No, yikes, Cory.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4839 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-10 21:58:02 +00:00
6c5a7b0751 Add missing statics, and remove the 'tildefriends check' command.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4838 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-10 16:50:00 +00:00
51a327c52d .well-known => C.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4837 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-10 16:10:58 +00:00
5a978bb30d CodeMirror update.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4836 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-10 15:10:33 +00:00
6801758cb3 staticDirectoryHandler => C.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4835 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-10 03:14:02 +00:00
14de3dd9e5 Linkify better?
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4834 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-09 02:05:49 +00:00
ed2d57fb4b Serve core static files without leaving C.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4833 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-09 01:21:57 +00:00
e87acc6286 robots.txt => C
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4832 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-08 01:08:05 +00:00
0de932bc9e Update CodeMirror.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4831 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-08 00:54:31 +00:00
d021d9f757 Automate fetching sqlite.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4830 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-08 00:17:30 +00:00
eb5da26004 Now this OpenSSL use is perfect.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4829 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-07 23:45:04 +00:00
6765254f43 libuv 1.48.0.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4828 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-07 23:12:32 +00:00
e98802f5b2 Fix https somehow.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4827 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-07 22:59:51 +00:00
af54b6483e Some success exporting and importing apps on android.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4826 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-07 00:05:07 +00:00
96167c3167 Fix more memory leaks and ssb shutdown order issues.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4825 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-06 17:42:17 +00:00
eecfdf482f Lit 3.1.2.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4824 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-02-01 01:18:11 +00:00
7ceb865206 ios OpenSSL => 3.2.1.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4823 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-31 04:45:17 +00:00
b919670706 Updated CodeMirror.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4822 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-31 03:11:38 +00:00
72120b8842 OpenSSL 3.2.1, and some android SDK updates.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4821 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-31 03:06:47 +00:00
1e53c08d9d Android OpenSSL => 3.2.1.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4820 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-31 02:22:16 +00:00
2d1b6a09e9 sqlite-amalgamation-3450100.zip
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4819 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-31 02:15:24 +00:00
7f661d9af9 Appease the analyzer, but also how did this ever work?
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4818 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-31 02:04:10 +00:00
81c66bdddd Allow downloads and top navigation from sandboxed pages. Trying to make the wiki more sensible.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4817 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-29 00:00:20 +00:00
4bd46a1657 Working on 0.0.16 now.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4816 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-28 23:55:17 +00:00
244a752ae1 Some wiki link fixes. Not enough.
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4815 ed5197a5-7fde-0310-b194-c3ffbd925b24
2024-01-28 23:54:28 +00:00
2720 changed files with 18973 additions and 562286 deletions

20
.clang-format Normal file
View 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
View File

@ -0,0 +1,2 @@
# Add prettier to the project
41024ddb7961b04a5688bbc997cb74de6fab4763

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
db.*
deps/ios_toolchain/
deps/openssl/
dist/
.keys
**/node_modules
out
*.swo
*.swp
.zsign_cache/

14
.prettierignore Normal file
View File

@ -0,0 +1,14 @@
node_modules
src
deps
.clang-format
# Minified files
**/*.min.css
**/*.min.js
**/leaflet.*
**/commonmark*
**/w3.css
apps/ssb/tribute.esm.js
apps/api/app.js
**/emojis.json

5
.prettierrc.yaml Normal file
View File

@ -0,0 +1,5 @@
trailingComma: 'es5'
useTabs: true
semi: true
singleQuote: true
bracketSpacing: false

37
CONTRIBUTING.md Normal file
View 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`.

2614
Doxyfile Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,9 +3,12 @@
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 15
VERSION_NUMBER := 0.0.15
VERSION_NAME := Medium English breakfast tea.
VERSION_CODE := 17
VERSION_NUMBER := 0.0.17-wip
VERSION_NAME := Please enjoy responsibly.
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3450200.zip
LIBUV_URL := https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz
PROJECT = tildefriends
BUILD_DIR ?= out
@ -14,6 +17,18 @@ UNAME_M := $(shell uname -m)
ANDROID_SDK ?= ~/Android/Sdk
ifeq ($(UNAME_M),x86_64)
ifneq ($(UNAME_S),Haiku)
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
endif
endif
ifeq ($(UNAME_M),aarch64)
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
endif
ifeq ($(UNAME_S),Darwin)
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
else ifeq ($(UNAME_S),Linux)
@ -47,14 +62,15 @@ CFLAGS += \
-Wextra \
-Wno-unused-parameter \
-MMD \
-MP \
-ffunction-sections \
-fdata-sections \
-fno-exceptions \
-g
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-33
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.0.10792818
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-34
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.2.11394342
ANDROID_MIN_SDK_VERSION := 24
ANDROID_TARGET_SDK_VERSION := 34
@ -206,18 +222,6 @@ $(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
$(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/lib
ifeq ($(UNAME_M),x86_64)
ifneq ($(UNAME_S),Haiku)
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
endif
endif
ifeq ($(UNAME_M),aarch64)
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
endif
get_objs = \
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
@ -244,7 +248,6 @@ $(APP_OBJS): CFLAGS += \
-Ideps/quickjs \
-Ideps/sqlite \
-Ideps/valgrind \
-Ideps/xopt \
-Wdouble-promotion \
-Werror
ifeq ($(UNAME_M),x86_64)
@ -496,18 +499,6 @@ $(SQLITE_OBJS): CFLAGS += \
-Wno-unused-function \
-Wno-unused-variable
XOPT_SOURCES := deps/xopt/xopt.c
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
$(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
-DHAVE_SNPRINTF \
-DHAVE_VSNPRINTF \
-DHAVE_VASNPRINTF \
-DHAVE_VASPRINTF \
-Dvsnprintf=rpl_vsnprintf
$(XOPT_OBJS): CFLAGS += \
-Wno-implicit-const-int-float-conversion \
-Wno-pointer-to-int-cast
QUICKJS_SOURCES := \
deps/quickjs/cutils.c \
deps/quickjs/libbf.c \
@ -636,8 +627,7 @@ ALL_APP_OBJS := \
$(QUICKJS_OBJS) \
$(SODIUM_OBJS) \
$(SQLITE_OBJS) \
$(UV_OBJS) \
$(XOPT_OBJS)
$(UV_OBJS)
DEPS = $(ALL_APP_OBJS:.o=.d)
-include $(DEPS)
@ -647,34 +637,34 @@ $(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
.PHONY: $(1)
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
@echo [link] $$@
@echo "[link] $$@"
@$$(CC) -o $$@ $$^ $$(LDFLAGS)
$(BUILD_DIR)/$(1)/%.o: %.c
@mkdir -p $$(dir $$@)
@echo [c] $$@
@echo "[c] $$@"
@$$(CC) $$(CFLAGS) -c $$< -o $$@
$(BUILD_DIR)/$(1)/%.o: %.m
@mkdir -p $$(dir $$@)
@echo [m] $$@
@echo "[m] $$@"
@$$(CC) $$(CFLAGS) -c $$< -o $$@
$(BUILD_DIR)/$(1)/%.o: %.S
@mkdir -p $$(dir $$@)
@echo [as] $$@
@echo "[as] $$@"
@$$(AS) -c $$< -o $$@
endef
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
src/version.h : $(firstword $(MAKEFILE_LIST))
@echo [version] $@
@echo "[version] $@"
@echo "#define VERSION_NUMBER \"$(VERSION_NUMBER)\"" > $@
@echo "#define VERSION_NAME \"$(VERSION_NAME)\"" >> $@
src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
@echo [android_version] $@
@echo "[android_version] $@"
@sed -i \
-e 's/versionCode=".*"/versionCode="$(VERSION_CODE)"/' \
-e 's/versionName=".*"/versionName="$(VERSION_NUMBER)"/' \
@ -685,12 +675,12 @@ src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
# Android support.
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
@mkdir -p $(dir $@)
@echo [aapt2] $@
@echo "[aapt2] $@"
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
@mkdir -p $(dir $@)
@echo [aapt2] $@
@echo "[aapt2] $@"
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
@ -701,21 +691,22 @@ JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
$(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)
out/apk/classes.dex: $(CLASS_FILES)
@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
PACKAGE_DIRS := \
apps/ \
core/ \
deps/codemirror/ \
deps/prettier/ \
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 := $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f))
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
@ -729,33 +720,40 @@ out/apk/TildeFriends-x86-release.unsigned.apk: out/apk/classes.dex out/androidre
out/apk/TildeFriends-arm-%.unsigned.apk:
@mkdir -p $(dir $@) out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/
@echo [aapt] $@
@echo "[aapt] $@"
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
@cp out/apk/res.apk $@
@cp out/apk/res.apk $@.zip
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
@zip -u $@ -q -9 $(RAW_FILES)
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
@zip -u $@.zip -q $(RAW_FILES)
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
out/apk/TildeFriends-x86-%.unsigned.apk:
@mkdir -p $(dir $@) out/apk-x86-$(BUILD_TYPE)/lib/x86_64/ out/apk-x86-$(BUILD_TYPE)/lib/x86/
@echo [aapt] $@
@echo "[aapt] $@"
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/tildefriends.so
@cp out/apk/res.apk $@
@cp out/apk/res.apk $@.zip
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
@zip -u $@ -q -9 $(RAW_FILES)
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@.zip -q -9 -r . && cd ../../
@zip -u $@.zip -q $(RAW_FILES)
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
out/%.apk: out/apk/%.unsigned.apk
@echo [apksigner] $(notdir $@)
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
@echo "[apksigner] $(notdir $@)"
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $<
release-apk: out/TildeFriends-arm-release.apk out/TildeFriends-x86-release.apk
out/%.zopfli.apk: out/%.apk
@echo "[zopfli] $(notdir $@)"
$(ANDROID_BUILD_TOOLS)/zipalign -f -z 4 $< $@.zopfli
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $@.zopfli
release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk
.PHONY: release-apk
releaseapkgo: out/TildeFriends-arm-release.apk
@ -782,7 +780,7 @@ ifeq ($(HAVE_LINUX_IOS),1)
endif
.SECONDARY:
out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
@echo [ipa] $@
@echo "[ipa] $@"
@rm -rf $@.tmp $@
@mkdir -p $@.tmp/Payload/tildefriends.app/
@cp -R $(dir $<)/* $@.tmp/Payload/tildefriends.app/
@ -810,18 +808,51 @@ apklog:
@adb logcat *:S tildefriends
.PHONY: apklog
fetchdeps:
@echo "[fetch] libuv"
@test -f out/deps/libuv.tar.gz && test "$$(cat out/deps/libuv.txt 2>/dev/null)" = $(LIBUV_URL) || (mkdir -p out/deps/ && curl -q $(LIBUV_URL) -o out/deps/libuv.tar.gz)
@test -d deps/libuv/ && test "$$(cat out/deps/libuv.txt 2>/dev/null)" = $(LIBUV_URL) || (rm -rf deps/libuv/ && mkdir -p deps/libuv/ && tar -C deps/libuv/ -m --strip=1 -xf out/deps/libuv.tar.gz)
@echo -n $(LIBUV_URL) > out/deps/libuv.txt
@echo "[fetch] sqlite"
@test -f out/deps/sqlite.zip && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p out/deps/ && curl -q $(SQLITE_URL) -o out/deps/sqlite.zip)
@test -d deps/sqlite/ && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p deps/sqlite/ && unzip -qDjo -d deps/sqlite/ out/deps/sqlite.zip)
@echo -n $(SQLITE_URL) > out/deps/sqlite.txt
@echo "[fetch] prettier"
@test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs
@test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs
@test -f deps/prettier/babel.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/babel.mjs
@test -f deps/prettier/estree.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/estree.mjs
.PHONY: fetchdeps
ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a
$(ANDROID_DEPS):
+@tools/ssl-android
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
ifeq ($(HAVE_WIN),1)
WINDOWS_DEPS := deps/openssl/mingw64/usr/local/lib/libssl.a
$(WINDOWS_DEPS):
+@tools/ssl-mingw64
$(filter $(BUILD_DIR)/win%,$(APP_OBJS)): | $(WINDOWS_DEPS)
endif
ifeq ($(UNAME_S),Darwin)
IOS_DEPS := deps/openssl/ios/ios64-xcrun/usr/local/lib/libssl.a
$(IOS_DEPS):
+@tools/ssl-ios
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
endif
clean:
rm -rf $(BUILD_DIR)
.PHONY: clean
dist: release-apk iosrelease-ipa
@echo "[export] $$(svn info --show-item url)"
@rm -rf tildefriends-$(VERSION_NUMBER)
@svn export -q . tildefriends-$(VERSION_NUMBER)
@echo "tildefriends-$(VERSION_NUMBER): $(VERSION_NAME)" > tildefriends-$(VERSION_NUMBER)/VERSION
@echo "[tar] tildefriends-$(VERSION_NUMBER).tar.xz"
@echo [archive] dist/tildefriends-$(VERSION_NUMBER).tar.xz
@rm -rf out/tildefriends-$(VERSION_NUMBER)
@mkdir -p dist/ out/tildefriends-$(VERSION_NUMBER)
@git archive main | tar -x -C out/tildefriends-$(VERSION_NUMBER)
@tar \
--exclude=apps/gg* \
--exclude=apps/welcome* \
--exclude=deps/libbacktrace/Isaac.Newton-Opticks.txt \
--exclude=deps/libsodium/builds/msvc/vs* \
@ -836,14 +867,15 @@ dist: release-apk iosrelease-ipa
--exclude=deps/sqlite/shell.c \
--exclude=deps/zlib/contrib/vstudio \
--exclude=deps/zlib/doc \
-caf tildefriends-$(VERSION_NUMBER).tar.xz tildefriends-$(VERSION_NUMBER)
@rm -rf tildefriends-$(VERSION_NUMBER)
-caf dist/tildefriends-$(VERSION_NUMBER).tar.xz \
-C out/ \
tildefriends-$(VERSION_NUMBER)
@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"
@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"
@cp out/tildefriends-release.ipa TildeFriends-$(VERSION_NUMBER).ipa
@cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
.PHONY: dist
dist-test: dist
@ -852,3 +884,15 @@ dist-test: dist
@docker build tildefriends-$(VERSION_NUMBER)/
@rm -rf tildefriends-$(VERSION_NUMBER)
.PHONY: dist-test
format:
@clang-format -i $(wildcard src/*.c src/*.h src/*.m)
.PHONY: format
prettier:
@npm run prettier
.PHONY: prettier
docs:
@doxygen
.PHONY: docs

View File

@ -1,37 +1,49 @@
# Tilde Friends
Tilde Friends is a tool for making and sharing.
A public instance lives at https://www.tildefriends.net/.
It is both a peer-to-peer social network client, participating in Secure
Scuttlebutt, as well as a platform for writing and running web applications.
## Goals
1. Make it easy and fun to run all sorts of web applications.
2. Provide security that is easy to understand and protects your data.
3. Make creating and sharing web applications accessible to anyone with a
browser.
## Building
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
all of those host platforms plus mingw64, iOS, and android.
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
are kept up to date in the tree.
2. To build, run `make debug` or `make release`. An executable will be
2. To build, run `make debug` or `make release`. An executable will be
generated in a subdirectory of `out/`.
3. It's possible to build for Android, iOS, and Windows on Linux, if you have
the right dependencies in the right places. `make windebug winrelease
iosdebug-ipa iosrelease-ipa release-apk`.
the right dependencies in the right places. `make windebug winrelease
iosdebug-ipa iosrelease-ipa release-apk`.
4. To build in docker, `docker build .`.
5. `make format` will normalize formatting to the coding standard.
## Running
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
privileges. Further administration can be done at
<http://localhost:12345/~core/admin/`>.
privileges. Further administration can be done at
<http://localhost:12345/~core/admin/>.
## 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
All code unless otherwise noted in is provided under the
[MIT](https://opensource.org/licenses/MIT) license.

View File

@ -1,4 +1,4 @@
{
"type": "tildefriends-app",
"emoji": "🎛"
}
"type": "tildefriends-app",
"emoji": "🎛"
}

View File

@ -18,9 +18,13 @@ async function main() {
for (let user of await core.users()) {
data.users[user] = await core.permissionsForUser(user);
}
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
await app.setDocument(
utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data))
);
} catch {
await app.setDocument('<span style="color: #f00">Only an administrator can modify these settings.</span>');
await app.setDocument(
'<span style="color: #f00">Only an administrator can modify these settings.</span>'
);
}
}
main();
main();

View File

@ -1,10 +1,12 @@
<!DOCTYPE html>
<!doctype html>
<html style="width: 100%">
<head>
<script>const g_data = $data;</script>
<script>
const g_data = $data;
</script>
</head>
<body style="color: #fff; width: 100%">
<h1>Tilde Friends Administration</h1>
</body>
<script type="module" src="script.js"></script>
</html>
</html>

View File

@ -3,25 +3,32 @@ import * as tfrpc from '/static/tfrpc.js';
function delete_user(user) {
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
tfrpc.rpc.delete_user(user).then(function() {
alert(`User "${user}" deleted successfully.`);
}).catch(function(error) {
alert(`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`);
});
tfrpc.rpc
.delete_user(user)
.then(function () {
alert(`User "${user}" deleted successfully.`);
})
.catch(function (error) {
alert(
`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`
);
});
}
}
function global_settings_set(key, value) {
tfrpc.rpc.global_settings_set(key, value).then(function() {
alert(`Set "${key}" to "${value}".`);
}).catch(function(error) {
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
});
tfrpc.rpc
.global_settings_set(key, value)
.then(function () {
alert(`Set "${key}" to "${value}".`);
})
.catch(function (error) {
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
});
}
window.addEventListener('load', function() {
const permission_template = (permission) =>
html` <code>${permission}</code>`;
window.addEventListener('load', function () {
const permission_template = (permission) => html` <code>${permission}</code>`;
function input_template(key, description) {
if (description.type === 'boolean') {
return html`
@ -62,26 +69,24 @@ window.addEventListener('load', function() {
}
const user_template = (user, permissions) => html`
<li>
<button @click=${(e) => delete_user(user)}>
Delete
</button>
${user}:
${permissions.map(x => permission_template(x))}
<button @click=${(e) => delete_user(user)}>Delete</button>
${user}: ${permissions.map((x) => permission_template(x))}
</li>
`;
const users_template = (users) =>
html`<h2>Users</h2>
<ul>
${Object.entries(users).map(u => user_template(u[0], u[1]))}
</ul>`;
<ul>
${Object.entries(users).map((u) => user_template(u[0], u[1]))}
</ul>`;
const page_template = (data) =>
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
<h2>Global Settings</h2>
<div>
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
${Object.keys(data.settings)
.sort()
.map((x) => html`${input_template(x, data.settings[x])}`)}
</div>
${users_template(data.users)}
</div>
`;
</div> `;
render(page_template(g_data), document.body);
});
});

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "📜",
"previous": "&miGORZ8BwjHg2YO0t4bms6SI28XWPYqnqOZ8u9zsbZc=.sha256"
}
"type": "tildefriends-app",
"emoji": "📜",
"previous": "&miGORZ8BwjHg2YO0t4bms6SI28XWPYqnqOZ8u9zsbZc=.sha256"
}

View File

@ -219,7 +219,7 @@ Parses an HTTP response.
* *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse.
`;
docs['sha1Digest()'] =`
docs['sha1Digest()'] = `
Calculates a SHA1 digest.
Completes synchronously.
@ -353,4 +353,4 @@ Call a remote function.
* **...** Parameters to pass to the function.
### Returns
The return value of the called function.
`;
`;

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "💻",
"previous": "&33ngNe0YrH3JScss6krlCwddZcXl8C5szonp7DYy4qA=.sha256"
}
"type": "tildefriends-app",
"emoji": "💻",
"previous": "&icsPplXHgmpkbNWyo/WTvRdT/A8BXxW4lJixOtP4ueQ=.sha256"
}

View File

@ -1,23 +1,40 @@
/**
* Fetches information about the applications
* @param apps Record<appName, blobId>
* @returns an object including the apps' name, emoji, and blobs ids
*/
async function fetch_info(apps) {
let result = {};
// For each app
for (let [key, value] of Object.entries(apps)) {
// Get it's blob and parse it
let blob = await ssb.blobGet(value);
blob = blob ? utf8Decode(blob) : '{}';
// Add it to the result object
result[key] = JSON.parse(blob);
}
return result;
}
/**
*
*
*/
async function fetch_shared_apps() {
let messages = {};
await ssb.sqlAsync(`
SELECT messages.*
await ssb.sqlAsync(
`
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts('"application/tildefriends"')
JOIN messages ON messages.rowid = messages_fts.rowid
ORDER BY timestamp
ORDER BY messages.timestamp
`,
[],
function(row) {
function (row) {
let content = JSON.parse(row.content);
for (let mention of content.mentions) {
if (mention?.type === 'application/tildefriends') {
@ -28,9 +45,13 @@ async function fetch_shared_apps() {
};
}
}
});
}
);
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)));
if (app_object) {
app_object.blob_id = app.blob;
@ -41,18 +62,26 @@ async function fetch_shared_apps() {
}
async function main() {
var apps = await fetch_info(await core.apps());
var core_apps = await fetch_info(await core.apps('core'));
let shared_apps = await fetch_shared_apps();
var doc = `<!DOCTYPE html>
<html>
<head>
<style>
const apps = await fetch_info(await core.apps());
const core_apps = await fetch_info(await core.apps('core'));
const shared_apps = await fetch_shared_apps();
const stylesheet = `
body {
color: whitesmoke;
font-family: sans-serif;
margin: 16px;
}
.container {
display: grid;
grid-template-columns: repeat(auto-fill, 64px);
gap: 1em;
justify-content: space-around;
background-color: #ffffff10;
border: 2px solid #073642;
border-radius: 8px;
}
.app {
height: 96px;
width: 64px;
@ -67,48 +96,87 @@ async function main() {
max-width: 64px;
text-overflow: ellipsis ellipsis;
overflow: hidden;
color: whitesmoke;
}
</style>
</head>
<body style="background: #888">
<h1 id="apps_title">Apps</h1>
<div id="apps" class="container"></div>
<h1>Shared Apps</h1>
<div id="shared_apps" class="container"></div>
<h1>Core Apps</h1>
<div id="core_apps" class="container"></div>
</body>
<script>
function populate_apps(id, name, apps) {
var list = document.getElementById(id);
for (let app of Object.keys(apps).sort()) {
let div = list.appendChild(document.createElement('div'));
div.classList.add('app');
`;
let href = name ? '/~' + name + '/' + app + '/' : ('/' + apps[app].blob_id + '/');
let icon_a = document.createElement('a');
let icon = document.createElement('div');
icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
icon.style.fontSize = 'xxx-large';
icon_a.appendChild(icon);
icon_a.href = href;
icon_a.target = '_top';
div.appendChild(icon_a);
const body = `
<h1 style="text-shadow: #808080 0 0 10px;">Welcome to Tilde Friends.</h1>
let a = document.createElement('a');
a.appendChild(document.createTextNode(app));
a.href = href;
a.target = '_top';
div.appendChild(a);
<h2>your apps</h2>
<div id="apps" class="container"></div>
<h2>shared apps</h2>
<div id="shared_apps" class="container"></div>
<h2>core apps</h2>
<div id="core_apps" class="container"></div>
`;
const script = `
/*
* Creates a list of apps
* @param id the id of the element to populate
* @param name (a username, 'core' or undefined)
* @param apps Object, a list of apps
*/
function populate_apps(id, name, apps) {
// Our target
var list = document.getElementById(id);
// For each app in the provided list
for (let app of Object.keys(apps).sort()) {
// Create the item
let div = list.appendChild(document.createElement('div'));
div.classList.add('app');
// The app's icon
let href = name ? '/~' + name + '/' + app + '/' : ('/' + apps[app].blob_id + '/');
let icon_a = document.createElement('a');
let icon = document.createElement('div');
icon.appendChild(document.createTextNode(apps[app].emoji || '📦'));
icon.style.fontSize = 'xxx-large';
icon_a.appendChild(icon);
icon_a.href = href;
icon_a.target = '_top';
div.appendChild(icon_a);
// The app's name
let a = document.createElement('a');
a.appendChild(document.createTextNode(app));
a.href = href;
a.target = '_top';
div.appendChild(a);
}
}
}
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('core_apps', 'core', ${JSON.stringify(core_apps)});
populate_apps('shared_apps', undefined, ${JSON.stringify(shared_apps)});
</script>
</html>`;
app.setDocument(doc);
populate_apps('apps', '${core.user.credentials?.session?.name}', ${JSON.stringify(apps)});
populate_apps('core_apps', 'core', ${JSON.stringify(core_apps)});
populate_apps('shared_apps', undefined, ${JSON.stringify(shared_apps)});
`;
// Build the document
const document = `
<!DOCTYPE html>
<html>
<head>
<style>
${stylesheet}
</style>
</head>
<body>
${body}
</body>
<script>
${script}
</script>
</html>`;
// Send it to the browser
app.setDocument(document);
}
main();

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🪵",
"previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256"
}
"type": "tildefriends-app",
"emoji": "🪵",
"previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256"
}

View File

@ -5,4 +5,4 @@ async function main() {
await app.setDocument(blog.render_html(blogs));
}
main();
main();

View File

@ -1,11 +1,19 @@
import * as commonmark from './commonmark.min.js';
function escape(text) {
return (text ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
return (text ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
}
function escapeAttribute(text) {
return (text ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#39;');
return (text ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
export async function get_blog_message(id) {
@ -13,7 +21,7 @@ export async function get_blog_message(id) {
await ssb.sqlAsync(
'SELECT author, timestamp, content FROM messages WHERE id = ?',
[id],
function(row) {
function (row) {
let content = JSON.parse(row.content);
message = {
author: row.author,
@ -21,7 +29,8 @@ export async function get_blog_message(id) {
blog: content?.blog,
title: content?.title,
};
});
}
);
if (message) {
await ssb.sqlAsync(
`
@ -34,9 +43,10 @@ export async function get_blog_message(id) {
ORDER BY sequence DESC LIMIT 1
`,
[message.author],
function(row) {
function (row) {
message.name = row.name;
});
}
);
}
return message;
}
@ -51,8 +61,12 @@ export function markdown(md) {
node = event.node;
if (event.entering) {
if (node.destination?.startsWith('&')) {
node.destination = '/' + node.destination + '/view?filename=' + node.firstChild?.literal;
} else if (node.destination?.startsWith('@') || node.destination?.startsWith('%')) {
node.destination =
'/' + node.destination + '/view?filename=' + node.firstChild?.literal;
} else if (
node.destination?.startsWith('@') ||
node.destination?.startsWith('%')
) {
node.destination = '/~core/ssb/#' + escape(node.destination);
}
}
@ -107,7 +121,7 @@ export function render_html(blogs) {
<h1>🪵Tilde Friends Blog</h1>
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
</div>
${blogs.map(blog_post => render_blog_post(blog_post)).join('\n')}
${blogs.map((blog_post) => render_blog_post(blog_post)).join('\n')}
</body>
</html>`;
}
@ -135,14 +149,15 @@ export function render_atom(blogs) {
<link href="${core.url}"/>
<id>${core.url}</id>
<updated>${new Date().toString()}</updated>
${blogs.map(blog_post => render_blog_post_atom(blog_post)).join('\n')}
${blogs.map((blog_post) => render_blog_post_atom(blog_post)).join('\n')}
</feed>`;
}
export async function get_posts() {
let blogs = [];
let ids = await ssb.getIdentities();
await ssb.sqlAsync(`
await ssb.sqlAsync(
`
WITH
blogs AS (
SELECT
@ -182,8 +197,11 @@ export async function get_posts() {
JOIN public ON public.author = blogs.author
LEFT OUTER JOIN names ON names.author = blogs.author
ORDER BY blogs.timestamp DESC LIMIT 20
`, [JSON.stringify(ids)], function(row) {
blogs.push(row);
});
`,
[JSON.stringify(ids)],
function (row) {
blogs.push(row);
}
);
return blogs;
}
}

View File

@ -2,30 +2,50 @@ import * as blog from './blog.js';
async function main() {
if (request.path.startsWith('%') && request.path.endsWith('.sha256')) {
let id = request.path.startsWith('%25') ? '%' + request.path.substring(3) : request.path;
let id = request.path.startsWith('%25')
? '%' + request.path.substring(3)
: request.path;
let message = await blog.get_blog_message(id);
if (message) {
respond({data: await blog.render_blog_post_html(message), content_type: 'text/html; charset=utf-8'});
respond({
data: await blog.render_blog_post_html(message),
content_type: 'text/html; charset=utf-8',
});
} else {
respond({data: `Message ${id} not found.`, content_type: 'text/html; charset=utf-8'});
respond({
data: `Message ${id} not found.`,
content_type: 'text/html; charset=utf-8',
});
}
} else if (request.path == 'atom') {
let blogs = await blog.get_posts();
respond({data: blog.render_atom(blogs), content_type: 'application/atom+xml'});
respond({
data: blog.render_atom(blogs),
content_type: 'application/atom+xml',
});
} else {
let blogs = await blog.get_posts();
for (let blog_post of blogs) {
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
if (request.path === title) {
respond({data: await blog.render_blog_post_html(blog_post), content_type: 'text/html; charset=utf-8'});
respond({
data: await blog.render_blog_post_html(blog_post),
content_type: 'text/html; charset=utf-8',
});
return;
}
}
respond({data: blog.render_html(blogs), content_type: 'text/html; charset=utf-8'});
respond({
data: blog.render_html(blogs),
content_type: 'text/html; charset=utf-8',
});
}
}
main().catch(function(error) {
respond({data: `<!DOCTYPE html>
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`, content_type: 'text/html'});
});
main().catch(function (error) {
respond({
data: `<!DOCTYPE html>
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`,
content_type: 'text/html',
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
{
"type": "tildefriends-app",
"emoji": "💽"
}
"type": "tildefriends-app",
"emoji": "💽"
}

View File

@ -51,7 +51,7 @@ async function key_list(db) {
app.setDocument(doc);
}
core.register('message', async function(message) {
core.register('message', async function (message) {
if (message.event == 'hashChange') {
let hash = message.hash.substring(1);
if (hash.startsWith(':shared:')) {
@ -67,4 +67,4 @@ core.register('message', async function(message) {
}
});
database_list();
database_list();

View File

@ -1,4 +1,4 @@
{
"type": "tildefriends-app",
"emoji": "➡️"
}
"type": "tildefriends-app",
"emoji": "➡️"
}

View File

@ -2,7 +2,7 @@ let g_about_cache = {};
async function query(sql, args) {
let result = [];
await ssb.sqlAsync(sql, args, function(row) {
await ssb.sqlAsync(sql, args, function (row) {
result.push(row);
});
return result;
@ -21,7 +21,8 @@ async function contacts_internal(id, last_row_id, following, max_row_id) {
json_extract(content, '$.type') = 'contact'
ORDER BY sequence
`,
[id, last_row_id, max_row_id]);
[id, last_row_id, max_row_id]
);
for (let row of contacts) {
let contact = JSON.parse(row.content);
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);
}
async function following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) {
let contacts = await Promise.all([...new Set(ids)].map(x => contact(x, last_row_id, following, max_row_id)));
async function following_deep_internal(
ids,
depth,
blocking,
last_row_id,
following,
max_row_id
) {
let contacts = await Promise.all(
[...new Set(ids)].map((x) => contact(x, last_row_id, following, max_row_id))
);
let result = {};
for (let i = 0; i < ids.length; i++) {
let id = ids[i];
let contact = contacts[i];
let all_blocking = Object.assign({}, contact.blocking, blocking);
let found = Object.keys(contact.following).filter(y => !all_blocking[y]);
let deeper = depth > 1 ? await following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : [];
let found = Object.keys(contact.following).filter((y) => !all_blocking[y]);
let deeper =
depth > 1
? await following_deep_internal(
found,
depth - 1,
all_blocking,
last_row_id,
following,
max_row_id
)
: [];
result[id] = [id, ...found, ...deeper];
}
return [...new Set(Object.values(result).flat())];
@ -68,10 +88,22 @@ async function following_deep(ids, depth, blocking) {
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
`, []))[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;
let store = JSON.stringify(cache);
await db.set('following', store);
@ -90,13 +122,15 @@ async function fetch_about(db, ids, users) {
};
}
let max_row_id = 0;
await ssb.sqlAsync(`
await ssb.sqlAsync(
`
SELECT MAX(rowid) AS max_row_id FROM messages
`,
[],
function(row) {
function (row) {
max_row_id = row.max_row_id;
});
}
);
for (let id of Object.keys(cache.about)) {
if (ids.indexOf(id) == -1) {
delete cache.about[id];
@ -129,17 +163,21 @@ async function fetch_about(db, ids, users) {
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,
max_row_id,
]);
]
);
for (let about of abouts) {
let content = JSON.parse(about.content);
if (content.about === about.author) {
delete content.type;
delete content.about;
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
cache.about[about.author] = Object.assign(
cache.about[about.author] || {},
content
);
}
}
cache.last_row_id = max_row_id;
@ -155,41 +193,41 @@ async function getAbout(db, id) {
if (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;
let f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {about: {}, sequence: 0, version: k_version};
}
await ssb.sqlAsync(
"SELECT "+
" sequence, "+
" content "+
"FROM messages "+
"WHERE "+
" author = ?1 AND "+
" sequence > ?2 AND "+
" json_extract(content, '$.type') = 'about' AND "+
" json_extract(content, '$.about') = ?1 "+
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
"ORDER BY sequence",
'SELECT ' +
' sequence, ' +
' content ' +
'FROM messages ' +
'WHERE ' +
' author = ?1 AND ' +
' sequence > ?2 AND ' +
" json_extract(content, '$.type') = 'about' AND " +
" json_extract(content, '$.about') = ?1 " +
'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
'ORDER BY sequence',
[id, f.sequence],
function(row) {
function (row) {
f.sequence = row.sequence;
if (row.content) {
let about = {};
try {
about = JSON.parse(row.content);
} catch {
}
} catch {}
delete about.about;
delete about.type;
f.about = Object.assign(f.about, about);
}
});
}
);
let j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":about", j);
await db.set(id + ':about', j);
}
g_about_cache[id] = f.about;
return f.about;
@ -198,15 +236,15 @@ async function getAbout(db, id) {
async function getSize(db, id) {
let size = 0;
await ssb.sqlAsync(
"SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
'SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1',
[id],
function (row) {
size += row.size;
});
}
);
return size;
}
async function getSizes(ids) {
let sizes = {};
await ssb.sqlAsync(
@ -221,7 +259,8 @@ async function getSizes(ids) {
[JSON.stringify(ids)],
function (row) {
sizes[row.author] = row.size;
});
}
);
return sizes;
}
@ -241,7 +280,10 @@ function niceSize(bytes) {
}
function escape(value) {
return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
}
async function main() {
@ -249,19 +291,27 @@ async function main() {
let db = await database('ssb');
let whoami = await ssb.getIdentities();
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, {});
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([
fetch_about(db, following, {}),
getSizes(following),
]);
await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
following.sort((a, b) => ((sizes[b] ?? 0) - (sizes[a] ?? 0)));
following.sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0));
for (let id of following) {
tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
}
await app.setDocument('<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' + tree + '</ul>\n</body>\n</html>');
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();

View File

@ -1,5 +0,0 @@
{
"type": "tildefriends-app",
"emoji": "🗺",
"previous": "&0XSp+xdQwVtQ88bXzvWdH15Ex63hv5zUKTa4zx7HBGM=.sha256"
}

View File

@ -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

View File

@ -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;
}

View File

@ -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();

View File

@ -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>

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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 };

View File

@ -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);

View File

@ -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`,
});
}

View File

@ -1,5 +0,0 @@
{
"type": "tildefriends-app",
"emoji": "🪪",
"previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256"
}

View File

@ -1,87 +0,0 @@
import * as tfrpc from '/tfrpc.js';
tfrpc.register(async function get_private_key(id) {
return bip39Words(await ssb.getPrivateKey(id));
});
tfrpc.register(async function create_id(id) {
return await ssb.createIdentity();
});
tfrpc.register(async function add_id(id) {
return await ssb.addIdentity(bip39Bytes(id));
});
tfrpc.register(async function delete_id(id) {
return await ssb.deleteIdentity(id);
});
tfrpc.register(async function reload() {
await main();
});
async function main() {
let ids = await ssb.getIdentities();
await app.setDocument(`<body style="color: #fff">
<script>const handler = {};</script>
<script type="module">
import * as tfrpc from '/static/tfrpc.js';
handler.export_id = async function export_id(event) {
let id = event.srcElement.dataset.id;
let element = document.createElement('textarea');
element.value = await tfrpc.rpc.get_private_key(id);
element.style = 'width: 100%; read-only: true';
element.readOnly = true;
event.srcElement.parentElement.appendChild(element);
event.srcElement.onclick = event => handler.hide_id(event, element);
}
handler.add_id = async function add_id(event) {
let id = document.getElementById('add_id').value;
try {
let new_id = await tfrpc.rpc.add_id(id);
alert('Successfully imported: ' + new_id);
await tfrpc.rpc.reload();
} catch (e) {
alert('Error importing identity: ' + e);
}
}
handler.create_id = async function create_id(event) {
try {
let id = await tfrpc.rpc.create_id();
alert('Successfully created: ' + id);
await tfrpc.rpc.reload();
} catch (e) {
alert('Error creating identity: ' + e);
}
}
handler.hide_id = function hide_id(event, element) {
element.parentNode.removeChild(element);
event.srcElement.onclick = handler.export_id;
}
handler.delete_id = async function delete_id(event) {
let id = event.srcElement.dataset.id;
try {
if (prompt('Are you sure you want to delete "' + id + '"? It cannot be recovered without the exported phrase.\\n\\nEnter the word "DELETE" to confirm you wish to delete it.') === 'DELETE') {
if (await tfrpc.rpc.delete_id(id)) {
alert('Successfully deleted ID: ' + id);
}
await tfrpc.rpc.reload();
}
} catch (e) {
alert('Error deleting ID: ' + e);
}
}
</script>
<h1>SSB Identity Management</h1>
<h2>Create a new identity</h2>
<button id="create_id" onclick="handler.create_id()">Create Identity</button>
<h2>Import an SSB Identity from 12 BIP39 English Words</h2>
<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button>
<h2>Identities</h2>
<ul>`+
ids.map(id => `<li>
<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button>
<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button>
${id}
</li>`).join('\n')+
` </ul>
</body>`);
}
main();

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🦟",
"previous": "&TegdzvFE+im94shygaHkgDYSaSrwY2h0OKUXSRPBQDM=.sha256"
}
"type": "tildefriends-app",
"emoji": "🦟",
"previous": "&TegdzvFE+im94shygaHkgDYSaSrwY2h0OKUXSRPBQDM=.sha256"
}

View File

@ -67,7 +67,7 @@ tfrpc.register(function getHash(id, message) {
tfrpc.register(function setHash(hash) {
return app.setHash(hash);
});
ssb.addEventListener('message', async function(id) {
ssb.addEventListener('message', async function (id) {
await tfrpc.rpc.notifyNewMessage(id);
});
tfrpc.register(async function store_blob(blob) {
@ -88,18 +88,18 @@ tfrpc.register(function apps() {
tfrpc.register(async function try_decrypt(id, content) {
return await ssb.privateMessageDecrypt(id, content);
});
ssb.addEventListener('broadcasts', async function() {
ssb.addEventListener('broadcasts', async function () {
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());
});
async function main() {
if (typeof(database) !== 'undefined') {
if (typeof database !== 'undefined') {
g_database = await database('ssb');
}
await app.setDocument(utf8Decode(await getFile('index.html')));
}
main();
main();

View File

@ -1,14 +1,16 @@
<!DOCTYPE html>
<!doctype html>
<html style="color: #fff">
<head>
<title>Tilde Friends</title>
<base target="_top">
<base target="_top" />
</head>
<body>
<tf-issues-app/>
<script>window.litDisableBundleWarning = true;</script>
<tf-issues-app />
<script>
window.litDisableBundleWarning = true;
</script>
<script src="commonmark.min.js"></script>
<script src="commonmark-linkify.js" type="module"></script>
<script src="script.js" type="module"></script>
</body>
</html>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -31,7 +31,12 @@ class TfIdPickerElement extends LitElement {
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>`)}
${this.ids.map(
(id) =>
html`<option ?selected=${id == this.selected} value=${id}>
${id}
</option>`
)}
</select>
`;
} else {
@ -57,13 +62,15 @@ class TfComposeElement extends LitElement {
}
submit() {
this.dispatchEvent(new CustomEvent('tf-submit', {
bubbles: true,
composed: true,
detail: {
value: this.renderRoot.getElementById('input').value,
},
}));
this.dispatchEvent(
new CustomEvent('tf-submit', {
bubbles: true,
composed: true,
detail: {
value: this.renderRoot.getElementById('input').value,
},
})
);
this.renderRoot.getElementById('input').value = '';
this.input();
}
@ -96,7 +103,8 @@ class TfIssuesAppElement extends LitElement {
async load() {
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
messages.id = messages_refs.message
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
@ -107,7 +115,9 @@ class TfIssuesAppElement extends LitElement {
SELECT * FROM issues
UNION
SELECT * FROM edits ORDER BY timestamp
`, [k_project]);
`,
[k_project]
);
for (let message of messages) {
let content = JSON.parse(message.content);
switch (content.type) {
@ -123,7 +133,7 @@ class TfIssuesAppElement extends LitElement {
break;
case 'issue-edit':
case 'post':
for (let issue of (content.issues || [])) {
for (let issue of content.issues || []) {
if (issues[issue.link]) {
if (issue.open !== undefined) {
issues[issue.link].open = issue.open;
@ -136,7 +146,9 @@ class TfIssuesAppElement extends LitElement {
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) {
for (let issue of this.issues) {
if (issue.id == this.selected.id) {
@ -150,11 +162,20 @@ class TfIssuesAppElement extends LitElement {
return html`
<tr>
<td>${issue.open ? '☐ open' : '☑ closed'}</td>
<td style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis">${issue.author}</td>
<td style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer" @click=${() => this.selected = issue}>
<td
style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
>
${issue.author}
</td>
<td
style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer"
@click=${() => (this.selected = issue)}
>
${issue.text.split('\n')?.[0]}
</td>
<td>${new Date(issue.updated ?? issue.created).toLocaleDateString()}</td>
<td>
${new Date(issue.updated ?? issue.created).toLocaleDateString()}
</td>
</tr>
`;
}
@ -170,13 +191,21 @@ class TfIssuesAppElement extends LitElement {
<div>${new Date(update.timestamp).toLocaleString()}</div>
<div>${update.author}</div>
<div>${message}</div>
<div>${update.open !== undefined ? (update.open ? 'issue opened' : 'issue closed') : undefined}</div>
<div>
${update.open !== undefined
? update.open
? 'issue opened'
: 'issue closed'
: undefined}
</div>
</div>
`;
}
async set_open(id, open) {
if (confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)) {
if (
confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)
) {
let whoami = this.shadowRoot.getElementById('picker').selected;
await tfrpc.rpc.appendMessage(whoami, {
type: 'issue-edit',
@ -207,7 +236,9 @@ class TfIssuesAppElement extends LitElement {
type: 'post',
text: event.detail.value,
root: this.selected.id,
branch: this.selected.updates.length ? this.selected.updates[this.selected.updates.length - 1].id : this.selected.id,
branch: this.selected.updates.length
? this.selected.updates[this.selected.updates.length - 1].id
: this.selected.id,
issues: [
{
link: this.selected.id,
@ -226,16 +257,18 @@ class TfIssuesAppElement extends LitElement {
return html`
${header}
<div>
<input type="button" value="Back" @click=${() => this.selected = undefined}></input>
${this.selected.open ?
html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>` :
html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`}
<input type="button" value="Back" @click=${() => (this.selected = undefined)}></input>
${
this.selected.open
? html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>`
: html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`
}
</div>
<div>${new Date(this.selected.created).toLocaleString()}</div>
<div>${this.selected.author}</div>
<div>${this.selected.id}</div>
<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
${this.selected.updates.map(x => this.render_update(x))}
${this.selected.updates.map((x) => this.render_update(x))}
<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
`;
} else {
@ -250,11 +283,11 @@ class TfIssuesAppElement extends LitElement {
<th>Title</th>
<th>Date</th>
</tr>
${this.issues.map(x => this.render_issue_table_row(x))}
${this.issues.map((x) => this.render_issue_table_row(x))}
</table>
`;
}
}
}
customElements.define('tf-issues-app', TfIssuesAppElement);
customElements.define('tf-issues-app', TfIssuesAppElement);

View File

@ -1,20 +1,32 @@
import * as linkify from './commonmark-linkify.js';
function image(node, entering) {
if (node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')) {
if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')
) {
if (entering) {
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
this.lit(
'<video style="max-width: 100%; max-height: 480px" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</video>');
}
} else if (node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')) {
} else if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')
) {
if (entering) {
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
this.lit(
'<audio style="height: 32px; max-width: 100%" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
@ -24,7 +36,11 @@ function image(node, entering) {
} else {
if (entering) {
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)) {
this.lit('<img src="" alt="');
} else {
@ -56,14 +72,20 @@ export function markdown(md) {
node = event.node;
if (event.entering) {
if (node.type == 'link') {
if (node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')) {
if (
node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')
) {
node.destination = '#' + node.destination;
} else if (node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')) {
} else if (
node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')
) {
node.destination = '#' + node.destination;
} else if (node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')) {
} else if (
node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')
) {
node.destination = '/' + node.destination + '/view';
}
} else if (node.type == 'image') {
@ -88,4 +110,4 @@ export function human_readable_size(bytes) {
}
}
return `${Math.round(v * 10) / 10} ${u}`;
}
}

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "📝",
"previous": "&2hdIDbBrAg63T2X1MzdGSF7yiqHvlnfF0PnInQLp0DA=.sha256"
}
"type": "tildefriends-app",
"emoji": "📝",
"previous": "&b//KqE4Vx6kOSBRODK1p/8wjOLKZJ+CBB5IkaBt5YsM=.sha256"
}

View File

@ -47,7 +47,7 @@ tfrpc.register(async function get_blob(id) {
});
let g_new_message_resolve;
let g_new_message_promise = new Promise(function(resolve, reject) {
let g_new_message_promise = new Promise(function (resolve, reject) {
g_new_message_resolve = resolve;
});
@ -55,9 +55,9 @@ function new_message() {
return g_new_message_promise;
}
ssb.addEventListener('message', function(id) {
ssb.addEventListener('message', function (id) {
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;
});
if (resolve) {
@ -104,8 +104,7 @@ async function process_message(whoami, collection, message, kind, parent) {
if (!x) {
return;
}
if (content.type !== kind ||
(parent && content.parent !== parent)) {
if (content.type !== kind || (parent && content.parent !== parent)) {
return;
}
}
@ -113,7 +112,10 @@ async function process_message(whoami, collection, message, kind, parent) {
if (content?.tombstone) {
delete collection[content.key];
} else {
collection[content.key] = Object.assign(collection[content.key] || {}, content);
collection[content.key] = Object.assign(
collection[content.key] || {},
content
);
}
} else {
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();
data = data ?? {};
let rowid = 0;
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
rowid = row.rowid;
});
await ssb.sqlAsync(
'SELECT MAX(rowid) AS rowid FROM messages',
[],
function (row) {
rowid = row.rowid;
}
);
while (true) {
if (rowid == max_rowid) {
await new_message();
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
rowid = row.rowid;
});
await ssb.sqlAsync(
'SELECT MAX(rowid) AS rowid FROM messages',
[],
function (row) {
rowid = row.rowid;
}
);
}
let modified = false;
let rows = [];
await ssb.sqlAsync(`
await ssb.sqlAsync(
`
SELECT messages.id, author, content, timestamp
FROM messages
JOIN json_each(?1) AS id ON messages.author = id.value
@ -150,9 +161,10 @@ tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
content LIKE '"%')
`,
[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent],
function(row) {
function (row) {
rows.push(row);
});
}
);
max_rowid = rowid;
for (let row of rows) {
if (await process_message(whoami, data, row, kind, parent)) {
@ -170,4 +182,4 @@ async function main() {
await app.setDocument(utf8Decode(await getFile('index.html')));
}
main();
main();

View File

@ -1,14 +1,16 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<base target="_top">
<base target="_top" />
</head>
<body style="color: #fff">
<tf-journal-app></tf-journal-app>
<script src="commonmark.min.js"></script>
<script>window.litDisableBundleWarning = true;</script>
<script>
window.litDisableBundleWarning = true;
</script>
<script src="tf-journal-app.js" type="module"></script>
<script src="tf-journal-entry.js" type="module"></script>
<script src="tf-id-picker.js" type="module"></script>
</body>
</html>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,8 +2,8 @@ import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
/*
** Provide a list of IDs, and this lets the user pick one.
*/
** Provide a list of IDs, and this lets the user pick one.
*/
class TfIdentityPickerElement extends LitElement {
static get properties() {
return {
@ -19,18 +19,25 @@ class TfIdentityPickerElement extends LitElement {
changed(event) {
this.selected = event.srcElement.value;
this.dispatchEvent(new Event('change', {
srcElement: this,
}));
this.dispatchEvent(
new Event('change', {
srcElement: this,
})
);
}
render() {
return html`
<select @change=${this.changed} style="max-width: 100%">
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
${(this.ids ?? []).map(
(id) =>
html`<option ?selected=${id == this.selected} value=${id}>
${id}
</option>`
)}
</select>
`;
}
}
customElements.define('tf-id-picker', TfIdentityPickerElement);
customElements.define('tf-id-picker', TfIdentityPickerElement);

View File

@ -28,9 +28,14 @@ class TfJournalAppElement extends LitElement {
async read_journals() {
let max_rowid;
let journals;
while (true)
{
[max_rowid, journals] = await tfrpc.rpc.collection([this.whoami], 'journal-entry', undefined, max_rowid, journals);
while (true) {
[max_rowid, journals] = await tfrpc.rpc.collection(
[this.whoami],
'journal-entry',
undefined,
max_rowid,
journals
);
this.journals = Object.assign({}, journals);
console.log('JOURNALS', this.journals);
}
@ -52,7 +57,11 @@ class TfJournalAppElement extends LitElement {
};
message.recps = [this.whoami];
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);
await tfrpc.rpc.appendMessage(this.whoami, message);
}
@ -62,14 +71,19 @@ class TfJournalAppElement extends LitElement {
let self = this;
return html`
<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>
<tf-journal-entry
whoami=${this.whoami}
.journals=${this.journals}
@publish=${this.on_journal_publish}></tf-journal-entry>
@publish=${this.on_journal_publish}
></tf-journal-entry>
`;
}
}
customElements.define('tf-journal-app', TfJournalAppElement);
customElements.define('tf-journal-app', TfJournalAppElement);

View File

@ -30,13 +30,15 @@ class TfJournalEntryElement extends LitElement {
async on_publish() {
console.log('publish', this.text);
this.dispatchEvent(new CustomEvent('publish', {
bubbles: true,
detail: {
key: this.shadowRoot.getElementById('date_picker').value,
text: this.text,
},
}));
this.dispatchEvent(
new CustomEvent('publish', {
bubbles: true,
detail: {
key: this.shadowRoot.getElementById('date_picker').value,
text: this.text,
},
})
);
}
back_dates(count) {
@ -63,22 +65,33 @@ class TfJournalEntryElement extends LitElement {
console.log('RENDER ENTRY', this.key, this.journals?.[this.key]);
return html`
<select id="date_picker" @change=${this.on_date_change}>
${this.back_dates(10).map(x => html`
<option value=${x}>${x}</option>
`)}
${this.back_dates(10).map(
(x) => html` <option value=${x}>${x}</option> `
)}
</select>
<div style="display: inline-flex; flex-direction: row">
<button ?disabled=${this.text == this.journals?.[this.key]?.text} @click=${this.on_publish}>Publish</button>
<button
?disabled=${this.text == this.journals?.[this.key]?.text}
@click=${this.on_publish}
>
Publish
</button>
<button @click=${this.on_discard}>Discard</button>
</div>
<div style="display: flex; flex-direction: row">
<textarea
style="flex: 1 1; min-height: 10em"
@input=${this.on_edit} .value=${this.text ?? this.journals?.[this.key]?.text ?? ''}></textarea>
<div style="flex: 1 1">${unsafeHTML(this.markdown(this.text ?? this.journals?.[this.key]?.text))}</div>
@input=${this.on_edit}
.value=${this.text ?? this.journals?.[this.key]?.text ?? ''}
></textarea>
<div style="flex: 1 1">
${unsafeHTML(
this.markdown(this.text ?? this.journals?.[this.key]?.text)
)}
</div>
</div>
`;
}
}
customElements.define('tf-journal-entry', TfJournalEntryElement);
customElements.define('tf-journal-entry', TfJournalEntryElement);

5
apps/room.json Normal file
View File

@ -0,0 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "📦",
"previous": "&IU+TwyM7TznD8NBfnw7tgW2zxVlMqTVxSqWFjuosLwo=.sha256"
}

13
apps/room/app.js Normal file
View 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();

View File

@ -1,4 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "👟"
}
"type": "tildefriends-app",
"emoji": "👟",
"previous": "&lYZRnT2UGQxXxYISbuaZewik9AuxBpcJumakwrePw5c=.sha256"
}

View File

@ -27,4 +27,4 @@ tfrpc.register(async function store_message(message) {
async function main() {
await app.setDocument(utf8Decode(await getFile('index.html')));
}
main();
main();

View File

@ -1,14 +1,16 @@
<!DOCTYPE html>
<!doctype html>
<html style="color: #fff">
<head>
<title>Tilde Friends</title>
<base target="_top">
<base target="_top" />
</head>
<body>
<tf-sneaker-app/>
<script>window.litDisableBundleWarning = true;</script>
<tf-sneaker-app />
<script>
window.litDisableBundleWarning = true;
</script>
<script src="filesaver.min.js"></script>
<script src="jszip.min.js"></script>
<script src="script.js" type="module"></script>
</body>
</html>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,8 @@ class TfSneakerAppElement extends LitElement {
async search() {
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
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
@ -31,15 +32,17 @@ class TfSneakerAppElement extends LitElement {
HAVING MAX(messages.sequence)
ORDER BY COUNT(*) DESC
`,
[`"${q.replaceAll('"', '""')}"`]);
this.feeds = Object.fromEntries(result.map(x => [x.id, x.name]));
[`"${q.replaceAll('"', '""')}"`]
);
this.feeds = Object.fromEntries(result.map((x) => [x.id, x.name]));
}
format_message(message) {
const k_flag_sequence_before_author = 1;
let out = {
previous: message.previous ?? null,
};
if (message.sequence_before_author) {
if (message.flags & k_flag_sequence_before_author) {
out.sequence = message.sequence;
out.author = message.author;
} else {
@ -70,24 +73,104 @@ class TfSneakerAppElement extends LitElement {
return true;
}
if (startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
startsWith(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
if (
startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
startsWith(
data,
[0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]
) ||
startsWith(data, [0xff, 0xd8, 0xff, 0xee]) ||
startsWith(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
startsWith(data, [
0xff,
0xd8,
0xff,
0xe1,
null,
null,
0x45,
0x78,
0x69,
0x66,
0x00,
0x00,
])
) {
return '.jpg';
} else if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
} else if (
startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
) {
return '.png';
} else if (startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
} else if (
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])
) {
return '.gif';
} else if (startsWith(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) {
} else if (
startsWith(data, [
0x52,
0x49,
0x46,
0x46,
null,
null,
null,
null,
0x57,
0x45,
0x42,
0x50,
])
) {
return '.webp';
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
return '.svg';
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
} else if (
startsWith(data, [
null,
null,
null,
null,
0x66,
0x74,
0x79,
0x70,
0x6d,
0x70,
0x34,
0x32,
])
) {
return '.mp3';
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) ||
startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
} else if (
startsWith(data, [
null,
null,
null,
null,
0x66,
0x74,
0x79,
0x70,
0x69,
0x73,
0x6f,
0x6d,
]) ||
startsWith(data, [
null,
null,
null,
null,
0x66,
0x74,
0x79,
0x70,
0x6d,
0x70,
0x34,
0x32,
])
) {
return '.mp4';
} else {
return '.bin';
@ -98,17 +181,34 @@ class TfSneakerAppElement extends LitElement {
let all_messages = '';
let sequence = -1;
let messages_done = 0;
let messages_max = (await tfrpc.rpc.query('SELECT MAX(sequence) AS total FROM messages WHERE author = ?', [id]))[0].total;
let messages_max = (
await tfrpc.rpc.query(
'SELECT MAX(sequence) AS total FROM messages WHERE author = ?',
[id]
)
)[0].total;
while (true) {
let messages = await tfrpc.rpc.query(
'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100',
[id, sequence]
`
SELECT author, id, sequence, timestamp, hash, json(content) AS content, signature, flags
FROM messages
WHERE author = ? AND SEQUENCE > ?
ORDER BY sequence LIMIT 100
`,
[id, sequence]
);
if (messages?.length) {
all_messages += messages.map(x => JSON.stringify(this.format_message(x))).join('\n') + '\n';
all_messages +=
messages
.map((x) => JSON.stringify(this.format_message(x)))
.join('\n') + '\n';
sequence = messages[messages.length - 1].sequence;
messages_done += messages.length;
this.progress = {name: 'messages', value: messages_done, max: messages_max};
this.progress = {
name: 'messages',
value: messages_done,
max: messages_max,
};
} else {
break;
}
@ -122,7 +222,8 @@ class TfSneakerAppElement extends LitElement {
FROM messages
JOIN messages_refs ON messages.id = messages_refs.message
WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
[id]);
[id]
);
let blobs_done = 0;
for (let row of blobs) {
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}`);
}
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++;
}
@ -161,7 +265,7 @@ class TfSneakerAppElement extends LitElement {
file = await zip.loadAsync(file);
let messages = [];
let blobs = [];
file.forEach(function(path, entry) {
file.forEach(function (path, entry) {
if (!entry.dir) {
if (path.startsWith('message/classic/')) {
messages.push(entry);
@ -181,7 +285,11 @@ class TfSneakerAppElement extends LitElement {
continue;
}
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)) {
success.messages++;
}
@ -202,7 +310,13 @@ class TfSneakerAppElement extends LitElement {
let progress;
if (this.progress) {
if (this.progress.max) {
progress = html`<div><label for="progress">${this.progress.name}</label><progress value=${this.progress.value} max=${this.progress.max}></progress></div>`;
progress = html`<div>
<label for="progress">${this.progress.name}</label
><progress
value=${this.progress.value}
max=${this.progress.max}
></progress>
</div>`;
} else {
progress = html`<div><span>${this.progress.name}</span></div>`;
}
@ -218,15 +332,19 @@ class TfSneakerAppElement extends LitElement {
<input type="text" id="search" @keypress=${this.keypress}></input>
<input type="button" value="Search Users" @click=${this.search}></input>
<ul>
${Object.entries(this.feeds).map(([id, name]) => html`
<li>
${this.progress ? undefined : html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
${name}
<code style="color: #ccc">${id}</code>
</li>
`)}
${Object.entries(this.feeds).map(
([id, name]) => html`
<li>
${this.progress
? undefined
: html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
${name}
<code style="color: #ccc">${id}</code>
</li>
`
)}
</ul>
`;
}
}
customElements.define('tf-sneaker-app', TfSneakerAppElement);
customElements.define('tf-sneaker-app', TfSneakerAppElement);

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🐌",
"previous": "&h+PXCrnUHtHHfKyUaLW+Y1dP/JpWwG9cbRNjxOCVqw0=.sha256"
}
"type": "tildefriends-app",
"emoji": "🐌",
"previous": "&Xs1X5TzLCk6KVr+5IDc80JAHYxJyoD10cXKBUYpFqWQ=.sha256"
}

View File

@ -76,7 +76,7 @@ tfrpc.register(function getHash(id, message) {
tfrpc.register(function setHash(hash) {
return app.setHash(hash);
});
ssb.addEventListener('message', async function(id) {
ssb.addEventListener('message', async function (id) {
await tfrpc.rpc.notifyNewMessage(id);
});
tfrpc.register(async function store_blob(blob) {
@ -100,18 +100,18 @@ tfrpc.register(async function try_decrypt(id, content) {
tfrpc.register(async function encrypt(id, recipients, content) {
return await ssb.privateMessageEncrypt(id, recipients, content);
});
ssb.addEventListener('broadcasts', async function() {
ssb.addEventListener('broadcasts', async function () {
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());
});
async function main() {
if (typeof(database) !== 'undefined') {
if (typeof database !== 'undefined') {
g_database = await database('ssb');
}
await app.setDocument(utf8Decode(await getFile('index.html')));
}
main();
main();

View File

@ -39,7 +39,7 @@ function splitMatches(text, regexp) {
return result;
}
const regex = new RegExp("(?<!\w)#[\\w-]+");
const regex = new RegExp("(?<!\\w)#[\\w-]+");
function split(textNodes) {
const text = textNodes.map(n => n.literal).join("");

View File

@ -4,14 +4,14 @@ function get_emojis() {
if (g_emojis) {
return Promise.resolve(g_emojis);
}
return fetch('emojis.json').then(function(result) {
return fetch('emojis.json').then(function (result) {
g_emojis = result.json();
return g_emojis;
});
}
export function picker(callback, anchor) {
get_emojis().then(function(json) {
get_emojis().then(function (json) {
let div = document.createElement('div');
div.id = 'emoji_picker';
div.style.color = '#000';
@ -36,7 +36,7 @@ export function picker(callback, anchor) {
div.appendChild(input);
let list = document.createElement('div');
div.appendChild(list);
div.addEventListener('mousedown', function(event) {
div.addEventListener('mousedown', function (event) {
event.stopPropagation();
});
@ -72,9 +72,11 @@ export function picker(callback, anchor) {
list.appendChild(header);
let any = false;
for (let entry of Object.entries(row[1])) {
if (search &&
if (
search &&
search.length &&
entry[0].toLowerCase().indexOf(search) == -1) {
entry[0].toLowerCase().indexOf(search) == -1
) {
continue;
}
let emoji = document.createElement('span');
@ -109,4 +111,4 @@ export function picker(callback, anchor) {
document.body.addEventListener('mousedown', cleanup);
window.addEventListener('keydown', key_down);
});
}
}

View File

@ -1,9 +1,9 @@
<!DOCTYPE html>
<!doctype html>
<html style="color: #fff">
<head>
<title>Tilde Friends</title>
<base target="_top">
<link rel="stylesheet" href="tribute.css"/>
<base target="_top" />
<link rel="stylesheet" href="tribute.css" />
<style>
.tribute-container {
color: #000;
@ -11,12 +11,14 @@
</style>
</head>
<body style="background-color: #223a5e">
<tf-app class="w3-deep-purple"/>
<script>window.litDisableBundleWarning = true;</script>
<tf-app class="w3-deep-purple" />
<script>
window.litDisableBundleWarning = true;
</script>
<script src="filesaver.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="script.js" type="module"></script>
</body>
</html>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -14,4 +14,4 @@ import * as tf_tab_news_feed from './tf-tab-news-feed.js';
import * as tf_tab_search from './tf-tab-search.js';
import * as tf_tab_connections from './tf-tab-connections.js';
import * as tf_tab_query from './tf-tab-query.js';
import * as tf_tag from './tf-tag.js';
import * as tf_tag from './tf-tag.js';

View File

@ -34,9 +34,13 @@ class TfElement extends LitElement {
this.users = {};
this.loaded = false;
this.tags = [];
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || []; });
tfrpc.rpc.getConnections().then(c => { self.connections = c || []; });
tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
tfrpc.rpc.getBroadcasts().then((b) => {
self.broadcasts = b || [];
});
tfrpc.rpc.getConnections().then((c) => {
self.connections = c || [];
});
tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
tfrpc.register(function hashChanged(hash) {
self.set_hash(hash);
});
@ -86,9 +90,14 @@ class TfElement extends LitElement {
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
`, []))[0].max_row_id;
`,
[]
)
)[0].max_row_id;
for (let id of Object.keys(cache.about)) {
if (ids.indexOf(id) == -1) {
delete cache.about[id];
@ -98,7 +107,7 @@ class TfElement extends LitElement {
let abouts = await tfrpc.rpc.query(
`
SELECT
messages.*
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM
messages,
json_each(?1) AS following
@ -109,7 +118,7 @@ class TfElement extends LitElement {
json_extract(messages.content, '$.type') = 'about'
UNION
SELECT
messages.*
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM
messages,
json_each(?2) AS following
@ -120,17 +129,21 @@ class TfElement extends LitElement {
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,
max_row_id,
]);
]
);
for (let about of abouts) {
let content = JSON.parse(about.content);
if (content.about === about.author) {
delete content.type;
delete content.about;
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
cache.about[about.author] = Object.assign(
cache.about[about.author] || {},
content
);
}
}
cache.last_row_id = max_row_id;
@ -145,15 +158,13 @@ class TfElement extends LitElement {
async fetch_new_message(id) {
let messages = await tfrpc.rpc.query(
`
SELECT messages.*
SELECT messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.id = ?
`,
[
JSON.stringify(this.following),
id,
]);
[JSON.stringify(this.following), id]
);
if (messages && messages.length) {
this.unread = [...this.unread, ...messages];
this.unread = this.unread.slice(this.unread.length - 1024);
@ -173,7 +184,7 @@ class TfElement extends LitElement {
}
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();
this.ids = (await tfrpc.rpc.getIdentities()) || [];
if (this.ids && !this.whoami) {
@ -185,17 +196,32 @@ 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>
<tf-id-picker
id="picker"
style="flex: 1 1 auto"
selected=${this.whoami}
.ids=${this.ids}
.users=${this.users}
@change=${this._handle_whoami_changed}
></tf-id-picker>
<button
class="w3-button w3-dark-grey w3-border"
style="flex: 0 0 auto"
@click=${this.create_identity}
id="create_identity"
>
Create Identity
</button>
</div>
`;
}
async load_recent_tags() {
let start = new Date();
this.tags = await tfrpc.rpc.query(`
this.tags = await tfrpc.rpc.query(
`
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'
ORDER BY timestamp DESC LIMIT 1024),
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
@ -207,7 +233,9 @@ class TfElement extends LitElement {
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
by_message AS (SELECT DISTINCT id, tag FROM combined)
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
`, [new Date() - 7 * 24 * 60 * 60 * 1000]);
`,
[new Date() - 7 * 24 * 60 * 60 * 1000]
);
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
}
@ -241,23 +269,53 @@ class TfElement extends LitElement {
let users = this.users;
if (this.tab === 'news') {
return html`
<tf-tab-news id="tf-tab-news" .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
<tf-tab-news
id="tf-tab-news"
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
hash=${this.hash}
.unread=${this.unread}
@refresh=${() => (this.unread = [])}
></tf-tab-news>
`;
} else if (this.tab === 'connections') {
return html`
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
<tf-tab-connections
.users=${this.users}
.connections=${this.connections}
.broadcasts=${this.broadcasts}
></tf-tab-connections>
`;
} else if (this.tab === 'mentions') {
return html`
<tf-tab-mentions .following=${this.following} whoami=${this.whoami} .users=${this.users}}></tf-tab-mentions>
<tf-tab-mentions
.following=${this.following}
whoami=${this.whoami}
.users="${this.users}}"
></tf-tab-mentions>
`;
} else if (this.tab === 'search') {
return html`
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
<tf-tab-search
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
query=${this.hash?.startsWith('#q=')
? decodeURIComponent(this.hash.substring(3))
: null}
></tf-tab-search>
`;
} else if (this.tab === 'query') {
return html`
<tf-tab-query .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#sql=') ? decodeURIComponent(this.hash.substring(5)) : null}></tf-tab-query>
<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>
`;
}
}
@ -280,7 +338,7 @@ class TfElement extends LitElement {
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
this.loading = true;
this.load().finally(function() {
this.load().finally(function () {
self.loading = false;
});
}
@ -295,21 +353,32 @@ class TfElement extends LitElement {
let tabs = html`
<div class="w3-bar w3-black">
${Object.entries(k_tabs).map(([k, v]) => html`
<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>
`)}
${Object.entries(k_tabs).map(
([k, v]) => html`
<button
title=${v}
class="w3-bar-item w3-padding-large w3-hover-gray tab ${self.tab ==
v
? 'w3-red'
: 'w3-black'}"
@click=${() => self.set_tab(v)}
>
${k}
</button>
`
)}
</div>
`;
let contents =
!this.loaded ?
this.loading ?
html`<div>Loading...</div>` :
html`<div>Select or create an identity.</div>` :
this.render_tab();
let contents = !this.loaded
? this.loading
? html`<div>Loading...</div>`
: html`<div>Select or create an identity.</div>`
: this.render_tab();
return html`
${this.render_id_picker()}
${tabs}
${this.tags.map(x => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`)}
${this.render_id_picker()} ${tabs}
${this.tags.map(
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
)}
${contents}
`;
}

View File

@ -58,7 +58,9 @@ class TfComposeElement extends LitElement {
link: link,
};
}
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
draft.mentions[link].name = name.startsWith('@')
? name.substring(1)
: name;
updated = true;
}
if (updated) {
@ -72,34 +74,39 @@ class TfComposeElement extends LitElement {
let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = this.process_text(edit.value);
let content_warning = this.renderRoot.getElementById('content_warning');
let content_warning_preview = this.renderRoot.getElementById('content_warning_preview');
let content_warning_preview = this.renderRoot.getElementById(
'content_warning_preview'
);
if (content_warning && content_warning_preview) {
content_warning_preview.innerText = content_warning.value;
}
}
notify(draft) {
this.dispatchEvent(new CustomEvent('tf-draft', {
bubbles: true,
composed: true,
detail: {
id: this.branch,
draft: draft
},
}));
this.dispatchEvent(
new CustomEvent('tf-draft', {
bubbles: true,
composed: true,
detail: {
id: this.branch,
draft: draft,
},
})
);
}
change() {
let draft = this.get_draft();
draft.text = this.renderRoot.getElementById('edit')?.value;
draft.content_warning = this.renderRoot.getElementById('content_warning')?.value;
draft.content_warning =
this.renderRoot.getElementById('content_warning')?.value;
this.notify(draft);
}
convert_to_format(buffer, type, mime_type) {
return new Promise(function(resolve, reject) {
return new Promise(function (resolve, reject) {
let img = new Image();
img.onload = function() {
img.onload = function () {
let canvas = document.createElement('canvas');
let width_scale = Math.min(img.width, 1024) / img.width;
let height_scale = Math.min(img.height, 1024) / img.height;
@ -109,13 +116,17 @@ class TfComposeElement extends LitElement {
let context = canvas.getContext('2d');
context.drawImage(img, 0, 0, canvas.width, canvas.height);
let data_url = canvas.toDataURL(mime_type);
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
let result = atob(data_url.split(',')[1])
.split('')
.map((x) => x.charCodeAt(0));
resolve(result);
};
img.onerror = function(event) {
img.onerror = function (event) {
reject(new Error('Failed to load image.'));
};
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
let raw = Array.from(new Uint8Array(buffer))
.map((b) => String.fromCharCode(b))
.join('');
let original = `data:${type};base64,${btoa(raw)}`;
img.src = original;
});
@ -131,7 +142,11 @@ class TfComposeElement extends LitElement {
let best_buffer;
let best_type;
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
let test_buffer = await self.convert_to_format(buffer, file.type, format);
let test_buffer = await self.convert_to_format(
buffer,
file.type,
format
);
if (!best_buffer || test_buffer.length < best_buffer.length) {
best_buffer = test_buffer;
best_type = format;
@ -157,7 +172,7 @@ class TfComposeElement extends LitElement {
edit.value += `\n![${name}](${id})`;
self.change();
self.input();
} catch(e) {
} catch (e) {
alert(e?.message);
}
}
@ -201,11 +216,15 @@ class TfComposeElement extends LitElement {
to = [...to];
message.recps = to;
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);
}
try {
await tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
await tfrpc.rpc.appendMessage(this.whoami, message).then(function () {
edit.value = '';
self.change();
self.notify(undefined);
@ -230,7 +249,7 @@ class TfComposeElement extends LitElement {
let edit = this.renderRoot.getElementById('edit');
let input = document.createElement('input');
input.type = 'file';
input.onchange = function(event) {
input.onchange = function (event) {
let file = event.target.files[0];
self.add_file(file);
};
@ -241,12 +260,15 @@ class TfComposeElement extends LitElement {
this.last_autocomplete = text;
let results = [];
try {
let rows = await tfrpc.rpc.query(`
SELECT messages.content FROM messages_fts(?)
let rows = await tfrpc.rpc.query(
`
SELECT json(messages.content) FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
WHERE messages.content LIKE ?
ORDER BY timestamp DESC LIMIT 10
`, ['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]);
`,
['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]
);
for (let row of rows) {
for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
@ -265,15 +287,18 @@ class TfComposeElement extends LitElement {
let tribute = new Tribute({
collection: [
{
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
selectTemplate: function(item) {
values: Object.entries(this.users).map((x) => ({
key: x[1].name,
value: x[0],
})),
selectTemplate: function (item) {
return `[@${item.original.key}](${item.original.value})`;
},
},
{
trigger: '&',
values: this.autocomplete,
selectTemplate: function(item) {
selectTemplate: function (item) {
return `![${item.original.key}](${item.original.value})`;
},
},
@ -293,8 +318,11 @@ class TfComposeElement extends LitElement {
let encrypt = this.renderRoot.getElementById('encrypt_to');
if (encrypt) {
let tribute = new Tribute({
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
selectTemplate: function(item) {
values: Object.entries(this.users).map((x) => ({
key: x[1].name,
value: x[0],
})),
selectTemplate: function (item) {
return item.original.value;
},
});
@ -311,20 +339,30 @@ class TfComposeElement extends LitElement {
render_mention(mention) {
let self = this;
return html`
<div style="display: flex; flex-direction: row">
<div style="align-self: center; margin: 0.5em">
<button class="w3-button w3-dark-grey" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}>🚮</button>
return html` <div style="display: flex; flex-direction: row">
<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>
</div>
<div style="display: flex; flex-direction: column">
<h3>${mention.name}</h3>
<div style="padding-left: 1em">
${Object.entries(mention)
.filter((x) => x[0] != 'name')
.map(
(x) =>
html`<div>
<span style="font-weight: bold">${x[0]}</span>: ${x[1]}
</div>`
)}
</div>
<div style="display: flex; flex-direction: column">
<h3>${mention.name}</h3>
<div style="padding-left: 1em">
${Object.entries(mention)
.filter(x => x[0] != 'name')
.map(x => html`<div><span style="font-weight: bold">${x[0]}</span>: ${x[1]}</div>`)}
</div>
</div>
</div>`;
</div>
</div>`;
}
render_attach_app() {
@ -359,12 +397,21 @@ class TfComposeElement extends LitElement {
return html`
<div class="w3-card-4 w3-margin w3-padding">
<select id="select" class="w3-select w3-dark-grey">
${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
${Object.keys(self.apps).map(
(app) => html`<option value=${app}>${app}</option>`
)}
</select>
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}>Attach</button>
<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Cancel</button>
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}>
Attach
</button>
<button
class="w3-button w3-dark-grey"
@click=${() => (this.apps = null)}
>
Cancel
</button>
</div>
`;
`;
}
}
@ -374,9 +421,16 @@ class TfComposeElement extends LitElement {
self.apps = await tfrpc.rpc.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-dark-grey" @click=${attach_app}>
Attach App
</button>`;
} else {
return html`<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Discard App</button>`;
return html`<button
class="w3-button w3-dark-grey"
@click=${() => (this.apps = null)}
>
Discard App
</button>`;
}
}
@ -435,11 +489,13 @@ class TfComposeElement extends LitElement {
<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt(undefined)}>🚮</button>
</div>
<ul>
${draft.encrypt_to.map(x => html`
${draft.encrypt_to.map(
(x) => html`
<li>
<tf-user id=${x} .users=${this.users}></tf-user>
<input type="button" class="w3-button w3-dark-grey" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter(id => id != x))}></input>
</li>`)}
<input type="button" class="w3-button w3-dark-grey" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
</li>`
)}
</ul>
`;
}
@ -455,34 +511,65 @@ class TfComposeElement extends LitElement {
let self = this;
let draft = self.get_draft();
let content_warning =
draft.content_warning !== undefined ?
html`<div class="w3-panel w3-round-xlarge w3-blue">
<p id="content_warning_preview">${draft.content_warning}</p>
</div>` :
undefined;
let encrypt = draft.encrypt_to !== undefined ?
undefined :
html`<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt([])}>🔐</button>`;
draft.content_warning !== undefined
? html`<div class="w3-panel w3-round-xlarge w3-blue">
<p id="content_warning_preview">${draft.content_warning}</p>
</div>`
: undefined;
let encrypt =
draft.encrypt_to !== undefined
? undefined
: html`<button
class="w3-button w3-dark-grey"
@click=${() => this.set_encrypt([])}
>
🔐
</button>`;
let result = html`
<div class="w3-card-4 w3-blue-grey w3-padding" style="box-sizing: border-box">
<div
class="w3-card-4 w3-blue-grey w3-padding"
style="box-sizing: border-box"
>
${this.render_encrypt()}
<div style="display: flex; flex-direction: row; width: 100%; gap: 4px">
<div style="flex: 1 0 50%">
<p><textarea class="w3-input w3-dark-grey w3-border" style="resize: vertical" placeholder="Write a post here." id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste}>${draft.text}</textarea></p>
<p>
<textarea
class="w3-input w3-dark-grey w3-border"
style="resize: vertical"
placeholder="Write a post here."
id="edit"
@input=${this.input}
@change=${this.change}
@paste=${this.paste}
>
${draft.text}</textarea
>
</p>
</div>
<div style="flex: 1 0 50%">
${content_warning}
<div id="preview"></div>
</div>
</div>
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
${this.render_attach_app()}
${this.render_content_warning()}
<button class="w3-button w3-dark-grey" id="submit" @click=${this.submit}>Submit</button>
<button class="w3-button w3-dark-grey" @click=${this.attach}>Attach</button>
${this.render_attach_app_button()}
${encrypt}
<button class="w3-button w3-dark-grey" @click=${this.discard}>Discard</button>
${Object.values(draft.mentions || {}).map((x) =>
self.render_mention(x)
)}
${this.render_attach_app()} ${this.render_content_warning()}
<button
class="w3-button w3-dark-grey"
id="submit"
@click=${this.submit}
>
Submit
</button>
<button class="w3-button w3-dark-grey" @click=${this.attach}>
Attach
</button>
${this.render_attach_app_button()} ${encrypt}
<button class="w3-button w3-dark-grey" @click=${this.discard}>
Discard
</button>
</div>
`;
return result;

View File

@ -3,8 +3,8 @@ import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
/*
** Provide a list of IDs, and this lets the user pick one.
*/
** Provide a list of IDs, and this lets the user pick one.
*/
class TfIdentityPickerElement extends LitElement {
static get properties() {
return {
@ -24,15 +24,28 @@ class TfIdentityPickerElement extends LitElement {
changed(event) {
this.selected = event.srcElement.value;
this.dispatchEvent(new Event('change', {
srcElement: this,
}));
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
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>
`;
}

View File

@ -31,14 +31,27 @@ class TfMessageElement extends LitElement {
}
show_reply() {
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: {
encrypt_to: this.message?.decrypted?.recps,
}}});
let event = new CustomEvent('tf-draft', {
bubbles: true,
composed: true,
detail: {
id: this.message?.id,
draft: {
encrypt_to: this.message?.decrypted?.recps,
},
},
});
this.dispatchEvent(event);
}
discard_reply() {
this.dispatchEvent(new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.id, draft: undefined}}));
this.dispatchEvent(
new CustomEvent('tf-draft', {
bubbles: true,
composed: true,
detail: {id: this.id, draft: undefined},
})
);
}
render_votes() {
@ -53,12 +66,19 @@ class TfMessageElement extends LitElement {
return expression;
}
}
return html`<div>${(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)}
</span>
`)}</div>`;
return html`<div>
${(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)}
</span>
`
)}
</div>`;
}
render_raw() {
@ -72,30 +92,40 @@ class TfMessageElement extends LitElement {
content: this.message?.content,
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) {
let reaction = emoji;
let message = this.message.id;
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
tfrpc.rpc.appendMessage(
this.whoami,
{
if (
confirm(
'Are you sure you want to react with ' +
reaction +
' to ' +
message +
'?'
)
) {
tfrpc.rpc
.appendMessage(this.whoami, {
type: 'vote',
vote: {
link: message,
value: 1,
expression: reaction,
},
}).catch(function(error) {
})
.catch(function (error) {
alert(error?.message);
});
}
}
react(event) {
emojis.picker(x => this.vote(x));
emojis.picker((x) => this.vote(x));
}
show_image(link) {
@ -129,7 +159,10 @@ class TfMessageElement extends LitElement {
body_click(event) {
if (event.srcElement.tagName == 'IMG') {
this.show_image(event.srcElement.src);
} else if (event.srcElement.tagName == 'DIV' && event.srcElement.classList.contains('img_caption')) {
} else if (
event.srcElement.tagName == 'DIV' &&
event.srcElement.classList.contains('img_caption')
) {
let next = event.srcElement.nextSibling;
if (next.style.display == 'block') {
next.style.display = 'none';
@ -140,50 +173,77 @@ class TfMessageElement extends LitElement {
}
render_mention(mention) {
if (!mention?.link || typeof(mention.link) != 'string') {
if (!mention?.link || typeof mention.link != 'string') {
return html` <pre>${JSON.stringify(mention)}</pre>`;
} else if (mention?.link?.startsWith('&') &&
mention?.type?.startsWith('image/')) {
} else if (
mention?.link?.startsWith('&') &&
mention?.type?.startsWith('image/')
) {
return html`
<img src=${'/' + mention.link + '/view'} style="max-width: 128px; max-height: 128px" title=${mention.name} @click=${() => this.show_image('/' + mention.link + '/view')}>
<img
src=${'/' + mention.link + '/view'}
style="max-width: 128px; max-height: 128px"
title=${mention.name}
@click=${() => this.show_image('/' + mention.link + '/view')}
/>
`;
} else if (mention.link?.startsWith('&') &&
mention.name?.startsWith('audio:')) {
} else if (
mention.link?.startsWith('&') &&
mention.name?.startsWith('audio:')
) {
return html`
<audio controls style="height: 32px">
<source src=${'/' + mention.link + '/view'}></source>
</audio>
`;
} else if (mention.link?.startsWith('&') &&
mention.name?.startsWith('video:')) {
} else if (
mention.link?.startsWith('&') &&
mention.name?.startsWith('video:')
) {
return html`
<video controls style="max-height: 240px; max-width: 128px">
<source src=${'/' + mention.link + '/view'}></source>
</video>
`;
} else if (mention.link?.startsWith('&') &&
mention?.type === 'application/tildefriends') {
} else if (
mention.link?.startsWith('&') &&
mention?.type === 'application/tildefriends'
) {
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
return html` <a href=${'#' + encodeURIComponent(mention.link)}>${mention.name}</a>`;
return html` <a href=${'#' + encodeURIComponent(mention.link)}
>${mention.name}</a
>`;
} else if (mention.link?.startsWith('#')) {
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}>${mention.link}</a>`;
} else if (Object.keys(mention).length == 2 && mention.link && mention.name) {
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}
>${mention.link}</a
>`;
} else if (
Object.keys(mention).length == 2 &&
mention.link &&
mention.name
) {
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
} else {
return html` <pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>`;
return html` <pre style="white-space: pre-wrap">
${JSON.stringify(mention, null, 2)}</pre
>`;
}
}
render_mentions() {
let mentions = this.message?.content?.mentions || [];
mentions = mentions.filter(x => this.message?.content?.text?.indexOf(x.link) === -1);
mentions = mentions.filter(
(x) => this.message?.content?.text?.indexOf(x.link) === -1
);
if (mentions.length) {
let self = this;
return html`
<fieldset style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black">
<fieldset
style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black"
>
<legend>Mentions</legend>
${mentions.map(x => self.render_mention(x))}
${mentions.map((x) => self.render_mention(x))}
</fieldset>
`;
}
@ -194,28 +254,55 @@ class TfMessageElement extends LitElement {
return 0;
}
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);
}
return total;
}
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) {
this.set_expanded(!this.expanded[(this.message.id || '') + (tag || '')], tag);
this.set_expanded(
!this.expanded[(this.message.id || '') + (tag || '')],
tag
);
}
render_children() {
let self = this;
if (this.message.child_messages?.length) {
if (!this.expanded[this.message.id]) {
return html`<button class="w3-button w3-dark-grey" @click=${() => self.set_expanded(true)}>+ ${this.total_child_messages(this.message) + ' More'}</button>`;
return html`<button
class="w3-button w3-dark-grey"
@click=${() => self.set_expanded(true)}
>
+ ${this.total_child_messages(this.message) + ' More'}
</button>`;
} 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-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>`
)}`;
}
}
}
@ -231,13 +318,12 @@ class TfMessageElement extends LitElement {
}
if (Array.isArray(content.mentions)) {
for (let mention of content.mentions) {
if (typeof mention?.link === 'string' &&
mention.link.startsWith('#')) {
if (typeof mention?.link === 'string' && mention.link.startsWith('#')) {
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() {
@ -250,54 +336,110 @@ class TfMessageElement extends LitElement {
switch (this.format) {
case 'raw':
if (content?.type == 'post' || content?.type == 'blog') {
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'md'}>Markdown</button>`;
raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'md')}
>
Markdown
</button>`;
} else {
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'message')}
>
Message
</button>`;
}
break;
case 'md':
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'message')}
>
Message
</button>`;
break;
case 'decrypted':
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</button>`;
raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'raw')}
>
Raw
</button>`;
break;
default:
if (this.message.decrypted) {
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'decrypted'}>Decrypted</button>`;
raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'decrypted')}
>
Decrypted
</button>`;
} else {
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</button>`;
raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'raw')}
>
Raw
</button>`;
}
break;
}
function small_frame(inner) {
let body;
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"
style="background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
>
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
${raw_button}
${self.format == 'raw' ? self.render_raw() : inner}
<span style="padding-right: 8px"
><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(
self.message.timestamp
).toLocaleString()}</span
>
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
${self.render_votes()}
</div>
`;
}
if (this.message?.type === 'contact_group') {
return html`
<div class="w3-card-4" style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
${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>`;
return html` <div
class="w3-card-4"
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
>
${this.message.messages.map(
(x) =>
html`<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
></tf-message>`
)}
</div>`;
} else if (this.message.placeholder) {
return html`
<div class="w3-card-4" style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
<div>${this.render_votes()}</div>
${(this.message.child_messages || []).map(x => html`
<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>
`)}
</div>`;
} else if (typeof(content?.type === 'string')) {
return html` <div
class="w3-card-4"
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
>
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
(placeholder)
<div>${this.render_votes()}</div>
${(this.message.child_messages || []).map(
(x) => html`
<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
></tf-message>
`
)}
</div>`;
} else if (typeof (content?.type === 'string')) {
if (content.type == 'about') {
let name;
let image;
@ -307,7 +449,7 @@ class TfMessageElement extends LitElement {
}
if (content.image !== undefined) {
image = html`
<div><img src=${'/' + (typeof(content.image?.link) == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
<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) {
@ -317,42 +459,55 @@ class TfMessageElement extends LitElement {
</div>
`;
}
let update = content.about == this.message.author ?
html`<div style="font-weight: bold">Updated profile.</div>` :
html`<div style="font-weight: bold">Updated profile for <tf-user id=${content.about} .users=${this.users}></tf-user>.</div>`;
return small_frame(html`
${update}
${name}
${image}
${description}
`);
let update =
content.about == this.message.author
? html`<div style="font-weight: bold">Updated profile.</div>`
: html`<div style="font-weight: bold">
Updated profile for
<tf-user id=${content.about} .users=${this.users}></tf-user>.
</div>`;
return small_frame(html` ${update} ${name} ${image} ${description} `);
} else if (content.type == 'contact') {
return html`
<div>
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
is
${
content.blocking === true ? 'blocking' :
content.blocking === false ? 'no longer blocking' :
content.following === true ? 'following' :
content.following === false ? 'no longer following' :
'?'
}
<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user>
${content.blocking === true
? 'blocking'
: content.blocking === false
? 'no longer blocking'
: content.following === true
? 'following'
: content.following === false
? 'no longer following'
: '?'}
<tf-user
id=${this.message.content.contact}
.users=${this.users}
></tf-user>
</div>
`;
} else if (content.type == 'post') {
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${this.message.content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}></tf-compose>
` : html`
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button>
`;
let reply =
this.drafts[this.message?.id] !== undefined
? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
></tf-compose>
`
: html`
<button
class="w3-button w3-dark-grey"
@click=${this.show_reply}
>
Reply
</button>
`;
let self = this;
let body;
switch (this.format) {
@ -360,32 +515,47 @@ class TfMessageElement extends LitElement {
body = this.render_raw();
break;
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;
case 'message':
body = unsafeHTML(tfutils.markdown(content.text));
break;
case 'decrypted':
body = html`<pre
style="white-space: pre-wrap; overflow-wrap: anywhere"
>
${JSON.stringify(content, null, 2)}</pre
>`;
break;
}
let content_warning = html`
<div class="w3-panel w3-round-xlarge w3-blue" style="cursor: pointer" @click=${x => this.toggle_expanded(':cw')}><p>${content.contentWarning}</p></div>
`;
let content_html =
html`
${this.render_channels()}
<div @click=${this.body_click}>${body}</div>
${this.render_mentions()}
`;
let payload =
content.contentWarning ?
self.expanded[(this.message.id || '') + ':cw'] ?
html`
${content_warning}
${content_html}
` :
content_warning :
content_html;
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
<div
class="w3-panel w3-round-xlarge w3-blue"
style="cursor: pointer"
@click=${(x) => this.toggle_expanded(':cw')}
>
<p>${content.contentWarning}</p>
</div>
`;
let content_html = html`
${this.render_channels()}
<div @click=${this.body_click}>${body}</div>
${this.render_mentions()}
`;
let payload = content.contentWarning
? self.expanded[(this.message.id || '') + ':cw']
? html` ${content_warning} ${content_html} `
: content_warning
: content_html;
let is_encrypted = this.message?.decrypted
? html`<span style="align-self: center">🔓</span>`
: undefined;
let style_background = this.message?.decrypted
? 'rgba(255, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.1)';
return html`
<style>
code {
@ -401,26 +571,37 @@ class TfMessageElement extends LitElement {
display: block;
}
</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"
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
<span style="flex: 1"></span>
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
<span style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
</div>
${payload}
${this.render_votes()}
${payload} ${this.render_votes()}
<p>
${reply}
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
<button class="w3-button w3-dark-grey" @click=${this.react}>
React
</button>
</p>
${this.render_children()}
</div>
`;
} else if (content.type === 'issue') {
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
let is_encrypted = this.message?.decrypted
? html`<span style="align-self: center">🔓</span>`
: undefined;
let style_background = this.message?.decrypted
? 'rgba(255, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.1)';
return html`
<style>
code {
@ -436,31 +617,41 @@ class TfMessageElement extends LitElement {
display: block;
}
</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"
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
<span style="flex: 1"></span>
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
<span style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
</div>
${content.text}
${this.render_votes()}
${content.text} ${this.render_votes()}
<p>
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
<button class="w3-button w3-dark-grey" @click=${this.react}>
React
</button>
</p>
${this.render_children()}
</div>
`;
} else if (content.type === 'blog') {
let self = this;
tfrpc.rpc.get_blob(content.blog).then(function(data) {
tfrpc.rpc.get_blob(content.blog).then(function (data) {
self.blog_data = data;
});
let payload =
this.expanded[(this.message.id || '') + ':blog'] ?
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
undefined;
let payload = this.expanded[(this.message.id || '') + ':blog']
? html`<div>
${this.blog_data
? unsafeHTML(tfutils.markdown(this.blog_data))
: 'Loading...'}
</div>`
: undefined;
let body;
switch (this.format) {
case 'raw':
@ -473,7 +664,7 @@ class TfMessageElement extends LitElement {
body = html`
<div
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>
<div style="display: flex; flex-direction: row">
<img src=/${content.thumbnail}/view></img>
@ -484,17 +675,26 @@ class TfMessageElement extends LitElement {
`;
break;
}
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${this.message.content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}></tf-compose>
` : html`
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button>
`;
let reply =
this.drafts[this.message?.id] !== undefined
? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
></tf-compose>
`
: html`
<button
class="w3-button w3-dark-grey"
@click=${this.show_reply}
>
Reply
</button>
`;
return html`
<style>
code {
@ -510,11 +710,17 @@ class TfMessageElement extends LitElement {
display: block;
}
</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"
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
<span style="flex: 1"></span>
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
<span style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
</div>
@ -522,37 +728,52 @@ class TfMessageElement extends LitElement {
${this.render_mentions()}
<div>
${reply}
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
<button class="w3-button w3-dark-grey" @click=${this.react}>
React
</button>
</div>
${this.render_votes()}
${this.render_children()}
${this.render_votes()} ${this.render_children()}
</div>
`;
} else if (content.type === 'pub') {
return small_frame(html`
<style>
span {
overflow-wrap: anywhere;
}
</style>
<span>
<div>
🍻 <tf-user .users=${this.users} id=${content.address.key}></tf-user>
</div>
<pre>${content.address.host}:${content.address.port}</pre>
</span>`);
return small_frame(
html` <style>
span {
overflow-wrap: anywhere;
}
</style>
<span>
<div>
🍻
<tf-user
.users=${this.users}
id=${content.address.key}
></tf-user>
</div>
<pre>${content.address.host}:${content.address.port}</pre>
</span>`
);
} else if (content.type === 'channel') {
return small_frame(html`
<div>
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} <a href=${'#q=' + encodeURIComponent('#' + content.channel)}>#${content.channel}</a>
${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
<a href=${'#q=' + encodeURIComponent('#' + content.channel)}
>#${content.channel}</a
>
</div>
`);
} else if (typeof(this.message.content) == 'string') {
} else if (typeof this.message.content == 'string') {
if (this.message?.decrypted) {
if (this.format == 'decrypted') {
return small_frame(html`<span>🔓</span><pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`);
return small_frame(
html`<span>🔓</span>
<pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`
);
} else {
return small_frame(html`<span>🔓</span><div>${this.message.decrypted.type}</div>`);
return small_frame(
html`<span>🔓</span>
<div>${this.message.decrypted.type}</div>`
);
}
} else {
return small_frame(html`<span>🔒</span>`);
@ -566,4 +787,4 @@ class TfMessageElement extends LitElement {
}
}
customElements.define('tf-message', TfMessageElement);
customElements.define('tf-message', TfMessageElement);

View File

@ -61,7 +61,7 @@ class TfNewsElement extends LitElement {
message.parent_message = message.content.vote.link;
} else if (message.content.type == 'post') {
if (message.content.root) {
if (typeof(message.content.root) === 'string') {
if (typeof message.content.root === 'string') {
let m = ensure_message(message.content.root);
if (!m.child_messages) {
m.child_messages = [];
@ -89,8 +89,7 @@ class TfNewsElement extends LitElement {
for (let message of messages) {
try {
message.content = JSON.parse(message.content);
} catch {
}
} catch {}
if (!messages_by_id[message.id]) {
messages_by_id[message.id] = message;
link_message(message);
@ -100,8 +99,12 @@ class TfNewsElement extends LitElement {
message.parent_message = placeholder.parent_message;
message.child_messages = placeholder.child_messages;
message.votes = placeholder.votes;
if (placeholder.parent_message && messages_by_id[placeholder.parent_message]) {
let children = messages_by_id[placeholder.parent_message].child_messages;
if (
placeholder.parent_message &&
messages_by_id[placeholder.parent_message]
) {
let children =
messages_by_id[placeholder.parent_message].child_messages;
children.splice(children.indexOf(placeholder), 1);
children.push(message);
}
@ -116,7 +119,10 @@ class TfNewsElement extends LitElement {
let latest = 0;
for (let message of messages || []) {
if (message.latest_subtree_timestamp === undefined) {
message.latest_subtree_timestamp = Math.max(message.timestamp ?? 0, this.update_latest_subtree_timestamp(message.child_messages));
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);
}
@ -127,20 +133,22 @@ class TfNewsElement extends LitElement {
function recursive_sort(messages, top) {
if (messages) {
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 {
messages.sort((a, b) => a.timestamp - b.timestamp);
}
for (let message of messages) {
recursive_sort(message.child_messages, false);
}
return messages.map(x => Object.assign({}, x));
return messages.map((x) => Object.assign({}, x));
} else {
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);
return recursive_sort(roots, true);
}
@ -167,10 +175,22 @@ class TfNewsElement extends LitElement {
load_and_render(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`
<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>
`;
}
@ -180,4 +200,4 @@ class TfNewsElement extends LitElement {
}
}
customElements.define('tf-news', TfNewsElement);
customElements.define('tf-news', TfNewsElement);

View File

@ -36,23 +36,29 @@ class TfProfileElement extends LitElement {
this.following = undefined;
this.blocking = undefined;
let result = await tfrpc.rpc.query(`
let result = await tfrpc.rpc.query(
`
SELECT json_extract(content, '$.following') AS following
FROM messages WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? AND
following IS NOT NULL
ORDER BY sequence DESC LIMIT 1
`, [this.whoami, this.id]);
`,
[this.whoami, this.id]
);
this.following = result?.[0]?.following ?? false;
result = await tfrpc.rpc.query(`
result = await tfrpc.rpc.query(
`
SELECT json_extract(content, '$.blocking') AS blocking
FROM messages WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? AND
blocking IS NOT NULL
ORDER BY sequence DESC LIMIT 1
`, [this.whoami, this.id]);
`,
[this.whoami, this.id]
);
this.blocking = result?.[0]?.blocking ?? false;
}
}
@ -60,13 +66,16 @@ class TfProfileElement extends LitElement {
async initial_load() {
this.server_follows_me = undefined;
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
FROM messages
WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
`, [server_id, this.whoami]);
`,
[server_id, this.whoami]
);
let is_followed = false;
for (let row of followed) {
is_followed = row.following != 0;
@ -75,11 +84,18 @@ class TfProfileElement extends LitElement {
}
modify(change) {
tfrpc.rpc.appendMessage(this.whoami,
Object.assign({
type: 'contact',
contact: this.id,
}, change)).catch(function(error) {
tfrpc.rpc
.appendMessage(
this.whoami,
Object.assign(
{
type: 'contact',
contact: this.id,
},
change
)
)
.catch(function (error) {
alert(error?.message);
});
}
@ -122,11 +138,14 @@ class TfProfileElement extends LitElement {
message[key] = this.editing[key];
}
}
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
self.editing = null;
}).catch(function(error) {
alert(error?.message);
});
tfrpc.rpc
.appendMessage(this.whoami, message)
.then(function () {
self.editing = null;
})
.catch(function (error) {
alert(error?.message);
});
}
discard_edits() {
@ -137,17 +156,21 @@ class TfProfileElement extends LitElement {
let self = this;
let input = document.createElement('input');
input.type = 'file';
input.onchange = function(event) {
input.onchange = function (event) {
let file = event.target.files[0];
file.arrayBuffer().then(function(buffer) {
let bin = Array.from(new Uint8Array(buffer));
return tfrpc.rpc.store_blob(bin);
}).then(function(id) {
self.editing = Object.assign({}, self.editing, {image: id});
console.log(self.editing);
}).catch(function(e) {
alert(e.message);
});
file
.arrayBuffer()
.then(function (buffer) {
let bin = Array.from(new Uint8Array(buffer));
return tfrpc.rpc.store_blob(bin);
})
.then(function (id) {
self.editing = Object.assign({}, self.editing, {image: id});
console.log(self.editing);
})
.catch(function (e) {
alert(e.message);
});
};
input.click();
}
@ -166,15 +189,22 @@ class TfProfileElement extends LitElement {
}
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.load();
let self = this;
let profile = this.users[this.id] || {};
tfrpc.rpc.query(
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
[this.id]).then(function(result) {
tfrpc.rpc
.query(
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
[this.id]
)
.then(function (result) {
self.size = result[0].size;
});
let edit;
@ -184,52 +214,75 @@ class TfProfileElement extends LitElement {
if (this.editing) {
let server_follow;
if (this.server_follows_me === true) {
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(false)}>Server, Stop Following Me</button>`;
server_follow = html`<button
class="w3-button w3-dark-grey"
@click=${() => this.server_follow_me(false)}
>
Server, Stop Following Me
</button>`;
} else if (this.server_follows_me === false) {
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(true)}>Server, Follow Me</button>`;
server_follow = html`<button
class="w3-button w3-dark-grey"
@click=${() => this.server_follow_me(true)}
>
Server, Follow Me
</button>`;
}
edit = html`
<button class="w3-button w3-dark-grey" @click=${this.save_edits}>Save Profile</button>
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>Discard</button>
<button class="w3-button w3-dark-grey" @click=${this.save_edits}>
Save Profile
</button>
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>
Discard
</button>
${server_follow}
`;
} else {
edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>Edit Profile</button>`;
edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>
Edit Profile
</button>`;
}
}
if (this.id !== this.whoami &&
this.following !== undefined) {
follow =
this.following ?
html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>Unfollow</button>` :
html`<button class="w3-button w3-dark-grey" @click=${this.follow}>Follow</button>`;
if (this.id !== this.whoami && this.following !== undefined) {
follow = this.following
? html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>
Unfollow
</button>`
: html`<button class="w3-button w3-dark-grey" @click=${this.follow}>
Follow
</button>`;
}
if (this.id !== this.whoami &&
this.blocking !== undefined) {
block =
this.blocking ?
html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>Unblock</button>` :
html`<button class="w3-button w3-dark-grey" @click=${this.block}>Block</button>`;
if (this.id !== this.whoami && this.blocking !== undefined) {
block = this.blocking
? html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>
Unblock
</button>`
: html`<button class="w3-button w3-dark-grey" @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 class="w3-container">
<div>
<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-dark-grey" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
</div>
<div><label for="description">Description:</label></div>
<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-dark-grey" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
<div>
<label for="public_web_hosting">Public Web Hosting:</label>
<input class="w3-check w3-dark-grey" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${event => self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked})}></input>
<input class="w3-check w3-dark-grey" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
</div>
<div>
<button class="w3-button w3-dark-grey" @click=${this.attach_image}>Attach Image</button>
</div>
</div>
</div>` : null;
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
</div>`
: null;
let image =
typeof profile.image == 'string' ? profile.image : profile.image?.link;
image = this.editing?.image ?? image;
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">
@ -256,4 +309,4 @@ class TfProfileElement extends LitElement {
}
}
customElements.define('tf-profile', TfProfileElement);
customElements.define('tf-profile', TfProfileElement);

File diff suppressed because it is too large Load Diff

View File

@ -23,10 +23,10 @@ class TfTabConnectionsElement extends LitElement {
this.connections = [];
this.stored_connections = [];
this.users = {};
tfrpc.rpc.getAllIdentities().then(function(identities) {
tfrpc.rpc.getAllIdentities().then(function (identities) {
self.identities = identities || [];
});
tfrpc.rpc.getStoredConnections().then(function(connections) {
tfrpc.rpc.getStoredConnections().then(function (connections) {
self.stored_connections = connections || [];
});
}
@ -43,10 +43,12 @@ class TfTabConnectionsElement extends LitElement {
render_room_peers(connection) {
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) {
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)}`)}`;
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)}`)}`;
}
}
@ -58,7 +60,12 @@ class TfTabConnectionsElement extends LitElement {
let self = this;
return html`
<li>
<button class="w3-button w3-dark-grey" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}>Connect</button>
<button
class="w3-button w3-dark-grey"
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
>
Connect
</button>
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
</li>
`;
@ -67,7 +74,12 @@ class TfTabConnectionsElement extends LitElement {
render_broadcast(connection) {
return html`
<li>
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(connection)}>Connect</button>
<button
class="w3-button w3-dark-grey"
@click=${() => tfrpc.rpc.connect(connection)}
>
Connect
</button>
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
${this.render_connection_summary(connection)}
</li>
@ -81,11 +93,20 @@ class TfTabConnectionsElement extends LitElement {
render_connection(connection) {
return html`
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.closeConnection(connection.id)}>Close</button>
<button
class="w3-button w3-dark-grey"
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
>
Close
</button>
<tf-user id=${connection.id} .users=${this.users}></tf-user>
${connection.tunnel !== undefined ? '🚇' : html`(${connection.host}:${connection.port})`}
${connection.tunnel !== undefined
? '🚇'
: html`(${connection.host}:${connection.port})`}
<ul>
${this.connections.filter(x => x.tunnel === this.connections.indexOf(connection)).map(x => html`<li>${this.render_connection(x)}</li>`)}
${this.connections
.filter((x) => x.tunnel === this.connections.indexOf(connection))
.map((x) => html`<li>${this.render_connection(x)}</li>`)}
${this.render_room_peers(connection.id)}
</ul>
`;
@ -97,34 +118,58 @@ class TfTabConnectionsElement extends LitElement {
<div class="w3-container">
<h2>New Connection</h2>
<textarea class="w3-input w3-dark-grey" id="code"></textarea>
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}>Connect</button>
<button
class="w3-button w3-dark-grey"
@click=${() =>
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
>
Connect
</button>
<h2>Broadcasts</h2>
<ul>
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
${this.broadcasts
.filter((x) => x.address)
.map((x) => self.render_broadcast(x))}
</ul>
<h2>Connections</h2>
<ul>
${this.connections.filter(x => x.tunnel === undefined).map(x => html`
<li>${this.render_connection(x)}</li>
`)}
${this.connections
.filter((x) => x.tunnel === undefined)
.map((x) => html` <li>${this.render_connection(x)}</li> `)}
</ul>
<h2>Stored Connections (WIP)</h2>
<ul>
${this.stored_connections.map(x => html`
<li>
<button class="w3-button w3-dark-grey" @click=${() => self.forget_stored_connection(x)}>Forget</button>
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(x)}>Connect</button>
${x.address}:${x.port} <tf-user id=${x.pubkey} .users=${self.users}></tf-user>
</li>
`)}
${this.stored_connections.map(
(x) => html`
<li>
<button
class="w3-button w3-dark-grey"
@click=${() => self.forget_stored_connection(x)}
>
Forget
</button>
<button
class="w3-button w3-dark-grey"
@click=${() => tfrpc.rpc.connect(x)}
>
Connect
</button>
${x.address}:${x.port}
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
</li>
`
)}
</ul>
<h2>Local Accounts</h2>
<ul>
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
${this.identities.map(
(x) =>
html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`
)}
</ul>
</div>
`;
}
}
customElements.define('tf-tab-connections', TfTabConnectionsElement);
customElements.define('tf-tab-connections', TfTabConnectionsElement);

View File

@ -27,15 +27,21 @@ class TfTabMentionsElement extends LitElement {
async load() {
console.log('Loading...', this.whoami);
let results = await tfrpc.rpc.query(`
SELECT messages.*
let results = await tfrpc.rpc.query(
`
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.author != ?
ORDER BY timestamp DESC limit 20
`,
['"' + this.whoami.replace('"', '""') + '"', JSON.stringify(this.following), this.whoami]);
[
'"' + this.whoami.replace('"', '""') + '"',
JSON.stringify(this.following),
this.whoami,
]
);
console.log('Done.');
this.messages = results;
}
@ -58,8 +64,15 @@ class TfTabMentionsElement extends LitElement {
this.load();
}
return html`
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
<tf-news
id="news"
whoami=${this.whoami}
.messages=${this.messages}
.users=${this.users}
.expanded=${this.expanded}
@tf-expand=${this.on_expand}
></tf-news>
`;
}
}
customElements.define('tf-tab-mentions', TfTabMentionsElement);
customElements.define('tf-tab-mentions', TfTabMentionsElement);

View File

@ -33,69 +33,70 @@ class TfTabNewsFeedElement extends LitElement {
if (this.hash.startsWith('#@')) {
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
WHERE messages.author = ?
ORDER BY sequence DESC
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
JOIN messages_refs ON mine.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT * FROM mine
`,
[
this.hash.substring(1),
]);
[this.hash.substring(1)]
);
return r;
} else if (this.hash.startsWith('#%')) {
return await tfrpc.rpc.query(
`
SELECT messages.*
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
WHERE id = ?1
UNION
SELECT messages.*
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages JOIN messages_refs
ON messages.id = messages_refs.message
WHERE messages_refs.ref = ?1
`,
[
this.hash.substring(1),
]);
[this.hash.substring(1)]
);
} else {
let promises = [];
const k_following_limit = 256;
for (let i = 0; i < this.following.length; i += k_following_limit) {
promises.push(tfrpc.rpc.query(
`
WITH news AS (SELECT messages.*
promises.push(
tfrpc.rpc.query(
`
WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp > ? AND messages.timestamp < ?
ORDER BY messages.timestamp DESC)
SELECT messages.*
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news
JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT messages.*
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news
JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id
UNION
SELECT news.* FROM news
`,
[
JSON.stringify(this.following.slice(i, i + k_following_limit)),
this.start_time,
/*
** Don't show messages more than a day into the future to prevent
** messages with far-future timestamps from staying at the top forever.
*/
new Date().valueOf() + 24 * 60 * 60 * 1000,
]));
[
JSON.stringify(this.following.slice(i, i + k_following_limit)),
this.start_time,
/*
** Don't show messages more than a day into the future to prevent
** messages with far-future timestamps from staying at the top forever.
*/
new Date().valueOf() + 24 * 60 * 60 * 1000,
]
)
);
}
return [].concat(...(await Promise.all(promises)));
}
@ -106,29 +107,26 @@ class TfTabNewsFeedElement extends LitElement {
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
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
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp > ?
AND messages.timestamp <= ?
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
JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
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
JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id
UNION
SELECT news.* FROM news
`,
[
JSON.stringify(this.following),
this.start_time,
last_start_time,
]);
[JSON.stringify(this.following), this.start_time, last_start_time]
);
this.messages = await this.decrypt([...more, ...this.messages]);
}
@ -139,14 +137,12 @@ class TfTabNewsFeedElement extends LitElement {
let content;
try {
content = JSON.parse(message?.content);
} catch {
}
if (typeof(content) === 'string') {
} catch {}
if (typeof content === 'string') {
let decrypted;
try {
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
} catch {
}
} catch {}
if (decrypted) {
try {
message.decrypted = JSON.parse(decrypted);
@ -165,34 +161,51 @@ class TfTabNewsFeedElement extends LitElement {
}
render() {
if (!this.messages ||
if (
!this.messages ||
this._messages_hash !== this.hash ||
this._messages_following !== this.following) {
console.log(`loading messages for ${this.whoami} (following ${this.following.length})`);
this._messages_following !== this.following
) {
console.log(
`loading messages for ${this.whoami} (following ${this.following.length})`
);
let self = this;
this.messages = [];
this._messages_hash = this.hash;
this._messages_following = this.following;
this.fetch_messages().then(this.decrypt.bind(this)).then(function(messages) {
self.messages = messages;
console.log(`loading mesages done for ${self.whoami}`);
}).catch(function(error) {
alert(JSON.stringify(error, null, 2));
});
this.fetch_messages()
.then(this.decrypt.bind(this))
.then(function (messages) {
self.messages = messages;
console.log(`loading mesages done for ${self.whoami}`);
})
.catch(function (error) {
alert(JSON.stringify(error, null, 2));
});
}
let more;
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
more = html`
<p>
<button class="w3-button w3-dark-grey" @click=${this.load_more}>Load More</button>
<button class="w3-button w3-dark-grey" @click=${this.load_more}>
Load More
</button>
</p>
`;
}
return html`
<tf-news id="news" whoami=${this.whoami} .users=${this.users} .messages=${this.messages} .following=${this.following} .drafts=${this.drafts} .expanded=${this.expanded}></tf-news>
<tf-news
id="news"
whoami=${this.whoami}
.users=${this.users}
.messages=${this.messages}
.following=${this.following}
.drafts=${this.drafts}
.expanded=${this.expanded}
></tf-news>
${more}
`;
}
}
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);

View File

@ -28,7 +28,7 @@ class TfTabNewsElement extends LitElement {
this.cache = {};
this.drafts = {};
this.expanded = {};
tfrpc.rpc.localStorageGet('drafts').then(function(d) {
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}');
});
}
@ -48,7 +48,9 @@ class TfTabNewsElement extends LitElement {
let news = this.shadowRoot?.getElementById('news');
if (news) {
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'));
}
}
@ -62,11 +64,16 @@ class TfTabNewsElement extends LitElement {
let type = 'private';
try {
type = JSON.parse(message.content).type || type;
} catch {
}
} catch {}
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) {
@ -96,23 +103,52 @@ class TfTabNewsElement extends LitElement {
}
on_keypress(event) {
if (event.target === document.body &&
event.key == '.') {
if (event.target === document.body && event.key == '.') {
this.show_more();
}
}
render() {
let profile = this.hash.startsWith('#@') ?
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
let profile = this.hash.startsWith('#@')
? html`<tf-profile
id=${this.hash.substring(1)}
whoami=${this.whoami}
.users=${this.users}
></tf-profile>`
: undefined;
return html`
<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-dark-grey"
@click=${this.show_more}
>
${this.new_messages_text()}
</button>
</p>
<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div>
<div><tf-compose id="tf-compose" whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
<div>
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
</div>
<div>
<tf-compose
id="tf-compose"
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
@tf-draft=${this.draft}
></tf-compose>
</div>
${profile}
<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>
`;
}
}

View File

@ -41,7 +41,7 @@ class TfTabQueryElement extends LitElement {
await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query));
let start_time = new Date();
try {
this.results = await tfrpc.rpc.query(query, [])
this.results = await tfrpc.rpc.query(query, []);
} catch (error) {
this.error = error;
}
@ -79,8 +79,15 @@ class TfTabQueryElement extends LitElement {
} else {
let keys = Object.keys(this.results[0]).sort();
return html`<table style="width: 100%; max-width: 100%">
<tr>${keys.map(key => html`<th>${key}</th>`)}</tr>
${this.results.map(row => html`<tr>${keys.map(key => html`<td>${row[key]}</td>`)}</tr>`)}
<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>`;
}
}
@ -100,15 +107,30 @@ class TfTabQueryElement extends LitElement {
let self = this;
return html`
<div style="display: flex; flex-direction: row; gap: 4px">
<textarea id="search" rows=8 class="w3-input w3-dark-grey" style="flex: 1; resize: vertical" @keydown=${this.search_keydown}>${this.query}</textarea>
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Execute</button>
<textarea
id="search"
rows="8"
class="w3-input w3-dark-grey"
style="flex: 1; resize: vertical"
@keydown=${this.search_keydown}
>
${this.query}</textarea
>
<button
class="w3-button w3-dark-grey"
@click=${(event) =>
self.search(self.renderRoot.getElementById('search').value)}
>
Execute
</button>
</div>
<div ?hidden=${this.duration === undefined}>
Took ${this.duration / 1000.0} seconds.
</div>
<div ?hidden=${this.duration === undefined}>Took ${this.duration / 1000.0} seconds.</div>
<div ?hidden=${this.duration !== undefined}>Executing...</div>
${this.render_error()}
${this.render_results()}
${this.render_error()} ${this.render_results()}
`;
}
}
customElements.define('tf-tab-query', TfTabQueryElement);
customElements.define('tf-tab-query', TfTabQueryElement);

View File

@ -27,23 +27,25 @@ class TfTabSearchElement extends LitElement {
async search(query) {
console.log('Searching...', this.whoami, query);
let search = this.renderRoot.getElementById('search');
if (search ) {
if (search) {
search.value = query;
search.focus();
search.select();
}
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
let results = await tfrpc.rpc.query(`
SELECT messages.*
let results = await tfrpc.rpc.query(
`
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value
ORDER BY timestamp DESC limit 100
`,
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]);
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
);
console.log('Done.');
search = this.renderRoot.getElementById('search');
if (search ) {
if (search) {
search.value = query;
search.focus();
search.select();
@ -84,4 +86,4 @@ class TfTabSearchElement extends LitElement {
}
}
customElements.define('tf-tab-search', TfTabSearchElement);
customElements.define('tf-tab-search', TfTabSearchElement);

View File

@ -17,8 +17,12 @@ class TfTagElement extends LitElement {
render() {
let number = this.count ? html` (${this.count})` : undefined;
return html`<a href="#q=${this.tag}" style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px">${this.tag}${number}</a>`;
return html`<a
href="#q=${this.tag}"
style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px"
>${this.tag}${number}</a
>`;
}
}
customElements.define('tf-tag', TfTagElement);
customElements.define('tf-tag', TfTagElement);

View File

@ -20,25 +20,28 @@ class TfUserElement extends LitElement {
render() {
let name = this.users?.[this.id]?.name;
name = name !== undefined ?
html`<a target="_top" href=${'#' + this.id}>${name}</a>` :
html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
name =
name !== undefined
? html`<a target="_top" href=${'#' + this.id}>${name}</a>`
: html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
if (this.users[this.id]) {
let image = this.users[this.id].image;
image = typeof(image) == 'string' ? image : image?.link;
return html`
<div style="display: inline-block; font-weight: bold">
<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" ?hidden=${image === undefined} src="${image ? '/' + image + '/view' : undefined}">
${name}
</div>`;
image = typeof image == 'string' ? image : image?.link;
return html` <div style="display: inline-block; font-weight: bold">
<img
style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%"
?hidden=${image === undefined}
src="${image ? '/' + image + '/view' : undefined}"
/>
${name}
</div>`;
} else {
return html`
<div style="display: inline-block; font-weight: bold">
${name}
</div>`;
return html` <div style="display: inline-block; font-weight: bold">
${name}
</div>`;
}
}
}
customElements.define('tf-user', TfUserElement);
customElements.define('tf-user', TfUserElement);

View File

@ -2,20 +2,32 @@ import * as linkify from './commonmark-linkify.js';
import * as hashtagify from './commonmark-hashtag.js';
function image(node, entering) {
if (node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')) {
if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')
) {
if (entering) {
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
this.lit(
'<video style="max-width: 100%; max-height: 480px" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</video>');
}
} else if (node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')) {
} else if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')
) {
if (entering) {
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
this.lit(
'<audio style="height: 32px; max-width: 100%" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
@ -25,7 +37,11 @@ function image(node, entering) {
} else {
if (entering) {
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)) {
this.lit('<img src="" alt="');
} else {
@ -50,22 +66,28 @@ export function markdown(md) {
let writer = new commonmark.HtmlRenderer();
writer.image = image;
let parsed = reader.parse(md || '');
parsed = linkify.transform(parsed);
parsed = hashtagify.transform(parsed);
parsed = linkify.transform(parsed);
let walker = parsed.walker();
let event, node;
while ((event = walker.next())) {
node = event.node;
if (event.entering) {
if (node.type == 'link') {
if (node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')) {
if (
node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')
) {
node.destination = '#' + node.destination;
} else if (node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')) {
} else if (
node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')
) {
node.destination = '#' + node.destination;
} else if (node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')) {
} else if (
node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')
) {
node.destination = '/' + node.destination + '/view';
}
} else if (node.type == 'image') {
@ -90,4 +112,4 @@ export function human_readable_size(bytes) {
}
}
return `${Math.round(v * 10) / 10} ${u}`;
}
}

View File

@ -1,32 +1,32 @@
.tribute-container {
position: absolute;
top: 0;
left: 0;
height: auto;
overflow: auto;
display: block;
z-index: 999999;
position: absolute;
top: 0;
left: 0;
height: auto;
overflow: auto;
display: block;
z-index: 999999;
}
.tribute-container ul {
margin: 0;
margin-top: 2px;
padding: 0;
list-style: none;
background: #efefef;
margin: 0;
margin-top: 2px;
padding: 0;
list-style: none;
background: #efefef;
}
.tribute-container li {
padding: 5px 5px;
cursor: pointer;
padding: 5px 5px;
cursor: pointer;
}
.tribute-container li.highlight {
background: #ddd;
background: #ddd;
}
.tribute-container li span {
font-weight: bold;
font-weight: bold;
}
.tribute-container li.no-match {
cursor: default;
cursor: default;
}
.tribute-container .menu-highlighted {
font-weight: bold;
}
font-weight: bold;
}

View File

@ -1,4 +1,4 @@
{
"type": "tildefriends-app",
"emoji": "☑️"
}
"type": "tildefriends-app",
"emoji": "☑️"
}

View File

@ -27,7 +27,8 @@ async function todo_add(list) {
let set = new Set(names);
set.add(list);
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;
}
@ -42,7 +43,8 @@ async function todo_remove(list) {
let set = new Set(names);
set.delete(list);
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);
return exchanged;
@ -79,4 +81,4 @@ async function main() {
await app.setDocument(utf8Decode(getFile('index.html')));
}
main();
main();

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<title>TODO</title>
@ -8,4 +8,4 @@
<tf-todos></tf-todos>
</body>
<script src="script.js" type="module"></script>
</html>
</html>

View File

@ -4,7 +4,7 @@ import * as tfrpc from '/static/tfrpc.js';
class TodosElement extends LitElement {
static get properties() {
return {
lists: {type: Array}
lists: {type: Array},
};
}
@ -12,11 +12,14 @@ class TodosElement extends LitElement {
super();
this.lists = [];
let self = this;
tfrpc.rpc.todo_get_all().then(function(lists) {
self.lists = lists;
}).catch(function(error) {
console.log(error);
});
tfrpc.rpc
.todo_get_all()
.then(function (lists) {
self.lists = lists;
})
.catch(function (error) {
console.log(error);
});
}
async new_list() {
@ -32,9 +35,15 @@ class TodosElement extends LitElement {
return html`
<div>
<div style="display: flex">
${this.lists.map(x => html`
<tf-todo-list name=${x.name} .items=${x.items} @change=${this.refresh}></tf-todo-list>
`)}
${this.lists.map(
(x) => html`
<tf-todo-list
name=${x.name}
.items=${x.items}
@change=${this.refresh}
></tf-todo-list>
`
)}
</div>
<input type="button" @click=${this.new_list} value="+ List"></input>
</div>`;
@ -59,16 +68,22 @@ class TodoListElement extends LitElement {
save() {
let self = this;
console.log('saving', self.name, self.items);
tfrpc.rpc.todo_set(self.name, self.items).then(function() {
console.log('saved', self.name, self.items);
}).catch(function(error) {
console.log(error);
});
tfrpc.rpc
.todo_set(self.name, self.items)
.then(function () {
console.log('saved', self.name, self.items);
})
.catch(function (error) {
console.log(error);
});
}
remove_item(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();
}
@ -106,20 +121,20 @@ class TodoListElement extends LitElement {
let self = this;
if (index === this.editing) {
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
id="edit"
type="text"
value=${item.text}
@change=${event => self.input_change(event, item)}
@keydown=${event => self.input_keydown(event, item)}
@blur=${x => self.input_blur(item)}></input>
<span @click=${x => self.remove_item(item)} style="cursor: pointer">❎</span></div>
@change=${(event) => self.input_change(event, item)}
@keydown=${(event) => self.input_keydown(event, item)}
@blur=${(x) => self.input_blur(item)}></input>
<span @click=${(x) => self.remove_item(item)} style="cursor: pointer">❎</span></div>
`;
} else {
return html`
<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>
<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>
`;
}
}
@ -139,14 +154,17 @@ class TodoListElement extends LitElement {
rename(new_name) {
let self = this;
return tfrpc.rpc.todo_rename(this.name, new_name).then(function() {
self.dispatchEvent(new Event('change'));
self.editing_name = false;
}).catch(function(error) {
console.log(error);
alert(error.message);
self.editing_name = false;
});
return tfrpc.rpc
.todo_rename(this.name, new_name)
.then(function () {
self.dispatchEvent(new Event('change'));
self.editing_name = false;
})
.catch(function (error) {
console.log(error);
alert(error.message);
self.editing_name = false;
});
}
name_blur(new_name) {
@ -163,19 +181,25 @@ class TodoListElement extends LitElement {
render() {
let self = this;
let name = this.editing_name ?
html`<input
let name = this.editing_name
? html`<input
type="text"
id="edit"
@keydown=${event => self.name_keydown(event)}
@blur=${event => self.name_blur(event.srcElement.value)}
value=${this.name}></input>` :
html`<h2 @click=${x => this.editing_name = true}>${this.name}</h2>`;
@keydown=${(event) => self.name_keydown(event)}
@blur=${(event) => self.name_blur(event.srcElement.value)}
value=${this.name}></input>`
: html`<h2 @click=${(x) => (this.editing_name = true)}>${this.name}</h2>`;
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}
${(this.items || []).filter(item => !item.x).map(x => self.render_item(x))}
${(this.items || []).filter(item => item.x).map(x => self.render_item(x))}
${(this.items || [])
.filter((item) => !item.x)
.map((x) => self.render_item(x))}
${(this.items || [])
.filter((item) => item.x)
.map((x) => self.render_item(x))}
<button @click=${self.add_item}>+ Item</button>
<button @click=${self.remove_list}>- List</button>
</div>
@ -184,4 +208,4 @@ class TodoListElement extends LitElement {
}
customElements.define('tf-todo-list', TodoListElement);
customElements.define('tf-todos', TodosElement);
customElements.define('tf-todos', TodosElement);

1
apps/user_settings.json Normal file
View File

@ -0,0 +1 @@
{"type": "tildefriends-app", "emoji": "⚙️"}

60
apps/user_settings/app.js Normal file
View File

@ -0,0 +1,60 @@
import * as tfrpc from '/tfrpc.js';
tfrpc.register(async function getIdentities() {
return ssb.getIdentities();
});
tfrpc.register(async function createID(id) {
return await ssb.createIdentity();
});
tfrpc.register(async function getPrivateKey(id) {
return bip39Words(await ssb.getPrivateKey(id));
});
tfrpc.register(async function addID(id) {
return await ssb.addIdentity(bip39Bytes(id));
});
tfrpc.register(async function deleteID(id) {
return await ssb.deleteIdentity(id);
});
tfrpc.register(async function getThemes() {
// TODO
return ['solarized', 'gruvbox', 'light'];
});
tfrpc.register(async function getTheme() {
// TODO
return 'gruvbox';
});
tfrpc.register(async function setTheme() {
// TODO
console.warn('setTheme called - not implemented');
return null;
});
tfrpc.register(async function reload() {
await main();
});
async function main() {
// Get body.html
const body = utf8Decode(await getFile('body.html'));
// Build the document
const document = `
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/static/tildefriends-v1.css"/>
<script src="tf-theme-picker.js" type="module"></script>
<script src="tf-password-form.js" type="module"></script>
<script src="tf-delete-account-btn.js" type="module"></script>
<script src="tf-identity-manager.js" type="module"></script>
</head>
<body class="flex-column">
${body}
</body>
</html>`;
// Send it to the browser
app.setDocument(document);
}
main();

View File

@ -0,0 +1,20 @@
<h1>Your settings</h1>
<div class="box flex-column">
<h2>Appearance</h2>
<tf-theme-picker></tf-theme-picker>
</div>
<div class="box flex-column">
<h2>Danger Zone</h2>
<h3>Manage your identities</h3>
<tf-identity-manager></tf-identity-manager>
<h3>Change my password</h3>
<tf-password-form></tf-password-form>
<h3>Delete your account</h3>
<tf-delete-account-btn></tf-delete-account-btn>
</div>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,36 @@
import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
class TfDeleteAccountButtonElement extends LitElement {
static get properties() {
return {};
}
constructor() {
super();
}
deleteAccount() {
const res = confirm(
'Are you really sure you want to delete your account ?'
);
if (!res) return;
console.warn('TODO');
}
render() {
return html`
<link rel="stylesheet" href="/static/tildefriends-v1.css" />
<span>This action is irreversible !</span>
<button class="red" @click=${this.deleteAccount}>
[Not implemented] Delete my Tilde Friends account
</button>
`;
}
}
customElements.define('tf-delete-account-btn', TfDeleteAccountButtonElement);

Some files were not shown because too many files have changed in this diff Show More