Compare commits

...

695 Commits

Author SHA1 Message Date
6609a5f340 core: Length of undefined is 0. It's fine. Quiet some errors.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m51s
2024-12-18 20:54:13 -05:00
d9972cb349 tests: Work around an intermittent -t=auto failure. The 'Edit Profile' click is getting lost as things rapidly update? I haven't ever seen it as a human clicking.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m8s
2024-12-18 20:09:50 -05:00
28d2539432 ssb: A first pass at showing private messages next to channels. #84
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-18 20:03:53 -05:00
f28386b71f ssb: Off by one on the unread line.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m32s
2024-12-18 12:46:02 -05:00
53717076f5 ssb: Fix some unread marker issues.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-18 12:43:25 -05:00
a9aa928629 tests: Prefer tf_printf.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 3m33s
2024-12-17 20:41:27 -05:00
8df121148d update: c-ares 1.34.4.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m29s
2024-12-15 08:33:38 -05:00
5e23c32ae8 build: Fix a potential null dereference?
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 21m9s
2024-12-15 07:53:24 -05:00
9c0f6481c0 ssb: Try to go easier on the main thread, still.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 14m8s
2024-12-14 21:36:33 -05:00
68ae45dd58 ssb: Prevent -t=bench from stalling.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m50s
2024-12-11 20:53:25 -05:00
3091747438 ssb: prettier.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-11 20:35:32 -05:00
2f266b8dd4 ssb: Attempt to request more feeds as more contact messages come in. #83
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-11 20:26:28 -05:00
ee20b87ee2 ssb: Alt+up/down to cycle through channels.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m53s
2024-12-11 12:53:04 -05:00
83e025d0bb update: CodeMirror.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-11 12:41:42 -05:00
5115c6e217 ssb: Fix an instance of channels being stuck unread.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m22s
2024-12-10 21:09:55 -05:00
76f6a94de5 ssb: Fix replication hops usage. Thanks @Cashew.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m15s
2024-12-10 19:18:01 -05:00
954830be18 ssb: Allow encrypting/decrypting with the server identity as an admin.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m45s
2024-12-10 12:43:07 -05:00
ea70299a45 update: sqlite 3.47.2.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m59s
2024-12-08 16:47:21 -05:00
88da071ed6 ssb: We can load more messages by author, now.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m44s
2024-12-08 09:40:02 -05:00
1dbf162a71 ssb: Bring back the updating date while loading.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m54s
2024-12-07 14:58:01 -05:00
1c0964753b ssb: Correctness around loading messages by time range.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m39s
2024-12-07 14:25:19 -05:00
daa1c7f577 ssb: Don't miss contact messages that aren't followed by non-follow messages. 2024-12-07 14:08:53 -05:00
854416ceb2 ssb: Make the depth arg to ssb.following() match the docs.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m42s
2024-12-07 11:28:33 -05:00
2230351e3e ssb: Show the load more button for mentions.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-07 10:38:34 -05:00
7da3244da2 ssb: prettier.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m29s
2024-12-05 20:47:02 -05:00
bfeb0c2988 update: prettier. 2024-12-05 20:46:23 -05:00
d4e75c1dec ssb: Move mentions into the channels sidebar.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-05 20:45:20 -05:00
405bddcde0 ssb: Make the tab bar stay on top of the content. Weird, the random things that were showing up on top.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m35s
2024-12-04 21:23:17 -05:00
8a27c45ab1 ssb: An experiment in including hashtag mentions in channel content. Also sort the channel list like I thought I already did.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m54s
2024-12-04 20:50:46 -05:00
10b15896b3 ssb: Fix the loading cancel button.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m19s
2024-12-04 20:28:57 -05:00
0e97bbe37c android: Fix some crashes, callstacks, and warnings I'm seeing in the logs.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m27s
2024-12-04 20:05:50 -05:00
e0d7e90894 ssb: Add an overlay for the sidebar so that it can be closed by tapping back on the content.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m42s
2024-12-03 19:40:08 -05:00
5d13f6aab6 wiki: Back to latest commonmark built as mjs.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m26s
2024-12-02 18:45:23 -05:00
1ebfbbe89e wiki: Go back to the last version that worked.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m58s
2024-12-02 17:57:08 -05:00
91ad43fdfc ssb: A more plausibly correct way to load new messages correctly.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 14m51s
2024-12-01 18:20:57 -05:00
6fe6fc180d ssb: New theme, better load, remove debug prints.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m11s
2024-12-01 16:27:59 -05:00
d84d0bec38 ssb: This index help channel status load faster.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-12-01 16:26:40 -05:00
7e7b1c6ee1 ssb: Make #hashtags direct to channels.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m25s
2024-12-01 15:32:35 -05:00
effb354d1b ssb: Working toward a more sensible unread indication and user interface for setting read/unread.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m5s
2024-12-01 12:56:31 -05:00
ba7d1ad35f core: This case is not a good cause to crash.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 28m0s
2024-12-01 09:48:15 -05:00
3ca2b19502 ssb: Canceling loads, more mobile-friendly sidebar, and respond to channel subscription changes.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m30s
2024-11-30 17:49:36 -05:00
8e0d91dcf5 security: Setting global settings requires approval.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m8s
2024-11-30 16:58:48 -05:00
cd2c2587ae ssb: Merge in the new very work in progress channels interface.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m1s
2024-11-30 15:05:14 -05:00
53044696ba ssb: Just request blobs for all references from about messages for now. Much faster than narrowing down to the most recent images.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m21s
2024-11-29 10:28:16 -05:00
6d6927213f Revert "ssb: Try harder to replicate profile images, even if they were set before our blob replication cutoff."
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
This reverts commit 7f4e2617ee.
2024-11-29 08:54:54 -05:00
be1b5bce4f test: Simplify my selection helper syntax a bit.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m17s
2024-11-28 19:58:51 -05:00
4b4fd0735b test: Make -t auto a bit more resilient by hoisting all the retries into one place that makes sense to me.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-11-28 17:51:22 -05:00
c565b2a31f bot: Make sure release messages get through.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-11-28 11:11:25 -05:00
55f2261905 prettier: Update the copy of prettier used in the editor. 2024-11-28 11:00:59 -05:00
51912f2b83 ssb: Update emojis.json, and add a script to regenerate it.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-11-28 09:16:07 -05:00
7f4e2617ee ssb: Try harder to replicate profile images, even if they were set before our blob replication cutoff.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-11-27 21:42:54 -05:00
960a385202 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 14m58s
2024-11-27 15:20:32 -05:00
21f48d3485 build: Let's start work on 0.0.26.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m23s
2024-11-27 12:24:42 -05:00
7f9605e55f nix: Update for 0.0.25. 2024-11-27 12:23:52 -05:00
cc409dc3f7 build: Let's build 0.0.25.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m0s
2024-11-27 12:10:17 -05:00
af6091760c ssb+docs: prettier.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-27 12:07:00 -05:00
e1d93c003c docs: Update docs from wiki.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m5s
2024-11-27 10:13:16 -05:00
ff9dd2dd03 haiku: Disable a bit of a test that is giving me an SQLITE_PROTOCOL error only on Haiku.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-27 15:05:23 -05:00
7a306bb3d2 build: Fix a regex warning.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m11s
2024-11-27 14:36:50 -05:00
7ffc148358 build: I wanted to get the binary out of the makefile to appease F-Droid, and one thing lead to another.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-27 09:28:14 -05:00
50fef2edfa build: Fix on OpenBSD. TIL awk.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m11s
2024-11-27 09:06:02 -05:00
aa40084010 build: Redid this thing in sed to make it work on more platforms.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m16s
2024-11-26 22:55:01 -05:00
740d788c7c storage: Show accounts with the most follows, for help pruning accounts.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m5s
2024-11-26 16:25:15 -05:00
4c2fa2c1b3 storage: Show totals, too.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m26s
2024-11-26 16:05:28 -05:00
4350c7b7a9 storage: Add a little app to show something about feed sizes. 2024-11-26 15:59:02 -05:00
595f14d98d docs: Update some docs links to the gitea wiki and generally refresh the README.md slightly.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m38s
2024-11-26 11:42:33 -05:00
2e95d6ea63 docs: Add the Tilde Friends gitea wiki as a git submodule to replace the docs directory. Maybe I will succeed at doing something with it if it is more web-facing.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-26 11:30:57 -05:00
0da6abeb98 ssb: We can trace request names these days.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-26 11:14:30 -05:00
e4e050e8e7 ssb: Fix some message link encoding.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m41s
2024-11-26 08:42:51 -05:00
5bc082b75e build: Prepare a changelog for the next release.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m53s
2024-11-25 21:12:00 -05:00
beedbd7646 build: Attempt to self-document the makefile. 2024-11-25 21:11:36 -05:00
507b069ffe cleanup: prettier. 2024-11-25 20:05:40 -05:00
71444b0427 ssb: Shutdown fixes.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m12s
2024-11-25 17:14:16 -05:00
a08bba438e update: sqlite 3.47.1. 2024-11-25 13:16:20 -05:00
df1e6711af ssb: Add a setting to periodically clean up un-followed feeds. #80
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 16m5s
2024-11-25 12:53:28 -05:00
f6d4e934e3 ssb: Adjust the follow/hops policies. Replication defaults to 2 hops, counted in the same way as the docs, and is configurable. #79
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-11-25 11:20:01 -05:00
d5bd4c6735 test: Use -t=auto to update some screenshots. 2024-11-25 09:53:11 -05:00
eb12ba6ed2 test: Use -t=auto to generate some screenshots, detect -t=auto failure more reliably, exercise setting the initial profile, and fix various bugs that fell out.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-11-25 09:38:49 -05:00
6e83c08535 ssb: Add an index that helps me calculate feed size about 8x faster. 2024-11-23 17:50:32 -05:00
b6bfdec48d ssb: Move the refresh/sync button to the navigation bar as an experiment.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-23 16:49:33 -05:00
f9ec796291 bot: Give more information about new issues.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-11-23 13:50:23 -05:00
3beb1d0683 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m46s
2024-11-20 20:26:30 -05:00
8836c7f0ca cleanup: prettier + format. 2024-11-20 20:24:58 -05:00
ef5ce1d6e1 web: Show the little graphs if the Tilde Friends verison thingy is expanded. I want to be able to optionally see these on mobile.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-20 20:06:33 -05:00
0ea1213139 ssb: Use the same refresh character in two places. 2024-11-20 19:46:41 -05:00
51fe372f60 ssb: Stick the stylesheet on document.body. No more fonts changing when various dialogs show up. 2024-11-20 19:44:27 -05:00
eb8f9f8936 web: Add some meta tags to make it show up better in search engines / embeds maybe. #43 2024-11-20 19:24:13 -05:00
afc1524874 bot: Scrape my changes better from gitea RSS.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m33s
2024-11-18 22:58:51 -05:00
fbb975625c update: speedscope 1.21.0.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-17 19:07:27 -05:00
53e75d8209 cleanup: Consolidate countof macros.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 20m32s
2024-11-13 20:22:42 -05:00
5bdf970c10 ssb: Don't list broadcasts for identities to which we are already connected.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-13 19:23:04 -05:00
50089f72c6 ssb: We can show what state a connection is in. 2024-11-13 19:15:59 -05:00
62e15e0208 update: CodeMirror. 2024-11-13 19:03:01 -05:00
3d8b02a7f3 ssb+issues+core: prettier 2024-11-13 18:58:09 -05:00
20701d9cf1 ssb: Missed a few connection failures for context.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m5s
2024-11-13 18:44:14 -05:00
fa94442eb2 ssb: Populate more connection errors with context. 2024-11-13 18:35:17 -05:00
68ff77e172 ssb: Hook up connect error messages more thoroughly. 2024-11-13 18:20:14 -05:00
102e9be3a8 update: c-ares 1.34.3. 2024-11-13 17:54:10 -05:00
92bf01a183 ssb+issues: Fix missing dependencies of my commonmarkjs extensions.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m56s
2024-11-12 21:47:15 -05:00
559504ae29 security: Use commonmarkjs with {safe: true} as intended.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-12 20:43:03 -05:00
9b00b41a1e ssb: Connection result preliminary hookup to ui, and fix some fallout.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m5s
2024-11-11 22:24:54 -05:00
b1f6ad17e1 ssb: Pass around reasons for failing to connect. This will help get that information to the ui when I finish hooking it up. 2024-11-11 22:12:41 -05:00
e7979fe9db test: Add more retries until selenium cooperates.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m51s
2024-11-11 21:06:04 -05:00
7a276adbbc ssb: Size blob ID buffers appropriately. 2024-11-11 21:05:29 -05:00
db4997fdc4 bot: Remove the header.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m7s
2024-11-11 08:19:46 -05:00
44ebb841f0 bot: Fix empty buttfeed posts, and use requested RSS feed for Habitat.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m35s
2024-11-10 19:30:36 -05:00
09ae4e2096 ssb: Handle the inevitable %25 in a document hash better?
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m14s
2024-11-09 18:04:58 -05:00
0b46efe4ea test: Hack around an intermitted -t=auto failure.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m49s
2024-11-09 15:09:08 -05:00
f1dda43e66 ui: Click off the identity menu to close it.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-09 14:52:54 -05:00
ce483138d7 ssb: Fighting with profile CSS. 2024-11-09 14:41:40 -05:00
73cc39226d bot: Some fixes to get SecureScuttlebuttFeed running.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m26s
2024-11-09 09:24:13 -05:00
57257f63dd bot: Add a little script to post about recent development activity from a handful of RSS feeds I've gathered.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m1s
2024-11-09 09:01:34 -05:00
88b25790e8 ssb: Remove some pointless logging.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m3s
2024-11-06 20:56:10 -05:00
e01defc4aa update: CodeMirror. 2024-11-06 20:49:03 -05:00
cb50c43e93 build: We all cope in our own ways.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-06 20:42:49 -05:00
5908d15f91 js: Move default global settings to C.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-11-06 20:29:48 -05:00
f66cfaec12 http: Fix some caching issues.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m8s
2024-11-06 12:41:54 -05:00
259f92c53b http: Populate query and headers for handler.js like we used to.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m27s
2024-11-04 21:46:38 -05:00
a84f850e91 http: Bring back handler.js support, mostly. Partly in C, this time.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m25s
2024-11-03 21:09:57 -05:00
5a765e6f07 js: Also unused. 2024-11-03 07:44:31 -05:00
791889c659 js: Remove some unused code. 2024-11-03 07:38:52 -05:00
5da63faf1f http+js: Move app blob handling from JS to C. handler.js support has been temporarily removed. 2024-11-02 21:37:14 -04:00
30d108fc35 http: URL pattern matcher fixes. 2024-11-02 20:10:55 -04:00
a09fefab5e http: Add a more expressive but still nowhere near regex URL pattern matcher.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 14m57s
2024-11-02 19:22:04 -04:00
f74ca1c236 test: Remove some debug prints, whoops.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m13s
2024-11-02 16:32:05 -04:00
30e027092b test: Cover more ways to request apps and files.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m15s
2024-11-02 15:43:03 -04:00
fd4ac7c9b9 test: Test some expectes results from http requests to various paths.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 9m46s
2024-11-02 14:11:54 -04:00
4482049b94 log: Show the version number in the welcome banner.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m53s
2024-11-02 08:45:47 -04:00
5839380437 update: CodeMirror to latest. 2024-11-02 08:45:47 -04:00
2152470fdc update: libbacktrace to latest. 2024-11-02 08:45:47 -04:00
93b2a81495 test: Fix -t=publish on haiku.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m30s
2024-11-01 18:55:27 -04:00
e139e952c0 ssb: Fix some spacing in the permissions dialog.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m25s
2024-11-01 18:18:16 -04:00
cf1c57ccb8 build: Let's start work on 0.0.25.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m50s
2024-11-01 18:01:10 -04:00
f7a2138488 nix: Update version to 0.0.24. 2024-10-30 19:40:12 -04:00
9614d03bef ssb: Fix a timer leak I observed trying to wrap up 0.0.24.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m17s
2024-10-30 19:32:24 -04:00
32a335c676 test: Retry harder. 2024-10-30 19:32:05 -04:00
06e27fc1e0 docs: Update the 0.0.24 changelog. 2024-10-30 19:31:33 -04:00
1f40e8dcd9 build: Let's build 0.0.24.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m58s
2024-10-30 12:56:20 -04:00
77ff8cef1f db: Fix the db app, and show a message instead of an ugly error when you're not signed in.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m58s
2024-10-29 20:19:50 -04:00
ef844fbccb build: Oh, you can generate a .flatpak file, if that's your thing.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 14m50s
2024-10-27 18:50:07 -04:00
070dc5a4c0 build: flatpak filesystem access tweaks.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m38s
2024-10-27 14:37:37 -04:00
177ef1cdcc build: A flatpak experiment. I still don't get it.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-10-27 14:31:11 -04:00
4b1ebf02e1 js: Remove a stale /save reference, and exercise re-saving an app in the test.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m36s
2024-10-27 14:05:20 -04:00
863e50203e js: Move /save to C.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m51s
2024-10-27 13:42:56 -04:00
01b8c209de core: Testing a theory to encourage clean shutdowns.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m30s
2024-10-25 15:34:43 -04:00
30e92f2bc1 js: Fix typo in /delete. 2024-10-25 15:34:01 -04:00
02accabb4a js: Oh yeah, administrators can delete core apps still.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 12m2s
2024-10-25 15:20:54 -04:00
fa00a41fe0 js: Move app delete to C.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 14m34s
2024-10-25 13:58:06 -04:00
2e66666bdf ssb: Indicate which connections are one-shot / sync now connections.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m19s
2024-10-25 12:53:45 -04:00
4fe3c9a751 test: Verify that deleting apps actually does something. 2024-10-25 12:34:22 -04:00
0a35e14590 js: Fix database.getall().
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m6s
2024-10-23 21:50:34 -04:00
e979c176e3 test: Exercise nominally creating and deleting an app.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m15s
2024-10-23 18:48:42 -04:00
a0d9c3dc29 js: Move the global 404 response to C. 2024-10-23 18:27:36 -04:00
efcb68351c docs: Reminding myself how to make a release. 2024-10-23 18:06:56 -04:00
94e8bf2e1c test: Add some nominal testing for the new publish command.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m34s
2024-10-23 15:57:44 -04:00
82d1a294a6 ssb: prettier.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m38s
2024-10-23 15:38:49 -04:00
de20274589 ssb: Add a publish command that can be used to publish messages from the command-line. 2024-10-23 15:38:07 -04:00
2f193e64c8 ssb: Show muxrpc command names when possible in verbose logging. 2024-10-23 15:37:28 -04:00
86751362cb ssb: Indicate which muxrpc sends failed, and use that to fix some replication nonsense and log noise.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m22s
2024-10-23 14:13:55 -04:00
4118323631 build: Fix some cflags not making it to libsodium. Disable the libsodium -flto warning a more different way.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m25s
2024-10-23 12:02:41 -04:00
0d134f7f10 update: CodeMirror to latest. 2024-10-23 11:34:30 -04:00
409724cfcd update: OpenSSL 3.4.0. 2024-10-23 11:32:45 -04:00
799a33be40 update: libbacktrace to latest. 2024-10-23 11:20:23 -04:00
2fa9c66495 update: libuv 1.49.2. 2024-10-23 11:05:15 -04:00
ad818a8e7e update: sqlite 3.47.0. 2024-10-23 11:01:42 -04:00
581f72b3f8 ssb: Disallow rich text paste on Firefox. Didn't realize it doesn't support contenteditable='plaintext-only'.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m1s
2024-10-17 12:41:50 -04:00
1dd7e4347c js: Kill setGlobalSettings.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m0s
2024-10-16 21:02:48 -04:00
36cc9398c7 js: Move storePermission to C. 2024-10-16 20:36:53 -04:00
68817feeec js: Kill getSessionProcessBlob.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 14m37s
2024-10-16 19:50:31 -04:00
97661e2ca2 http: Fix some headers. 2024-10-16 19:26:26 -04:00
72def5ae6d js: Move /view to C. 2024-10-16 19:16:45 -04:00
e638b155a1 js: Kill gGlobalSettings. Decouples things a bit. 2024-10-16 18:11:08 -04:00
32db18b0d6 ssb: Close the reactions list dialog by clicking off of it.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 14m49s
2024-10-16 12:35:10 -04:00
b653a5250d build: Appease gcc 14.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m48s
2024-10-15 12:41:47 -04:00
30329f7cad ssb: No duplicate connections, even with tunnels. This is confusing.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m32s
2024-10-14 12:44:21 -04:00
29a1478c86 ssb: No duplicate tunnels.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 14m41s
2024-10-13 18:13:31 -04:00
c882bf31ec docs: grammar
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m26s
2024-10-13 14:58:21 -04:00
17ccb8f083 update: libuv 1.49.1.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-10-13 14:46:24 -04:00
0e7d2a8b0e ssb: The identity app now lets you switch out the server identity if you are an administrator.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-10-13 14:40:14 -04:00
3743543ef8 welcome: prettier 2024-10-13 14:19:55 -04:00
700dd7b45a build: Appease OpenBSD.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 14m41s
2024-10-11 19:04:49 -04:00
c2eb73fd8a update: c-ares 1.34.1.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m18s
2024-10-11 18:25:09 -04:00
e1f4f7f95b ssb: Don't stretch profile images when fitting into a circle. I swear I did this ages ago.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m23s
2024-10-10 21:41:34 -04:00
37401409c6 ios: Include data in the app. How did this ever work?
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-10-10 21:32:35 -04:00
b282631cd5 ios: Mobile provision junk.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 14m58s
2024-10-10 20:43:13 -04:00
9618d3b3f3 welcome: Link to the open testing Google Play build.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m24s
2024-10-09 12:20:37 -04:00
c9f997d121 update: commonmark 0.31.2.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m38s
2024-10-08 20:40:29 -04:00
f1dee2a089 ssb: Why would I not log the host of failed DNS requests? 2024-10-08 20:16:04 -04:00
8273277c91 editor: Fix in-browser prettification of html files. 2024-10-08 20:15:04 -04:00
9758844da3 editor: Fix visible whitespace toggle. 2024-10-08 19:56:58 -04:00
e41c7fbbc7 welcome: prettier
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 20m10s
2024-10-08 19:41:55 -04:00
24db8a5a49 welcome: Slight wording updates.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-10-08 19:28:23 -04:00
36e82b9873 ssb: Sync now connects to room members one level deep.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-10-08 19:10:33 -04:00
8a32f2b8b1 Latest CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m57s
2024-10-08 12:44:49 -04:00
277830bc3c format: Sort includes. Yes, please. 2024-10-08 12:19:44 -04:00
a8fa969114 Lit 3.2.1. 2024-10-08 12:18:37 -04:00
c3f3dced68 docs: I figured out how to disable the gitea release zips lacking submodules (DISABLE_DOWNLOAD_SOURCE_ARCHIVES), so remove the caveat about them from the docs.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 20m10s
2024-10-06 12:16:30 -04:00
85fce59c0c ssb: Sync on demand fixes. Avoid keeping message streams live in this mode.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m17s
2024-10-06 11:50:49 -04:00
8a6147d512 ssb: Beginnings of a "sync now" mode for mobile.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m52s
2024-10-06 11:14:37 -04:00
e799b256b2 ssb: Even more muxrpc activity status fixes.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 21m51s
2024-10-05 21:00:50 -04:00
b222dc0ca8 Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-10-05 20:44:17 -04:00
c52c6b04ca ssb: muxrpc activity status fixes. 2024-10-05 20:44:01 -04:00
b95eed46bb ssb: Fix a request leak in tunnel.connect.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m11s
2024-10-04 22:05:17 -04:00
7c36a543da ssb: Fix a leaked request and a shutdown error.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m35s
2024-10-04 12:39:39 -04:00
90e000c18e ssb: Fix activity indication of muxrpc requests expiring.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m54s
2024-10-03 12:41:45 -04:00
1bb9d737d8 ssb: Show activity for each muxrpc request.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m23s
2024-10-02 20:43:51 -04:00
9a5db2ec51 appimage: Put update information in the appimage.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m42s
2024-10-02 20:03:44 -04:00
dbed29a044 Update prettier. And run it some more.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m57s
2024-10-02 18:49:17 -04:00
681859531c muxrpc: Simplifying comparing RPC names. This has just always bugged me.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-10-02 18:46:12 -04:00
8e1ad6b16a js: Unused external. 2024-10-02 18:12:28 -04:00
5448f1ba2d Update CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m30s
2024-10-02 18:00:40 -04:00
e43da4e1a3 welcome: Better OpenSSL link.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-10-02 17:56:36 -04:00
eaa9da49cc welcome: F-Droid/AppImage links.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-10-02 17:55:01 -04:00
40873b529c build: Suppress a warning in libuv on arm.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m47s
2024-09-30 12:37:41 -04:00
8cc4c19d73 Prettier and generated files I've missed.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m45s
2024-09-30 12:15:27 -04:00
bb9c18faf1 Some missing log newlines. 2024-09-30 12:13:57 -04:00
fabdfb76b9 android: readParcelable compatibility.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m37s
2024-09-29 08:18:46 -04:00
bce263a928 android: Use FileObserver, which is actually compatible with api level 24 which we claim to support.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m39s
2024-09-29 00:17:38 -04:00
195920e476 android: Avoid a ClosedWatchServiceException.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m40s
2024-09-28 23:25:28 -04:00
a821d895c5 docs: Give working advice on how to get the tree and dependencies.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m3s
2024-09-28 07:11:47 -04:00
ab1b6ec27d build: Add a dependency off appimagetool??
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m34s
2024-09-27 22:10:01 -04:00
6dc099809f build: This creates a working AppImage.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-27 21:19:18 -04:00
03c8b75994 Let's start work on 0.0.24.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m37s
2024-09-25 20:26:57 -04:00
38887452ad nix => 0.0.23.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-09-25 20:20:14 -04:00
7512edad59 build: I forgot to build the .xz as part of dist.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m13s
2024-09-25 20:02:45 -04:00
944c895bcd Generated 0.0.23 files. Oops.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-25 19:55:12 -04:00
e7d87ee8e2 I think that worked. Let's build 0.0.23.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-25 19:49:52 -04:00
cfdbd10635 ci: Oh, we can not require fuse?
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 1m47s
2024-09-25 19:45:05 -04:00
d3a2d8733f ci: Maybe we don't need to manually load it?
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 1m41s
2024-09-25 19:37:17 -04:00
a7e623d817 ci: apt install fuse3?
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 52s
2024-09-25 19:35:47 -04:00
3f0c37cea4 ci: modprobe harder.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 46s
2024-09-25 19:33:30 -04:00
2c96a6d22a ci: modprobe fuse? Disable things to get this going faster until it works.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 2m28s
2024-09-25 19:30:33 -04:00
57b4214a72 AppImages require FUSE to run.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m21s
2024-09-25 19:10:13 -04:00
433b3d39d9 ci: Build the appimage, for real.nofoolin.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m9s
2024-09-25 18:50:06 -04:00
26441ed45c Let's try to artifact the appimage.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m35s
2024-09-25 12:49:32 -04:00
92cd38c2a0 Change notes.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-25 12:33:06 -04:00
3b5a06794f libuv 1.49.0.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m39s
2024-09-25 12:11:17 -04:00
d104409272 Prevent votes from overflowing.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m23s
2024-09-23 12:43:48 -04:00
e5f58c2898 Produce user info for the server identity for admin users.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m52s
2024-09-19 12:22:38 -04:00
f83863ef01 This doc was ancient, so paste some of the latest from the wiki in. It's something.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m18s
2024-09-18 20:16:35 -04:00
837f069cf5 Update CodeMirror. 2024-09-18 20:16:35 -04:00
9f057dc29a Add F-Droid and c-ares to the welcome page.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-18 20:01:36 -04:00
c4904f176c Distinguish the server identity in the identity app.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m46s
2024-09-18 19:09:44 -04:00
d3a5aba703 A brave new world where admin users can use the server identity.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m31s
2024-09-17 12:47:28 -04:00
9e283e427c Fix viewing apps by blob ID URL.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m18s
2024-09-16 12:45:06 -04:00
133ba31d66 c-ares 1.33.1. 2024-09-15 08:57:22 -04:00
241a65a92a sus. Disable a warning in c-ares during ltcg with gcc 13 on Haiku.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 19m2s
2024-09-15 12:52:28 -04:00
0b54795bab Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m29s
2024-09-11 20:28:06 -04:00
6208193de5 Fix plumbing for replies on the search tab.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-11 20:25:55 -04:00
c53321532f Update CodeMirror. 2024-09-11 20:18:57 -04:00
34f25e3e06 How did I not have an index on type? Wow.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-11 19:53:07 -04:00
c46244366e I don't know what GHES is, but it says to use this version, so sure.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 15m50s
2024-09-11 19:52:43 -04:00
6518af04fc It finally built. Now let's upload some artifacts.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 16m15s
2024-09-11 19:33:37 -04:00
bf137ff1f7 Seriously? More volume mount config.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 20m30s
2024-09-11 19:00:30 -04:00
1877955b62 Syntax?
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-11 18:54:39 -04:00
50d0875de2 Mount volumes across another container?
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-09-11 18:49:06 -04:00
bf151e6b7d I am confused.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-11 18:41:55 -04:00
82893402d0 Maybe we can sign here?
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 17m55s
2024-09-10 21:54:36 -04:00
8049102787 More silent OpenSSL build for mingw.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 19m24s
2024-09-10 21:19:15 -04:00
f42cc3d9fd Find the Android SDKs you just installed.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-10 21:01:20 -04:00
5f9a5208db Appease the sdkmanager version number whatever.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 21m56s
2024-09-10 20:34:25 -04:00
6df506d238 Spelling.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 5m7s
2024-09-10 20:25:51 -04:00
2bd3354256 yaml better??
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 2m32s
2024-09-10 20:22:06 -04:00
b55aaa1d18 Maybe CI android?? 2024-09-10 20:20:40 -04:00
34e19505bd No longer need this test. 2024-09-09 15:57:43 -04:00
6e06ec0904 Clean up connections that don't handshake in time.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 11m26s
2024-09-09 15:25:10 -04:00
a5814074fe OpenSSL 3.3.2.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m26s
2024-09-04 20:24:32 -04:00
d7479df5a2 Latest CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m23s
2024-09-04 20:11:28 -04:00
34508aa0ae dist slightly more in parallel. Exclude dotfiles from data.zip.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-09-04 20:07:26 -04:00
ae096b2c9c Try harder to make webview localStorage work on different versions. I suspect that's what #73 is about.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m43s
2024-09-04 12:50:12 -04:00
95d036e34a Build an AppImage. Why not?
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 6m27s
2024-08-28 20:55:52 -04:00
4af5e8ec42 I guess prettier says this, now.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 6m30s
2024-08-28 20:24:26 -04:00
2a5f71bd5d This now lives in the fdroiddata repository.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 7m26s
2024-08-28 19:59:49 -04:00
97fb63dda1 Actually 0.0.23-wip. 2024-08-28 19:59:34 -04:00
87d42e3b3b 0.0.23-wip again. Let's gooooo.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 7m45s
2024-08-28 19:49:36 -04:00
0394129a4c nix => 0.0.22 again.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-28 19:42:31 -04:00
3c499c834b Fix stale data being saved when setting global settings.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 8m53s
2024-08-28 19:39:05 -04:00
17d6cc7d46 Let's try 0.0.22 again.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 7m47s
2024-08-28 19:20:55 -04:00
646bd7dc38 Fix changing boolean settings. 2024-08-28 19:16:19 -04:00
56e483782d Let's start work on 0.0.23. Clean out some libuv non-submodule cruft while I'm in here. 2024-08-28 19:10:16 -04:00
e1b9066b26 nix => 0.0.22.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 8m56s
2024-08-28 18:49:35 -04:00
7114ce2516 Let's release 0.0.22.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m54s
2024-08-28 18:40:10 -04:00
9240c6570a Changelog updates. Almost ready for a release.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m48s
2024-08-26 12:27:46 -04:00
f80a44ccd7 Title case settings names in the admin app.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-08-26 12:24:36 -04:00
e6f5eb244e Missing port.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m6s
2024-08-25 22:00:35 -04:00
ab62e83110 Fixed some peer ID brokenness.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-08-25 21:56:01 -04:00
aeefb9e536 Configure c-ares for haiku a bit better.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m16s
2024-08-25 13:48:57 -04:00
ee0efa536a Fix and assert against some more unsafe cross-thread JSContext use.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m10s
2024-08-25 13:30:46 -04:00
2523130fdc Fix some weird layout in the admin app on mobile.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m16s
2024-08-25 13:03:19 -04:00
c024777184 #buildfix
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m58s
2024-08-25 12:45:42 -04:00
5951d7cd2d Kill some warnings.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 3m41s
2024-08-25 10:07:44 -04:00
011670c70b Pass along and use the actual port we're listening on for peers.exchange.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 4m5s
2024-08-25 09:50:28 -04:00
6cebd6c769 Try to be more static.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 3m49s
2024-08-25 09:39:05 -04:00
546ae5cbf1 Latest CodeMirror. 2024-08-24 21:57:13 -04:00
f543cc642e Clean up some error'd RPC requests. Don't send blobs.createWants if we're not replicating.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 3m59s
2024-08-24 10:39:47 -04:00
8ac3c5ea22 Keep c-ares initialized. Fixes android, which can't just be re-initialized.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 3m48s
2024-08-22 12:43:20 -04:00
63918f0680 Make blobs.has do its work off the main thread so it doesn't violate that assert, and make the test cover such things a bit better.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 3m48s
2024-08-21 22:55:40 -04:00
bfb3d8b8a2 Add an option to disable account registation, and fix use of a JSContext from the wrong thread along the way.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-21 20:56:21 -04:00
e38ff99607 Special treatment to make TXT record lookup work on android.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-21 20:27:43 -04:00
b0e3d922c8 libuv busy loop in uv__run_timers with -flto. Sigh.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-21 19:40:07 -04:00
a15bb8e994 Don't rely on being idle to do anything. Fixes JS job starvation on slow machines more.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-21 12:53:38 -04:00
6f487100cd Format. 2024-08-20 12:35:42 -04:00
0693a2315f Fix async job starvation if everything is running too slowly.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-20 12:26:34 -04:00
f360e886ff Make -t peer_exchange complete and test that something happened.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-19 12:29:40 -04:00
6ea08cc5dc Add the beginnings of a peers.exchange test and begin to fix fallout.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m3s
2024-08-15 12:48:24 -04:00
347c706d6f ci: undefined reference to arc4random_buf
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m11s
2024-08-15 12:12:58 -04:00
5f5e6616c7 Install graphviz for building docs.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 6m1s
2024-08-14 21:16:31 -04:00
657bcadc7e Work-in-progress, untested, naive peer exchange. Intended to be disabled by default by a setting.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 5m56s
2024-08-14 21:07:16 -04:00
107666cc60 Add a setting to toggle whether replication is allowed, to be able to make a pure room, or even less, node. 2024-08-14 20:02:46 -04:00
b37669184a doxygen -u # 1.9.8 2024-08-14 20:01:21 -04:00
163a01f224 sqlite 3.46.1. 2024-08-14 19:43:57 -04:00
3d58094199 Fix some sanitizer issues, and disable LTO in debug builds to save some iteration time. 2024-08-14 19:40:20 -04:00
463951a4f1 Track/show the origin of each broadcast (discovery/room/peer exchange). 2024-08-14 19:23:01 -04:00
34804d5162 Fix android crashing in c-ares and a makefile typo.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-14 18:55:34 -04:00
3895c33915 Implement prompt() for android. #72
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-14 12:45:22 -04:00
17f4eb1a56 Make it easier to copy ids from the profile view.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-08-11 16:26:24 -04:00
0abdffdea6 Fix OpenBSD.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-11 11:17:49 -04:00
d32999f178 Decouple DNS-based seed discovery from the broadcast timer.
Some checks are pending
Build Tilde Friends / Build-All (push) Waiting to run
2024-08-08 18:50:54 -04:00
f621feb843 Fix some builds and make the windows build actually succeed at resolving what I want.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 4m13s
2024-08-07 22:25:38 -04:00
8d277f029d Support using a seeds host for bootstrapping connections. 2024-08-07 21:03:39 -04:00
1788a02338 Add c-ares. These are the hoops I have to jump through to be able to provide some bootstrap nodes. 2024-08-07 20:21:39 -04:00
ba0800d16c Lit 3.2.0. 2024-08-06 12:19:10 -04:00
4008c7d8f6 Latest CodeMirror. 2024-08-06 12:18:54 -04:00
610a2e2afc Latest libbacktrace. 2024-08-06 12:18:32 -04:00
6f3715d1eb Latest libsodium stable. 2024-08-06 12:18:21 -04:00
b78ecaa814 F-Droid looks all set for now. Let's start 0.0.22. 2024-08-06 12:17:26 -04:00
e6f5399d53 Clear out timestamp and file modes on classes.dex, too.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 5m21s
2024-08-05 12:43:22 -04:00
0e5806cadd Re-add classes.dex to the F-Droid APK. (!)
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m55s
2024-08-05 12:26:10 -04:00
68c9d4afa7 Found some docs that say the icon.png max size is 512x512.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 5m23s
2024-08-04 21:54:42 -04:00
f0ea38fe49 Just set SOURCE_DATE_EPOCH=1. Using the last commit time is complicated (have to rebuild OpenSSL every commit/release). This only affects a debug string that we don't expose.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m51s
2024-08-04 12:54:02 -04:00
b0332f923e Debugging a SOURCE_DATE_EPOCH thing.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m58s
2024-08-04 12:16:46 -04:00
8a76c25394 Silence some OpenSSL build output so I can see what else is going on. Also install the signed fdroid APK in dist.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-08-04 12:10:52 -04:00
fd96126e3e Ooh, can I just exclude OpenSSL submodules? I don't want to see those.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m51s
2024-08-04 11:49:04 -04:00
ff3fbedc18 Fix inconsistent file modes in zip.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 4m4s
2024-08-04 11:25:06 -04:00
8791419f8e Sort better, and actually use ndk r26d.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 5m8s
2024-08-04 10:21:04 -04:00
5447b247a0 Back to r26d, and pin the timezome to get SOURCE_DATE_EPOCH to work correctly.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m16s
2024-08-04 09:54:33 -04:00
aabbb10564 for fdroid: Use android ndk r27, set SOURCE_DATE_EPOCH for the android ssl build, and remove a non-determinism in AndroidManifest.xml.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m50s
2024-08-04 09:36:46 -04:00
3ccd6c9a3e I missed.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m3s
2024-08-02 22:26:01 -04:00
c290240de7 Make a release to make sure F-Droid can pick it up.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-08-02 22:20:18 -04:00
8e799b174b Address some fdroid zip non-determinism.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 5m15s
2024-08-02 21:55:00 -04:00
a9c3a93989 Add some images for F-Droid.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m1s
2024-08-02 20:37:27 -04:00
3ef8698f42 Put android:versionCode and such back in the static AndroidManifest.xml. I forgot that F-Droid needs to see it. 2024-08-02 20:37:05 -04:00
fa4e843c30 Update default.nix. Did I do it right finally?
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 5m34s
2024-07-31 20:14:40 -04:00
9a4d11f4d9 Attempt to shrink OpenSSL on android again, ineffectively.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m14s
2024-07-31 19:58:41 -04:00
eed2b8d618 Latest CodeMirror. 2024-07-31 19:49:52 -04:00
13f02c2aca Preparing to release 0.0.21.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 5m33s
2024-07-31 12:50:35 -04:00
d50f8fbc8b ios: ssl fix.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 5m15s
2024-07-27 21:31:31 -04:00
155238a516 build: I mean -flto=auto.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 5m19s
2024-07-27 11:08:28 -04:00
427fcdbdca build: -flto all the things.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m34s
2024-07-25 16:02:14 -04:00
ca05d402a7 An exercise in stripping down the win32 .exe size.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m12s
2024-07-24 15:25:36 -04:00
c5a80b68ca Fixed more aab build issues. 2024-07-24 14:03:21 -04:00
c1fb15b135 ci tweaks and aab fixes.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m24s
2024-07-24 13:50:48 -04:00
4b2c131836 ci: Install doxygen for docs.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 7m30s
2024-07-24 13:04:52 -04:00
9ca1e69b3c Let's try to build in docker, too.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 6m48s
2024-07-24 12:56:27 -04:00
082d041d44 Update the android app icon / launch icon.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2024-07-24 12:50:31 -04:00
221f276c4b Simplify stats sending.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m16s
2024-07-24 12:15:05 -04:00
24cec21465 Move last remnant of static file handling from core to C. 2024-07-24 12:06:24 -04:00
9f71ec6194 Minor android cleanup.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m12s
2024-07-24 11:27:37 -04:00
bb36afc390 Use android ndk r27 (LTS) if available. 2024-07-24 11:20:35 -04:00
b53bf0ff64 Disallow rich text in the ssb compose box.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m13s
2024-07-22 14:42:37 -04:00
3ebc6f2436 Prettier.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m14s
2024-07-22 14:19:12 -04:00
2eef6778a6 Latest CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m20s
2024-07-20 17:13:43 -04:00
81fabec810 Latest libbacktrace. 2024-07-20 17:12:43 -04:00
dc6e7924b5 Is this how I install dependencies on the gitea runner?
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 6m14s
2024-07-18 12:42:08 -04:00
48dec5a2c8 More submodules.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 2m22s
2024-07-16 22:32:26 -04:00
9b500e1da9 Now actually build something.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 7s
2024-07-16 22:30:36 -04:00
a038820112 Add the demo gitea action.
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 47s
2024-07-16 22:20:45 -04:00
70a15973b6 An fdroid build config that worked locally, for me. 2024-07-16 21:45:29 -04:00
09b6a00731 Fix android build with not enough -j. 2024-07-16 20:22:07 -04:00
883c3cf0e9 Clean up this core file. 2024-07-16 19:01:20 -04:00
a46bb8183c Fix OpenBSD compile. 2024-07-14 16:59:23 -04:00
d5d5a7b012 Build a separate .apk for fdroid with its own app ID. 2024-07-14 16:18:47 -04:00
a120efdc91 May as well dist the .aab. 2024-07-10 20:52:40 -04:00
d48f4b06eb Another f-droid directory. 2024-07-10 20:42:50 -04:00
f078912736 Add some recommended fdroid metadata. 2024-07-10 19:45:04 -04:00
63b0f0dedd Fix fdroid build with OpenSSL in-tree. 2024-07-10 19:35:49 -04:00
84c22dbf5f Move to OpenSSL as a git submodule. Redundant for platforms where it's not used, but makes fdroid easier. 2024-07-10 19:25:01 -04:00
b8cd1232be Have a little category, as a treat. 2024-07-09 19:30:33 -04:00
a518ab07f4 Add a WIP .fdroid.yml that seems to actually build something. 2024-07-09 19:28:45 -04:00
9e5a1ee975 Ugg. 2024-07-09 19:21:27 -04:00
95bf3f0316 This is almost doing something. 2024-07-09 19:19:01 -04:00
d69dd513bc Another silly fdroid test. 2024-07-09 19:08:40 -04:00
525cdf571a Testing a thing for fdroid. 2024-07-07 17:14:13 -04:00
9cfe0a8804 Add a 'JavaScript disabled' message. #56 2024-07-04 14:35:53 -04:00
50b54599ef Minor cleanup. 2024-07-04 13:18:23 -04:00
ed6bef6d24 Get android running its sandbox in a seprate, isolated service process. So that we support not extracting the native code from the APK, so that we support distributing as an .aab file, so that we may one day release on the app store. 2024-07-04 13:02:39 -04:00
71268636df Steps toward following all the inconvenient, changing android rules:
* Set android:debuggable=false.
 * Call native code through JNI only.  Having a native executable on disk and exec-ing it no longer seems possible.
 * Do all the Tilde Friends things in one process, without a proper sandbox, until I can wire up a restricted service worker process.
 * Jam Android App Bundle (.aab) building into the makefile.
 * Yuck.
2024-06-30 13:32:17 -04:00
568729ecd6 Stop auto-updating the version in default.nix. Will do it manually only on release. 2024-06-29 08:33:51 -04:00
9139725be6 Merge pull request 'build: fix the nix derivation' (#69) from tasiaiso/tildefriends:tasiaiso-0-0-20 into main
Reviewed-on: cory/tildefriends#69
2024-06-29 08:33:48 -04:00
969a8da6bf
build: update nix package to 0.0.20 2024-06-28 10:37:26 +02:00
2338b26329 Start working on 0.0.21. 2024-06-26 20:47:44 -04:00
d4df206740 Oh, I think I see how to nix this now. 2024-06-26 20:36:54 -04:00
8a93cdd33c Let's release 0.0.20. 2024-06-26 20:29:07 -04:00
92b31de4a9 Latest libbacktrace. 2024-06-26 20:20:41 -04:00
5452f3f623 Appease -fsanitize. 2024-06-26 20:20:34 -04:00
256614dbaf Actually stop stomping settings. 2024-06-26 19:58:59 -04:00
049449b213 I think this is how I lost settings. 2024-06-26 19:44:45 -04:00
85b46336b1 Better draft cleanup on submit. 2024-06-26 19:30:58 -04:00
590afa7b01 Fix content warnings. 2024-06-26 19:27:15 -04:00
574292b798 Reduce some common log noise. 2024-06-23 15:11:18 -04:00
21cf503a59 Fix a navigation bar option I neglected to button-ify. 2024-06-23 11:47:12 -04:00
3630cdbfe0 Consolidate the acount/login navigation bar options to try to save some space on mobile. 2024-06-20 20:41:27 -04:00
0f3be229e6 Actually, let's minify this thing using svgomg. 2024-06-20 20:07:58 -04:00
8e5a024d3d SVG favicon. 2024-06-20 20:05:00 -04:00
410bb7c09d Fix a ref count mistake and add a long-overdue tf_util_print_backtrace() that helped me find it. 2024-06-20 19:49:21 -04:00
9de8b0f449 Oops. 2024-06-20 12:36:21 -04:00
d47c3a1222 Fix a ref/unref mismatch. 2024-06-17 21:45:51 -04:00
df99b3aa90 Trying to catch an issue I think I saw in the debugger. 2024-06-17 21:23:48 -04:00
0090850e10 Forgot the other end of blobs.get. 2024-06-17 20:59:25 -04:00
9efd64bd18 Actually enforce _tf_ssb_assert_not_main_thread. 2024-06-17 12:36:54 -04:00
b16c37e48b Make ssb.privateMessageDecrypt do its work not on the main thread. I think that's finally everything for real. 2024-06-16 17:22:26 -04:00
3ee2c00726 Build fix. 2024-06-16 17:08:10 -04:00
d5a7e19f1a Move the bulk of ssb.privateMessageEncrypt work (CPU + DB) off the main thread. 2024-06-16 17:07:12 -04:00
9b52415b35 Make ssb.setServerFollowingMe not use the DB from the main thread. Two left?? 2024-06-16 16:22:59 -04:00
dbe24494d9 Remove ssb.messageContentGet. It's easy to do this with ssb.sqlAsync, and this wasn't being used productively. Three uses of DB on the main thread remaining. 2024-06-16 16:02:39 -04:00
3eab5a5f70 Make ssb.forgetStoredConnection not use the DB on the main thread. Four remaining? 2024-06-16 15:57:19 -04:00
548febfb22 Make ssb.storedConnections do its DB work not on the main thread. Five remaining by my new count? 2024-06-16 15:29:59 -04:00
b40f72443a A little format, as a treat. 2024-06-16 12:18:19 -04:00
2c03496373 Make databases.list, database.remove, and database.getLike all do their DB work off the main thread. That's the last thing I'm aware of. 2024-06-16 12:17:51 -04:00
b6a937c954 Move db.exchange DB work off of the main thread. 2024-06-16 10:16:39 -04:00
63776d40bd Update codemirror. 2024-06-16 09:23:14 -04:00
cb3c7afade Move ssb.getPrivateKey's DB work off the main thread. 2024-06-16 08:07:02 -04:00
991022adfc Move ssb.appendMessageWithIdentity's DB work off the main thread. 2024-06-16 07:51:06 -04:00
2bc71a18a6 Make ssb.deleteIdentity not block the main thread with DB work. 2024-06-14 17:39:24 -04:00
57ca864fbb Build fix. 2024-06-12 21:08:41 -04:00
a09edfb612 ssb.addIdentity without hitting the DB from the main thread. 2024-06-12 21:06:30 -04:00
7997a739ab ssb.createIdentity without hitting the database from the main thread. 2024-06-12 20:47:48 -04:00
248b258413 Make database.getAll() not block the main thread on database access. 2024-06-12 20:29:39 -04:00
0423ed7fb4 Login without hitting the DB from the main thread. 2024-06-12 20:12:35 -04:00
c29378c2f8 Yes, curl, follow redirects. 2024-06-10 21:19:06 -04:00
163fbd85e7 Fix docs. 2024-06-10 20:23:11 -04:00
58bb86ebe1 Make http.auth_query async and get its DB work off the main thread. 2024-06-10 20:22:28 -04:00
c5140ee8e8 Move DB work for ssb.getIdentities() and ssb.getAllIdentities() off the main thread. 2024-06-10 17:18:29 -04:00
6270fd8118 We don't need to go to the DB to get our public key. 2024-06-10 16:56:21 -04:00
3fff706848 Get the code of conduct and JWT signing key without hitting the database from the main thread. 2024-06-10 16:37:12 -04:00
c259defab5 Move database.get and database.set off the main thread. 2024-06-10 15:30:14 -04:00
e5fee5c306 Buildfix. 2024-06-10 12:01:49 -04:00
9d35b4bdfb Resuming work to move all DB access off the main thread. 2024-06-10 11:45:20 -04:00
9497d7cf64 Fix some shutdown hangs/leaks. 2024-06-06 20:31:24 -04:00
c7d3e602cb Fix &-mentions while I'm at it. 2024-06-06 20:14:00 -04:00
0076eb4ed4 Fix autocomplete again/more. #65 2024-06-06 20:05:24 -04:00
6070bde413 Avoid a null dereference. 2024-06-06 19:57:36 -04:00
c7a6d426f0 Fix autocomplete on Chrome, because contenteditable and shadowRoots are tricksy, and this module for @mentions is aging. #65 2024-06-06 19:52:37 -04:00
f66cf0f802 Unused. 2024-06-06 19:11:48 -04:00
e4b6c81024 No need to show your identity in the navigation bar if you have a name. 2024-06-06 18:51:40 -04:00
44d784cd04 OpenSSL 3.3.1. 2024-06-05 12:51:26 -04:00
0394201113 Merge pull request 'buld(nix): Misc Nix-related improvements' (#68) from tasiaiso/tildefriends:tasiaiso-nix-misc into main
Reviewed-on: cory/tildefriends#68
2024-06-04 20:16:15 -04:00
e270c16516 lit 3.1.4. 2024-06-04 20:10:27 -04:00
4c10538632
buld(nix): Misc Nix-related improvements
- Nixpkgs 23.11 is deprecated, use 24.05 instead
- update flake.lock
- add glibc as a build dependency
- add doxygen and graphviz as development dependencies for `make format`
2024-06-04 15:22:18 +02:00
71329c5532 format+prettier 2024-06-03 12:36:34 -04:00
feb4bf9e87 Limit message sends in a continued attempt to fix intermittent runaway memory usage. #64 2024-06-02 12:38:12 -04:00
5d5567e94c Reworking the emoji picker to use w3-modal, in a step toward doing the same for the currently broken @autocomplete. 2024-05-30 12:40:21 -04:00
684e6fb9cb Merge pull request 'nix: update version to 0.0.19' (#66) from tasiaiso/tildefriends:tasiaiso-nix-update into main
Reviewed-on: cory/tildefriends#66
2024-05-30 12:12:45 -04:00
ee21fa6d03
nix: update version to 0.0.19 2024-05-30 11:34:57 +02:00
7a2974e54f Working on 0.0.20. 2024-05-29 20:17:33 -04:00
f4dfc1dd98 Let's release 0.0.19. 2024-05-29 19:50:59 -04:00
2eebfa9a7a Make the websocket disconnect message not pop up a modal dialog so that it's less annoying when it happens in the normal course of events. #60 2024-05-27 20:35:40 -04:00
10097ffeb8 Update codemirror. 2024-05-27 08:23:45 -04:00
cbe1f54a2a libsodium 1.0.20. 2024-05-27 08:21:48 -04:00
4d8f081a59 Update libbacktrace to latest. 2024-05-27 08:21:10 -04:00
29e79c9484 Fix collapsing images taking extra clicks. 2024-05-25 08:09:44 -04:00
ba35869b0a sqlite 3.46.0. 2024-05-25 07:46:15 -04:00
580688381e prettier 2024-05-22 20:52:10 -04:00
e63d69a440 Missing generated semicolon. Sigh. 2024-05-22 20:44:28 -04:00
be64fe04fb Auto-update all the versions. 2024-05-22 20:35:48 -04:00
801ab20723 Merge pull request 'Add Nix support' (#62) from tasiaiso/tildefriends:tasiaiso-nix into main
Reviewed-on: cory/tildefriends#62
2024-05-22 20:31:11 -04:00
d974a5e044 An experiment in controlling memory usage when syncing. uv_read_stop when we have too active message/blob writes to the database and uv_read_start when we're back under control. #64 2024-05-22 19:53:33 -04:00
1be94ae0be Removed ssb.addEventListener and ssb.removeEventListener from the public API. Can do the same thing with core.register. 2024-05-22 18:51:21 -04:00
b883e6a485 Fix username/id extending off the screen in the welcome line. 2024-05-22 12:33:18 -04:00
a0210379ae Avoid confusing log output when responding with a method not found error. 2024-05-20 12:39:21 -04:00
e56dc207d1 Fix some shutdown issues in connection tracker code. 2024-05-16 12:41:48 -04:00
523c9c9ad2 Move mime type shenanigans from JS => C. 2024-05-15 19:25:48 -04:00
74bb2151c1 Fix shutdown issues with in-flight SSB connection attempts. 2024-05-15 12:37:13 -04:00
f79d7b35a4 Disallow creating accounts as a guest. #52 2024-05-14 12:41:17 -04:00
3b36496dac
chore: a bit more doc 2024-05-12 21:17:38 +02:00
4ebd6c24a9
chore: missing period in description 2024-05-12 21:15:30 +02:00
05451d98b3
Merge branch 'tasiaiso-nix' of https://dev.tildefriends.net/tasiaiso/tildefriends into tasiaiso-nix 2024-05-12 21:13:43 +02:00
22a4bce3c8
docs(nix): add documentation in default.nix 2024-05-12 21:13:22 +02:00
76d499f00b Merge branch 'main' into tasiaiso-nix 2024-05-12 14:56:12 -04:00
f0772f9b99
build(nix): add Nix support 2024-05-12 20:34:03 +02:00
46e711f0a5 Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-05-12 10:40:14 -04:00
abffac3f82 Show missing profile images more deliberately. 2024-05-12 10:40:06 -04:00
27b275548e Fix docs. 2024-05-12 08:37:14 -04:00
93ce253d1e prettier 2024-05-12 08:23:34 -04:00
a5af312b39 Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-05-12 08:23:23 -04:00
4b5e8e8a43 Consolidate similar request tags in the connection list. #59 2024-05-12 08:21:47 -04:00
443dd4d168 Merge pull request 'chore: code formatting' (#58) from tasiaiso/tildefriends:tasiaiso-format into main
Reviewed-on: cory/tildefriends#58
2024-05-12 08:05:02 -04:00
907479df84 Merge branch 'main' into tasiaiso-format 2024-05-12 07:52:33 -04:00
9887a78e98 prettier 2024-05-12 07:48:34 -04:00
f669371349 Show tab names on large enough screens. Inspired by tasio's #61. 2024-05-12 06:58:01 -04:00
24c720c79a Merge branch 'main' into tasiaiso-format 2024-05-12 02:06:09 -04:00
4485234980
chore(style): tell prettier to ignore code block 2024-05-12 08:01:37 +02:00
b6871c0b1f
chore: code formatting 2024-05-11 23:44:09 +02:00
47838d5e48 More name info issues. 2024-05-11 10:53:21 -04:00
69fccd56d3 Add a little guidance about how to set your name. It's a common confusion. 2024-05-11 10:40:34 -04:00
ca00c4fb5d Fix multiple issues getting identity info. 2024-05-11 10:23:07 -04:00
427ca3f265 Indicate both the server account and your own accounts in the ssb connections tab. 2024-05-11 09:58:24 -04:00
c1a80e50e7 Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-05-11 09:50:06 -04:00
52962f3a5e Remove the :auth key. We can sign JWTs with :admin, and it's one less magic key. 2024-05-11 09:50:00 -04:00
b3f095b61f Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-05-11 09:33:48 -04:00
a5004c8ba9 Indicate the local server identity. 2024-05-11 09:33:38 -04:00
7d9b1b508b Print a little colorful message when we've started about where to connect. Multiple people have pointed out that it's not obvious that it's working. 2024-05-11 09:18:30 -04:00
5e265dfc83 Make sure the first user can admin. 2024-05-11 09:03:56 -04:00
3a43d6f8ac Build fix. 2024-05-11 09:03:37 -04:00
11a6649847 Add back a verify command. Remove unused and not very useful ssb.getMessage(). Make field ordering shenanigans more explicit. 2024-05-11 08:48:50 -04:00
7caf4a0173 Fix numerous issues around setting the first registered used as an admin. 2024-05-10 22:21:59 -04:00
385524352c Refactor most uses of uv_queue_work to go through a helper that keeps track of thread business, traces, and is generally less code. 2024-05-08 21:00:37 -04:00
5ca5323782 Fix /speedscope/ => deps/speedscope/index.html. 2024-05-08 20:57:53 -04:00
ba6da856bb Let trace truncate names more if it means we can generate valid JSON. 2024-05-08 20:56:44 -04:00
c0e72246cc Trying to understand a lingering 'previous message doesn't exist.' And format. 2024-05-08 12:20:57 -04:00
c7ab5447ea Move / redirect handling to C 2024-05-05 15:24:15 -04:00
5fdd461159 Fix setting multiline admin settings. 2024-05-05 15:19:00 -04:00
421955f2a0 getIdentityInfo => C. 2024-05-05 13:48:22 -04:00
a28f6985ed getActiveIdentity => C. 2024-05-05 12:55:32 -04:00
8244dddab7 Latest libbacktrace. 2024-05-05 12:03:57 -04:00
a5ca436eaa Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-05-02 20:35:24 -04:00
d7fc1c2c88 Minor style/layout changes. 2024-05-02 20:35:00 -04:00
382627ef8d Latest codemirror. 2024-05-02 20:11:54 -04:00
17667b4cf8 make format 2024-05-02 20:10:56 -04:00
5231ec22e7 More trying to clean up lingering requests. 2024-05-02 19:59:54 -04:00
929ae1b709 After eyeballing lingering requests, clean up requests after the response to an async (non-streaming) request is done. 2024-05-02 19:37:38 -04:00
f01f7a5ab9 Show active RPC requests in the connections tab. Probably TMI, but I want greater introspection into what is going on, and this seemed like a positive step. 2024-05-02 19:02:23 -04:00
a2dce833f8 Fix another shutdown issue. 2024-05-02 12:30:22 -04:00
de6c7a4fd4 SSB app stylin'. 2024-05-01 12:34:36 -04:00
4edee0f7f6 Allow importing from a single app .json. 2024-04-30 21:43:14 -04:00
988a807fa4 Admin app styles. 2024-04-30 19:18:33 -04:00
5258e4253d Better identity app layout on mobile. 2024-04-29 12:24:35 -04:00
09ba86dec5 Show replies to gatherings. 2024-04-28 12:55:17 -04:00
78d8a1aa23 Set the core room app icon. 2024-04-28 12:33:19 -04:00
22def15209 Merge branch 'main' of https://dev.tildefriends.net/cory/tildefriends 2024-04-28 12:25:31 -04:00
4cbda7a849 Improve file errors so that it doesn't look like everything has failed when we see there's no https cert available. 2024-04-28 12:25:12 -04:00
be85a620ef Fix app delete. 2024-04-28 12:11:13 -04:00
0b07b678b4 Theme the identity app a bit. 2024-04-28 11:57:35 -04:00
4733ce9287 Fix ssb draft discard. 2024-04-28 11:23:28 -04:00
48d6bf4c15 Hook up onJsAlert on android. 2024-04-28 11:04:29 -04:00
8c759bcbac Hide the reactions button if there aren't any. 2024-04-26 18:19:13 -04:00
b5ed7014f6 Fix attaching files (aka WebView file picking) on Android. 2024-04-26 18:10:22 -04:00
6cd9dea186 Merge v0.0.18 compose fixes. 2024-04-24 20:35:36 -04:00
202b416acf More ssb compose fixes. 2024-04-24 20:32:09 -04:00
93d46f5610 Fix some ssb compose issues. 2024-04-24 20:20:18 -04:00
c5ddf3ac99 Fix some ssb compose issues. 2024-04-24 20:19:14 -04:00
a9cb913a47 Working on 0.0.19. 2024-04-24 19:29:17 -04:00
b7b5d4f1a5 Calling it 0.0.18. 2024-04-24 19:24:10 -04:00
a947396bad Prettier. 2024-04-24 19:23:13 -04:00
d528bc808e Add retries around some 'test -t auto' intermittent failures. 2024-04-22 12:37:06 -04:00
c6fd05c2cf ssb tf-compose fixes. 2024-04-21 18:53:47 -04:00
d6bb9d311a An experiment in improving ssb text entry. 2024-04-21 18:24:57 -04:00
53b4cbbf8c CSS, ugh. 2024-04-21 15:24:23 -04:00
628716ec28 Add a thing to inspect reactions. #48 2024-04-21 14:18:06 -04:00
bd14168627 Don't pop up the error modal for connecting/status messages. 2024-04-18 12:46:06 -04:00
96037d4da6 Android pull refresh fixes. Sigh. 2024-04-17 22:37:24 -04:00
5448e773d8 Rejiggled error display. 2024-04-17 20:56:33 -04:00
848ef21c7c Build a windows standalone executable with attached data for dist. #28 2024-04-17 20:16:07 -04:00
2ecae7da93 Implement my own hokey pull to refresh on Android. Nobody's got time for all those dependencies. 2024-04-17 19:55:14 -04:00
d9ce569eb9 Add a thing to encourage editing your profile. 2024-04-17 17:21:44 +01:00
eacaf392b1 Lit 3.1.3. 2024-04-16 12:31:53 -04:00
ce16592b6a Update codemirror. 2024-04-16 12:30:27 -04:00
295d76d354 sqlite 3.45.3. 2024-04-16 12:16:36 -04:00
23b3c998bd Re-CSS'd the identity dropdown. 2024-04-14 17:47:47 -04:00
b5e966c9a1 Make the wiki app use the global id picker. 2024-04-14 13:53:51 +01:00
96cb6f4b12 Make the issues app use the global id picker, fix jsonb issues, and fix the global id picker. 2024-04-14 13:47:28 +01:00
e2c0f82ec0 Size the identity picker a tad better. 2024-04-14 13:10:32 +01:00
dbf28c03e6 Let's get account names to the UI. 2024-04-13 21:51:18 -04:00
26165e30de Fix -t auto. 2024-04-13 20:32:17 -04:00
c52331a23a format/prettier 2024-04-13 20:07:39 -04:00
8007e71e1d Make the ssb app use the global identity picker. 2024-04-13 19:52:40 -04:00
28d08e013f Trying to make the ssb connections tab not overflow all the layouts. Dunno. 2024-04-13 19:29:31 -04:00
64bbd383de Trying to make the navigation bar fit again with a new dropdown. Good grief, CSS. 2024-04-13 16:52:30 -04:00
8a9f53102b Needs some color. 2024-04-13 13:33:57 -04:00
0412b97170 WIP managing a per-app current identity from the Tilde Friends navigation bar. 2024-04-13 13:22:59 -04:00
c8b8a8fc03 Oops, the tf-auth lit was all wrong. 2024-04-13 10:28:35 -04:00
95d3090b9b Fix the app permissions list missing its reset buttons. 2024-04-13 10:03:06 -04:00
49129ee6dd Welcome changes. Added a first steps blurb. 2024-04-12 02:32:21 +01:00
6a7ecb0d4a Update codemirror to latest. 2024-04-11 20:36:14 -04:00
1ceeed1007 prettier + clang-format. 2024-04-11 18:36:31 -04:00
a7922ff44e Get commonmark blockquotes on-theme. 2024-04-11 01:30:49 +01:00
a421604ed5 OpenSSL 3.3.0. 2024-04-10 19:44:49 -04:00
7d182db32f Move to submodules: libsodium, quickjs, crypt_blowfish, libbacktrace, libuv, picohttpparser. Kudos to @tasioiso in #45. 2024-04-10 19:25:18 -04:00
c5cb9979d3 Move zlib to a submodule, informed by @tasiaiso's #45. And fixup [archive] dist/tildefriends-0.0.18-wip.tar.xz
[cp] TildeFriends-x86-0.0.18-wip.apk
[cp] TildeFriends-arm-0.0.18-wip.apk
[cp] TildeFriends-0.0.18-wip.ipa to support it.
2024-04-10 19:09:31 -04:00
b9a73106ed Better CSS for the ssb app to fill the iframe? 2024-04-10 18:43:48 -04:00
c674cca482 Move some DB things out of httpd. 2024-04-04 21:00:59 -04:00
81d1228b92 Experimenting with w3.css themes. 2024-04-04 20:35:09 -04:00
6ae61d5b81 Fix wiki vs. JSONB. 2024-04-05 00:11:55 +01:00
9cb872eec2 Remove JS functions: hmacsha256sign, hmac2ha256verify, parseHttpRequest, sha1Digest, and maskBytes. These are no longer needed with httpd and auth in C 2024-04-03 21:14:52 -04:00
68e8c010b7 Show recently used emojis in the emoji picker. #16 2024-04-04 02:12:43 +01:00
9671413906 Make it easier to @mention the person to whom you are replying. 2024-04-04 00:50:59 +01:00
4c8d24c319 Consolidate markdown linkification, and add support for authors, blobs, and messages. 2024-04-04 00:18:39 +01:00
e50144bd34 Validate exit codes more thoroughly. C'mon, Cory. 2024-04-02 20:32:47 -04:00
9f3171e3f1 Remove auth.js. #7 2024-04-02 20:11:36 -04:00
cc92748747 Move sending refresh tokens out of JS. 2024-04-02 12:42:31 -04:00
0a0b0c1adb Make sure we don't leak the session string when reassigning it. 2024-04-02 12:20:59 -04:00
92a74026a6 Format the new auth code. 2024-04-01 12:53:47 -04:00
3fa1c6c420 Tidied up getting an auth key slightly. 2024-04-01 12:53:00 -04:00
b04eccdbda Move the auth handler out of JS. #7 2024-03-31 16:15:50 -04:00
9ce30dee70 Start working on 0.0.18. 2024-03-27 19:08:10 -04:00
3c0b680b8e Let's release 0.0.17. 2024-03-27 18:59:40 -04:00
895356897b archive whichever branch. 2024-03-25 18:10:07 -04:00
9164be2f37 Fix loading from not standalone zip. 2024-03-25 16:34:27 -04:00
5385264f94 Fix an http use after free during shutdown. 2024-03-25 16:31:09 -04:00
610e756c07 Ever closer to the elusive clean http shutdown. 2024-03-25 16:23:45 -04:00
15c9f8f458 Rudimentary support for building the executable with data attached. Pushed some things around in the makefile to fix issues along the way. #46 2024-03-25 13:50:17 -04:00
fb704a5b83 I will get better at keeping this tree clean. 2024-03-25 12:56:33 -04: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
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
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: cory/tildefriends#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
4226 changed files with 38893 additions and 1119282 deletions

View File

@ -14,7 +14,7 @@ IndentWidth: 4
MaxEmptyLinesToKeep: 1
ObjCBlockIndentWidth: 4
ObjCBreakBeforeNestedBlockParam: false
SortIncludes: false
SortIncludes: true
TabWidth: 4
UseTab: Always
...

2
.git-blame-ignore-revs Normal file
View File

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

View File

@ -0,0 +1,38 @@
name: Build Tilde Friends
run-name: ${{ gitea.actor }} running 🚀
on: [push]
jobs:
Build-All:
runs-on: ubuntu-latest
container:
valid_volumes: ['/opt/keys']
volumes:
- /opt/keys:/opt/keys
steps:
- name: check out code
uses: actions/checkout@v4
with:
submodules: true
- run: ln -s /opt/keys .keys
- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
packages: 'tools platform-tools build-tools;34.0.0 platforms;android-34 ndk;26.3.11579264'
- run: sudo apt update && sudo apt install -y doxygen graphviz mingw-w64 libgpgme11
- run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all docs
- run: docker build .
- uses: actions/upload-artifact@v3
with:
path: out/TildeFriends-release.fdroid.apk
- uses: actions/upload-artifact@v3
with:
path: out/winrelease/tildefriends.exe
- uses: actions/upload-artifact@v3
with:
path: out/tildefriends-x86_64.AppImage

20
.gitignore vendored
View File

@ -1,4 +1,18 @@
.keys
out
**/node_modules
build/
*.core
db.*
deps/ios_toolchain/
deps/openssl/
dist/
.flatpak-builder
.keys
logs/
**/node_modules
out
repo/
result
*.swo
*.swp
tmp/
unsigned/
.zsign_cache/

31
.gitmodules vendored Normal file
View File

@ -0,0 +1,31 @@
[submodule "deps/zlib"]
path = deps/zlib
url = https://github.com/madler/zlib.git
[submodule "deps/libsodium"]
path = deps/libsodium
url = https://github.com/jedisct1/libsodium.git
[submodule "deps/quickjs"]
path = deps/quickjs
url = https://github.com/bellard/quickjs.git
[submodule "deps/crypt_blowfish"]
path = deps/crypt_blowfish
url = https://github.com/openwall/crypt_blowfish.git
[submodule "deps/libbacktrace"]
path = deps/libbacktrace
url = https://github.com/ianlancetaylor/libbacktrace.git
[submodule "deps/libuv"]
path = deps/libuv
url = https://github.com/libuv/libuv.git
[submodule "deps/picohttpparser"]
path = deps/picohttpparser
url = https://github.com/h2o/picohttpparser.git
[submodule "deps/openssl_src"]
path = deps/openssl_src
url = https://github.com/openssl/openssl.git
shallow = true
[submodule "deps/c-ares"]
path = deps/c-ares
url = https://github.com/c-ares/c-ares.git
[submodule "docs"]
path = docs
url = https://dev.tildefriends.net/cory/tildefriends.wiki.git

15
.prettierignore Normal file
View File

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

5
.prettierrc.yaml Normal file
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`.

2815
Doxyfile Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,9 +3,27 @@
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 16
VERSION_NUMBER := 0.0.16-wip
VERSION_NAME := Medium English breakfast tea.
## == Tilde Friends build. ==
##
## This is a list of all supported build targets.
##
## Consider passing -j$(nproc) or adding it to your $MAKEFLAGS to build in
## parallel (faster).
##
## Useful variables to override:
## CC := Compiler.
## AS := Assembler.
## LD := Linker.
## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 31
VERSION_NUMBER := 0.0.26-wip
VERSION_NAME := This program kills fascists.
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470200.zip
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool
PROJECT = tildefriends
BUILD_DIR ?= out
@ -13,6 +31,12 @@ UNAME_S := $(shell uname -s)
UNAME_M := $(shell uname -m)
ANDROID_SDK ?= ~/Android/Sdk
BUNDLETOOL = out/bundletool.jar
HAVE_WIN := 0
export SOURCE_DATE_EPOCH=1
export TZ=UTC
ifeq ($(UNAME_S),Darwin)
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
@ -26,7 +50,8 @@ BUILD_TYPES := debug release
CFLAGS += -Dstatic_assert=_Static_assert
LDFLAGS += \
-lbsd \
-lnetwork
-lnetwork \
-Wno-stringop-overflow
else ifeq ($(UNAME_S),OpenBSD)
BUILD_TYPES := debug release
CFLAGS += \
@ -36,7 +61,6 @@ LDFLAGS += \
-lc++abi
HAVE_ANDROID := 0
HAVE_LINUX_IOS := 0
HAVE_WIN := 0
else
$(error Unexpected host platform $(UNAME_S).)
endif
@ -46,18 +70,23 @@ CFLAGS += \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-unknown-warning-option \
-MMD \
-MP \
-ffunction-sections \
-fdata-sections \
-fno-exceptions \
-g
LDFLAGS += \
-Wno-attributes \
-Wno-aggressive-loop-optimizations \
-flto=auto
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-34
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.1.10909125
ANDROID_MIN_SDK_VERSION := 24
ANDROID_TARGET_SDK_VERSION := 34
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-$(ANDROID_TARGET_SDK_VERSION)
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.3.11579264
ANDROID_ARMV7A_TARGETS := \
out/androiddebug-armv7a/tildefriends \
@ -86,7 +115,7 @@ BUILD_TYPES += \
androidrelease-x86 \
androiddebug-x86_64 \
androidrelease-x86_64
all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk
all: out/TildeFriends-arm-debug.apk out/TildeFriends-arm-release.apk out/TildeFriends-x86-debug.apk out/TildeFriends-x86-release.apk out/TildeFriends-release.fdroid.apk
endif
WINDOWS_TARGETS := \
@ -146,19 +175,26 @@ ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(ALL_TARGETS))
NONMACOS_TARGETS := $(filter-out $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS),$(ALL_TARGETS))
DEADSTRIP_TARGETS := $(filter-out $(ANDROID_TARGETS),$(NONMACOS_TARGETS))
ifneq ($(UNAME_S),OpenBSD)
$(NONMACOS_TARGETS): LDFLAGS += -static-libgcc
endif
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
$(filter-out $(ANDROID_TARGETS) $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
$(filter-out $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
$(ANDROID_TARGETS): CFLAGS += \
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
-fPIC \
-fdebug-compilation-dir . \
-fomit-frame-pointer \
-fno-asynchronous-unwind-tables \
-funwind-tables
-funwind-tables \
-Wno-unknown-warning-option
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
$(RELEASE_TARGETS): CFLAGS += \
-DNDEBUG \
-flto
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
$(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32
@ -201,22 +237,25 @@ $(ANDROID_X86_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86/usr/local/lib
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
$(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections
$(IOS_TARGETS): CFLAGS += -mios-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
$(DEADSTRIP_TARGETS): LDFLAGS += -Wl,--gc-sections
$(IOS_TARGETS): CFLAGS += -miphoneos-version-min=9.0 -Ideps/openssl/ios/ios64-xcrun/usr/local/include
$(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=9.0 -Ldeps/openssl/ios/ios64-xcrun/usr/local/lib
$(IOSSIM_TARGETS): CFLAGS += -Ideps/openssl/ios/iossimulator-xcrun/usr/local/include
$(IOSSIM_TARGETS): LDFLAGS += -Ldeps/openssl/ios/iossimulator-xcrun/usr/local/lib
ifeq ($(UNAME_M),x86_64)
ifeq ($(UNAME_S),Linux)
all: appimage
endif
ifneq ($(UNAME_S),Haiku)
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
out/debug/tildefriends: 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
out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
out/debug/tildefriends: LDFLAGS += -fsanitize=address -fsanitize=undefined
endif
get_objs = \
@ -234,6 +273,8 @@ APP_SOURCES_ios := $(wildcard src/*.m)
APP_OBJS := $(call get_objs,APP_SOURCES)
$(APP_OBJS): CFLAGS += \
-Ideps/base64c/include \
-Ideps/c-ares/include \
-Ideps/c-ares_config \
-Ideps/crypt_blowfish \
-Ideps/libbacktrace \
-Ideps/libsodium \
@ -245,7 +286,6 @@ $(APP_OBJS): CFLAGS += \
-Ideps/quickjs \
-Ideps/sqlite \
-Ideps/valgrind \
-Ideps/xopt \
-Wdouble-promotion \
-Werror
ifeq ($(UNAME_M),x86_64)
@ -253,6 +293,108 @@ $(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/macos% $(BUILD_DIR)/ios%,$(APP_O
-fanalyzer
endif
ARES_SOURCES := \
deps/c-ares/src/lib/ares_addrinfo2hostent.c \
deps/c-ares/src/lib/ares_addrinfo_localhost.c \
deps/c-ares/src/lib/ares_android.c \
deps/c-ares/src/lib/ares_cancel.c \
deps/c-ares/src/lib/ares_close_sockets.c \
deps/c-ares/src/lib/ares_conn.c \
deps/c-ares/src/lib/ares_cookie.c \
deps/c-ares/src/lib/ares_data.c \
deps/c-ares/src/lib/ares_destroy.c \
deps/c-ares/src/lib/ares_free_hostent.c \
deps/c-ares/src/lib/ares_free_string.c \
deps/c-ares/src/lib/ares_freeaddrinfo.c \
deps/c-ares/src/lib/ares_getaddrinfo.c \
deps/c-ares/src/lib/ares_getenv.c \
deps/c-ares/src/lib/ares_gethostbyaddr.c \
deps/c-ares/src/lib/ares_gethostbyname.c \
deps/c-ares/src/lib/ares_getnameinfo.c \
deps/c-ares/src/lib/ares_hosts_file.c \
deps/c-ares/src/lib/ares_init.c \
deps/c-ares/src/lib/ares_library_init.c \
deps/c-ares/src/lib/ares_metrics.c \
deps/c-ares/src/lib/ares_options.c \
deps/c-ares/src/lib/ares_parse_into_addrinfo.c \
deps/c-ares/src/lib/ares_process.c \
deps/c-ares/src/lib/ares_qcache.c \
deps/c-ares/src/lib/ares_query.c \
deps/c-ares/src/lib/ares_search.c \
deps/c-ares/src/lib/ares_send.c \
deps/c-ares/src/lib/ares_set_socket_functions.c \
deps/c-ares/src/lib/ares_socket.c \
deps/c-ares/src/lib/ares_sortaddrinfo.c \
deps/c-ares/src/lib/ares_strerror.c \
deps/c-ares/src/lib/ares_sysconfig.c \
deps/c-ares/src/lib/ares_sysconfig_files.c \
deps/c-ares/src/lib/ares_sysconfig_mac.c \
deps/c-ares/src/lib/ares_sysconfig_win.c \
deps/c-ares/src/lib/ares_update_servers.c \
deps/c-ares/src/lib/ares_version.c \
deps/c-ares/src/lib/dsa/ares_array.c \
deps/c-ares/src/lib/dsa/ares_htable.c \
deps/c-ares/src/lib/dsa/ares_htable_asvp.c \
deps/c-ares/src/lib/dsa/ares_htable_dict.c \
deps/c-ares/src/lib/dsa/ares_htable_strvp.c \
deps/c-ares/src/lib/dsa/ares_htable_szvp.c \
deps/c-ares/src/lib/dsa/ares_htable_vpvp.c \
deps/c-ares/src/lib/dsa/ares_llist.c \
deps/c-ares/src/lib/dsa/ares_slist.c \
deps/c-ares/src/lib/event/ares_event_configchg.c \
deps/c-ares/src/lib/event/ares_event_epoll.c \
deps/c-ares/src/lib/event/ares_event_kqueue.c \
deps/c-ares/src/lib/event/ares_event_poll.c \
deps/c-ares/src/lib/event/ares_event_select.c \
deps/c-ares/src/lib/event/ares_event_thread.c \
deps/c-ares/src/lib/event/ares_event_wake_pipe.c \
deps/c-ares/src/lib/event/ares_event_win32.c \
deps/c-ares/src/lib/inet_net_pton.c \
deps/c-ares/src/lib/inet_ntop.c \
deps/c-ares/src/lib/legacy/ares_create_query.c \
deps/c-ares/src/lib/legacy/ares_expand_name.c \
deps/c-ares/src/lib/legacy/ares_expand_string.c \
deps/c-ares/src/lib/legacy/ares_fds.c \
deps/c-ares/src/lib/legacy/ares_getsock.c \
deps/c-ares/src/lib/legacy/ares_parse_a_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_aaaa_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_caa_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_mx_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_naptr_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_ns_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_ptr_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_soa_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_srv_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_txt_reply.c \
deps/c-ares/src/lib/legacy/ares_parse_uri_reply.c \
deps/c-ares/src/lib/record/ares_dns_mapping.c \
deps/c-ares/src/lib/record/ares_dns_multistring.c \
deps/c-ares/src/lib/record/ares_dns_name.c \
deps/c-ares/src/lib/record/ares_dns_parse.c \
deps/c-ares/src/lib/record/ares_dns_record.c \
deps/c-ares/src/lib/record/ares_dns_write.c \
deps/c-ares/src/lib/str/ares_buf.c \
deps/c-ares/src/lib/str/ares_str.c \
deps/c-ares/src/lib/str/ares_strsplit.c \
deps/c-ares/src/lib/util/ares_iface_ips.c \
deps/c-ares/src/lib/util/ares_math.c \
deps/c-ares/src/lib/util/ares_rand.c \
deps/c-ares/src/lib/util/ares_threads.c \
deps/c-ares/src/lib/util/ares_timeval.c \
deps/c-ares/src/lib/util/ares_uri.c \
deps/c-ares/src/lib/windows_port.c \
deps/c-ares/src/lib/ares_timeout.c
ARES_OBJS := $(call get_objs,ARES_SOURCES)
$(ARES_OBJS): CFLAGS += \
-Ideps/c-ares/include \
-Ideps/c-ares/src/lib \
-Ideps/c-ares/src/lib/include \
-Ideps/c-ares_config/ \
-D_GNU_SOURCE \
-Wno-unused-function \
-Wno-deprecated-declarations \
-Wno-unused-result
BLOWFISH_SOURCES := \
deps/crypt_blowfish/crypt_blowfish.c \
deps/crypt_blowfish/crypt_gensalt.c \
@ -380,10 +522,17 @@ $(UV_OBJS): CFLAGS += \
-Wno-incompatible-pointer-types \
-Wno-maybe-uninitialized \
-Wno-sign-compare \
-Wno-unknown-attributes \
-Wno-unused-but-set-parameter \
-Wno-unused-but-set-variable \
-Wno-unused-result \
-Wno-unused-variable
-Wno-unused-variable \
-Wno-nonnull
$(UV_OBJS): CFLAGS += -fno-lto
$(filter out/win%,$(UV_OBJS)): \
CFLAGS += \
-Wno-cast-function-type \
-Wno-missing-braces
ifeq ($(UNAME_S),Linux)
$(UV_OBJS): CFLAGS += \
-D_GNU_SOURCE
@ -497,18 +646,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 \
@ -588,7 +725,7 @@ $(MINIUNZIP_OBJS): CFLAGS += \
LDFLAGS += \
-pthread \
-lm
debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
$(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
-lssl \
-lcrypto
ifneq ($(UNAME_S),Haiku)
@ -623,13 +760,31 @@ $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
-framework UIKit \
-framework WebKit
unix: debug release
win: windebug winrelease
all: $(BUILD_TYPES)
##
## Common targets:
##
debug: ## Build a debug executable for the current platform.
release: ## Build a release executable for the current platform.
all: $(BUILD_TYPES) ## Build all targets that appear possible to build on this machine.
unix: debug release ## Build all UNIX targets.
win: windebug winrelease ## Build all Windows targets.
.PHONY: all win unix
##
## Windows targets:
##
windebug: ## Build a debug win32 executable.
winrelease: ## Build a release win32 executable.
##
## MacOS targets:
##
macosdebug: ## Build a MacOS debug executable.
macosrelease: ## Build a MacOS release executable.
ALL_APP_OBJS := \
$(APP_OBJS) \
$(ARES_OBJS) \
$(BLOWFISH_OBJS) \
$(LIBBACKTRACE_OBJS) \
$(MINIUNZIP_OBJS) \
@ -637,8 +792,7 @@ ALL_APP_OBJS := \
$(QUICKJS_OBJS) \
$(SODIUM_OBJS) \
$(SQLITE_OBJS) \
$(UV_OBJS) \
$(XOPT_OBJS)
$(UV_OBJS)
DEPS = $(ALL_APP_OBJS:.o=.d)
-include $(DEPS)
@ -683,7 +837,18 @@ src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
-e 's/android:targetSdkVersion="[[:digit:]]*"/android:targetSdkVersion="$(ANDROID_TARGET_SDK_VERSION)"/' \
$@
# Android support.
##
## Android targets:
##
androiddebug: ## Build a debug 64-bit ARM Android APK.
androidrelease: ## Build a release 64-bit ARM Android APK.
androiddebug-armv7a: ## Build a debug 32-bit ARM Android APK.
androidrelease-armv7a: ## Build a release 32-bit ARM Android APK.
androiddebug-x86: ## Build a debug x86 Android APK.
androidrelease-x86: ## Build a release x86 Android APK.
androiddebug-x86_64: ## Build a debug x86_64 Android APK.
androidrelease-x86_64: ## Build a release x86_64 Android APK.
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
@mkdir -p $(dir $@)
@echo "[aapt2] $@"
@ -695,20 +860,37 @@ out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
@mkdir -p $(dir $@)
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
@echo [aapt2 link] res.apk
@mkdir -p out/apk/
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat \
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
--manifest src/android/AndroidManifest.xml \
-o out/apk/res.apk \
--java out/gen/
out/apk/res.fdroid.apk out/gen_fdroid/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
@echo [aapt2 link] res.fdroid.apk
@mkdir -p out/apk/
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat \
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
--rename-manifest-package com.unprompted.tildefriends.fdroid \
--manifest src/android/AndroidManifest.xml \
-o out/apk/res.fdroid.apk \
--java out/gen_fdroid/
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
$(CLASS_FILES) &: $(JAVA_FILES)
@echo "[javac] $(CLASS_FILES)"
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
@javac --release 8 -encoding UTF-8 -Xlint:deprecation -XDuseUnsharedTable=true -classpath $(ANDROID_PLATFORM)/android.jar:$(ANDROID_BUILD_TOOLS)/core-lambda-stubs.jar -d out/classes $(JAVA_FILES)
out/apk/classes.dex: $(CLASS_FILES)
@mkdir -p $(dir $@)
@echo "[d8] $@"
@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
@$(ANDROID_BUILD_TOOLS)/d8 --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
PACKAGE_DIRS := \
apps/ \
@ -717,55 +899,155 @@ PACKAGE_DIRS := \
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 := $(sort $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f -not -name '.*')))
out/apk/TildeFriends-arm-debug.unsigned.apk: BUILD_TYPE := debug
out/apk/TildeFriends-arm-release.unsigned.apk: BUILD_TYPE := release
out/apk/TildeFriends-x86-debug.unsigned.apk: BUILD_TYPE := debug
out/apk/TildeFriends-x86-release.unsigned.apk: BUILD_TYPE := release
out/apk/TildeFriends-release.fdroid.unsigned.apk: BUILD_TYPE := release
out/apk/TildeFriends-arm-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
out/apk/TildeFriends-arm-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends $(RAW_FILES) out/apk/res.apk
out/apk/TildeFriends-x86-debug.unsigned.apk: out/apk/classes.dex out/androiddebug-x86_64/tildefriends out/androiddebug-x86/tildefriends $(RAW_FILES) out/apk/res.apk
out/apk/TildeFriends-x86-release.unsigned.apk: out/apk/classes.dex out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.apk
out/apk/TildeFriends-release.fdroid.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-armv7a/tildefriends out/androidrelease-x86_64/tildefriends out/androidrelease-x86/tildefriends $(RAW_FILES) out/apk/res.fdroid.apk
$(BUNDLETOOL):
@echo [curl] $(BUNDLETOOL_URL) TO $@
@curl -q -L --create-dirs -o $@ $(BUNDLETOOL_URL)
out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGETS)) $(RAW_FILES) out/apk/res.apk src/android/AndroidManifest.xml $(BUNDLETOOL)
@rm -rf out/aab/staging/
@mkdir -p out/aab/staging
@$(ANDROID_BUILD_TOOLS)/aapt2 link --proto-format -o out/aab/temporary.apk \
-I $(ANDROID_PLATFORM)/android.jar \
--min-sdk-version $(ANDROID_MIN_SDK_VERSION) \
--target-sdk-version $(ANDROID_TARGET_SDK_VERSION) \
--manifest src/android/AndroidManifest.xml \
-R out/res/layout_activity_main.xml.flat \
-R out/res/drawable_icon.xml.flat \
--auto-add-overlay
@unzip out/aab/temporary.apk -d out/aab/staging/
@mkdir -p out/aab/staging/root/deps
@mkdir -p out/aab/staging/classes
@mkdir -p out/aab/staging/dex
@mkdir -p out/aab/staging/manifest
@mv out/aab/staging/AndroidManifest.xml out/aab/staging/manifest/AndroidManifest.xml
@cp out/apk/classes.dex out/aab/staging/dex/
@rm -fv out/base.zip
@mkdir -p out/aab/staging/lib/arm64-v8a out/aab/staging/lib/armeabi-v7a out/aab/staging/lib/x86_64 out/aab/staging/lib/x86
@cp out/androidrelease/tildefriends out/aab/staging/lib/arm64-v8a/libtildefriends.so
@cp out/androidrelease-armv7a/tildefriends out/aab/staging/lib/armeabi-v7a/libtildefriends.so
@cp out/androidrelease-x86_64/tildefriends out/aab/staging/lib/x86_64/libtildefriends.so
@cp out/androidrelease-x86/tildefriends out/aab/staging/lib/x86/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/arm64-v8a/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/armeabi-v7a/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86_64/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86/libtildefriends.so
@cp -r apps/ out/aab/staging/root/
@rm -rf out/aab/staging/root/apps/welcome*
@cp -r core/ out/aab/staging/root/
@cp -r deps/prettier/ out/aab/staging/root/deps/
@cp -r deps/lit/ out/aab/staging/root/deps/
@cp -r deps/codemirror/ out/aab/staging/root/deps/
@cd out/aab/staging/; zip -r ../base.zip *; cd ../../../
@java -jar $(BUNDLETOOL) build-bundle --overwrite --config=src/android/BundleConfig.json --modules=out/aab/base.zip --output=$@
@jarsigner -keystore .keys/android.jks $@ androidKey -storepass android
aab: out/TildeFriends.aab ## Build an Android App Bundle.
.PHONY: aab
out/TildeFriends.apks: out/TildeFriends.aab $(BUNDLETOOL)
@java -jar $(BUNDLETOOL) build-apks --bundle out/TildeFriends.aab --overwrite --output $@ --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android
aabgo: out/TildeFriends.apks $(BUNDLETOOL)
@java -jar $(BUNDLETOOL) install-apks --apks out/TildeFriends.apks
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
out/apk/TildeFriends-arm-%.unsigned.apk:
@mkdir -p $(dir $@) out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/
@echo "[aapt] $@"
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/tildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/tildefriends.so
@cp out/apk/res.apk $@
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-arm-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
@cp out/apk/res.apk $@.zip
@cp out/apk/classes.dex out/apk-arm-$(BUILD_TYPE)/
@cd out/apk-arm-$(BUILD_TYPE) && zip -u ../../$@ -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 -9 $(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] $@"
@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/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-x86-$(BUILD_TYPE)/lib/x86/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-x86-$(BUILD_TYPE)/lib/x86/libtildefriends.so
@cp out/apk/res.apk $@.zip
@cp out/apk/classes.dex out/apk-x86-$(BUILD_TYPE)/
@cd out/apk-x86-$(BUILD_TYPE) && zip -u ../../$@ -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 -9 $(RAW_FILES)
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
out/apk/TildeFriends-%.fdroid.unsigned.apk:
@rm -rf out/apk-fdroid-$(BUILD_TYPE) out/apk-fdroid-$(BUILD_TYPE)-raw
@mkdir -p $(dir $@) out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/ out/apk-fdroid-$(BUILD_TYPE)/lib/x86/ out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/ out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/
@echo "[aapt] $@"
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
@cp out/android$(BUILD_TYPE)-x86/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/x86/libtildefriends.so
@cp out/android$(BUILD_TYPE)/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
@cp out/android$(BUILD_TYPE)-armv7a/tildefriends out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/x86_64/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/x86/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/arm64-v8a/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk-fdroid-$(BUILD_TYPE)/lib/armeabi-v7a/libtildefriends.so
@cp out/apk/res.fdroid.apk $@.zip
@cp out/apk/classes.dex out/apk-fdroid-$(BUILD_TYPE)/classes.dex
@touch -d @0 out/apk-fdroid-$(BUILD_TYPE)/classes.dex out/apk-fdroid-$(BUILD_TYPE)/lib/*/libtildefriends.so
@chmod 755 out/apk-fdroid-$(BUILD_TYPE)/classes.dex out/apk-fdroid-$(BUILD_TYPE)/lib/*/libtildefriends.so
@cd out/apk-fdroid-$(BUILD_TYPE) && zip -X -u ../../$@.zip -q classes.dex lib/*/libtildefriends.so && cd ../../
@mkdir out/apk-fdroid-$(BUILD_TYPE)-raw
@for i in $(RAW_FILES); do mkdir -p $$(dirname out/apk-fdroid-$(BUILD_TYPE)-raw/$$i) && cp $$i out/apk-fdroid-$(BUILD_TYPE)-raw/$$i && touch -d @0 out/apk-fdroid-$(BUILD_TYPE)-raw/$$i && chmod 644 out/apk-fdroid-$(BUILD_TYPE)-raw/$$i; done
@cd out/apk-fdroid-$(BUILD_TYPE)-raw && zip -X -u ../../$@.zip -q $(RAW_FILES) && cd ../../
@$(ANDROID_BUILD_TOOLS)/zipalign -f 4 $@.zip $@
out/%.apk: out/apk/%.unsigned.apk
@echo "[apksigner] $(notdir $@)"
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $<
release-apk: out/TildeFriends-arm-release.apk out/TildeFriends-x86-release.apk
out/%.zopfli.apk: out/%.apk
@echo "[zopfli] $(notdir $@)"
$(ANDROID_BUILD_TOOLS)/zipalign -f -z 4 $< $@.zopfli
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $@.zopfli
release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk ## Build an Android release APK.
.PHONY: release-apk
releaseapkgo: out/TildeFriends-arm-release.apk
fdroid: out/apk/TildeFriends-release.fdroid.unsigned.apk ## Build Android APK for distribution on F-Droid.
.PHONY: fdroid
apkgo: out/TildeFriends-arm-debug.apk ## Build, install, and run a debug Android APK.
@adb install -r $<
@adb shell am start com.unprompted.tildefriends/.MainActivity
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
.PHONY: apkgo
releaseapkgo: out/TildeFriends-arm-release.apk ## Build, install, and run a release Android APK.
@adb install -r $<
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
.PHONY: releaseapkgo
# iOS Support
apklog: ## Display Android log output.
@adb logcat *:S tildefriends
.PHONY: apklog
##
## iPhoneOS targets:
##
iosdebug: ## Build a debug iPhoneOS executable.
iosrelease: ## Build a release iPhoneOS executable.
out/%.app/Info.plist: src/ios/Info.plist
@mkdir -p $(dir $@)
@cp -v $< $@
@ -773,12 +1055,13 @@ out/%.app/tildefriends.png: src/ios/tildefriends.png
@mkdir -p $(dir $@)
@cp -v $< $@
out/%/data.zip: $(RAW_FILES)
out/data.zip: $(RAW_FILES)
@zip -u $@ -q -9 $(RAW_FILES)
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/tildefriends-%.app/data.zip
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip
@mkdir -p $(dir $@)
@cp -v $< $@
@cp -v out/data.zip $(@D)/
ifeq ($(HAVE_LINUX_IOS),1)
@zsign -q -k .keys/apple.p12 -f -m src/ios/embedded.mobileprovision $(realpath $(dir $@))
endif
@ -791,53 +1074,110 @@ out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
@rm -rf $@.tmp/
iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends
iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends
iosdebug-app: out/tildefriends-iosdebug.app/tildefriends
iosrelease-app: out/tildefriends-iosrelease.app/tildefriends
iosdebug-ipa: out/tildefriends-debug.ipa
iosrelease-ipa: out/tildefriends-release.ipa
out/%/tildefriends.standalone: out/%/tildefriends out/data.zip
@echo "[standalone] $@"
@cat $< out/data.zip > $@
@chmod +x $@
out/%/tildefriends.standalone.exe: out/%/tildefriends.exe out/data.zip
@echo "[standalone] $@"
@cat $< out/data.zip > $@
@chmod +x $@
iossimdebug-app: out/tildefriends-iossimdebug.app/tildefriends ## Build a debug iOS Simulator .app directory.
iossimrelease-app: out/tildefriends-iossimrelease.app/tildefriends ## Build a release iOS Simulator .app directory.
iosdebug-app: out/tildefriends-iosdebug.app/tildefriends ## Build a debug iOS .app directory.
iosrelease-app: out/tildefriends-iosrelease.app/tildefriends ## Build a release iOS .app directory.
iosdebug-ipa: out/tildefriends-debug.ipa ## Build a debug iOS .ipa.
iosrelease-ipa: out/tildefriends-release.ipa ## Build a release iOS .ipa.
.PHONY: iossimdebug-app iossimrelease-app iosdebug-app iosrelease-app
ios%go: out/tildefriends-ios%.app/tildefriends
ideviceinstaller -i $(realpath $(dir $<))
iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends
iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends ## Build, install, and run an iOS debug build.
xcrun simctl install booted out/tildefriends-iossimdebug.app/
xcrun simctl launch booted com.unprompted.tildefriends
.PHONY: iossimdebuggo
apklog:
@adb logcat *:S tildefriends
.PHONY: apklog
ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a
$(ANDROID_DEPS):
+@ANDROID_NDK_ROOT=$(ANDROID_NDK) tools/ssl-android
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
fetchdeps:
@echo "[fetch] libuv"
@test -f out/deps/libuv.tar.gz || (mkdir -p out/deps/ && curl -q https://dist.libuv.org/dist/v1.48.0/libuv-v1.48.0.tar.gz -o out/deps/libuv.tar.gz)
@test -d deps/libuv/ || (mkdir -p deps/libuv/ && tar -C deps/libuv/ -m --strip=1 -xf out/deps/libuv.tar.gz)
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
##
## Linux package targets:
##
out/tildefriends-x86_64.AppImage: out/release/tildefriends out/data.zip
@echo "[appimage] $$@"
@rm -rf out/tildefriends.AppDir
@mkdir -p out/tildefriends.AppDir/usr/bin
@mkdir -p out/tildefriends.AppDir/usr/share/applications
@mkdir -p out/tildefriends.AppDir/usr/share/icons/hicolor/scalable/apps
@mkdir -p out/tildefriends.AppDir/usr/share/tildefriends
@echo $(APPIMAGETOOL_MD5) > out/appimagetool.md5
@test -x out/appimagetool || curl -q -L -o out/appimagetool $(APPIMAGETOOL_URL) && md5sum -c out/appimagetool.md5 && chmod +x out/appimagetool
@echo "[Desktop Entry]\nName=tildefriends\nExec=/usr/bin/tildefriends\nIcon=/usr/share/icons/hicolor/scalable/apps/tildefriends\nType=Application\nCategories=Network" > out/tildefriends.AppDir/tildefriends.desktop
@cp src/ios/tildefriends.svg out/tildefriends.AppDir/usr/share/icons/hicolor/scalable/apps/
@cp src/ios/tildefriends.svg out/tildefriends.AppDir/
@cp out/release/tildefriends out/tildefriends.AppDir/usr/bin/
@cp out/data.zip out/tildefriends.AppDir/usr/share/tildefriends/data.zip
@echo "#!/bin/sh\n\$${APPDIR}/usr/bin/tildefriends run -z \$$APPDIR/usr/share/tildefriends/data.zip" > out/tildefriends.AppDir/AppRun
@chmod +x out/tildefriends.AppDir/AppRun
@cd out; ./appimagetool --appimage-extract; cd ..
@cd out; unset SOURCE_DATE_EPOCH; PATH=$$PATH:squashfs-root/usr/bin ARCH=x86_64 squashfs-root/usr/bin/appimagetool -u 'zsync|https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage.zsync' tildefriends.AppDir tildefriends-x86_64.AppImage; cd ..
appimage: out/tildefriends-x86_64.AppImage ## Build an AppImage.
.PHONY: appimage
flatpak: out/ ## Build a flatpak.
flatpak-builder --force-clean --user --install-deps-from=flathub --install --repo=out/flatpak-repo out/flatpak src/com.unprompted.tildefriends.yml
flatpak build-bundle out/flatpak-repo out/tildefriends.flatpak com.unprompted.tildefriends
.PHONY: flatpak
##
## Targets for release management:
##
fetchdeps: ## Update various external sources that live in the tree that can't be pulled in as git submodules.
@echo "[fetch] sqlite"
@test -f out/deps/sqlite.zip || (mkdir -p out/deps/ && curl -q https://www.sqlite.org/2024/sqlite-amalgamation-3450100.zip -o out/deps/sqlite.zip)
@test -d deps/sqlite/ || (mkdir -p deps/sqlite/ && unzip -qDj -d deps/sqlite/ out/deps/sqlite.zip)
@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
.PHONE: fetchdeps
.PHONY: fetchdeps
clean:
rm -rf $(BUILD_DIR)
.PHONY: clean
shots: ## Copy generated screenshots from `tildefriends test -t=auto` into place in the metadata/ directory.
@echo [shots] $(wildcard out/screenshot*.png)
@cp -f out/screenshot*.png metadata/en-US/images/phoneScreenshots/
.PHONY: shots
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"
tarball: ## Build an all-inclusive source tarball (.tar.xz).
@echo [archive] out/tildefriends-$(VERSION_NUMBER).tar.xz
@rm -rf out/tildefriends-$(VERSION_NUMBER)
@mkdir -p out/tildefriends-$(VERSION_NUMBER)
@git ls-files --recurse-submodules | tar -c -T- | 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* \
@ -852,23 +1192,79 @@ 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 out/tildefriends-$(VERSION_NUMBER).tar.xz \
-C out/ \
tildefriends-$(VERSION_NUMBER)
.PHONY: tarball
dist: ## Build versions of all distributables for release.
dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) out/TildeFriends-release.fdroid.apk appimage tarball
@mkdir -p dist/
@echo "[cp] tildefriends-$(VERSION_NUMBER).tar.xz"
@cp out/tildefriends-$(VERSION_NUMBER).tar.xz dist/tildefriends-$(VERSION_NUMBER).tar.xz
@echo "[cp] TildeFriends-x86-$(VERSION_NUMBER).apk"
@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
@test $(HAVE_WIN) && echo "[cp] tildefriends-$(VERSION_NUMBER).exe"
@test $(HAVE_WIN) && cp out/winrelease/tildefriends.standalone.exe dist/tildefriends-$(VERSION_NUMBER).exe
@echo "[cp] TildeFriends-$(VERSION_NUMBER).aab"
@cp out/TildeFriends.aab dist/TildeFriends-$(VERSION_NUMBER).aab
@echo "[cp] TildeFriends-$(VERSION_NUMBER).fdroid.apk"
@cp out/TildeFriends-release.fdroid.apk dist/TildeFriends-$(VERSION_NUMBER).fdroid.apk
@echo "[cp] TildeFriends-x86_64-$(VERSION_NUMBER).AppImage"
@cp out/tildefriends-x86_64.AppImage dist/TildeFriends-x86_64-$(VERSION_NUMBER).AppImage
.PHONY: dist
dist-test: dist
dist-test: dist ## Exercise some built distributable files, making sure they work as intended.
@tar -xf tildefriends-$(VERSION_NUMBER).tar.xz
@$(MAKE) -C tildefriends-$(VERSION_NUMBER)/ debug release
@docker build tildefriends-$(VERSION_NUMBER)/
@rm -rf tildefriends-$(VERSION_NUMBER)
.PHONY: dist-test
format:
##
## Targets for tidying up:
##
format: ## Standardize formatting of C source.
@clang-format -i $(wildcard src/*.c src/*.h src/*.m)
.PHONY: format
prettier: ## Standardize formatting of JavaScript and Markdown source.
@npm run prettier
.PHONY: prettier
clean: ## Clean all generated files from the out/ directory.
rm -rf $(BUILD_DIR)
.PHONY: clean
##
## Documentation:
##
help: ## Display this help message.
@awk \
-F: \
-vG=$$(tput setaf 2) \
-vO=$$(tput setaf 3) \
-vB=$$(tput setaf 4) \
-vM=$$(tput setaf 5) \
-vC=$$(tput setaf 6) \
-vR=$$(tput sgr0) ' \
/^## ==.*==$$/ { sub(/^## ?/, ""); printf "%s%s%s\n", C, $$0, R } \
/^##.*:=.*/ { sub(/^## ?/, ""); sub(/:=/, ":"); printf " %s%-20s%s %s%s%s\n", M, $$1, R, O, $$2, R } \
/^##/ { sub(/^## ?/, ""); print $$0 } \
/^[[:alnum:]-]+:.*##/ { \
sub(/:.*##\s?/, ":"); \
printf " %s%-20s%s %s%s%s\n", G, $$1, R, O, $$2, R \
} \
' < $(filter-out %.d,$(MAKEFILE_LIST))
@echo "" # Blank line.
.PHONY: help
.DEFAULT_GOAL := help
docs: ## Build HTML docs.
@doxygen
.PHONY: docs

View File

@ -1,4 +1,5 @@
# Tilde Friends
Tilde Friends is a tool for making and sharing.
A public instance lives at https://www.tildefriends.net/.
@ -7,37 +8,70 @@ 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
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
all of those host platforms plus mingw64, iOS, and android.
## Getting the Source
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
generated in a subdirectory of `out/`.
3. It's possible to build for Android, iOS, and Windows on Linux, if you have
the right dependencies in the right places. `make windebug winrelease
iosdebug-ipa iosrelease-ipa release-apk`.
4. To build in docker, `docker build .`.
5. `make format` will normalize formatting to the coding standard.
Tilde Friends uses git submodules, so either:
```
git clone --recurse-submodules https://dev.tildefriends.net/cory/tildefriends.git
```
or:
```
git clone https://dev.tildefriends.net/cory/tildefriends.git
cd tildefriends
git submodule update --init --recursive
```
The `.tar.xz` source releases are all-inclusive.
## Building
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. It's possible
to build for Android, iOS, and Windows on Linux, if you have the right
dependencies in the right places.
### Requirements
On Linux only, system OpenSSL libraries (`libssl-dev`, in debian-speak) are
assumed to be available.
On MacOS, Xcode's command-line tools are expected to be available.
### Build Commands
Run `make` with no arguments to see available build targets and options. `make
debug` is a good place to start.
To build in docker, `docker build .`.
`make format` and `make prettier` 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.
By default, running the built `out/debug/tildefriends` executable will start a
web server at <http://localhost:12345/>. It expects to be run with the
repository root as the current working directory. `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
privileges. Further administration can be done at
<http://localhost:12345/~core/admin/>.
## Documentation
Docs are a work in progress:
<https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
<https://dev.tildefriends.net/cory/tildefriends/wiki>.
## License
All code unless otherwise noted in is provided under the
[MIT](https://opensource.org/licenses/MIT) license.

View File

@ -1,4 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🎛"
}
"type": "tildefriends-app",
"emoji": "🎛",
"previous": "&R49FywYF8CXPhoSEydLbSCgvCddeyTiBwGuDU/gqY+M=.sha256"
}

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,41 @@
<!DOCTYPE html>
<!doctype html>
<html style="width: 100%">
<head>
<script>const g_data = $data;</script>
<script>
const g_data = $data;
</script>
<link rel="stylesheet" href="w3.css" />
<!-- prettier-ignore -->
<style>
/* 2018 Valiant Poppy */
.w3-theme-l5 {color:#000 !important; background-color:#fbf3f3 !important}
.w3-theme-l4 {color:#000 !important; background-color:#f3d7d6 !important}
.w3-theme-l3 {color:#000 !important; background-color:#e6afae !important}
.w3-theme-l2 {color:#fff !important; background-color:#da8785 !important}
.w3-theme-l1 {color:#fff !important; background-color:#cd5f5d !important}
.w3-theme-d1 {color:#fff !important; background-color:#a93634 !important}
.w3-theme-d2 {color:#fff !important; background-color:#96302e !important}
.w3-theme-d3 {color:#fff !important; background-color:#832a28 !important}
.w3-theme-d4 {color:#fff !important; background-color:#702423 !important}
.w3-theme-d5 {color:#fff !important; background-color:#5e1e1d !important}
.w3-theme-light {color:#000 !important; background-color:#fbf3f3 !important}
.w3-theme-dark {color:#fff !important; background-color:#5e1e1d !important}
.w3-theme-action {color:#fff !important; background-color:#5e1e1d !important}
.w3-theme {color:#fff !important; background-color:#bd3d3a !important}
.w3-text-theme {color:#bd3d3a !important}
.w3-border-theme {border-color:#bd3d3a !important}
.w3-hover-theme:hover {color:#fff !important; background-color:#bd3d3a !important}
.w3-hover-text-theme:hover {color:#bd3d3a !important}
.w3-hover-border-theme:hover {border-color:#bd3d3a !important}
</style>
</head>
<body style="color: #fff; width: 100%">
<h1>Tilde Friends Administration</h1>
<body class="w3-theme-l4">
<header class="w3-row w3-padding w3-header w3-theme-l1">
<h1>Tilde Friends Administration</h1>
</header>
</body>
<script type="module" src="script.js"></script>
</html>
</html>

View File

@ -3,85 +3,113 @@ 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>`;
function title_case(name) {
return name
.split('_')
.map((x) => x.charAt(0).toUpperCase() + x.substring(1))
.join(' ');
}
window.addEventListener('load', function () {
const permission_template = (permission) => html` <code>${permission}</code>`;
function input_template(key, description) {
if (description.type === 'boolean') {
return html`
<div style="margin-top: 1em">
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
<div>
<input type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
<div>${description.description}</div>
</div>
</div>
<li class="w3-row">
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
<div class="w3-quarter w3-padding">${description.description}</div>
<div class="w3-quarter w3-padding w3-center"><input class="w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input></div>
<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstChild.checked)}>Set</button>
</li>
`;
} else if (description.type === 'textarea') {
return html`
<div style="margin-top: 1em"">
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
<div style="width: 100%; padding: 0; margin: 0">
<div style="width: 90%; padding: 0 margin: 0">
<textarea style="vertical-align: top; width: 100%" rows=20 cols=80 id=${'gs_' + key}>${description.value}</textarea>
</div>
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstElementChild.value)}>Set</button>
<div>${description.description}</div>
</div>
</div>
<li class="w3-row">
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold"
>${title_case(key)}</label
>
<div class="w3-rest w3-padding">${description.description}</div>
<textarea
class="w3-input"
style="vertical-align: top; resize: vertical"
id=${'gs_' + key}
>
${description.value}</textarea
>
<button
class="w3-button w3-right w3-quarter w3-theme-action"
@click=${(e) =>
global_settings_set(
key,
e.srcElement.previousElementSibling.value
)}
>
Set
</button>
</li>
`;
} else {
return html`
<div style="margin-top: 1em">
<label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
<div>
<input type="text" value="${description.value}" id=${'gs_' + key}></input>
<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
<div>${description.description}</div>
</div>
</div>
<li class="w3-row">
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
<div class="w3-quarter w3-padding">${description.description}</div>
<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input>
<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
</li>
`;
}
}
const user_template = (user, permissions) => html`
<li>
<button @click=${(e) => delete_user(user)}>
<li class="w3-card w3-margin">
<button
class="w3-button w3-theme-action"
@click=${(e) => delete_user(user)}
>
Delete
</button>
${user}:
${permissions.map(x => permission_template(x))}
${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>`;
html` <header class="w3-container w3-theme-l2"><h2>Users</h2></header>
<ul class="w3-ul">
${Object.entries(users).map((u) => user_template(u[0], u[1]))}
</ul>`;
const page_template = (data) =>
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
<h2>Global Settings</h2>
<div>
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header>
<div class="w3-container">
<ul class="w3-ul">
${Object.keys(data.settings)
.sort()
.map((x) => html`${input_template(x, data.settings[x])}`)}
</ul>
</div>
${users_template(data.users)}
</div>
`;
</div> `;
render(page_template(g_data), document.body);
});
});

235
apps/admin/w3.css Normal file
View File

@ -0,0 +1,235 @@
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
a{background-color:transparent}a:active,a:hover{outline-width:0}
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
button,input{overflow:visible}button,select{text-transform:none}
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
[type=checkbox],[type=radio]{padding:0}
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
/* End extract */
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
.w3-main,#main{transition:margin-left .4s}
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
.w3-bar .w3-button{white-space:normal}
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
.w3-responsive{display:block;overflow-x:auto}
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
@media (max-width:1205px){.w3-auto{max-width:95%}}
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
.w3-display-position{position:absolute}
.w3-circle{border-radius:50%}
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
.w3-left{float:left!important}.w3-right{float:right!important}
.w3-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important}
/* Colors */
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}

View File

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

View File

@ -21,7 +21,7 @@ function* treeify(prefix, o) {
function markdown(md) {
let parsed = new commonmark.Parser().parse(md ?? '*undocumented*');
return new commonmark.HtmlRenderer().render(parsed);
return new commonmark.HtmlRenderer({safe: true}).render(parsed);
}
function document(api) {

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": "&RdVEsVscZm3aWzcMrEZS8mskO5tUmvaEUihex2MMfZQ=.sha256"
}
"type": "tildefriends-app",
"emoji": "💻",
"previous": "&icsPplXHgmpkbNWyo/WTvRdT/A8BXxW4lJixOtP4ueQ=.sha256"
}

View File

@ -26,14 +26,15 @@ async function fetch_info(apps) {
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') {
@ -44,10 +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;

View File

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🪵",
"previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256"
}
"type": "tildefriends-app",
"emoji": "🪵",
"previous": "&3jabNEk6W2uolzTvfXX6fcWF50N3501vtgZ6ZxFVJ1s=.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,16 +43,17 @@ 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;
}
export function markdown(md) {
let reader = new commonmark.Parser({safe: true});
let writer = new commonmark.HtmlRenderer();
let reader = new commonmark.Parser();
let writer = new commonmark.HtmlRenderer({safe: true});
let parsed = reader.parse(md || '');
let walker = parsed.walker();
let event, node;
@ -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;
}
}

File diff suppressed because one or more lines are too long

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,5 @@
{
"type": "tildefriends-app",
"emoji": "💽"
}
"type": "tildefriends-app",
"emoji": "💽",
"previous": "&uQzkIe/Aj8yNhLKe3hEq+5fEJsGwIUx8NVBjJKwoV2U=.sha256"
}

View File

@ -51,7 +51,20 @@ async function key_list(db) {
app.setDocument(doc);
}
core.register('message', async function(message) {
function load() {
if (core.user?.credentials?.session) {
database_list();
} else {
app.setDocument(`<!DOCTYPE html>
<html>
<body style="background: #888">
<h1>Must be signed in to examine databases.</h1>
</body>
</html>`);
}
}
core.register('message', async function (message) {
if (message.event == 'hashChange') {
let hash = message.hash.substring(1);
if (hash.startsWith(':shared:')) {
@ -62,9 +75,9 @@ core.register('message', async function(message) {
} else if (hash.length) {
key_list(await database(hash.split(':').slice(1).join(':')));
} else {
database_list();
load();
}
}
});
database_list();
load();

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

120
apps/gg/lit-all.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🪪",
"previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256"
}
"type": "tildefriends-app",
"emoji": "🪪",
"previous": "&5kw/2PgcySwOYCmAkjHTR2xTkIx3i7UjQmtQ8MfgWw8=.sha256"
}

View File

@ -1,5 +1,7 @@
import * as tfrpc from '/tfrpc.js';
const is_admin = core.user?.credentials?.permissions?.administration;
tfrpc.register(async function get_private_key(id) {
return bip39Words(await ssb.getPrivateKey(id));
});
@ -15,10 +17,44 @@ tfrpc.register(async function delete_id(id) {
tfrpc.register(async function reload() {
await main();
});
tfrpc.register(async function make_server(id) {
return await ssb.swapWithServerIdentity(id);
});
async function main() {
let ids = await ssb.getIdentities();
await app.setDocument(`<body style="color: #fff">
let server_id = await ssb.getServerIdentity();
await app.setDocument(
`
<head>
<link rel="stylesheet" href="w3.css"></link>
<style>
/* "2018 Sargasso Sea" */
.w3-theme-l5 {color:#000 !important; background-color:#f3f4f7 !important}
.w3-theme-l4 {color:#000 !important; background-color:#d7dbe3 !important}
.w3-theme-l3 {color:#000 !important; background-color:#b0b6c8 !important}
.w3-theme-l2 {color:#fff !important; background-color:#8892ac !important}
.w3-theme-l1 {color:#fff !important; background-color:#636f8e !important}
.w3-theme-d1 {color:#fff !important; background-color:#40485c !important}
.w3-theme-d2 {color:#fff !important; background-color:#394052 !important}
.w3-theme-d3 {color:#fff !important; background-color:#323848 !important}
.w3-theme-d4 {color:#fff !important; background-color:#2b303d !important}
.w3-theme-d5 {color:#fff !important; background-color:#242833 !important}
.w3-theme-light {color:#000 !important; background-color:#f3f4f7 !important}
.w3-theme-dark {color:#fff !important; background-color:#242833 !important}
.w3-theme-action {color:#fff !important; background-color:#242833 !important}
.w3-theme {color:#fff !important; background-color:#485167 !important}
.w3-text-theme {color:#485167 !important}
.w3-border-theme {border-color:#485167 !important}
.w3-hover-theme:hover {color:#fff !important; background-color:#485167 !important}
.w3-hover-text-theme:hover {color:#485167 !important}
.w3-hover-border-theme:hover {border-color:#485167 !important}
</style>
</head>
<body class="w3-theme-l3">
<script>const handler = {};</script>
<script type="module">
import * as tfrpc from '/static/tfrpc.js';
@ -26,7 +62,8 @@ async function main() {
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.style = 'width: 100%; height: auto; read-only: true; resize: none';
element.classList.add('w3-input');
element.readOnly = true;
event.srcElement.parentElement.appendChild(element);
event.srcElement.onclick = event => handler.hide_id(event, element);
@ -47,7 +84,7 @@ async function main() {
alert('Successfully created: ' + id);
await tfrpc.rpc.reload();
} catch (e) {
alert('Error creating identity: ' + e);
alert('Error creating identity: ' + e.message);
}
}
handler.hide_id = function hide_id(event, element) {
@ -67,21 +104,50 @@ async function main() {
alert('Error deleting ID: ' + e);
}
}
handler.make_server = async function make_server(event) {
let id = event.srcElement.dataset.id;
try {
if (confirm('Are you sure you want to make "' + id + '" the server identity?\\n\\nFor it to take effect, you will need to both sign in again and restart Tilde Friends.')) {
await tfrpc.rpc.make_server(id);
}
} catch (e) {
alert('Error making server ID: ' + e);
}
}
</script>
<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>`);
<header class="w3-theme w3-padding"><h1>SSB Identity Management</h1></header>
<div class="w3-card-4 w3-margin">
<header class="w3-container w3-theme-l2"><h2>Create a new identity</h2></header>
<footer class="w3-padding">
<button id="create_id" onclick="handler.create_id()" class="w3-button w3-theme">Create Identity</button>
</footer>
</div>
<div class="w3-card-4 w3-margin">
<header class="w3-container w3-theme-l2"><h2>Import an SSB Identity from 12 BIP39 English Words</h2></header>
<textarea id="add_id" style="width: 100%" rows="4" class="w3-input"></textarea>
<footer class="w3-padding">
<button id="add" onclick="handler.add_id(event)" class="w3-button w3-theme">Import Identity</button>
</footer>
</div>
<div class="w3-card-4 w3-margin">
<header class="w3-container w3-theme-l2"><h2>Identities</h2></header>
<ul class="w3-ul">` +
(ids ?? [])
.map(
(
id
) => `<li style="overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis">
<button onclick="handler.export_id(event)" data-id="${id}" class="w3-button w3-theme">Export Identity</button>
<button onclick="handler.delete_id(event)" data-id="${id}" class="w3-button w3-theme">Delete Identity</button>
${is_admin && id != server_id ? `<button onclick="handler.make_server(event)" data-id="${id}" class="w3-button w3-theme">Make Server Identity</button>` : ''}
${id}${id == server_id ? ' <div class="w3-tag w3-theme-l4 w3-round">🖥 local server</div>' : ''}
</li>`
)
.join('\n') +
` </ul>
</div>
</body>`
);
}
main();

235
apps/identity/w3.css Normal file
View File

@ -0,0 +1,235 @@
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
a{background-color:transparent}a:active,a:hover{outline-width:0}
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
button,input{overflow:visible}button,select{text-transform:none}
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
[type=checkbox],[type=radio]{padding:0}
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
/* End extract */
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
.w3-main,#main{transition:margin-left .4s}
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
.w3-bar .w3-button{white-space:normal}
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
.w3-responsive{display:block;overflow-x:auto}
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
@media (max-width:1205px){.w3-auto{max-width:95%}}
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
.w3-display-position{position:absolute}
.w3-circle{border-radius:50%}
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
.w3-left{float:left!important}.w3-right{float:right!important}
.w3-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important}
/* Colors */
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}

View File

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

View File

@ -67,9 +67,6 @@ tfrpc.register(function getHash(id, message) {
tfrpc.register(function setHash(hash) {
return app.setHash(hash);
});
ssb.addEventListener('message', async function(id) {
await tfrpc.rpc.notifyNewMessage(id);
});
tfrpc.register(async function store_blob(blob) {
if (Array.isArray(blob)) {
blob = Uint8Array.from(blob);
@ -85,21 +82,26 @@ tfrpc.register(async function store_message(message) {
tfrpc.register(function apps() {
return core.apps();
});
tfrpc.register(function getActiveIdentity() {
return ssb.getActiveIdentity();
});
tfrpc.register(async function try_decrypt(id, content) {
return await ssb.privateMessageDecrypt(id, content);
});
ssb.addEventListener('broadcasts', async function() {
core.register('onMessage', async function (id) {
await tfrpc.rpc.notifyNewMessage(id);
});
core.register('onBroadcastsChanged', async function () {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
});
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();

File diff suppressed because one or more lines are too long

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

@ -4,43 +4,6 @@ import * as tfutils from './tf-utils.js';
const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256';
class TfIdPickerElement extends LitElement {
static get properties() {
return {
ids: {type: Array},
selected: {type: String},
};
}
constructor() {
super();
this.load();
}
async load() {
this.selected = await tfrpc.rpc.localStorageGet('whoami');
this.ids = (await tfrpc.rpc.getIdentities()) || [];
}
changed(event) {
this.selected = event.srcElement.value;
tfrpc.rpc.localStorageSet('whoami', this.selected);
}
render() {
if (this.ids) {
return html`
<select @change=${this.changed} style="max-width: 100%">
${(this.ids).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
</select>
`;
} else {
return html`<div>Loading...</div>`;
}
}
}
customElements.define('tf-id-picker', TfIdPickerElement);
class TfComposeElement extends LitElement {
static get properties() {
return {
@ -57,13 +20,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,18 +61,21 @@ class TfIssuesAppElement extends LitElement {
async load() {
let issues = {};
let messages = await tfrpc.rpc.query(`
WITH issues AS (SELECT messages.* FROM messages_refs JOIN messages ON
let messages = await tfrpc.rpc.query(
`
WITH issues AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM messages_refs JOIN messages ON
messages.id = messages_refs.message
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
edits AS (SELECT messages.* FROM issues JOIN messages_refs ON
edits AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM issues JOIN messages_refs ON
issues.id = messages_refs.ref JOIN messages ON
messages.id = messages_refs.message
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
SELECT * FROM issues
UNION
SELECT * FROM edits ORDER BY timestamp
`, [k_project]);
`,
[k_project]
);
for (let message of messages) {
let content = JSON.parse(message.content);
switch (content.type) {
@ -123,7 +91,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 +104,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 +120,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,14 +149,22 @@ 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?`)) {
let whoami = this.shadowRoot.getElementById('picker').selected;
if (
confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)
) {
let whoami = await tfrpc.rpc.getActiveIdentity();
await tfrpc.rpc.appendMessage(whoami, {
type: 'issue-edit',
issues: [
@ -192,7 +179,7 @@ class TfIssuesAppElement extends LitElement {
}
async create_issue(event) {
let whoami = this.shadowRoot.getElementById('picker').selected;
let whoami = await tfrpc.rpc.getActiveIdentity();
await tfrpc.rpc.appendMessage(whoami, {
type: 'issue',
project: k_project,
@ -202,12 +189,14 @@ class TfIssuesAppElement extends LitElement {
}
async reply_to_issue(event) {
let whoami = this.shadowRoot.getElementById('picker').selected;
let whoami = await tfrpc.rpc.getActiveIdentity();
await tfrpc.rpc.appendMessage(whoami, {
type: 'post',
text: event.detail.value,
root: this.selected.id,
branch: this.selected.updates.length ? this.selected.updates[this.selected.updates.length - 1].id : this.selected.id,
branch: this.selected.updates.length
? this.selected.updates[this.selected.updates.length - 1].id
: this.selected.id,
issues: [
{
link: this.selected.id,
@ -218,24 +207,23 @@ class TfIssuesAppElement extends LitElement {
}
render() {
let header = html`
<h1>Tilde Friends Issues</h1>
<tf-id-picker id="picker"></tf-id-picker>
`;
let header = html` <h1>Tilde Friends Issues</h1> `;
if (this.selected) {
return html`
${header}
<div>
<input type="button" value="Back" @click=${() => this.selected = undefined}></input>
${this.selected.open ?
html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>` :
html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`}
<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 +238,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,38 @@
import * as linkify from './commonmark-linkify.js';
var reUnsafeProtocol = /^javascript:|vbscript:|file:|data:/i;
var reSafeDataProtocol = /^data:image\/(?:png|gif|jpeg|webp)/i;
var potentiallyUnsafe = function (url) {
return reUnsafeProtocol.test(url) && !reSafeDataProtocol.test(url);
};
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 +42,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 {
@ -45,8 +67,8 @@ function image(node, entering) {
}
export function markdown(md) {
var reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer();
var reader = new commonmark.Parser();
var writer = new commonmark.HtmlRenderer({safe: true});
writer.image = image;
var parsed = reader.parse(md || '');
parsed = linkify.transform(parsed);
@ -56,14 +78,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 +116,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": "&5LpOTEnor/rYFk3axyfmmehAoq9aEwNQRH4jwNhRQ7o=.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) {
core.register('onMessage', 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();

File diff suppressed because one or more lines are too long

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

@ -18,8 +18,8 @@ class TfJournalEntryElement extends LitElement {
}
markdown(md) {
var reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer();
var reader = new commonmark.Parser();
var writer = new commonmark.HtmlRenderer({safe: true});
var parsed = reader.parse(md || '');
return writer.render(parsed);
}
@ -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": "&HXCdDG8gGYXElTyEFbg85jqa6lDXNL2ENPIA9UoJNbI=.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": "&DUxMMCJcuhm6S9jg/eKgEyWodkITu6Tg9g5I5wgLWFU=.sha256"
}
"type": "tildefriends-app",
"emoji": "🐌",
"previous": "&0gBRfD+3EaZD2S82zAnXT3hgGuNTnUncOh5vGwZwbSw=.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) {
core.register('onMessage', async function (id) {
await tfrpc.rpc.notifyNewMessage(id);
});
tfrpc.register(async function store_blob(blob) {
@ -100,18 +100,27 @@ 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() {
tfrpc.register(async function getActiveIdentity() {
return await ssb.getActiveIdentity();
});
tfrpc.register(async function sync() {
return await ssb.sync();
});
core.register('onBroadcastsChanged', async function () {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
});
core.register('onConnectionsChanged', async function() {
core.register('onConnectionsChanged', async function () {
await tfrpc.rpc.set('connections', await ssb.connections());
});
core.register('setActiveIdentity', async function (id) {
await tfrpc.rpc.set('identity', id);
});
async function main() {
if (typeof(database) !== 'undefined') {
if (typeof database !== 'undefined') {
g_database = await database('ssb');
}
await app.setDocument(utf8Decode(await getFile('index.html')));
}
main();
main();

View File

@ -1,90 +1,94 @@
function textNode(text) {
const node = new commonmark.Node("text", undefined);
node.literal = text;
return node;
const node = new commonmark.Node('text', undefined);
node.literal = text;
return node;
}
function linkNode(text, link) {
const linkNode = new commonmark.Node("link", undefined);
linkNode.destination = `#q=${encodeURIComponent(link)}`;
linkNode.appendChild(textNode(text));
return linkNode;
const linkNode = new commonmark.Node('link', undefined);
if (link.startsWith('#')) {
linkNode.destination = `#${encodeURIComponent('#' + link)}`;
} else {
linkNode.destination = link;
}
linkNode.appendChild(textNode(text));
return linkNode;
}
function splitMatches(text, regexp) {
// Regexp must be sticky.
regexp = new RegExp(regexp, "gm");
// Regexp must be sticky.
regexp = new RegExp(regexp, 'gm');
let i = 0;
const result = [];
let i = 0;
const result = [];
let match = regexp.exec(text);
while (match) {
const matchText = match[0];
let match = regexp.exec(text);
while (match) {
const matchText = match[0];
if (match.index > i) {
result.push([text.substring(i, match.index), false]);
}
if (match.index > i) {
result.push([text.substring(i, match.index), false]);
}
result.push([matchText, true]);
i = match.index + matchText.length;
result.push([matchText, true]);
i = match.index + matchText.length;
match = regexp.exec(text);
}
match = regexp.exec(text);
}
if (i < text.length) {
result.push([text.substring(i, text.length), false]);
}
if (i < text.length) {
result.push([text.substring(i, text.length), false]);
}
return result;
return result;
}
const regex = new RegExp("(?<!\\w)#[\\w-]+");
const regex = new RegExp('(?:https?://[^ ]+[^ .,])|(?:(?<!\\w)#[\\w-]+)|(?:@[A-Za-z0-9+/]+=.ed25519)|(?:[%&][A-Za-z0-9+/]+=.sha256)');
function split(textNodes) {
const text = textNodes.map(n => n.literal).join("");
const parts = splitMatches(text, regex);
const text = textNodes.map((n) => n.literal).join('');
const parts = splitMatches(text, regex);
return parts.map(part => {
if (part[1]) {
return linkNode(part[0], part[0]);
} else {
return textNode(part[0]);
}
});
return parts.map((part) => {
if (part[1]) {
return linkNode(part[0], part[0]);
} else {
return textNode(part[0]);
}
});
}
export function transform(parsed) {
const walker = parsed.walker();
let event;
const walker = parsed.walker();
let event;
let nodes = [];
while ((event = walker.next())) {
const node = event.node;
if (event.entering && node.type === "text") {
nodes.push(node);
} else {
if (nodes.length > 0) {
split(nodes)
.reverse()
.forEach(newNode => {
nodes[0].insertAfter(newNode);
});
let nodes = [];
while ((event = walker.next())) {
const node = event.node;
if (event.entering && node.type === 'text') {
nodes.push(node);
} else {
if (nodes.length > 0) {
split(nodes)
.reverse()
.forEach((newNode) => {
nodes[0].insertAfter(newNode);
});
nodes.forEach(n => n.unlink());
nodes = [];
}
}
}
nodes.forEach((n) => n.unlink());
nodes = [];
}
}
}
if (nodes.length > 0) {
split(nodes)
.reverse()
.forEach(newNode => {
nodes[0].insertAfter(newNode);
});
nodes.forEach(n => n.unlink());
}
if (nodes.length > 0) {
split(nodes)
.reverse()
.forEach((newNode) => {
nodes[0].insertAfter(newNode);
});
nodes.forEach((n) => n.unlink());
}
return parsed;
return parsed;
}

View File

@ -1,91 +0,0 @@
function textNode(text) {
const node = new commonmark.Node("text", undefined);
node.literal = text;
return node;
}
function linkNode(text, url) {
const urlNode = new commonmark.Node("link", undefined);
urlNode.destination = url;
urlNode.appendChild(textNode(text));
return urlNode;
}
function splitMatches(text, regexp) {
// Regexp must be sticky.
regexp = new RegExp(regexp, "gm");
let i = 0;
const result = [];
let match = regexp.exec(text);
while (match) {
const matchText = match[0];
if (match.index > i) {
result.push([text.substring(i, match.index), false]);
}
result.push([matchText, true]);
i = match.index + matchText.length;
match = regexp.exec(text);
}
if (i < text.length) {
result.push([text.substring(i, text.length), false]);
}
return result;
}
const urlRegexp = new RegExp("https?://[^ ]+[^ .,]");
function splitURLs(textNodes) {
const text = textNodes.map(n => n.literal).join("");
const parts = splitMatches(text, urlRegexp);
return parts.map(part => {
if (part[1]) {
return linkNode(part[0], part[0]);
} else {
return textNode(part[0]);
}
});
}
export function transform(parsed) {
const walker = parsed.walker();
let event;
let nodes = [];
while ((event = walker.next())) {
const node = event.node;
if (event.entering && node.type === "text") {
nodes.push(node);
} else {
if (nodes.length > 0) {
splitURLs(nodes)
.reverse()
.forEach(newNode => {
nodes[0].insertAfter(newNode);
});
nodes.forEach(n => n.unlink());
nodes = [];
}
}
}
if (nodes.length > 0) {
splitURLs(nodes)
.reverse()
.forEach(newNode => {
nodes[0].insertAfter(newNode);
});
nodes.forEach(n => n.unlink());
}
return parsed;
}

File diff suppressed because one or more lines are too long

View File

@ -1,112 +1,164 @@
import * as tfrpc from '/static/tfrpc.js';
import {html, render} from './lit-all.min.js';
import {styles} from './tf-styles.js';
let g_emojis;
function get_emojis() {
if (g_emojis) {
return Promise.resolve(g_emojis);
}
return fetch('emojis.json').then(function(result) {
return fetch('emojis.json').then(function (result) {
g_emojis = result.json();
return g_emojis;
});
}
export function picker(callback, anchor) {
get_emojis().then(function(json) {
let div = document.createElement('div');
div.id = 'emoji_picker';
div.style.color = '#000';
div.style.background = '#fff';
div.style.border = '1px solid #000';
div.style.display = 'block';
div.style.position = 'absolute';
div.style.minWidth = 'min(16em, 90vw)';
div.style.width = 'min(16em, 90vw)';
div.style.maxWidth = 'min(16em, 90vw)';
div.style.maxHeight = '16em';
div.style.overflow = 'scroll';
div.style.fontWeight = 'bold';
div.style.fontSize = 'xx-large';
let input = document.createElement('input');
input.type = 'text';
input.style.display = 'block';
input.style.boxSizing = 'border-box';
input.style.width = '100%';
input.style.margin = '0';
input.style.position = 'relative';
div.appendChild(input);
let list = document.createElement('div');
div.appendChild(list);
div.addEventListener('mousedown', function(event) {
event.stopPropagation();
});
async function get_recent(author) {
let recent = await tfrpc.rpc.query(
`
SELECT DISTINCT content ->> '$.vote.expression' AS value
FROM messages
WHERE author = ? AND
content ->> '$.type' = 'vote'
ORDER BY timestamp DESC LIMIT 10
`,
[author]
);
return recent.map((x) => x.value);
}
function cleanup() {
console.log('emoji cleanup');
div.parentElement.removeChild(div);
window.removeEventListener('keydown', key_down);
console.log('removing click');
document.body.removeEventListener('mousedown', cleanup);
}
export async function picker(callback, anchor, author) {
let json = await get_emojis();
let recent = await get_recent(author);
function key_down(event) {
if (event.key == 'Escape') {
cleanup();
}
}
let div = document.createElement('div');
div.id = 'emoji_picker';
div.style.color = '#000';
div.style.background = '#fff';
div.style.border = '1px solid #000';
div.style.display = 'block';
div.style.overflow = 'scroll';
div.style.fontWeight = 'bold';
div.style.fontSize = 'xx-large';
let input = document.createElement('input');
input.type = 'text';
input.style.display = 'block';
input.style.boxSizing = 'border-box';
input.style.width = '100%';
input.style.margin = '0';
input.style.position = 'relative';
div.appendChild(input);
let list = document.createElement('div');
div.appendChild(list);
div.addEventListener('mousedown', function (event) {
event.stopPropagation();
});
function chosen(event) {
console.log(event.srcElement.innerText);
callback(event.srcElement.innerText);
function key_down(event) {
if (event.key == 'Escape') {
cleanup();
}
}
function refresh() {
while (list.firstChild) {
list.removeChild(list.firstChild);
}
let search = input.value.toLowerCase();
let any_at_all = false;
for (let row of Object.entries(json)) {
let header = document.createElement('div');
header.appendChild(document.createTextNode(row[0]));
list.appendChild(header);
let any = false;
for (let entry of Object.entries(row[1])) {
if (search &&
search.length &&
entry[0].toLowerCase().indexOf(search) == -1) {
continue;
}
let emoji = document.createElement('span');
const k_size = '1.25em';
emoji.style.display = 'inline-block';
emoji.style.overflow = 'hidden';
emoji.style.cursor = 'pointer';
emoji.onclick = chosen;
emoji.title = entry[0];
emoji.appendChild(document.createTextNode(entry[1]));
list.appendChild(emoji);
any = true;
any_at_all = true;
}
if (!any) {
list.removeChild(header);
function chosen(event) {
console.log(event.srcElement.innerText);
callback(event.srcElement.innerText);
cleanup();
}
function refresh() {
while (list.firstChild) {
list.removeChild(list.firstChild);
}
let search = input.value.toLowerCase();
let any_at_all = false;
if (recent) {
let emoji_to_name = {};
for (let row of Object.values(json)) {
for (let entry of Object.entries(row)) {
emoji_to_name[entry[1]] = entry[0];
}
}
if (!any_at_all) {
list.appendChild(document.createTextNode('No matches found.'));
let header = document.createElement('div');
header.appendChild(document.createTextNode('Recent'));
list.appendChild(header);
let any = false;
for (let entry of recent) {
if (
search &&
search.length &&
(emoji_to_name[entry] || '').toLowerCase().indexOf(search) == -1
) {
continue;
}
let emoji = document.createElement('span');
const k_size = '1.25em';
emoji.style.display = 'inline-block';
emoji.style.overflow = 'hidden';
emoji.style.cursor = 'pointer';
emoji.onclick = chosen;
emoji.title = emoji_to_name[entry] || entry;
emoji.appendChild(document.createTextNode(entry));
list.appendChild(emoji);
any = true;
}
if (!any) {
list.removeChild(header);
}
}
refresh();
input.oninput = refresh;
document.body.appendChild(div);
div.style.position = 'fixed';
div.style.top = '50%';
div.style.left = '50%';
div.style.transform = 'translate(-50%, -50%)';
input.focus();
console.log('adding click');
document.body.addEventListener('mousedown', cleanup);
window.addEventListener('keydown', key_down);
});
}
for (let row of Object.entries(json)) {
let header = document.createElement('div');
header.appendChild(document.createTextNode(row[0]));
list.appendChild(header);
let any = false;
for (let entry of Object.entries(row[1])) {
if (
search &&
search.length &&
entry[0].toLowerCase().indexOf(search) == -1
) {
continue;
}
let emoji = document.createElement('span');
const k_size = '1.25em';
emoji.style.display = 'inline-block';
emoji.style.overflow = 'hidden';
emoji.style.cursor = 'pointer';
emoji.onclick = chosen;
emoji.title = entry[0];
emoji.appendChild(document.createTextNode(entry[1]));
list.appendChild(emoji);
any = true;
any_at_all = true;
}
if (!any) {
list.removeChild(header);
}
}
if (!any_at_all) {
list.appendChild(document.createTextNode('No matches found.'));
}
}
refresh();
input.oninput = refresh;
let modal = html`
<style>
${styles}
</style>
<div class="w3-modal" style="display: block">
<div class="w3-modal-content w3-card-4">${div}</div>
</div>
`;
let parent = document.createElement('div');
document.body.appendChild(parent);
function cleanup() {
parent.parentElement.removeChild(parent);
window.removeEventListener('keydown', key_down);
document.body.removeEventListener('mousedown', cleanup);
}
render(modal, parent);
input.focus();
document.body.addEventListener('mousedown', cleanup);
window.addEventListener('keydown', key_down);
}

File diff suppressed because one or more lines are too long

View File

@ -1,22 +1,24 @@
<!DOCTYPE html>
<html style="color: #fff">
<!doctype html>
<html>
<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;
}
</style>
</head>
<body style="background-color: #223a5e">
<tf-app class="w3-deep-purple"/>
<script>window.litDisableBundleWarning = true;</script>
<body style="margin: 0; padding: 0">
<tf-app></tf-app>
<tf-reactions-modal id="reactions_modal"></tf-reactions-modal>
<script>
window.litDisableBundleWarning = true;
</script>
<script src="filesaver.min.js"></script>
<script src="commonmark.min.js"></script>
<script src="commonmark-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

@ -1,17 +1,23 @@
import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import * as tf_id_picker from './tf-id-picker.js';
import * as tf_app from './tf-app.js';
import * as tf_message from './tf-message.js';
import * as tf_user from './tf-user.js';
import * as tf_compose from './tf-compose.js';
import * as tf_news from './tf-news.js';
import * as tf_profile from './tf-profile.js';
import * as tf_tab_mentions from './tf-tab-mentions.js';
import * as tf_reactions_modal from './tf-reactions-modal.js';
import * as tf_tab_news from './tf-tab-news.js';
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
import * as tf_tab_search from './tf-tab-search.js';
import * as tf_tab_connections from './tf-tab-connections.js';
import * as tf_tab_query from './tf-tab-query.js';
import * as tf_tag from './tf-tag.js';
import * as tf_tag from './tf-tag.js';
import * as tf_styles from './tf-styles.js';
window.addEventListener('load', function () {
let style = document.createElement('style');
style.innerText = tf_styles.styles;
document.body.appendChild(style);
});

View File

@ -16,7 +16,9 @@ class TfElement extends LitElement {
following: {type: Array},
users: {type: Object},
ids: {type: Array},
tags: {type: Array},
channels: {type: Array},
channels_unread: {type: Object},
channels_latest: {type: Object},
};
}
@ -33,10 +35,16 @@ class TfElement extends LitElement {
this.following = [];
this.users = {};
this.loaded = false;
this.tags = [];
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || []; });
tfrpc.rpc.getConnections().then(c => { self.connections = c || []; });
tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
this.channels = [];
this.channels_unread = {};
this.channels_latest = {};
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);
});
@ -48,26 +56,88 @@ class TfElement extends LitElement {
self.broadcasts = value;
} else if (name === 'connections') {
self.connections = value;
} else if (name === 'identity') {
self.whoami = value;
}
});
this.initial_load();
}
async initial_load() {
let whoami = await tfrpc.rpc.localStorageGet('whoami');
let whoami = await tfrpc.rpc.getActiveIdentity();
let ids = (await tfrpc.rpc.getIdentities()) || [];
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
this.ids = ids;
await this.load_channels();
}
async load_channels() {
let channels = await tfrpc.rpc.query(
`
SELECT
content ->> 'channel' AS channel,
content ->> 'subscribed' AS subscribed
FROM
messages
WHERE
author = ? AND
content ->> 'type' = 'channel'
ORDER BY sequence
`,
[this.whoami]
);
let channel_map = {};
for (let row of channels) {
if (row.subscribed) {
channel_map[row.channel] = true;
} else {
delete channel_map[row.channel];
}
}
this.channels = Object.keys(channel_map).sort();
}
connectedCallback() {
super.connectedCallback();
this._keydown = this.keydown.bind(this);
window.addEventListener('keydown', this._keydown);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('keydown', this._keydown);
}
keydown(event) {
if (event.altKey && event.key == 'ArrowUp') {
this.next_channel(1);
event.preventDefault();
} else if (event.altKey && event.key == 'ArrowDown') {
this.next_channel(-1);
event.preventDefault();
}
}
next_channel(delta) {
let channel_names = ['', '@'].concat(this.channels);
let index = channel_names.indexOf(this.hash.substring(1));
if (index != -1) {
index += delta;
this.set_hash(
'#' +
encodeURIComponent(
channel_names[(index + channel_names.length) % channel_names.length]
)
);
}
}
set_hash(hash) {
this.hash = hash || '#';
this.hash = decodeURIComponent(hash || '#');
if (this.hash.startsWith('#q=')) {
this.tab = 'search';
} else if (this.hash === '#connections') {
this.tab = 'connections';
} else if (this.hash === '#mentions') {
this.tab = 'mentions';
} else if (this.hash.startsWith('#sql=')) {
this.tab = 'query';
} else {
@ -86,9 +156,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 +173,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 +184,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 +195,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 +224,21 @@ 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]
);
for (let message of messages) {
if (message.author == this.whoami) {
let content = JSON.parse(message.content);
if (content?.type == 'channel') {
this.load_channels();
}
}
}
if (messages && messages.length) {
this.unread = [...this.unread, ...messages];
this.unread = this.unread.slice(this.unread.length - 1024);
@ -173,7 +258,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) {
@ -182,38 +267,36 @@ class TfElement extends LitElement {
}
}
render_id_picker() {
return html`
<div style="display: flex; gap: 8px">
<tf-id-picker id="picker" style="flex: 1 1 auto" selected=${this.whoami} .ids=${this.ids} .users=${this.users} @change=${this._handle_whoami_changed}></tf-id-picker>
<button class="w3-button w3-dark-grey w3-border" style="flex: 0 0 auto" @click=${this.create_identity} id="create_identity">Create Identity</button>
</div>
`;
}
async load_recent_tags() {
let start = new Date();
this.tags = await tfrpc.rpc.query(`
WITH
recent AS (SELECT id, content FROM messages
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
ORDER BY timestamp DESC LIMIT 1024),
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
FROM recent
WHERE json_extract(content, '$.channel') IS NOT NULL),
recent_mentions AS (SELECT recent.id, json_extract(mention.value, '$.link') AS tag
FROM recent, json_each(recent.content, '$.mentions') AS mention
WHERE json_valid(mention.value) AND tag LIKE '#%'),
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
by_message AS (SELECT DISTINCT id, tag FROM combined)
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
`, [new Date() - 7 * 24 * 60 * 60 * 1000]);
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
async get_latest_private(following) {
let latest = (await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages'))[0].latest;
const k_chunk_count = 256;
while (latest - k_chunk_count >= 0) {
let messages = await tfrpc.rpc.query(`
SELECT messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
JOIN json_each(?1) AS following ON messages.author = following.value
WHERE
messages.rowid > ?2 AND
messages.rowid <= ?3 AND
json(messages.content) LIKE '"%'
ORDER BY sequence DESC
`,
[
JSON.stringify(following),
latest - k_chunk_count,
latest,
]);
messages = (await this.decrypt(messages)).filter(x => x.decrypted);
if (messages.length) {
return Math.max(...messages.map(x => x.rowid));
}
latest -= k_chunk_count;
};
return -1;
}
async load() {
let whoami = this.whoami;
let tags = this.load_recent_tags();
let following = await tfrpc.rpc.following([whoami], 2);
let users = {};
let by_count = [];
@ -226,38 +309,137 @@ class TfElement extends LitElement {
};
by_count.push({count: v.of, id: id});
}
console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
let channels = tfrpc.rpc.query(
`
SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE messages.content ->> 'type' = 'post' AND messages.content ->> 'root' IS NULL
GROUP by channel
UNION
SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?2) AS following ON messages.author = following.value
UNION
SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?2) AS following ON messages.author = following.value
`,
[
JSON.stringify(this.channels),
JSON.stringify(Object.keys(following)),
'"' + this.whoami.replace('"', '""') + '"',
]
);
let latest_private = this.get_latest_private(Object.keys(following));
this.channels_unread = JSON.parse(
(await tfrpc.rpc.databaseGet('unread')) ?? '{}'
);
let start_time = new Date();
users = await this.fetch_about(Object.keys(following).sort(), users);
console.log(
'about took',
(new Date() - start_time) / 1000.0,
'seconds for',
Object.keys(users).length,
'users'
);
start_time = new Date();
channels = await channels;
console.log('channels took', (new Date() - start_time) / 1000.0);
this.channels_latest = Object.fromEntries(
channels.map((x) => [x.channel, x.rowid])
);
let self = this;
latest_private.then(function(latest) {
self.channels_latest = Object.assign({}, self.channels_latest, {'🔐': latest});
console.log('private took', (new Date() - start_time) / 1000.0);
});
this.following = Object.keys(following);
this.users = users;
await tags;
console.log(`load finished ${whoami} => ${this.whoami}`);
this.whoami = whoami;
this.loaded = whoami;
}
channel_set_unread(event) {
this.channels_unread[event.detail.channel ?? ''] = event.detail.unread;
this.channels_unread = Object.assign({}, this.channels_unread);
tfrpc.rpc.databaseSet('unread', JSON.stringify(this.channels_unread));
}
async decrypt(messages) {
let whoami = this.whoami;
return Promise.all(messages.map(async function (message) {
let content;
try {
content = JSON.parse(message?.content);
} catch {}
if (typeof content === 'string') {
let decrypted;
try {
decrypted = await tfrpc.rpc.try_decrypt(whoami, content);
} catch {}
if (decrypted) {
try {
message.decrypted = JSON.parse(decrypted);
} catch {
message.decrypted = decrypted;
}
}
}
return message;
}));
}
render_tab() {
let following = this.following;
let users = this.users;
if (this.tab === 'news') {
return html`
<tf-tab-news id="tf-tab-news" .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
<tf-tab-news
id="tf-tab-news"
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
hash=${this.hash}
.unread=${this.unread}
@refresh=${() => (this.unread = [])}
?loading=${this.loading}
.channels=${this.channels}
.channels_latest=${this.channels_latest}
.channels_unread=${this.channels_unread}
@channelsetunread=${this.channel_set_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>
`;
} else if (this.tab === 'mentions') {
return html`
<tf-tab-mentions .following=${this.following} whoami=${this.whoami} .users=${this.users}}></tf-tab-mentions>
<tf-tab-connections
.users=${this.users}
.connections=${this.connections}
.broadcasts=${this.broadcasts}
></tf-tab-connections>
`;
} 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>
`;
}
}
@ -268,19 +450,21 @@ class TfElement extends LitElement {
await tfrpc.rpc.setHash('#');
} else if (tab === 'connections') {
await tfrpc.rpc.setHash('#connections');
} else if (tab === 'mentions') {
await tfrpc.rpc.setHash('#mentions');
} else if (tab === 'query') {
await tfrpc.rpc.setHash('#sql=');
}
}
refresh() {
tfrpc.rpc.sync();
}
render() {
let self = this;
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
this.loading = true;
this.load().finally(function() {
this.load().finally(function () {
self.loading = false;
});
}
@ -288,29 +472,56 @@ class TfElement extends LitElement {
const k_tabs = {
'📰': 'news',
'📡': 'connections',
'@': 'mentions',
'🔍': 'search',
'👩‍💻': 'query',
};
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>
`)}
<div
class="w3-bar w3-theme-l1"
style="position: sticky; top: 0; z-index: 10"
>
<button
class="w3-bar-item w3-button w3-circle w3-ripple"
@click=${this.refresh}
>
</button>
${Object.entries(k_tabs).map(
([k, v]) => html`
<button
title=${v}
class="w3-bar-item w3-padding w3-hover-theme tab ${self.tab == v
? 'w3-theme-l2'
: 'w3-theme-l1'}"
@click=${() => self.set_tab(v)}
>
${k}
<span class=${self.tab == v ? '' : 'w3-hide-small'}
>${v.charAt(0).toUpperCase() + v.substring(1)}</span
>
</button>
`
)}
</div>
`;
let contents =
!this.loaded ?
this.loading ?
html`<div>Loading...</div>` :
html`<div>Select or create an identity.</div>` :
this.render_tab();
let contents = !this.loaded
? this.loading
? html`<div
class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge"
>
Loading...
</div>
${this.render_tab()}`
: html`<div>Select or create an identity.</div>`
: this.render_tab();
return html`
${this.render_id_picker()}
${tabs}
${this.tags.map(x => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`)}
${contents}
<div
style="width: 100vw; min-height: 100vh; height: 100%"
class="w3-theme-dark"
>
${tabs} ${contents}
</div>
`;
}
}

View File

@ -1,4 +1,4 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import {LitElement, html, unsafeHTML, live} from './lit-all.min.js';
import * as tfutils from './tf-utils.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
@ -13,6 +13,8 @@ class TfComposeElement extends LitElement {
branch: {type: String},
apps: {type: Object},
drafts: {type: Object},
author: {type: String},
channel: {type: String},
};
}
@ -25,6 +27,7 @@ class TfComposeElement extends LitElement {
this.branch = undefined;
this.apps = undefined;
this.drafts = {};
this.author = undefined;
}
process_text(text) {
@ -58,11 +61,13 @@ 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) {
this.requestUpdate();
setTimeout(() => this.notify(draft), 0);
}
return tfutils.markdown(text);
}
@ -70,36 +75,31 @@ class TfComposeElement extends LitElement {
input(event) {
let edit = this.renderRoot.getElementById('edit');
let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = this.process_text(edit.value);
preview.innerHTML = this.process_text(edit.innerText);
let content_warning = this.renderRoot.getElementById('content_warning');
let content_warning_preview = this.renderRoot.getElementById('content_warning_preview');
if (content_warning && content_warning_preview) {
content_warning_preview.innerText = content_warning.value;
}
let draft = this.get_draft();
draft.text = edit.innerText;
draft.content_warning = content_warning?.value;
setTimeout(() => this.notify(draft), 0);
}
notify(draft) {
this.dispatchEvent(new CustomEvent('tf-draft', {
bubbles: true,
composed: true,
detail: {
id: this.branch,
draft: draft
},
}));
}
change() {
let draft = this.get_draft();
draft.text = this.renderRoot.getElementById('edit')?.value;
draft.content_warning = this.renderRoot.getElementById('content_warning')?.value;
this.notify(draft);
this.dispatchEvent(
new CustomEvent('tf-draft', {
bubbles: true,
composed: true,
detail: {
id: this.branch,
draft: draft,
},
})
);
}
convert_to_format(buffer, type, mime_type) {
return new Promise(function(resolve, reject) {
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 +109,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 +135,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;
@ -154,10 +162,9 @@ class TfComposeElement extends LitElement {
size: buffer.length ?? buffer.byteLength,
};
let edit = self.renderRoot.getElementById('edit');
edit.value += `\n![${name}](${id})`;
self.change();
edit.innerText += `\n![${name}](${id})`;
self.input();
} catch(e) {
} catch (e) {
alert(e?.message);
}
}
@ -174,6 +181,13 @@ class TfComposeElement extends LitElement {
break;
}
}
event.preventDefault();
document.execCommand(
'insertText',
false,
event.clipboardData.getData('text/plain')
);
}
async submit() {
@ -182,7 +196,8 @@ class TfComposeElement extends LitElement {
let edit = this.renderRoot.getElementById('edit');
let message = {
type: 'post',
text: edit.value,
text: edit.innerText,
channel: this.channel,
};
if (this.root || this.branch) {
message.root = this.root;
@ -201,36 +216,30 @@ 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() {
edit.value = '';
self.change();
self.notify(undefined);
self.requestUpdate();
});
await tfrpc.rpc.appendMessage(this.whoami, message);
self.notify(undefined);
} catch (error) {
alert(error.message);
}
}
discard() {
let edit = this.renderRoot.getElementById('edit');
edit.value = '';
this.change();
let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = '';
this.notify(undefined);
}
attach() {
let self = this;
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 +250,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) AS content FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
WHERE messages.content LIKE ?
WHERE json(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) {
@ -262,19 +274,39 @@ class TfComposeElement extends LitElement {
}
firstUpdated() {
let values = Object.entries(this.users).map((x) => ({
key: x[1].name ?? x[0],
value: x[0],
}));
if (this.author) {
values = [].concat(
[
{
key: this.users[this.author]?.name,
value: this.author,
},
],
values
);
}
let tribute = new Tribute({
iframe: this.shadowRoot,
collection: [
{
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
selectTemplate: function(item) {
return `[@${item.original.key}](${item.original.value})`;
values: values,
selectTemplate: function (item) {
return item
? `[@${item.original.key}](${item.original.value})`
: undefined;
},
},
{
trigger: '&',
values: this.autocomplete,
selectTemplate: function(item) {
return `![${item.original.key}](${item.original.value})`;
selectTemplate: function (item) {
return item
? `![${item.original.key}](${item.original.value})`
: undefined;
},
},
],
@ -285,16 +317,20 @@ class TfComposeElement extends LitElement {
updated() {
super.updated();
let edit = this.renderRoot.getElementById('edit');
if (this.last_updated_text !== edit.value) {
if (this.last_updated_text !== edit.innerText) {
let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = this.process_text(edit.value);
this.last_updated_text = edit.value;
preview.innerHTML = this.process_text(edit.innerText);
this.last_updated_text = edit.innerText;
}
let encrypt = this.renderRoot.getElementById('encrypt_to');
if (encrypt) {
let tribute = new Tribute({
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
selectTemplate: function(item) {
iframe: this.shadowRoot,
values: Object.entries(this.users).map((x) => ({
key: x[1].name,
value: x[0],
})),
selectTemplate: function (item) {
return item.original.value;
},
});
@ -305,26 +341,35 @@ class TfComposeElement extends LitElement {
remove_mention(id) {
let draft = this.get_draft();
delete draft.mentions[id];
this.notify(draft);
this.requestUpdate();
setTimeout(() => this.notify(), 0);
}
render_mention(mention) {
let self = this;
return html`
<div style="display: flex; flex-direction: row">
<div style="align-self: center; margin: 0.5em">
<button class="w3-button w3-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-theme-d1"
title="Remove ${mention.name} mention"
@click=${() => self.remove_mention(mention.link)}
>
🚮
</button>
</div>
<div style="display: flex; flex-direction: column">
<h3>${mention.name}</h3>
<div style="padding-left: 1em">
${Object.entries(mention)
.filter((x) => x[0] != 'name')
.map(
(x) =>
html`<div>
<span style="font-weight: bold">${x[0]}</span>: ${x[1]}
</div>`
)}
</div>
<div 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() {
@ -358,13 +403,22 @@ class TfComposeElement extends LitElement {
if (this.apps) {
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>`)}
<select id="select" class="w3-select w3-theme-d1">
${Object.keys(self.apps).map(
(app) => html`<option value=${app}>${app}</option>`
)}
</select>
<button class="w3-button w3-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-theme-d1" @click=${attach_selected_app}>
Attach
</button>
<button
class="w3-button w3-theme-d1"
@click=${() => (this.apps = null)}
>
Cancel
</button>
</div>
`;
`;
}
}
@ -374,9 +428,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-theme-d1" @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-theme-d1"
@click=${() => (this.apps = null)}
>
Discard App
</button>`;
}
}
@ -394,15 +455,15 @@ class TfComposeElement extends LitElement {
return html`
<div class="w3-container w3-padding">
<p>
<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
<label for="cw">CW</label>
</p>
<input type="text" class="w3-input w3-border w3-dark-grey" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${self.input} value=${draft.content_warning}></input>
</div>
`;
} else {
return html`
<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning('')}></input>
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input>
<label for="cw">CW</label>
`;
}
@ -432,14 +493,16 @@ class TfComposeElement extends LitElement {
<div style="display: flex; flex-direction: row; width: 100%">
<label for="encrypt_to">🔐 To:</label>
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt(undefined)}>🚮</button>
<button class="w3-button w3-theme-d1" @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-theme-d1" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
</li>`
)}
</ul>
`;
}
@ -455,34 +518,61 @@ 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-theme-d2">
<p id="content_warning_preview">${draft.content_warning}</p>
</div>`
: undefined;
let encrypt =
draft.encrypt_to !== undefined
? undefined
: html`<button
class="w3-button w3-theme-d1"
@click=${() => this.set_encrypt([])}
>
🔐
</button>`;
let result = html`
<div class="w3-card-4 w3-blue-grey w3-padding" style="box-sizing: border-box">
<div
class="w3-card-4 w3-theme-d4 w3-padding-small"
style="box-sizing: border-box"
>
${this.channel !== undefined
? html`<p>To #${this.channel}:</p>`
: undefined}
${this.render_encrypt()}
<div style="display: flex; flex-direction: row; width: 100%; gap: 4px">
<div style="flex: 1 0 50%">
<p><textarea class="w3-input w3-dark-grey w3-border" style="resize: vertical" placeholder="Write a post here." id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste}>${draft.text}</textarea></p>
<div class="w3-container w3-padding-small">
<div class="w3-half">
<span
class="w3-input w3-theme-d1 w3-border"
style="resize: vertical; width: 100%; overflow: hidden; white-space: pre-wrap"
placeholder="Write a post here."
id="edit"
@input=${this.input}
@paste=${this.paste}
contenteditable="plaintext-only"
.innerText=${live(draft.text ?? '')}
></span>
</div>
<div style="flex: 1 0 50%">
<div class="w3-half w3-padding">
${content_warning}
<div id="preview"></div>
</div>
</div>
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
${this.render_attach_app()}
${this.render_content_warning()}
<button class="w3-button w3-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-theme-d1" id="submit" @click=${this.submit}>
Submit
</button>
<button class="w3-button w3-theme-d1" @click=${this.attach}>
Attach
</button>
${this.render_attach_app_button()} ${encrypt}
<button class="w3-button w3-theme-d1" @click=${this.discard}>
Discard
</button>
</div>
`;
return result;

View File

@ -1,41 +0,0 @@
import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
/*
** Provide a list of IDs, and this lets the user pick one.
*/
class TfIdentityPickerElement extends LitElement {
static get properties() {
return {
ids: {type: Array},
selected: {type: String},
users: {type: Object},
};
}
static styles = styles;
constructor() {
super();
this.ids = [];
this.users = {};
}
changed(event) {
this.selected = event.srcElement.value;
this.dispatchEvent(new Event('change', {
srcElement: this,
}));
}
render() {
return html`
<select class="w3-select w3-dark-grey w3-padding w3-border" @change=${this.changed} style="max-width: 100%; overflow: hidden">
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${this.users[id]?.name ? (this.users[id]?.name + ' - ') : undefined}<small>${id}</small></option>`)}
</select>
`;
}
}
customElements.define('tf-id-picker', TfIdentityPickerElement);

View File

@ -1,4 +1,4 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import {LitElement, html, render, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import * as tfutils from './tf-utils.js';
import * as emojis from './emojis.js';
@ -14,6 +14,8 @@ class TfMessageElement extends LitElement {
format: {type: String},
blog_data: {type: String},
expanded: {type: Object},
channel: {type: String},
channel_unread: {type: Number},
};
}
@ -28,17 +30,37 @@ class TfMessageElement extends LitElement {
this.drafts = {};
this.format = 'message';
this.expanded = {};
this.channel_unread = -1;
}
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},
})
);
}
show_reactions() {
let modal = document.getElementById('reactions_modal');
modal.users = this.users;
modal.votes = this.message?.votes || [];
}
render_votes() {
@ -53,12 +75,26 @@ 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>`;
if (this.message?.votes?.length) {
return html` <div class="w3-container">
<div
class="w3-button w3-bar w3-padding-small"
@click=${this.show_reactions}
>
${(this.message.votes || []).map(
(vote) => html`
<span
class="w3-bar-item w3-padding-small"
title="${this.users[vote.author]?.name ??
vote.author} ${new Date(vote.timestamp)}"
>
${normalize_expression(vote.content.vote.expression)}
</span>
`
)}
</div>
</div>`;
}
}
render_raw() {
@ -72,30 +108,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), null, this.whoami);
}
show_image(link) {
@ -129,9 +175,12 @@ 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') {
if (next.style.display != 'none') {
next.style.display = 'none';
} else {
next.style.display = 'block';
@ -140,50 +189,75 @@ 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=${'#' + 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="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,32 +268,74 @@ 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-theme-d1"
@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-theme-d1"
@click=${() => self.set_expanded(false)}
>
Collapse</button
>${(this.message.child_messages || []).map(
(x) =>
html`<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>`
)}`;
}
}
}
mark_unread() {
this.dispatchEvent(
new CustomEvent('channelsetunread', {
bubbles: true,
composed: true,
detail: {
channel: this.channel,
unread: this.message.rowid,
},
})
);
}
render_channels() {
let content = this.message?.content;
if (this?.messsage?.decrypted?.type == 'post') {
@ -231,13 +347,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() {
@ -245,59 +360,140 @@ class TfMessageElement extends LitElement {
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
}
let class_background = this.message?.decrypted
? 'w3-pale-red'
: this.message?.rowid >= this.channel_unread
? 'w3-theme-d2'
: 'w3-theme-d4';
let self = this;
let raw_button;
switch (this.format) {
case 'raw':
if (content?.type == 'post' || content?.type == 'blog') {
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'md'}>Markdown</button>`;
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'md')}
>
Markdown
</button>`;
} else {
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'message')}
>
Message
</button>`;
}
break;
case 'md':
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'message')}
>
Message
</button>`;
break;
case 'decrypted':
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</button>`;
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'raw')}
>
Raw
</button>`;
break;
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-theme-d1"
@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-theme-d1"
@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 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
>
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
${raw_button}
${self.format == 'raw' ? self.render_raw() : inner}
<span style="padding-right: 8px"
><a tfarget="_top" href=${'#' + encodeURIComponent(self.message.id)}
>%</a
>
${new Date(self.message.timestamp).toLocaleString()}</span
>
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
${self.render_votes()}
${(self.message.child_messages || []).map(
(x) => html`
<tf-message
.message=${x}
whoami=${self.whoami}
.users=${self.users}
.drafts=${self.drafts}
.expanded=${self.expanded}
channel=${self.channel}
channel_unread=${self.channel_unread}
></tf-message>
`
)}
</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 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
>
${this.message.messages.map(
(x) =>
html`<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></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 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
>
<a target="_top" href=${'#' + encodeURIComponent(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}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>
`
)}
</div>`;
} else if (typeof (content?.type === 'string')) {
if (content.type == 'about') {
let name;
let image;
@ -307,7 +503,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 +513,53 @@ 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}
author=${this.message.author}
></tf-compose>
`
: html`
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
Reply
</button>
`;
let self = this;
let body;
switch (this.format) {
@ -360,35 +567,44 @@ 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>`;
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-theme-l4"
style="cursor: pointer"
@click=${(x) => this.toggle_expanded(':cw')}
>
<p>${content.contentWarning}</p>
</div>
`;
let content_html = html`
${this.render_channels()}
<div @click=${this.body_click}>${body}</div>
${this.render_mentions()}
`;
let payload = content.contentWarning
? self.expanded[(this.message.id || '') + ':cw']
? html` ${content_warning} ${content_html} `
: content_warning
: content_html;
let is_encrypted = this.message?.decrypted
? html`<span style="align-self: center">🔓</span>`
: undefined;
return html`
<style>
code {
@ -404,26 +620,48 @@ 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 ${class_background} w3-border-theme"
style="margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
<span style="flex: 1"></span>
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
<span style="padding-right: 8px"
><a
target="_top"
href=${'#' + encodeURIComponent(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-theme-d1" @click=${this.react}>
React
</button>
${!content.root && this.message.rowid < this.channel_unread
? html`
<button
class="w3-button w3-theme-d1"
@click=${this.mark_unread}
>
Mark Unread
</button>
`
: undefined}
</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;
return html`
<style>
code {
@ -439,31 +677,45 @@ 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 ${class_background} w3-border-theme"
style="margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
<span style="flex: 1"></span>
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
<span style="padding-right: 8px"
><a
target="_top"
href=${'#' + encodeURIComponent(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-theme-d1" @click=${this.react}>
React
</button>
</p>
${this.render_children()}
</div>
`;
} else if (content.type === 'blog') {
let self = this;
tfrpc.rpc.get_blob(content.blog).then(function(data) {
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':
@ -476,7 +728,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>
@ -487,17 +739,24 @@ 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}
author=${this.message.author}
></tf-compose>
`
: html`
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
Reply
</button>
`;
return html`
<style>
code {
@ -513,11 +772,21 @@ 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 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
<span style="flex: 1"></span>
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
<span style="padding-right: 8px"
><a
target="_top"
href=${'#' + encodeURIComponent(self.message.id)}
>%</a
>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
</div>
@ -525,37 +794,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-theme-d1" @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=${'#' + 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>`);
@ -569,4 +853,4 @@ class TfMessageElement extends LitElement {
}
}
customElements.define('tf-message', TfMessageElement);
customElements.define('tf-message', TfMessageElement);

View File

@ -11,6 +11,8 @@ class TfNewsElement extends LitElement {
following: {type: Array},
drafts: {type: Object},
expanded: {type: Object},
channel: {type: String},
channel_unread: {type: Number},
};
}
@ -25,6 +27,7 @@ class TfNewsElement extends LitElement {
this.following = [];
this.drafts = {};
this.expanded = {};
this.channel_unread = -1;
}
process_messages(messages) {
@ -33,12 +36,13 @@ class TfNewsElement extends LitElement {
console.log('processing', messages.length, 'messages');
function ensure_message(id) {
function ensure_message(id, rowid) {
let found = messages_by_id[id];
if (found) {
return found;
} else {
let added = {
rowid: rowid,
id: id,
placeholder: true,
content: '"placeholder"',
@ -53,7 +57,7 @@ class TfNewsElement extends LitElement {
function link_message(message) {
if (message.content.type === 'vote') {
let parent = ensure_message(message.content.vote.link);
let parent = ensure_message(message.content.vote.link, message.rowid);
if (!parent.votes) {
parent.votes = [];
}
@ -61,15 +65,15 @@ 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') {
let m = ensure_message(message.content.root);
if (typeof message.content.root === 'string') {
let m = ensure_message(message.content.root, message.rowid);
if (!m.child_messages) {
m.child_messages = [];
}
m.child_messages.push(message);
message.parent_message = message.content.root;
} else {
let m = ensure_message(message.content.root[0]);
let m = ensure_message(message.content.root[0], message.rowid);
if (!m.child_messages) {
m.child_messages = [];
}
@ -89,8 +93,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 +103,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 +123,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 +137,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);
}
@ -154,6 +166,7 @@ class TfNewsElement extends LitElement {
} else {
if (group.length > 0) {
result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: 'contact_group',
messages: group,
});
@ -162,15 +175,55 @@ class TfNewsElement extends LitElement {
result.push(message);
}
}
if (group.length > 0) {
result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: 'contact_group',
messages: group,
});
}
return result;
}
load_and_render(messages) {
let messages_by_id = this.process_messages(messages);
let final_messages = this.group_following(this.finalize_messages(messages_by_id));
let final_messages = this.group_following(
this.finalize_messages(messages_by_id)
);
let unread_rowid = -1;
for (let message of final_messages) {
if (message.rowid >= this.channel_unread) {
unread_rowid = message.rowid;
}
}
return html`
<div style="display: flex; flex-direction: column">
${final_messages.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded} collapsed=true></tf-message>`)}
<div>
${final_messages.map(
(x) =>
html`
<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
collapsed="true"
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>
${x.rowid == unread_rowid && x != final_messages[0]
? html`<div style="display: flex; flex-direction: row">
<div
style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
></div>
<div style="color: #f00; padding: 8px">unread</div>
<div
style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
></div>
</div>`
: undefined}
`
)}
</div>
`;
}
@ -180,4 +233,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();
}
@ -165,16 +188,27 @@ class TfProfileElement extends LitElement {
}
}
copy_id() {
navigator.clipboard.writeText(this.id);
}
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,56 +218,89 @@ 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-theme-d1"
@click=${() => this.server_follow_me(false)}
>
Server, Stop Following Me
</button>`;
} else if (this.server_follows_me === false) {
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(true)}>Server, Follow Me</button>`;
server_follow = html`<button
class="w3-button w3-theme-d1"
@click=${() => this.server_follow_me(true)}
>
Server, Follow Me
</button>`;
}
edit = html`
<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
id="save_profile"
class="w3-button w3-theme-d1"
@click=${this.save_edits}
>
Save Profile
</button>
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
Discard
</button>
${server_follow}
`;
} else {
edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>Edit Profile</button>`;
edit = html`<button
id="edit_profile"
class="w3-button w3-theme-d1"
@click=${this.edit}
>
Edit Profile
</button>`;
}
}
if (this.id !== this.whoami &&
this.following !== undefined) {
follow =
this.following ?
html`<button class="w3-button w3-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-theme-d1" @click=${this.unfollow}>
Unfollow
</button>`
: html`<button class="w3-button w3-theme-d1" @click=${this.follow}>
Follow
</button>`;
}
if (this.id !== this.whoami &&
this.blocking !== undefined) {
block =
this.blocking ?
html`<button class="w3-button w3-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-theme-d1" @click=${this.unblock}>
Unblock
</button>`
: html`<button class="w3-button w3-theme-d1" @click=${this.block}>
Block
</button>`;
}
let edit_profile = this.editing ? html`
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-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
</div>
<div><label for="description">Description:</label></div>
<textarea class="w3-input w3-dark-grey" style="resize: vertical" rows="8" id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
<div>
<label for="public_web_hosting">Public Web Hosting:</label>
<input class="w3-check w3-dark-grey" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${event => self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked})}></input>
<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
</div>
<div>
<button class="w3-button w3-dark-grey" @click=${this.attach_image}>Attach Image</button>
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
</div>
</div>
</div>` : 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">
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
return html`<div class="w3-container" style="box-sizing: border-box; border: 2px solid black; background-color: rgba(255, 255, 255, 0.2)">
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
<input type="text" class="w3-input w3-border w3-theme-d1" readonly value=${this.id}></input>
<button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button>
<div style="display: flex; flex-direction: row; gap: 1em">
${edit_profile}
<div style="flex: 1 0 50%">
@ -247,13 +314,13 @@ class TfProfileElement extends LitElement {
Blocking ${profile.blocking} identities.
Blocked by ${profile.blocked} identities.
</div>
<div>
<p>
${edit}
${follow}
${block}
</div>
</p>
</div>`;
}
}
customElements.define('tf-profile', TfProfileElement);
customElements.define('tf-profile', TfProfileElement);

View File

@ -0,0 +1,72 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import {styles} from './tf-styles.js';
class TfReactionsModalElement extends LitElement {
static get properties() {
return {
users: {type: Object},
votes: {type: Array},
};
}
static styles = styles;
constructor() {
super();
this.votes = [];
this.users = {};
}
clear() {
this.votes = [];
}
render() {
let self = this;
return this.votes?.length
? html` <div
class="w3-modal w3-animate-opacity"
style="display: block; box-sizing: border-box"
@click=${this.clear}
>
<div
class="w3-modal-content w3-card-4 w3-theme-d1"
onclick="event.stopPropagation()"
>
<div class="w3-container w3-padding">
<header class="w3-container">
<h2>Reactions</h2>
<span class="w3-button w3-display-topright" @click=${this.clear}
>&times;</span
>
</header>
<ul class="w3-theme-dark w3-container w3-ul">
${this.votes.map(
(x) => html`
<li class="w3-bar">
<span class="w3-bar-item"
>${x?.content?.vote?.expression}</span
>
<tf-user
class="w3-bar-item"
id=${x.author}
.users=${this.users}
></tf-user>
<span class="w3-bar-item w3-right"
>${new Date(x?.timestamp).toLocaleString()}</span
>
</li>
`
)}
</ul>
<footer class="w3-container w3-padding">
<button class="w3-button" @click=${this.clear}>Close</button>
</footer>
</div>
</div>
</div>`
: undefined;
}
}
customElements.define('tf-reactions-modal', TfReactionsModalElement);

View File

@ -1,67 +1,52 @@
import {css} from './lit-all.min.js';
const tf = css`
a:link {
color: #bbf;
}
img {
max-width: min(640px, 100%);
max-height: min(480px, auto);
}
a:visited {
color: #ddd;
}
.tab {
border: 0;
padding: 8px;
margin: 0px;
cursor: pointer;
}
a:hover {
color: #ddf;
}
.tab:disabled {
color: #088;
background-color: #fff;
}
img {
max-width: min(640px, 100%);
max-height: min(480px, auto);
}
.content_warning {
border: 1px solid #fff;
border-radius: 1em;
padding: 8px;
margin: 4px;
}
.tab {
border: 0;
padding: 8px;
margin: 0px;
cursor: pointer;
}
div.img_caption {
color: #888;
cursor: pointer;
}
.tab:disabled {
color: #088;
background-color: #fff;
}
div.img_caption::after {
content: ' ±';
}
.content_warning {
border: 1px solid #fff;
border-radius: 1em;
padding: 8px;
margin: 4px;
}
pre code {
display: block;
padding: 8px;
}
div.img_caption {
color: #888;
cursor: pointer;
}
div.img_caption::after {
content: ' ±';
}
code {
background-color: #444;
padding-left: 3px;
padding-right: 3px;
border: 1px dotted #fff;
border-radius: 4px;
}
blockquote {
background-color: #607d8b;
border-left: 4px solid #fff;
padding: 8px;
padding-left: 12px;
}
blockquote {
border-left: 4px solid #fff;
padding: 8px;
padding-left: 12px;
}
`;
// prettier-ignore
const w3 = css`
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
@ -103,7 +88,7 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
@ -151,7 +136,7 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
@media (max-width:1205px){.w3-auto{max-width:95%}}
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
@ -300,4 +285,30 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
`;
export let styles = [tf, w3];
// prettier-ignore
const w3_2016_snorkel_blue = css`
.w3-theme-l5 {color:#000 !important; background-color:#e9f5ff !important}
.w3-theme-l4 {color:#000 !important; background-color:#b5dffd !important}
.w3-theme-l3 {color:#000 !important; background-color:#6bc0fc !important}
.w3-theme-l2 {color:#fff !important; background-color:#21a0fa !important}
.w3-theme-l1 {color:#fff !important; background-color:#0479cc !important}
.w3-theme-d1 {color:#fff !important; background-color:#024575 !important}
.w3-theme-d2 {color:#fff !important; background-color:#023e68 !important}
.w3-theme-d3 {color:#fff !important; background-color:#02365b !important}
.w3-theme-d4 {color:#fff !important; background-color:#022e4e !important}
.w3-theme-d5 {color:#fff !important; background-color:#012641 !important}
.w3-theme-light {color:#000 !important; background-color:#e9f5ff !important}
.w3-theme-dark {color:#fff !important; background-color:#012641 !important}
.w3-theme-action {color:#fff !important; background-color:#012641 !important}
.w3-theme {color:#fff !important; background-color:#034f84 !important}
.w3-text-theme {color:#034f84 !important}
.w3-border-theme {border-color:#034f84 !important}
.w3-hover-theme:hover {color:#fff !important; background-color:#034f84 !important}
.w3-hover-text-theme:hover {color:#034f84 !important}
.w3-hover-border-theme:hover {border-color:#034f84 !important}
`;
export let styles = [tf, w3, w3_2016_snorkel_blue];

View File

@ -7,35 +7,55 @@ class TfTabConnectionsElement extends LitElement {
return {
broadcasts: {type: Array},
identities: {type: Array},
my_identities: {type: Array},
connections: {type: Array},
stored_connections: {type: Array},
users: {type: Object},
server_identity: {type: String},
connect_attempt: {type: Object},
connect_message: {type: String},
connect_success: {type: Boolean},
};
}
static styles = styles;
static k_broadcast_emojis = {
discovery: '🏓',
room: '🚪',
peer_exchange: '🕸',
};
constructor() {
super();
let self = this;
this.broadcasts = [];
this.identities = [];
this.my_identities = [];
this.connections = [];
this.stored_connections = [];
this.users = {};
tfrpc.rpc.getAllIdentities().then(function(identities) {
tfrpc.rpc.getIdentities().then(function (identities) {
self.my_identities = identities || [];
});
tfrpc.rpc.getAllIdentities().then(function (identities) {
self.identities = identities || [];
});
tfrpc.rpc.getStoredConnections().then(function(connections) {
tfrpc.rpc.getStoredConnections().then(function (connections) {
self.stored_connections = connections || [];
});
tfrpc.rpc.getServerIdentity().then(function (identity) {
self.server_identity = identity;
});
}
render_connection_summary(connection) {
if (connection.address && connection.port) {
return html`(<small>${connection.address}:${connection.port}</small>)`;
return html`<div>
<small>${connection.address}:${connection.port}</small>
</div>`;
} else if (connection.tunnel) {
return html`(room peer)`;
return html`<div>room peer</div>`;
} else {
return JSON.stringify(connection);
}
@ -43,10 +63,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,18 +80,47 @@ 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-theme-d1"
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
>
Connect
</button>
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
</li>
`;
}
render_message(connection) {
return html`<div
?hidden=${this.connect_message === undefined ||
this.connect_attempt != connection}
style="cursor: pointer"
class=${'w3-panel ' + (this.connect_success ? 'w3-green' : 'w3-red')}
@click=${() => (this.connect_attempt = undefined)}
>
<p>${this.connect_message}</p>
</div>`;
}
render_broadcast(connection) {
let self = this;
return html`
<li>
<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)}
<div class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap">
<button
class="w3-bar-item w3-button w3-theme-d1"
@click=${() => self.connect(connection)}
>
Connect
</button>
<div class="w3-bar-item">
${TfTabConnectionsElement.k_broadcast_emojis[connection.origin]}
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
${this.render_connection_summary(connection)}
</div>
</div>
${this.render_message(connection)}
</li>
`;
}
@ -80,51 +131,157 @@ class TfTabConnectionsElement extends LitElement {
}
render_connection(connection) {
let requests = Object.values(
connection.requests.reduce(function (accumulator, value) {
let key = `${value.name}:${Math.sign(value.request_number)}`;
if (!accumulator[key]) {
accumulator[key] = Object.assign({count: 0}, value);
}
accumulator[key].count++;
return accumulator;
}, {})
);
return html`
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.closeConnection(connection.id)}>Close</button>
<button
class="w3-button w3-theme-d1"
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
>
Close
</button>
${connection.flags.one_shot ? '🔃' : undefined}
<tf-user id=${connection.id} .users=${this.users}></tf-user>
${connection.tunnel !== undefined ? '🚇' : html`(${connection.host}:${connection.port})`}
${connection.tunnel !== undefined
? '🚇'
: html`(${connection.host}:${connection.port})`}
<div>
${requests.map(
(x) => html`
<span
class=${'w3-tag w3-small ' +
(x.active ? 'w3-theme-l3' : 'w3-theme-d3')}
>${x.request_number > 0 ? '🟩' : '🟥'} ${x.name}
<span
class="w3-badge w3-white"
style=${x.count > 1 ? undefined : 'display: none'}
>${x.count}</span
></span
>
`
)}
</div>
<ul>
${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>
`;
}
connect(address) {
let self = this;
self.connect_attempt = address;
self.connect_message = undefined;
self.connect_success = false;
tfrpc.rpc
.connect(address)
.then(function () {
if (self.connect_attempt == address) {
self.connect_message = 'Connected.';
self.connect_success = true;
}
})
.catch(function (error) {
if (self.connect_attempt == address) {
self.connect_message = 'Error: ' + error;
self.connect_success = false;
}
});
}
render() {
let self = this;
return html`
<div class="w3-container">
<div class="w3-container" style="box-sizing: border-box">
<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>
<textarea class="w3-input w3-theme-d1" id="code"></textarea>
${this.render_message(this.renderRoot.getElementById('code')?.value)}
<button
class="w3-button w3-theme-d1"
@click=${() =>
self.connect(self.renderRoot.getElementById('code')?.value)}
>
Connect
</button>
<h2>Broadcasts</h2>
<ul>
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
<ul class="w3-ul w3-border">
${this.broadcasts
.filter((x) => x.address)
.filter(
(x) => self.connections.map((c) => c.id).indexOf(x.pubkey) == -1
)
.map((x) => self.render_broadcast(x))}
</ul>
<h2>Connections</h2>
<ul>
${this.connections.filter(x => x.tunnel === undefined).map(x => html`
<li>${this.render_connection(x)}</li>
`)}
<ul class="w3-ul w3-border">
${this.connections
.filter((x) => x.tunnel === undefined)
.map(
(x) => html`
<li class="w3-bar">${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>
`)}
<h2>Stored Connections</h2>
<ul class="w3-ul w3-border">
${this.stored_connections.map(
(x) => html`
<li>
<div class="w3-bar">
<button
class="w3-bar-item w3-button w3-theme-d1"
@click=${() => self.forget_stored_connection(x)}
>
Forget
</button>
<button
class="w3-bar-item w3-button w3-theme-d1"
@click=${() => this.connect(x)}
>
Connect
</button>
<div class="w3-bar-item">
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
<div><small>${x.address}:${x.port}</small></div>
</div>
</div>
${this.render_message(x)}
</li>
`
)}
</ul>
<h2>Local Accounts</h2>
<ul>
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
<ul class="w3-ul w3-border">
${this.identities.map(
(x) =>
html`<li class="w3-bar">
${x == this.server_identity
? html`<span class="w3-tag w3-medium w3-round w3-theme-l1"
>🖥 local server</span
>`
: undefined}
${this.my_identities.indexOf(x) != -1
? html`<span class="w3-tag w3-medium w3-round w3-theme-d1"
>😎 you</span
>`
: undefined}
<tf-user id=${x} .users=${this.users}></tf-user>
</li>`
)}
</ul>
</div>
`;
}
}
customElements.define('tf-tab-connections', TfTabConnectionsElement);
customElements.define('tf-tab-connections', TfTabConnectionsElement);

View File

@ -1,65 +0,0 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
class TfTabMentionsElement extends LitElement {
static get properties() {
return {
whoami: {type: String},
users: {type: Object},
following: {type: Array},
expanded: {type: Object},
messages: {type: Array},
};
}
static styles = styles;
constructor() {
super();
let self = this;
this.whoami = null;
this.users = {};
this.following = [];
this.expanded = {};
this.messages = [];
}
async load() {
console.log('Loading...', this.whoami);
let results = await tfrpc.rpc.query(`
SELECT messages.*
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.author != ?
ORDER BY timestamp DESC limit 20
`,
['"' + this.whoami.replace('"', '""') + '"', JSON.stringify(this.following), this.whoami]);
console.log('Done.');
this.messages = results;
}
on_expand(event) {
if (event.detail.expanded) {
let expand = {};
expand[event.detail.id] = true;
this.expanded = Object.assign({}, this.expanded, expand);
} else {
delete this.expanded[event.detail.id];
this.expanded = Object.assign({}, this.expanded);
}
}
render() {
let self = this;
if (!this.loading) {
this.loading = true;
this.load();
}
return html`
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
`;
}
}
customElements.define('tf-tab-mentions', TfTabMentionsElement);

View File

@ -12,6 +12,11 @@ class TfTabNewsFeedElement extends LitElement {
messages: {type: Array},
drafts: {type: Object},
expanded: {type: Object},
channels_unread: {type: Object},
channels_latest: {type: Object},
loading: {type: Number},
time_range: {type: Array},
time_loading: {type: Array},
};
}
@ -26,127 +31,230 @@ class TfTabNewsFeedElement extends LitElement {
this.following = [];
this.drafts = {};
this.expanded = {};
this.start_time = new Date().valueOf() - 24 * 60 * 60 * 1000;
this.channels_unread = {};
this.channels_latest = {};
this.start_time = new Date().valueOf();
this.time_range = [0, 0];
this.time_loading = undefined;
this.loading = 0;
}
async fetch_messages() {
if (this.hash.startsWith('#@')) {
let r = await tfrpc.rpc.query(
channel() {
return this.hash.startsWith('##')
? this.hash.substring(2)
: this.hash.substring(1);
}
async fetch_messages(start_time, end_time) {
this.time_loading = [start_time, end_time];
let result;
if (this.hash == '#@') {
result = await tfrpc.rpc.query(
`
WITH mine AS (SELECT messages.*
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?1)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages.author != ?1 AND
messages.timestamp >= ?3 AND
messages.timestamp < ?4
ORDER BY timestamp DESC limit 20
`,
[
'"' + this.whoami.replace('"', '""') + '"',
JSON.stringify(this.following),
start_time,
end_time,
]
);
} else if (this.hash.startsWith('#@')) {
result = await tfrpc.rpc.query(
`
WITH mine AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
WHERE messages.author = ?
ORDER BY sequence DESC
LIMIT 20)
SELECT messages.*
ORDER BY sequence DESC)
SELECT messages.rowid, 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
WHERE
mine.timestamp >= ?2 AND
mine.timestamp < ?3
UNION
SELECT * FROM mine
WHERE
mine.timestamp >= ?2 AND
mine.timestamp < ?3
`,
[
this.hash.substring(1),
]);
return r;
[this.hash.substring(1), start_time, end_time]
);
} else if (this.hash.startsWith('#%')) {
return await tfrpc.rpc.query(
result = 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),
]);
} else {
[this.hash.substring(1)]
);
} else if (this.hash.startsWith('##')) {
let promises = [];
const k_following_limit = 256;
for (let i = 0; i < this.following.length; i += k_following_limit) {
promises.push(tfrpc.rpc.query(
`
WITH news AS (SELECT messages.*
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp > ? AND messages.timestamp < ?
ORDER BY messages.timestamp DESC)
SELECT messages.*
promises.push(
tfrpc.rpc.query(
`
WITH news AS (SELECT messages.rowid, 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 < ? AND
messages.content ->> 'channel' = ?
ORDER BY messages.timestamp DESC)
SELECT messages.rowid, 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.rowid, 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 messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?5)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?1) AS following ON messages.author = following.value
JOIN json_tree(messages.content, '$.mentions') AS mention ON mention.value = '#' || ?4
WHERE
messages.timestamp >= ?2 AND
messages.timestamp < ?3
UNION
SELECT news.* FROM news
`,
[
JSON.stringify(this.following.slice(i, i + k_following_limit)),
start_time,
end_time,
this.hash.substring(2),
'"#' + this.hash.substring(2).replace('"', '""') + '"',
]
)
);
}
result = [].concat(...(await Promise.all(promises)));
} else if (this.hash == '#🔐') {
result = await tfrpc.rpc.query(
`
SELECT messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
JOIN json_each(?1) AS following ON messages.author = following.value
WHERE
messages.timestamp >= ?2 AND
messages.timestamp < ?3 AND
json(messages.content) LIKE '"%'
ORDER BY sequence DESC
`,
[JSON.stringify(this.following), start_time, end_time]
);
result = (await this.decrypt(result)).filter(x => x.decrypted);
} 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.rowid, 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.rowid, 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.rowid, 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)),
start_time,
end_time,
]
)
);
}
return [].concat(...(await Promise.all(promises)));
result = [].concat(...(await Promise.all(promises)));
}
this.time_loading = undefined;
return result;
}
update_time_range_from_messages(messages) {
this.time_range = [
messages.reduce(
(accumulator, current) => Math.min(accumulator, current.timestamp),
this.time_range[0]
),
messages.reduce(
(accumulator, current) => Math.max(accumulator, current.timestamp),
this.time_range[1]
),
];
}
async load_more() {
let last_start_time = this.start_time;
this.start_time = last_start_time - 24 * 60 * 60 * 1000;
let more = await tfrpc.rpc.query(
`
WITH news AS (SELECT messages.*
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp > ?
AND messages.timestamp <= ?
ORDER BY messages.timestamp DESC)
SELECT messages.*
FROM news
JOIN messages_refs ON news.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT messages.*
FROM news
JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id
UNION
SELECT news.* FROM news
`,
[
JSON.stringify(this.following),
this.start_time,
last_start_time,
]);
this.messages = await this.decrypt([...more, ...this.messages]);
this.loading++;
this.loading_canceled = false;
try {
let more = [];
while (!more.length && !this.loading_canceled) {
let last_start_time = this.start_time;
this.start_time = last_start_time - 7 * 24 * 60 * 60 * 1000;
more = await this.fetch_messages(this.start_time, last_start_time);
this.update_time_range_from_messages(
more.filter(
(x) =>
x.timestamp >= this.start_time && x.timestamp < last_start_time
)
);
}
this.messages = await this.decrypt([...more, ...this.messages]);
} finally {
this.loading--;
}
}
cancel_load() {
this.loading_canceled = true;
}
async decrypt(messages) {
console.log('decrypt');
let result = [];
for (let message of messages) {
let content;
try {
content = JSON.parse(message?.content);
} catch {
}
if (typeof(content) === 'string') {
} 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);
@ -160,39 +268,157 @@ class TfTabNewsFeedElement extends LitElement {
return result;
}
async add_messages(messages) {
this.messages = await this.decrypt([...messages, ...this.messages]);
async load_latest() {
this.loading++;
let now = new Date().valueOf();
let end_time = now + 24 * 60 * 60 * 1000;
let messages = [];
try {
messages = await this.fetch_messages(this.time_range[1], end_time);
messages = await this.decrypt(messages);
this.update_time_range_from_messages(
messages.filter(
(x) => x.timestamp >= this.time_range[1] && x.timestamp < end_time
)
);
} finally {
this.loading--;
}
this.messages = [...this.messages, ...messages];
console.log('done loading latest messages.');
}
render() {
if (!this.messages ||
this._messages_hash !== this.hash ||
this._messages_following !== this.following) {
console.log(`loading messages for ${this.whoami} (following ${this.following.length})`);
let self = this;
async load_messages() {
let self = this;
this.loading++;
let messages = [];
try {
this.messages = [];
this._messages_hash = this.hash;
this._messages_following = this.following;
this.fetch_messages().then(this.decrypt.bind(this)).then(function(messages) {
self.messages = messages;
console.log(`loading mesages done for ${self.whoami}`);
}).catch(function(error) {
alert(JSON.stringify(error, null, 2));
});
let now = new Date().valueOf();
let start_time = now - 24 * 60 * 60 * 1000;
this.start_time = start_time;
this.time_range = [this.start_time, now + 24 * 60 * 60 * 1000];
messages = await this.fetch_messages(
this.time_range[0],
this.time_range[1]
);
this.update_time_range_from_messages(
messages.filter(
(x) =>
x.timestamp >= this.time_range[0] &&
x.timestamp < this.time_range[1]
)
);
messages = await this.decrypt(messages);
if (!messages.length) {
let more = [];
while (!more.length && start_time >= 0) {
let last_start_time = start_time;
start_time = last_start_time - 7 * 24 * 60 * 60 * 1000;
more = await this.fetch_messages(start_time, last_start_time);
this.update_time_range_from_messages(
more.filter(
(x) => x.timestamp >= start_time && x.timestamp < last_start_time
)
);
}
messages = await this.decrypt([...more, ...this.messages]);
}
} finally {
this.loading--;
}
this.messages = messages;
this.time_loading = undefined;
console.log(`loading messages done for ${self.whoami}`);
}
mark_all_read() {
let newest = this.messages.reduce(
(accumulator, current) => Math.max(accumulator, current.rowid),
this.channels_latest[this.channel()] ?? -1
);
if (newest >= 0) {
this.dispatchEvent(
new CustomEvent('channelsetunread', {
bubbles: true,
composed: true,
detail: {
channel: this.channel(),
unread: newest + 1,
},
})
);
}
}
render() {
if (
!this.messages ||
this._messages_hash !== this.hash ||
this._messages_following !== this.following
) {
console.log(
`loading messages for ${this.whoami} (following ${this.following.length})`
);
this.load_messages();
}
let more;
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
if (!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-theme-d1" @click=${this.mark_all_read}>
Mark All Read
</button>
<button
?disabled=${this.loading}
class="w3-button w3-theme-d1"
@click=${this.load_more}
>
Load More
</button>
<button
class=${'w3-button w3-theme-d1' + (this.loading ? '' : ' w3-hide')}
@click=${this.cancel_load}
>
Cancel
</button>
<span
>Showing
${new Date(
this.time_loading
? Math.min(this.time_loading[0], this.time_range[0])
: this.time_range[0]
).toLocaleDateString()}
-
${new Date(
this.time_loading
? Math.max(this.time_loading[1], this.time_range[1])
: this.time_range[1]
).toLocaleDateString()}.</span
>
</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>
<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
Mark All Read
</button>
<tf-news
id="news"
whoami=${this.whoami}
.users=${this.users}
.messages=${this.messages}
.following=${this.following}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel()}
channel_unread=${this.channels_unread?.[this.channel()]}
></tf-news>
${more}
`;
}
}
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);
customElements.define('tf-tab-news-feed', TfTabNewsFeedElement);

View File

@ -12,6 +12,10 @@ class TfTabNewsElement extends LitElement {
following: {type: Array},
drafts: {type: Object},
expanded: {type: Object},
loading: {type: Boolean},
channels: {type: Array},
channels_unread: {type: Object},
channels_latest: {type: Object},
};
}
@ -28,7 +32,10 @@ class TfTabNewsElement extends LitElement {
this.cache = {};
this.drafts = {};
this.expanded = {};
tfrpc.rpc.localStorageGet('drafts').then(function(d) {
this.channels_unread = {};
this.channels_latest = {};
this.channels = [];
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}');
});
}
@ -47,8 +54,7 @@ class TfTabNewsElement extends LitElement {
let unread = this.unread;
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.load_latest();
this.dispatchEvent(new CustomEvent('refresh'));
}
}
@ -62,11 +68,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) {
@ -77,10 +88,7 @@ class TfTabNewsElement extends LitElement {
} else {
delete this.drafts[id];
}
/* Only trigger a re-render if we're creating a new draft or discarding an old one. */
if ((previous !== undefined) != (event.detail.draft !== undefined)) {
this.drafts = Object.assign({}, this.drafts);
}
this.drafts = Object.assign({}, this.drafts);
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
}
@ -96,23 +104,188 @@ class TfTabNewsElement extends LitElement {
}
on_keypress(event) {
if (event.target === document.body &&
event.key == '.') {
if (event.target === document.body && event.key == '.') {
this.show_more();
}
}
unread_status(channel) {
if (
this.channels_latest[channel] &&
(this.channels_unread[channel] === undefined ||
this.channels_unread[channel] < this.channels_latest[channel])
) {
return '🔵';
}
}
show_sidebar() {
this.renderRoot.getElementById('sidebar').style.display = 'block';
this.renderRoot.getElementById('sidebar_overlay').style.display = 'block';
}
hide_sidebar() {
this.renderRoot.getElementById('sidebar').style.display = 'none';
this.renderRoot.getElementById('sidebar_overlay').style.display = 'none';
}
async channel_toggle_subscribed() {
let channel = this.hash.substring(2);
let subscribed = this.channels.indexOf(channel) != -1;
subscribed = !subscribed;
await tfrpc.rpc.appendMessage(this.whoami, {
type: 'channel',
channel: channel,
subscribed: subscribed,
});
if (subscribed) {
this.channels = [].concat([channel], this.channels).sort();
} else {
this.channels = this.channels.filter((x) => x != channel);
}
}
channel() {
return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
}
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('#@') && this.hash != '#@'
? html`<tf-profile
class="tf-profile"
id=${this.hash.substring(1)}
whoami=${this.whoami}
.users=${this.users}
></tf-profile>`
: undefined;
let edit_profile;
if (
!this.loading &&
this.users[this.whoami]?.name === undefined &&
this.hash.substring(1) != this.whoami
) {
edit_profile = html` <div
class="w3-panel w3-padding w3-round w3-card-4 w3-theme-l3"
>
Follow your identity link above to edit your profile and set your
name.
</div>`;
}
return html`
<p class="w3-bar">
<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>
${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>
<div
class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left"
style="width: 2in; left: 0; z-index: 5"
id="sidebar"
>
<div
class="w3-right w3-button w3-hide-large"
@click=${this.hide_sidebar}
>
&times;
</div>
${this.hash.startsWith('##') &&
this.channels.indexOf(this.hash.substring(2)) == -1
? html`
<div class="w3-bar-item w3-theme-d2">Viewing</div>
<a
href="#"
class="w3-bar-item w3-button"
style="font-weight: bold"
>${this.hash.substring(2)}</a
>
`
: undefined}
<div class="w3-bar-item w3-theme-d2">Channels</div>
<a
href="#"
class="w3-bar-item w3-button"
style=${this.hash == '#' ? 'font-weight: bold' : undefined}
>general ${this.unread_status('')}</a
>
<a
href="#@"
class="w3-bar-item w3-button"
style=${this.hash == '#@' ? 'font-weight: bold' : undefined}
>@mentions ${this.unread_status('@')}</a
>
<a
href="#🔐"
class="w3-bar-item w3-button"
style=${this.hash == '#🔐' ? 'font-weight: bold' : undefined}
>🔐private ${this.unread_status('🔐')}</a
>
${this.channels.map(
(x) => html`
<a
href=${'#' + encodeURIComponent('#' + x)}
class="w3-bar-item w3-button"
style=${this.hash == '##' + x ? 'font-weight: bold' : undefined}
>#${x} ${this.unread_status(x)}</a
>
`
)}
</div>
<div
class="w3-overlay"
id="sidebar_overlay"
@click=${this.hide_sidebar}
></div>
<div style="margin-left: 2in; padding: 8px" id="main" class="w3-main">
<div
id="show_sidebar"
class="w3-left w3-button w3-hide-large"
@click=${this.show_sidebar}
>
&#9776;
</div>
<p>
<button class="w3-button w3-theme-d1" @click=${this.show_more}>
${this.new_messages_text()}
</button>
${this.hash.startsWith('##')
? html`
<button
class="w3-button w3-theme-d1"
@click=${this.channel_toggle_subscribed}
>
${this.channels.indexOf(this.hash.substring(2)) != -1
? 'Unsubscribe from #'
: 'Subscribe to #'}${this.hash.substring(2)}
</button>
`
: undefined}
</p>
<div class="w3-bar">
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
${edit_profile}
</div>
<div>
<tf-compose
id="tf-compose"
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
@tf-draft=${this.draft}
.channel=${this.channel()}
></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}
.channels_unread=${this.channels_unread}
.channels_latest=${this.channels_latest}
></tf-tab-news-feed>
</div>
`;
}
}

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-theme-d1"
style="flex: 1; resize: vertical"
@keydown=${this.search_keydown}
>
${this.query}</textarea
>
<button
class="w3-button w3-theme-d1"
@click=${(event) =>
self.search(self.renderRoot.getElementById('search').value)}
>
Execute
</button>
</div>
<div ?hidden=${this.duration === undefined}>
Took ${this.duration / 1000.0} seconds.
</div>
<div ?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

@ -5,6 +5,7 @@ import {styles} from './tf-styles.js';
class TfTabSearchElement extends LitElement {
static get properties() {
return {
drafts: {type: Object},
whoami: {type: String},
users: {type: Object},
following: {type: Array},
@ -22,28 +23,34 @@ class TfTabSearchElement extends LitElement {
this.users = {};
this.following = [];
this.expanded = {};
this.drafts = {};
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}');
});
}
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();
@ -68,6 +75,18 @@ class TfTabSearchElement extends LitElement {
}
}
draft(event) {
let id = event.detail.id || '';
let previous = this.drafts[id];
if (event.detail.draft !== undefined) {
this.drafts[id] = event.detail.draft;
} else {
delete this.drafts[id];
}
this.drafts = Object.assign({}, this.drafts);
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
}
render() {
if (this.query !== this.last_query) {
this.last_query = this.query;
@ -76,12 +95,12 @@ class TfTabSearchElement extends LitElement {
let self = this;
return html`
<div style="display: flex; flex-direction: row; gap: 4px">
<input type="text" class="w3-input w3-dark-grey" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
</div>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} .drafts=${this.drafts} @tf-expand=${this.on_expand} @tf-draft=${this.draft}></tf-news>
`;
}
}
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=${'#' + encodeURIComponent(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);

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