456 Commits

Author SHA1 Message Date
c74f90ef04 core: Fix stock apps not being loaded/updated. 2025-02-26 18:54:00 -05:00
26cb7e5a17 ios: Redid icon stuff. 2025-02-26 18:27:54 -05:00
2bad6672d8 ios: Generate Assets.car out of an overabundance of caution that F-Droid is going to complain about this as a binary file. 2025-02-26 12:43:42 -05:00
71c4011526 build: Let's prepare a 0.0.28 release. 2025-02-26 12:07:54 -05:00
5e81078f59 update: CodeMirror. 2025-02-26 12:05:07 -05:00
31b78e74df ssb: Following calculation fixes. It was not handled appropriately if an account was encountered multiple times with decreasing depths. 2025-02-25 21:52:15 -05:00
2ff689aab0 buttfeed: De-duplicate updates by link. 2025-02-24 12:16:31 -05:00
0a6f0ed3f7 ssb: Audit timer use that might cause unnecessary delays during shutdown. 2025-02-23 13:07:36 -05:00
bfec46673d ssb: Test an experiment that blobs are getting priority over messages during initial sync. 2025-02-22 11:57:57 -05:00
3a16614c72 docs: Remember to update the latest_release tag. 2025-02-22 11:23:36 -05:00
9c9efb845c ssb: Schedule the clock for reevaluation any time new messages are added. I think this will improve initial replication. 2025-02-21 22:30:14 -05:00
6192f1b94d room: Fix the room app port number multiple ways. 2025-02-21 22:05:28 -05:00
ce451b2449 cli: Fix the return value for get_profile. 2025-02-21 19:58:01 -05:00
8534e16469 docs: +sidebar. 2025-02-19 20:20:21 -05:00
f6cc6f2eae docs: Minor prep for 0.0.28. 2025-02-19 19:54:45 -05:00
0904425221 update: CodeMirror. 2025-02-19 19:04:59 -05:00
a729886522 update: sqlite 3.49.1. 2025-02-19 18:59:45 -05:00
e5dfedc7d1 ios: Drop min ios version to 14. Generate more of the Info.plist. We don't use non-exempt encryption. 2025-02-19 18:49:53 -05:00
f02423d084 ios: Now with more sed. 2025-02-18 21:46:04 -05:00
8f4b6e83eb ios: This fixes the build on macos and runs in the simulator. 2025-02-18 21:28:03 -05:00
3029919553 ios: Might as well generate the CFBundleShortVersionString from that other version. 2025-02-18 20:58:16 -05:00
ac67db0591 Merge pull request 'nix: fix build instructions fuckup' () from tasiaiso/tildefriends:tasiaiso-fix-fuckup-holy-shit into main
Reviewed-on: 
2025-02-18 19:26:56 -05:00
1e08838f5b ios: I was able to get an iOS build to app store connect with this. Sanity lost. 2025-02-18 19:03:25 -05:00
d814f7ee77 nix: update flake lockfile, fix update procedure 2025-02-18 22:41:00 +01:00
7edfb9d386 docs: I forgot to do this last release. 2025-02-17 21:16:38 -05:00
6928d6caba cleanup: prettier. 2025-02-16 15:52:27 -05:00
1a626875cf build: Link warning on macos. 2025-02-16 15:13:45 -05:00
6eb3b64334 build: Fix android aab build. 2025-02-16 14:41:14 -05:00
09f3595e93 ssb: Use the right shape for your own profile image. 2025-02-16 14:37:45 -05:00
11e89622d4 security: Respect the autologin setting better. 2025-02-16 14:07:14 -05:00
0fa8acc264 ios: Untested, but attempt auto-login. 2025-02-16 13:42:07 -05:00
afc7c64ed8 android: Fix launch args. 2025-02-16 13:41:26 -05:00
c794c1b885 admin: Global settings can be specified on the command-line. Removed some previous, less thorough ways of configuring things. 2025-02-16 13:37:25 -05:00
6247529799 build: Fix F-droid icon reference. 2025-02-16 11:49:45 -05:00
ad3eedc1fb android: Avoid the extra background on the icon, and set a different color for F-droid builds. 2025-02-16 11:43:27 -05:00
1cfac3cae6 ssb: Allow otherwise unrecognized incoming connections when not talking to strangers if they have a valid invite. 2025-02-16 08:45:08 -05:00
478bcd5d13 ssb: New incoming connections replace old ones for the same id. 2025-02-16 08:19:40 -05:00
6932da69b3 update: speedscope 1.22.2. 2025-02-15 21:08:46 -05:00
857f47bf55 ssb: Make the invite.use reponse flags match the docs exactly, and add some connection state paranoia. 2025-02-15 20:52:39 -05:00
373d742751 ssb: The docs show the response to invite.use() wrapped in {key: ..., value: ...}, so do that. 2025-02-15 08:19:23 -05:00
575622c522 ssb: Did that from the wrong thread. 2025-02-13 21:37:40 -05:00
a3e86ccb1d update: CodeMirror. 2025-02-12 19:43:19 -05:00
e491798ff1 auth: Auto-login fixes. 2025-02-12 19:37:06 -05:00
15df4ac236 auth: Skip auth on mobile. 2025-02-12 19:25:05 -05:00
017a74c4e4 ssb: Reload the profile on follow/unfollow/block/unblock. 2025-02-12 18:20:44 -05:00
6e5b1127f3 ssb: If we don't accept the invite, give up on the connection. 2025-02-12 18:08:40 -05:00
95f4f88949 ssb: Non-expiring invites were still broken. 2025-02-12 18:03:53 -05:00
92858882d7 update: libbacktrace. 2025-02-11 20:53:57 -05:00
4cba95ec8c update: CodeMirror. 2025-02-11 20:52:13 -05:00
bbb2f89d85 update: OpenSSL 3.4.1. 2025-02-11 20:48:41 -05:00
88213038b2 build: Link with -gz=zlib to make some of these distributables megabytes smaller. 2025-02-11 20:42:22 -05:00
cc18b41e76 update: sqlite 3.49.0. 2025-02-10 22:05:02 -05:00
f16127a238 build: Fix mixing directory for lipo targets. 2025-02-10 21:28:22 -05:00
2a6ecfaede build: Set an LD_LIBRARY_PATH for macos on linux. 2025-02-10 20:57:14 -05:00
cf4a09bf03 build: Use clang-19 on the worker, and cleanup some symlinks I commited in error. 2025-02-10 20:23:54 -05:00
20e60262fd build: Some clangs are more strict than quickjs. 2025-02-09 21:46:17 -05:00
9e3928c976 build: Some clangs are more strict about this. quickjs is not. 2025-02-09 21:45:05 -05:00
95d8768545 build: Try to get CI building for macos. 2025-02-09 20:47:20 -05:00
8679d09040 build: Build a fat macos binary. 2025-02-09 08:13:13 -05:00
4cc5c6acb3 build: On second thought, split the cross-build of macos into x86_64 and arm. 2025-02-06 12:48:17 -05:00
6f9b548b1a build: There seems to be a way to build for macos from Linux. Neat. 2025-02-05 20:50:41 -05:00
fbff3386a9 update: CoreMirror. 2025-02-05 18:58:03 -05:00
e5899fca58 ssb: Use the cache of private messages we built for the unread notification to actually show private messages. Still needs some work, but it's something. 2025-02-05 18:41:37 -05:00
dddec489b9 ssb: Results of staring at unexpected shs disconnections: wire up tunnel.endpoints and blobs.createWants more correctly/thoroughly. And add slightly more context when deliberately disconnected from the remote side. 2025-02-04 21:32:22 -05:00
b4049eaeaa welcome: Feeling fancy. Might revert later. 2025-02-04 12:53:04 -05:00
b3fd724b5a docs: Some updates to the README. 2025-02-04 12:39:59 -05:00
aca25be86a docs: Minor cleanup/normalization. 2025-02-03 12:39:23 -05:00
0f8cbdac57 docs: Bring back docs as a directory. Long live https://docs.tildefriends.net/ 2025-02-02 22:39:55 -05:00
630219d667 docs: Stop using docs as a submodule. It discouraged pull requests. 2025-02-02 22:39:11 -05:00
fef268e434 build: Oops. 2025-02-02 22:21:16 -05:00
e18dcf3a48 build: Maybe ssh? 2025-02-02 22:18:05 -05:00
e019320146 build: Forgot the actual rsync options I wanted. 2025-02-02 21:55:57 -05:00
65b31e14f9 build: +rsync. Formatting yaml. 2025-02-02 21:51:18 -05:00
9cb5cbcde9 build: OK, that fail's on me. 2025-02-02 21:47:05 -05:00
25914ff5a7 build: c'mon 2025-02-02 21:44:05 -05:00
c4af799279 build: Let's try to put docs somewhere. 2025-02-02 21:40:29 -05:00
3f8f0e14f4 build: Make a directory for a thing that fails intermittently?? 2025-02-02 17:12:03 -05:00
5414b30e7f build: Resolve some zsign/cmake warnings. 2025-02-02 17:07:59 -05:00
7aee897c1b cleanup: No longer need form parsing in JS. 2025-02-02 16:52:44 -05:00
4060f9cc11 build: jarsigner -> apksigner to suppress a warning. 2025-02-02 16:38:55 -05:00
5b526cbf5b build: Clean up / consolidate some OpenSSL options now that there is only one place. Also: deadstrip better? 2025-02-02 16:19:46 -05:00
f5065ff42b build: Indicate that we always use system OpenSSL on Haiku and BSD. 2025-02-02 21:08:46 -05:00
86b5546f5f build: Get rid of the ios-specific OpenSSL build script. Now there is only one. 2025-02-02 15:56:06 -05:00
43ae2a293b build: Fix cross-compiling for arm. 2025-02-02 15:55:12 -05:00
b8ddbd4255 build: Remove the mingw-specific OpenSSL build script. 2025-02-02 15:21:38 -05:00
87cdba1db8 build: Replace the android-specific script to build OpenSSL with the shared one. 2025-02-02 15:16:51 -05:00
df83187e33 cleanup: Remove File.writeFile(). It is no longer used or necessary. 2025-02-02 14:52:25 -05:00
eccdbf29ab build: Move built openssl to out where it belongs. 2025-02-02 14:39:36 -05:00
28d181f8bc build: Fix. 2025-02-02 14:28:40 -05:00
d386daf2ff build: Rename the macosdebug/macosrelease targets to debug/release. 2025-02-02 14:08:27 -05:00
0c1e116c1e build: More concise build context. 2025-02-02 13:04:22 -05:00
bb0ed67827 httpd: Move starting the http server into C. 2025-02-02 12:27:06 -05:00
b111d06851 ssb: I think this fixes missing a range in some circumstances. 2025-02-02 10:14:14 -05:00
79388845ea build: Remove the dependency on zsign on macos. 2025-02-01 22:30:35 -05:00
99faef2e77 build: valid bash? 2025-02-01 19:59:18 -05:00
21fffbfe10 build: Valid yaml? 2025-02-01 19:58:28 -05:00
64fb9f0c2a build: +zlib. 2025-02-01 19:55:02 -05:00
a42e0bef2c build: The journey continues. 2025-02-01 19:15:50 -05:00
45a09006e1 build: Build zsign for the sake of the ios build. 2025-02-01 18:52:20 -05:00
240484be4c build: Fix docker build, and speed it up a bit? 2025-02-01 18:19:44 -05:00
22f4d115e3 build: Finally ios is happy. Now let's un-break docker. 2025-02-01 18:07:56 -05:00
32920e0e5d build: Still trying to get ios to build on this worker. 2025-02-01 17:58:55 -05:00
f03a5918d1 build: Apparently we need file. 2025-02-01 17:19:28 -05:00
dd1870b52a build: Nope, OpenSSL assumes this directory is SDKs, so it is SDKs. 2025-02-01 16:51:10 -05:00
f0c1a8f98f build: Minor cleanup and a disappointing workaround for iOS cross-compiling toolchain issues I don't understand. 2025-02-01 16:25:23 -05:00
0c181d7e7c build: Maybe maybe maybe. 2025-02-01 14:55:26 -05:00
e3dc0e833a build: Some docs say the syntax is like this. 2025-02-01 14:17:15 -05:00
6de875edea build: Print some configuration that is confusing me. 2025-02-01 13:38:52 -05:00
986a55173f build: I think CI can cross-build ios. 2025-02-01 12:50:31 -05:00
59c8cabf02 build: fix? 2025-02-01 11:39:58 -05:00
a33b9fab07 build: I misunderstood these makefile conditionals. 2025-02-01 11:32:01 -05:00
f8a725e1e7 build: ios dist is conditional on having the SDK. 2025-02-01 11:02:21 -05:00
b3ab3af01b build: dist uses curl. 2025-02-01 10:10:59 -05:00
79f9463e56 build: Can I get CI to artifact the dist files? 2025-02-01 09:49:49 -05:00
4257b2ed51 ssb: Unknown message types need some padding. 2025-02-01 09:41:22 -05:00
6488ab60ec build: Docker build no longer needs libssl installed. 2025-02-01 09:40:34 -05:00
18bd3dfcf9 build: So close. +docker? 2025-02-01 09:00:06 -05:00
ec114e160d build: on node:23-bookworm-slim 2025-02-01 08:34:42 -05:00
de033af18f build: Oh, now I can use a normal image, maybe with a more reasonable compiler? Also, update libbacktrace. 2025-02-01 08:31:33 -05:00
e971c6fcb7 build: Go home, -fanalyze, you're confused. 2025-02-01 08:14:11 -05:00
d3c465391c build: +zip 2025-01-31 21:47:24 -05:00
f1fa19593d build: Better system OpenSSL logic? 2025-01-31 21:43:18 -05:00
60b6f9c73e build: +build-essential 2025-01-31 21:32:17 -05:00
55b95ddecb build: +make 2025-01-31 21:27:14 -05:00
0800a251b2 build: +unzip 2025-01-31 21:23:22 -05:00
82cf7a80eb build: Maybe I don't need sudo here? 2025-01-31 21:19:22 -05:00
379f3d12eb build: Maybe we can just install the git we need? 2025-01-31 21:15:19 -05:00
e52972d4d4 build: Version shenanigans? 2025-01-31 21:11:35 -05:00
1a0ca4dec2 build: I guess this requires node? 2025-01-31 21:08:57 -05:00
0bac9d8d5a build: Can I specify the image we build on like this? 2025-01-31 21:07:01 -05:00
a9608363c5 core: Don't actually start the http server unless we've specified a port or are asking for a port to be determined and written to file. 2025-01-31 20:57:59 -05:00
f1a2c5ae8e ssb: Silence some persistent broadcast noise. 2025-01-31 20:45:35 -05:00
192a81ede7 core: As an experiment, handle running from a subdirectory (generally out/debug or out/release, since people tend to do that). Remove unused File.stat(). 2025-01-31 20:37:14 -05:00
916aa5abbd cli: Fix create_invite's expire option. 2025-01-30 22:16:06 -05:00
d4a5cc6eee build: Let's start work on 0.0.28. 2025-01-30 12:46:52 -05:00
d19605cc8d build: Fixes nix. Now everybody is happy. 2025-01-30 12:35:43 -05:00
8d529327a4 build: Let's go back to nix trying to use its own OpenSSL. 2025-01-30 12:31:12 -05:00
697e2f2071 build: Nope, I don't understand nix. 2025-01-29 20:53:09 -05:00
f7fb112f21 build: I don't understand this nix error. 2025-01-29 20:32:35 -05:00
b2c0211190 build: nix? 2025-01-29 20:28:26 -05:00
c59e0ea6e5 build: Let's release 0.0.27. 2025-01-29 19:58:31 -05:00
6c2fc1444d ssb: Fix some SQL syntax for private messages. They need more work. 2025-01-29 12:15:46 -05:00
94d969fe46 update: CodeMirror. 2025-01-28 22:23:52 -05:00
a7bc3d301c cleanup: format. 2025-01-28 22:00:48 -05:00
18bab849f7 ssb: prettier. 2025-01-27 21:02:15 -05:00
04878fcc30 ssb: Fix multiple bugs with follow status lying. 2025-01-27 20:28:44 -05:00
ffe1299548 ssb: More room for activities in the emoji picker dialog. 2025-01-27 12:45:54 -05:00
64bdbf5725 build: Never mind. flatpak tools don't want to work in docker. 2025-01-27 12:11:41 -05:00
b82deb557f build: https://github.com/flatpak/flatpak-builder/issues/237 ?? 2025-01-26 21:43:18 -05:00
d529a48a11 build: flatpak maybe? 2025-01-26 21:14:55 -05:00
b711e4e6bd build: Can the CI worker build the flatpak? Let's find out. 2025-01-26 20:47:44 -05:00
186eecfbff build: I don't know why this works better, but it does. 2025-01-26 20:43:02 -05:00
d766c33f59 build: Stop using lto. Not worth the hassle. 2025-01-26 09:56:53 -05:00
2c9257f1a8 build: Trying to understand a build thing. 2025-01-26 09:33:45 -05:00
71ff604f90 ssb: Stop breaking up the news queries into chunks. It sometimes produced incorrect results, and it is not necessary if we're fast enough. 2025-01-25 20:13:05 -05:00
83e00763ea ssb: Be a bit more aggressive about loading latest messages. It bugs me when I don't see my own reactions in a channel or whatnot, and I think it has to do with the queried time ranges. 2025-01-25 18:00:06 -05:00
5b647e0937 buttfeed: Use markdown that works better in Manyverse. 2025-01-25 08:08:02 -05:00
e0444510f4 trace: Use our own sequential thread ids to be more compatible with different trace viewers. 2025-01-24 19:00:40 -05:00
82e876a892 ssb: Only re-populate the messages_stats table on first creation. 2025-01-24 18:06:31 -05:00
c728e05032 ssb: Add a lookup table of max sequence and timestamp per account.f 2025-01-22 19:19:50 -05:00
c7a8ce7060 apps: display: grid. 2025-01-22 19:13:15 -05:00
762b4339fe ssb: Better 'Load More', too. 2025-01-22 18:28:55 -05:00
f824b8988e ssb: Correctness around tunnel.endpoints. 2025-01-22 18:03:05 -05:00
6280d6d167 apps: Mobile CSS tweaks. 2025-01-21 21:46:48 -05:00
4f18e744b4 ssb: Unbreak the channel query. 2025-01-21 21:31:16 -05:00
01d8f720e8 ssb: Treat timestamps of related messages separately from those of the primary queried messages. 2025-01-21 21:07:37 -05:00
18cf058af3 ssb: Experimenting with the news query strategy. 2025-01-21 20:53:23 -05:00
e2406df367 apps: Messing with CSS. 2025-01-20 20:45:35 -05:00
1fd669bdb3 update: npm. 2025-01-20 20:25:31 -05:00
f6add12c80 build: Update the change notes. 2025-01-20 17:57:15 -05:00
0f643bfe39 ssb: Complete using an invite, following the pub and posting a pub message. 2025-01-20 17:47:30 -05:00
15be498e4b ssb: Don't show connections in various states of disconnectedness in the sidebar. 2025-01-20 16:57:22 -05:00
fba465dd62 ssb: Allow a concurrent connection if the other one is a connection in the process of closing. 2025-01-20 16:49:21 -05:00
19dbe354e7 core: Consolidate default global setting values in one place. 2025-01-20 14:23:41 -05:00
fca5d37b7e ssb: Add an option to control whether we talk to strangers. 2025-01-20 13:35:28 -05:00
aa04ad2dc2 ssb: Clean up used and expired invites. 2025-01-20 08:01:24 -05:00
7ef4d814ef ssb: Avoid showing a broken img when viewing a profile without one assigned. 2025-01-19 21:34:42 -05:00
3f3deb665c ssb: Add a command-line action to generate an invite, and verified that Patchwork can accept it. 2025-01-19 21:00:38 -05:00
97fc22ce57 ssb: Test the one message generated from an invite so far. 2025-01-19 18:38:26 -05:00
616f3ad76d ssb: Invite support progress. Now the pub accepts the invite, tracks its use, and follows. The client still needs to react. 2025-01-19 17:02:08 -05:00
faca63946c build: Fix make docs. 2025-01-19 16:03:21 -05:00
57bae341a2 build: Missed an include. 2025-01-19 16:02:51 -05:00
fd09a766d2 ssb: Work in progress invite support. We can generate them. We can connect using an invite code. We can't yet invite.use(). 2025-01-19 16:00:37 -05:00
11564a5292 core: Let's try some neutral colors to avoid clashing with app colors. 2025-01-18 21:56:43 -05:00
ac12e350bf ssb: People without profile images get emoji faces. I'm amazed it took me this long. 2025-01-18 21:17:07 -05:00
2def15337d update: speedscope 1.22.0. 2025-01-17 17:55:47 -05:00
3e3d58a4a9 ssb: Try harder to avoid doing things with new connections during shutdown. 2025-01-16 12:47:50 -05:00
5ce4f55228 update: speedscope 1.21.2. 2025-01-16 12:35:43 -05:00
21788fc7b0 ssb: Stared at the news feed queries for performance and correctness a bit. 2025-01-15 19:32:31 -05:00
a1c4382fde update: libuv 1.50.0. 2025-01-15 18:53:34 -05:00
364e95698e ssb: Remove the react confirmation dialog. 2025-01-15 12:34:02 -05:00
6eec142499 update: sqlite 3.48.0. 2025-01-15 12:14:46 -05:00
a8f6b3a39a ssb: pub messaged needed padding. 2025-01-14 21:50:38 -05:00
250933bf41 ssb: Suppress noisy output when running command-line actions with output intended to be parsed. 2025-01-14 21:37:11 -05:00
56c77c781a ssb: Placeholder messages needed padding. 2025-01-14 21:18:49 -05:00
f7602b39a1 ssb: The compose preview needed some padding. 2025-01-14 20:51:56 -05:00
8c86092356 ssb: Make the refresh icon a circle. 2025-01-14 20:29:12 -05:00
db0a4bff77 ssb: Use most recent post timestamps to feature more relevant people to follow. 2025-01-14 20:15:18 -05:00
e198ff9cb1 ssb: Show some suggested accounts to follow. 2025-01-12 14:54:09 -05:00
b8eaa5cf97 ssb: prettier. 2025-01-12 12:08:50 -05:00
0d597721bf ssb: Try harder to avoid a full re-render. It's disruptive. 2025-01-12 12:01:52 -05:00
003e0caada ssb: Distant/unknown users get a circle avatar, too. 2025-01-12 11:56:59 -05:00
053637cfb4 ssb: The sync button indicates if a one-shot sync is active. 2025-01-12 11:54:30 -05:00
8178213f1a ssb: Keep the previous db location for android. wip release notes. 2025-01-11 16:14:39 -05:00
b4222a41de update: CodeMirror. 2025-01-11 16:02:52 -05:00
f28e409ea5 ssb: Add a get_contacts command to enumerate follows, blocks, and friends. 2025-01-11 15:49:49 -05:00
287c6c06e1 core: More clean shutdown? 2025-01-11 14:48:12 -05:00
8216bdb4b3 core: Poking at task cleanup. 2025-01-11 14:41:46 -05:00
aa15da50ab ssb: Tried to do the right lit things to prevent unnecessary re-rendering. Ended up doing a lazy JSON thing. 2025-01-11 14:09:42 -05:00
02759c6f83 ssb: Hint at follow depth with profile image shape. Also, reload follow information the same way we re-determine channel unread status. Let's see if this feels good. 2025-01-11 13:48:06 -05:00
6b0c49752c ssb: Trying to learn about occasional errors updating connection info in the database. 2025-01-11 13:02:30 -05:00
2e4f792fc3 ssb: Try to consolidate the local users list. Man, CSS. 2025-01-11 12:48:44 -05:00
17eba059f0 ssb: Fix trying to connect to the same stored connection over and over. 2025-01-11 12:32:18 -05:00
e59a00922b ssb: Respond to tunnel.endpoints. Patchwork didn't seem to like that we responded to tunnel.isRoom but not this. 2025-01-11 09:23:12 -05:00
872201c886 ssb: Support publishing private messages from the command-line. 2025-01-08 20:16:17 -05:00
3352098284 ssb: Connections established for a one-shot sync timeout due to inactivity so that the sync eventually completes. 2025-01-07 12:48:21 -05:00
d0bbd7f24f ssb: Add a has_blob command. 2025-01-06 20:46:16 -05:00
7f87714b58 ssb: Add some missing padding. 2025-01-05 21:43:26 -05:00
5594bee618 ssb: Add get_identity, get_sequence, and get_profile commands. 2025-01-05 17:16:05 -05:00
c469ef23e6 ssb: Make the profile layout a little friendlier. 2025-01-05 15:49:57 -05:00
f6e74f2526 ssb: Try to fix profile layout yet again. Man, CSS. 2025-01-05 15:41:56 -05:00
10b6e9c537 ssb: Remove the weird option to make the server account follow you. Now that this account is admin-controlled, it's unnecessary. 2025-01-05 15:29:29 -05:00
3f27af30b7 ssb: Actually fall back to this default global setting value. 2025-01-05 15:17:41 -05:00
23db09f9b7 core: Default to loading into the ssb app. No more messing around. 2025-01-05 14:52:27 -05:00
d1b7681efc ssb: Don't show 'Loading...' forever in the ssb app when not signed in. Direct to the login page. 2025-01-05 12:52:52 -05:00
61ad405ad8 ssb: Too bright. 2025-01-04 21:38:52 -05:00
aff98110e0 ssb: Tidy up some of the more common reasons for disconnect. 2025-01-04 21:37:43 -05:00
2f36db9142 ssb: Don't display mentions for tags that are used in the text of a message. 2025-01-04 21:05:24 -05:00
aa86ee1066 cleanup: rm test.c 2025-01-04 17:12:00 -05:00
dbbcce8165 ssb: Don't store connections that aren't user-initiated. 2025-01-04 17:08:36 -05:00
1ed066ef0f ssb: Fix naked hashtag links (to the corresponding channel). 2025-01-04 13:03:58 -05:00
763f7d45d8 ssb: Prettier. 2025-01-04 12:41:04 -05:00
2328f3afb5 ssb: Ease up on excessively re-hitting the database for ebt.replicate even more. 2025-01-04 09:58:16 -05:00
2223245861 ssb: Maybe ease up on hammering the db for follows. 2025-01-03 18:17:54 -05:00
36226b01cd ssb: Manage new message handling from the new EBT code. 2025-01-03 17:04:38 -05:00
da31f9cadd cleanup: get/set sent clock is now unused. 2025-01-03 16:56:04 -05:00
9da4857066 ssb: Make the client a bit less aggressive about determining private messages every load. 2025-01-03 15:53:19 -05:00
75c71135ba ssb: No longer replicate every account we hear about. 2025-01-03 15:25:59 -05:00
0cb5025a16 core: Improve global setting grammar. 2025-01-03 14:10:27 -05:00
44d9f69434 ssb: Refactoring EBT implementation. I think this works not worse than before and will let me schedule message replication better in a future change. 2025-01-03 13:59:25 -05:00
3f343b283b ssb: Delete one more redundant global setting accessor. 2025-01-03 08:41:13 -05:00
03a28fc3c5 update: CodeMirror. 2025-01-03 08:19:41 -05:00
3513619221 ssb: Reduce message margins. 2025-01-02 21:16:55 -05:00
0c9f5769d3 build: Trying to fix flatpak build for some reason. 2025-01-02 17:45:27 -05:00
587a666ab6 build: Needed out/ earlier for ssl-local. 2025-01-02 17:40:29 -05:00
f26deea508 build: mkdir out/openssl-local. 2025-01-02 17:35:49 -05:00
b8e19040b5 ssb: Fiddling with render of encrypted messages. 2025-01-02 16:11:04 -05:00
7d9e0f4080 macos: Fix build. 2025-01-02 14:20:58 -05:00
16ce7fbc7b ssb: Fix the message encrypted icon placement. 2025-01-02 13:55:47 -05:00
639fce376a ssb: More uv_async_send paranoia still. 2025-01-02 13:01:09 -05:00
3cdbac5c22 build: Archive the windows .exe with data. 2025-01-02 13:00:42 -05:00
3dcafdf403 ssb: More uv_async_send paranoia. 2025-01-02 12:40:11 -05:00
cd2fe9f8d9 ssb: Fix a crash on Windows when we would call uv_async_send on a handle that had already been closed. Various other cleanup and improvements along the journey. 2025-01-02 12:35:58 -05:00
fd40596ce7 ssb: Every now and then I load in Chrome and see everywhere I used overflow: scroll when I wanted overflow: auto. 2025-01-02 08:58:10 -05:00
7ecda69703 ssb: tags never got an updated CSS treatment. 2025-01-02 08:49:30 -05:00
a3b76cd5c2 core: Let's try getting crash callstacks on win32 with a vectored exception handler. 2025-01-02 08:32:18 -05:00
54df862998 ssb: Continuing to untangle message CSS. 2025-01-01 16:44:16 -05:00
301b7a4911 ssb: Trying to untangle some message formatting ugliness. First step: some minor refactoring. 2025-01-01 15:45:11 -05:00
e0a048abe6 follow: This app had never been updated since jsonb, whoops. 2025-01-01 15:28:19 -05:00
671e3e19ff ssb: Try to stop dates from wrapping into a vertical line of single characters. 2024-12-31 08:39:02 -05:00
0c394c2e61 ssb: Trying to keep things CSS-ing off the screen. 2024-12-30 07:13:49 -05:00
4ecbb5234c ssb: Tweaking profile card CSS. 2024-12-29 15:51:51 -05:00
98f1700049 ssb: Fiddling with message card and compose CSS some more. 2024-12-29 15:42:12 -05:00
2f0b4a0187 ssb: Choose an unread notification that is a bit mire color-agnostic. 2024-12-29 15:15:28 -05:00
f66c6ed0c3 ssb: Fiddle with the placement of the hamburger menu, and fix the tf-compose placeholder text. 2024-12-29 15:11:14 -05:00
5d9785ac2d ssb: Why did this vertical alignment change? I will never know, but I can poke it back. 2024-12-29 14:59:12 -05:00
bb97a8cccc ssb: Show connections in the sidebar. Fiddle with tf-user CSS to make it fit. 2024-12-29 14:54:29 -05:00
571cf5b5b8 ssb: Color scheme is determined by day/hour/second. Couldn't help it. 2024-12-29 13:55:48 -05:00
1974ed1c03 ssb: We don't have to wait for channel status to finish load. 2024-12-29 13:41:57 -05:00
98275f7c87 cleanup: Remove a debug print. 2024-12-29 13:34:51 -05:00
eca8726909 ssb: Load and show new messages as they arrive. 2024-12-29 13:32:37 -05:00
baf125c450 ssb: Trying to get the sidebar to behave better. Fighting CSS. 2024-12-29 12:44:42 -05:00
efcc710d91 ssb: Let's assume we read our own messages. 2024-12-28 21:31:24 -05:00
5980ee4c86 cleanup: Unneeded #include. 2024-12-28 21:13:53 -05:00
db9b7a22c2 core: Report the c-ares version. 2024-12-28 20:38:07 -05:00
5e24d4f322 build: Support Xcode 16.2 on Linux, including cross-compiling OpenSSL. 2024-12-27 21:32:33 -05:00
2dd32cdce2 ssb: Tweak idle scheduling even more, still. Fixes -t=bench. 2024-12-27 15:51:33 -05:00
9cddd93dad ssb: Don't schedule duplicate history stream requests for the same account. Changes how we schedule idle work. Let's see if this is better. 2024-12-27 15:02:52 -05:00
4127898655 ssb: Avoid more work on shutdown. 2024-12-27 14:27:52 -05:00
45d48483d0 ssb: Avoid scheduling idle work while shutting down, so that we shut down sooner. 2024-12-27 14:03:14 -05:00
852c25296a ssb: Better errors for failing to decrypt private messages. 2024-12-27 13:38:09 -05:00
aea631138e ssb: Fix private messages starting with unread status when there are none. 2024-12-27 13:25:40 -05:00
683fdbb02a ssb: Fix channel status not updating reliably. 2024-12-27 13:23:29 -05:00
c3bbab35e2 ssb: Fix global settings defaults. 2024-12-27 13:16:11 -05:00
ba8941046e ssb: Consolidate some redundant connection code. 2024-12-27 12:27:54 -05:00
d202f4e00d auth: Provide some feedback about valid account names. 2024-12-27 11:39:38 -05:00
42da5d8d32 ssb: Experimenting with generating the w3 css theme colors. 2024-12-26 22:24:52 -05:00
5af3533598 tests: Clean up some warnings by avoiding in-memory databases. I never got that working well, and it's not representative of actual operation. 2024-12-26 20:17:17 -05:00
7843168fad ssb: Hook up some more disconnect messaging. 2024-12-26 20:12:04 -05:00
8f51eb63b0 ssb: More experimenting with the unread status strategy. 2024-12-26 19:36:04 -05:00
855f5f7af4 ssb: Before destroying a connection, show a message on why it is going away in the UI. 2024-12-24 17:23:22 -05:00
c85dd2655c ssb: Strip out the old disconnection debug information. 2024-12-24 16:44:52 -05:00
fb0e4060cd ssb: Instrument some more callbacks for hitches. 2024-12-24 16:33:08 -05:00
707b4990a6 build: Not all the toolchains support -Oz. Oh well. 2024-12-24 15:01:09 -05:00
9c8b922069 build: Use all the tricks to make release smaller on all the platforms. 2024-12-24 14:47:33 -05:00
d4b421421d ssb: prettier. 2024-12-24 14:22:24 -05:00
58e9646fa6 ssb: Populate reply information in posts. 2024-12-24 13:09:27 -05:00
500f172561 ssb: Double down on a loading indicator. 2024-12-24 12:45:25 -05:00
68f6c90ea4 ssb: Get saving the about cache off of the main load path. 2024-12-24 12:05:31 -05:00
41e91f2922 ssb: Consolidate global settings helpers. 2024-12-24 11:16:52 -05:00
999117cfeb clean: Remove a file I sloppily added. 2024-12-24 10:45:47 -05:00
6185df512f build: Add help for armdebug/release, and fix some make help alignment. 2024-12-24 10:39:11 -05:00
0cbf66c007 build: Let's try to artifact the x86_64 + ARM linux executables. 2024-12-24 10:31:09 -05:00
cd378b721d build: Support and test cross-compiling for linux-aarch64. 2024-12-24 10:01:14 -05:00
547d38d1ef ssb: Put the database in (ie, ~/.local/share/tildefriends/db.sqlite) by default. Unless it already exists in the working directory, so that nobody worries they've lost it. 2024-12-23 16:32:30 -05:00
dca56af5b9 build: Let's go static openssl on macos, too. 2024-12-23 14:41:31 -05:00
224442772e build: Let's try a thing. Use our own static openssl libraries built in-tree. 2024-12-23 14:22:27 -05:00
003951fdf7 ssb: Load more context for mentions. 2024-12-23 13:32:36 -05:00
d51b3da1b4 ssb: Fix channel cycling key shortcut. 2024-12-23 13:18:30 -05:00
69f4af84db ssb: Fix weird sidebar sizing. 2024-12-23 13:07:33 -05:00
771759b252 ssb: Show drafts in the sidebar. 2024-12-23 12:25:52 -05:00
20c7a71db6 ssb: Add a checkbox to reply in a new thread. Also, it's crab time. I'm sorry it took me so long. 2024-12-23 12:06:32 -05:00
8475ee0985 build: Let's start work on 0.0.27. 2024-12-23 11:23:51 -05:00
f42811d3d4 build: Let's release 0.0.26. 2024-12-23 11:12:26 -05:00
c3b1832cfb update: CodeMirror. 2024-12-23 11:08:36 -05:00
eb6753afe1 ssb: prettier. 2024-12-23 11:08:27 -05:00
5051cecb84 ssb: Make the emoji picker behave a little better, still. 2024-12-23 10:28:12 -05:00
cd03ede358 ssb: Work in progress trying to get the emoji picker in line with some of the other modal dialogs. 2024-12-22 14:45:16 -05:00
6563f8c738 ssb: Avoid unqualified emojis. I noticed the red heart was missing ever since I reprocessed the list. 2024-12-22 14:19:28 -05:00
e5279b4827 ssb: Show unread status on all message types. 2024-12-22 13:41:03 -05:00
79ff505963 ssb: Fix various channel / unread status / show new messages bugs. 2024-12-22 13:16:56 -05:00
8a67eba5fc ssb: Add a store_blob command. 2024-12-22 10:41:58 -05:00
6609a5f340 core: Length of undefined is 0. It's fine. Quiet some errors. 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. 2024-12-18 20:09:50 -05:00
28d2539432 ssb: A first pass at showing private messages next to channels. 2024-12-18 20:03:53 -05:00
f28386b71f ssb: Off by one on the unread line. 2024-12-18 12:46:02 -05:00
53717076f5 ssb: Fix some unread marker issues. 2024-12-18 12:43:25 -05:00
a9aa928629 tests: Prefer tf_printf. 2024-12-17 20:41:27 -05:00
8df121148d update: c-ares 1.34.4. 2024-12-15 08:33:38 -05:00
5e23c32ae8 build: Fix a potential null dereference? 2024-12-15 07:53:24 -05:00
9c0f6481c0 ssb: Try to go easier on the main thread, still. 2024-12-14 21:36:33 -05:00
68ae45dd58 ssb: Prevent -t=bench from stalling. 2024-12-11 20:53:25 -05:00
3091747438 ssb: prettier. 2024-12-11 20:35:32 -05:00
2f266b8dd4 ssb: Attempt to request more feeds as more contact messages come in. 2024-12-11 20:26:28 -05:00
ee20b87ee2 ssb: Alt+up/down to cycle through channels. 2024-12-11 12:53:04 -05:00
83e025d0bb update: CodeMirror. 2024-12-11 12:41:42 -05:00
5115c6e217 ssb: Fix an instance of channels being stuck unread. 2024-12-10 21:09:55 -05:00
76f6a94de5 ssb: Fix replication hops usage. Thanks @Cashew. 2024-12-10 19:18:01 -05:00
954830be18 ssb: Allow encrypting/decrypting with the server identity as an admin. 2024-12-10 12:43:07 -05:00
ea70299a45 update: sqlite 3.47.2. 2024-12-08 16:47:21 -05:00
88da071ed6 ssb: We can load more messages by author, now. 2024-12-08 09:40:02 -05:00
1dbf162a71 ssb: Bring back the updating date while loading. 2024-12-07 14:58:01 -05:00
1c0964753b ssb: Correctness around loading messages by time range. 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. 2024-12-07 11:28:33 -05:00
2230351e3e ssb: Show the load more button for mentions. 2024-12-07 10:38:34 -05:00
7da3244da2 ssb: prettier. 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. 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. 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. 2024-12-04 20:50:46 -05:00
10b15896b3 ssb: Fix the loading cancel button. 2024-12-04 20:28:57 -05:00
0e97bbe37c android: Fix some crashes, callstacks, and warnings I'm seeing in the logs. 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. 2024-12-03 19:40:08 -05:00
5d13f6aab6 wiki: Back to latest commonmark built as mjs. 2024-12-02 18:45:23 -05:00
1ebfbbe89e wiki: Go back to the last version that worked. 2024-12-02 17:57:08 -05:00
91ad43fdfc ssb: A more plausibly correct way to load new messages correctly. 2024-12-01 18:20:57 -05:00
6fe6fc180d ssb: New theme, better load, remove debug prints. 2024-12-01 16:27:59 -05:00
d84d0bec38 ssb: This index help channel status load faster. 2024-12-01 16:26:40 -05:00
7e7b1c6ee1 ssb: Make #hashtags direct to channels. 2024-12-01 15:32:35 -05:00
effb354d1b ssb: Working toward a more sensible unread indication and user interface for setting read/unread. 2024-12-01 12:56:31 -05:00
ba7d1ad35f core: This case is not a good cause to crash. 2024-12-01 09:48:15 -05:00
3ca2b19502 ssb: Canceling loads, more mobile-friendly sidebar, and respond to channel subscription changes. 2024-11-30 17:49:36 -05:00
8e0d91dcf5 security: Setting global settings requires approval. 2024-11-30 16:58:48 -05:00
cd2c2587ae ssb: Merge in the new very work in progress channels interface. 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. 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."
This reverts commit 7f4e2617ee.
2024-11-29 08:54:54 -05:00
be1b5bce4f test: Simplify my selection helper syntax a bit. 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. 2024-11-28 17:51:22 -05:00
c565b2a31f bot: Make sure release messages get through. 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. 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. 2024-11-27 21:42:54 -05:00
960a385202 update: CodeMirror. 2024-11-27 15:20:32 -05:00
21f48d3485 build: Let's start work on 0.0.26. 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. 2024-11-27 12:10:17 -05:00
af6091760c ssb+docs: prettier. 2024-11-27 12:07:00 -05:00
e1d93c003c docs: Update docs from wiki. 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. 2024-11-27 15:05:23 -05:00
7a306bb3d2 build: Fix a regex warning. 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. 2024-11-27 09:28:14 -05:00
50fef2edfa build: Fix on OpenBSD. TIL awk. 2024-11-27 09:06:02 -05:00
aa40084010 build: Redid this thing in sed to make it work on more platforms. 2024-11-26 22:55:01 -05:00
740d788c7c storage: Show accounts with the most follows, for help pruning accounts. 2024-11-26 16:25:15 -05:00
4c2fa2c1b3 storage: Show totals, too. 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. 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. 2024-11-26 11:30:57 -05:00
0da6abeb98 ssb: We can trace request names these days. 2024-11-26 11:14:30 -05:00
e4e050e8e7 ssb: Fix some message link encoding. 2024-11-26 08:42:51 -05:00
5bc082b75e build: Prepare a changelog for the next release. 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. 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. 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. 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. 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. 2024-11-23 16:49:33 -05:00
f9ec796291 bot: Give more information about new issues. 2024-11-23 13:50:23 -05:00
3beb1d0683 update: CodeMirror. 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. 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. 2024-11-20 19:24:13 -05:00
afc1524874 bot: Scrape my changes better from gitea RSS. 2024-11-18 22:58:51 -05:00
fbb975625c update: speedscope 1.21.0. 2024-11-17 19:07:27 -05:00
53e75d8209 cleanup: Consolidate countof macros. 2024-11-13 20:22:42 -05:00
5bdf970c10 ssb: Don't list broadcasts for identities to which we are already connected. 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. 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. 2024-11-12 21:47:15 -05:00
559504ae29 security: Use commonmarkjs with {safe: true} as intended. 2024-11-12 20:43:03 -05:00
9b00b41a1e ssb: Connection result preliminary hookup to ui, and fix some fallout. 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. 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. 2024-11-11 08:19:46 -05:00
44ebb841f0 bot: Fix empty buttfeed posts, and use requested RSS feed for Habitat. 2024-11-10 19:30:36 -05:00
09ae4e2096 ssb: Handle the inevitable %25 in a document hash better? 2024-11-09 18:04:58 -05:00
0b46efe4ea test: Hack around an intermitted -t=auto failure. 2024-11-09 15:09:08 -05:00
f1dda43e66 ui: Click off the identity menu to close it. 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. 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. 2024-11-09 09:01:34 -05:00
88b25790e8 ssb: Remove some pointless logging. 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. 2024-11-06 20:42:49 -05:00
5908d15f91 js: Move default global settings to C. 2024-11-06 20:29:48 -05:00
f66cfaec12 http: Fix some caching issues. 2024-11-06 12:41:54 -05:00
259f92c53b http: Populate query and headers for handler.js like we used to. 2024-11-04 21:46:38 -05:00
a84f850e91 http: Bring back handler.js support, mostly. Partly in C, this time. 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. 2024-11-02 19:22:04 -04:00
f74ca1c236 test: Remove some debug prints, whoops. 2024-11-02 16:32:05 -04:00
30e027092b test: Cover more ways to request apps and files. 2024-11-02 15:43:03 -04:00
fd4ac7c9b9 test: Test some expectes results from http requests to various paths. 2024-11-02 14:11:54 -04:00
4482049b94 log: Show the version number in the welcome banner. 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. 2024-11-01 18:55:27 -04:00
e139e952c0 ssb: Fix some spacing in the permissions dialog. 2024-11-01 18:18:16 -04:00
cf1c57ccb8 build: Let's start work on 0.0.25. 2024-11-01 18:01:10 -04:00
f7a2138488 nix: Update version to 0.0.24. 2024-10-30 19:40:12 -04:00
179 changed files with 13388 additions and 7365 deletions
.dockerignore
.gitea/workflows
.gitignore.gitmodulesDockerfileDoxyfileGNUmakefileREADME.md
apps
core
default.nix
deps
docs
flake.lockflake.nix
metadata/en-US
package-lock.jsonrun.log
src
tools

@ -1,4 +1,3 @@
.svn
db.sqlite
out/**/*.o
out/**/*.d
.git
db.sqlite*
out/

@ -6,15 +6,51 @@ jobs:
Build-All:
runs-on: ubuntu-latest
container:
valid_volumes: ['/opt/keys']
image: node:23-bookworm-slim
valid_volumes:
- '/opt/keys'
- '/opt/deps'
volumes:
- /opt/keys:/opt/keys
- /opt/deps:/opt/deps
steps:
- name: check out code
- name: Install build dependencies
run: >
apt update && apt install -y \
build-essential \
clang-19 \
cmake \
curl \
docker.io \
doxygen \
file \
gcc-aarch64-linux-gnu \
git \
graphviz \
libgpgme11 \
libssl-dev \
mingw-w64 \
rsync \
unzip \
zip \
zlib1g-dev
- name: Get code
uses: actions/checkout@v4
with:
submodules: true
- run: ln -s /opt/keys .keys
- name: Setup environment
run: |
update-alternatives --install /usr/bin/clang clang /usr/bin/clang-19 100
update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-19 100
ln -s /opt/keys .keys
ln -sf /opt/deps/ios_toolchain deps/
ln -sf /opt/deps/macos_toolchain deps/
- name: Build documentation
run: |
mkdir -p out/html/ ~/.ssh/
make docs
echo 'pildefriends ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3Kde5vDO0TrMBDK0IGGeNGe/XinWAZkSQ/rXxwUjt' >> ~/.ssh/known_hosts
rsync -avP --delete -e "ssh -i /opt/keys/ssh.ed25519" out/html/ tfdocs@pildefriends:docs/html/
- name: Setup JDK
uses: actions/setup-java@v3
with:
@ -24,15 +60,12 @@ jobs:
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
- name: Docker build
run: DOCKER_BUILDKIT=1 docker build .
- name: Build
run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist docs
- name: Upload artifacts
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
name: dist
path: dist/*

4
.gitignore vendored

@ -1,11 +1,13 @@
build/
*.core
db.*
deps/ios_toolchain/
deps/ios_toolchain
deps/macos_toolchain
deps/openssl/
dist/
.flatpak-builder
.keys
**/.DS_Store
logs/
**/node_modules
out

3
.gitmodules vendored

@ -26,3 +26,6 @@
[submodule "deps/c-ares"]
path = deps/c-ares
url = https://github.com/c-ares/c-ares.git
[submodule "deps/zsign"]
path = deps/zsign
url = https://github.com/zhlynn/zsign.git

@ -1,19 +1,16 @@
FROM bitnami/minideb:bullseye AS build
FROM bitnami/minideb:bookworm AS build
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
libc6-dev \
libssl-dev \
perl \
make
COPY . /app
RUN make -C /app -j $(nproc) release
FROM bitnami/minideb:bullseye
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libssl1.1
FROM bitnami/minideb:bookworm
COPY --from=build /app/out/release/tildefriends /app/out/release/tildefriends
COPY --from=build /app/apps /app/apps

@ -943,7 +943,7 @@ WARN_LOGFILE =
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
# Note: If this tag is empty the current directory is searched.
INPUT = src/
INPUT = README.md docs/ src/
# This tag can be used to specify the character encoding of the source files
# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
@ -1110,7 +1110,7 @@ FILTER_SOURCE_PATTERNS =
# (index.html). This can be useful if you have a project on for instance GitHub
# and want to reuse the introduction page also for the doxygen output.
USE_MDFILE_AS_MAINPAGE =
USE_MDFILE_AS_MAINPAGE = README.md
# The Fortran standard specifies that for fixed formatted Fortran code all
# characters from position 72 are to be considered as comment. A common
@ -1680,7 +1680,7 @@ DISABLE_INDEX = NO
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
GENERATE_TREEVIEW = NO
GENERATE_TREEVIEW = YES
# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the
# FULL_SIDEBAR option determines if the side bar is limited to only the treeview

@ -3,11 +3,27 @@
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 29
VERSION_NUMBER := 0.0.24
VERSION_NAME := Honey bunches of boats.
## == 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.
SQLITE_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470000.zip
VERSION_CODE := 33
VERSION_CODE_IOS := 9
VERSION_NUMBER := 0.0.28
VERSION_NAME := This program kills fascists.
IPHONEOS_VERSION_MIN=14.0
SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3490100.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
@ -20,18 +36,24 @@ UNAME_M := $(shell uname -m)
ANDROID_SDK ?= ~/Android/Sdk
BUNDLETOOL = out/bundletool.jar
HAVE_WIN := 0
HAVE_WIN :=
HAVE_CROSS_AARCH64 :=
USE_SYSTEM_SSL :=
export SOURCE_DATE_EPOCH=1
export TZ=UTC
ifeq ($(UNAME_S),Darwin)
BUILD_TYPES := macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease
BUILD_TYPES := debug release iosdebug iosrelease iossimdebug iossimrelease
else ifeq ($(UNAME_S),Linux)
BUILD_TYPES := debug release
HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1,0)
HAVE_LINUX_IOS = $(if $(shell which deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang),1,0)
HAVE_WIN = $(if $(shell which x86_64-w64-mingw32-gcc-win32),1,0)
HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1)
HAVE_LINUX_IOS = $(if $(shell which deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang),1)
HAVE_LINUX_MACOS = $(if $(shell which deps/macos_toolchain/bin/oa64-clang),1)
HAVE_WIN = $(if $(shell which x86_64-w64-mingw32-gcc-win32),1)
ifneq ($(UNAME_M),aarch64)
HAVE_CROSS_AARCH64 = $(if $(shell which aarch64-linux-gnu-gcc),1)
endif
else ifeq ($(UNAME_S),Haiku)
BUILD_TYPES := debug release
CFLAGS += -Dstatic_assert=_Static_assert
@ -39,6 +61,7 @@ LDFLAGS += \
-lbsd \
-lnetwork \
-Wno-stringop-overflow
USE_SYSTEM_SSL := 1
else ifeq ($(UNAME_S),OpenBSD)
BUILD_TYPES := debug release
CFLAGS += \
@ -46,18 +69,24 @@ CFLAGS += \
LDFLAGS += \
-lexecinfo \
-lc++abi
HAVE_ANDROID := 0
HAVE_LINUX_IOS := 0
HAVE_ANDROID :=
HAVE_LINUX_IOS :=
HAVE_LINUX_MACOS :=
USE_SYSTEM_SSL := 1
else
$(error Unexpected host platform $(UNAME_S).)
endif
# Everything is set above.
$(info Building Tilde Friends $(VERSION_NUMBER) android=$(if $(HAVE_ANDROID),1,0) win=$(if $(HAVE_WIN),1,0) cross_aarch64=$(if $(HAVE_CROSS_AARCH64),1,0) cross_ios=$(if $(HAVE_LINUX_IOS),1,0) cross_macos=$(if $(HAVE_LINUX_MACOS),1,0) system_ssl=$(if $(USE_SYSTEM_SSL),1,0))
CFLAGS += \
-std=gnu11 \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-cast-function-type-mismatch \
-Wno-unknown-warning-option \
-Wno-unused-parameter \
-MMD \
-MP \
-ffunction-sections \
@ -66,8 +95,7 @@ CFLAGS += \
-g
LDFLAGS += \
-Wno-attributes \
-Wno-aggressive-loop-optimizations \
-flto=auto
-Wno-aggressive-loop-optimizations
ANDROID_MIN_SDK_VERSION := 24
ANDROID_TARGET_SDK_VERSION := 34
@ -110,14 +138,38 @@ WINDOWS_TARGETS := \
out/winrelease/tildefriends.exe
ifeq ($(HAVE_WIN),1)
BUILD_TYPES += windebug winrelease
all: out/windebug/tildefriends.standalone.exe out/winrelease/tildefriends.standalone.exe
endif
LINUX_TARGETS := \
AARCH64_TARGETS := \
out/armdebug/tildefriends \
out/armrelease/tildefriends
ifeq ($(HAVE_CROSS_AARCH64),1)
BUILD_TYPES += armdebug armrelease
endif
HOST_TARGETS := \
out/debug/tildefriends \
out/release/tildefriends
ifeq ($(UNAME_S),Darwin)
MACOS_TARGETS := \
out/macosdebug/tildefriends \
out/macosrelease/tildefriends
out/debug/tildefriends \
out/release/tildefriends
else ifeq ($(UNAME_S),Linux)
ifeq ($(HAVE_LINUX_MACOS),1)
MACOS_TARGETS := \
out/macosdebug-arm/tildefriends \
out/macosrelease-arm/tildefriends \
out/macosdebug-x86_64/tildefriends \
out/macosrelease-x86_64/tildefriends
all: out/macosdebug/tildefriends.standalone
all: out/macosrelease/tildefriends.standalone
else
MACOS_TARGETS :=
endif
else
MACOS_TARGETS :=
endif
IOS_TARGETS := \
out/iosdebug/tildefriends \
out/iosrelease/tildefriends
@ -131,44 +183,60 @@ ifeq ($(HAVE_LINUX_IOS),1)
BUILD_TYPES += iosdebug iosrelease
all: $(IOS_APPS)
endif
ifeq ($(HAVE_LINUX_MACOS),1)
BUILD_TYPES += \
macosdebug-arm \
macosrelease-arm \
macosdebug-x86_64 \
macosrelease-x86_64
all: $(IOS_APPS)
endif
ifeq ($(UNAME_S),Darwin)
all: $(IOS_APPS) \
out/tildefriends-iossimdebug.app/tildefriends \
out/tildefriends-iossimrelease.app/tildefriends
endif
ifeq ($(HAVE_CROSS_AARCH64),1)
all: out/armrelease/tildefriends.standalone
endif
DEBUG_TARGETS := \
out/debug/tildefriends \
out/windebug/tildefriends.exe \
out/iosdebug/tildefriends \
out/iossimdebug/tildefriends \
out/macosdebug/tildefriends \
out/androiddebug/tildefriends \
out/androiddebug-armv7a/tildefriends \
out/androiddebug-x86_64/tildefriends \
out/androiddebug-x86/tildefriends
out/androiddebug-x86/tildefriends \
out/armdebug/tildefriends \
out/macosdebug-arm/tildefriends \
out/macosdebug-x86_64/tildefriends
RELEASE_TARGETS := \
out/release/tildefriends \
out/winrelease/tildefriends.exe \
out/iosrelease/tildefriends \
out/iossimrelease/tildefriends \
out/macosrelease/tildefriends \
out/androidrelease/tildefriends \
out/androidrelease-armv7a/tildefriends \
out/androidrelease-x86_64/tildefriends \
out/androidrelease-x86/tildefriends
out/androidrelease-x86/tildefriends \
out/armrelease/tildefriends \
out/macosrelease-arm/tildefriends \
out/macosrelease-x86_64/tildefriends
ALL_TARGETS = $(DEBUG_TARGETS) $(RELEASE_TARGETS)
ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(ALL_TARGETS))
NONMACOS_TARGETS := $(filter-out $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS),$(ALL_TARGETS))
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 $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += -rdynamic
$(filter-out $(WINDOWS_TARGETS),$(ALL_TARGETS)): LDFLAGS += \
-rdynamic \
-gz=zlib
$(ANDROID_TARGETS): CFLAGS += \
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
-fPIC \
@ -179,31 +247,42 @@ $(ANDROID_TARGETS): CFLAGS += \
-Wno-unknown-warning-option
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
$(DEBUG_TARGETS): LDFLAGS += -Og
$(RELEASE_TARGETS): CFLAGS += \
-DNDEBUG \
-flto
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
-DNDEBUG
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Oz
$(ANDROID_RELEASE_TARGETS): LDFLAGS += -Oz
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -Os
$(NONANDROID_RELEASE_TARGETS): LDFLAGS += -Os
$(WINDOWS_TARGETS): CC = x86_64-w64-mingw32-gcc-win32
$(WINDOWS_TARGETS): AS = $(CC)
$(WINDOWS_TARGETS): CFLAGS += \
-D_WIN32_WINNT=0x0A00 \
-DWINVER=0x0A00 \
-DNTDDI_VERSION=NTDDI_WIN10 \
-Ideps/openssl/mingw64/usr/local/include
-Iout/openssl/$(UNAME_S)/mingw64/usr/local/include
$(WINDOWS_TARGETS): LDFLAGS += \
-static \
-lm \
-Ldeps/openssl/mingw64/usr/local/lib
-Lout/openssl/$(UNAME_S)/mingw64/usr/local/lib
$(AARCH64_TARGETS): CC = aarch64-linux-gnu-gcc
$(AARCH64_TARGETS): AS = $(CC)
$(AARCH64_TARGETS): CFLAGS += -Iout/openssl/Linux/aarch64/usr/local/include
$(AARCH64_TARGETS): LDFLAGS += -Lout/openssl/Linux/aarch64/usr/local/lib
ifeq ($(UNAME_S),Darwin)
$(MACOS_TARGETS): CC = xcrun clang
$(HOST_TARGETS): CC = xcrun clang
$(IOS_TARGETS): IOS_SYSROOT := $(shell xcrun --sdk iphoneos --show-sdk-path)
$(IOS_TARGETS): CC = xcrun --sdk iphoneos clang -isysroot $(IOS_SYSROOT) -arch arm64
$(IOSSIM_TARGETS): IOSSIM_SYSROOT := $(shell xcrun --sdk iphonesimulator --show-sdk-path)
$(IOSSIM_TARGETS): CC = xcrun --sdk iphonesimulator clang -isysroot $(IOSSIM_SYSROOT) -arch x86_64
else ifeq ($(UNAME_S),Linux)
$(IOS_TARGETS): IOS_SYSROOT := deps/iPhoneOS17.0.sdk
$(IOS_TARGETS): CC = PATH=$$PATH:deps/ios_toolchain/target/bin deps/ios_toolchain/target/bin/arm-apple-darwin11-clang
$(IOS_TARGETS): CFLAGS += -isysroot deps/ios_toolchain/target/SDKs/iPhoneOS18.2.sdk -arch arm64 -DTARGET_OS_IPHONE=1
$(IOS_TARGETS): LDFLAGS += -isysroot deps/ios_toolchain/target/SDKs/iPhoneOS18.2.sdk
$(IOS_TARGETS): CC = PATH=$$PATH:deps/ios_toolchain/target/bin LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:deps/ios_toolchain/target/lib deps/ios_toolchain/target/bin/arm-apple-darwin11-clang
$(filter $(BUILD_DIR)/macosdebug-x86_64/%,$(ALL_TARGETS)): CC = PATH=deps/macos_toolchain/bin:$$PATH LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:deps/macos_toolchain/lib deps/macos_toolchain/bin/o64-clang
$(filter $(BUILD_DIR)/macosdebug-arm/%,$(ALL_TARGETS)): CC = PATH=deps/macos_toolchain/bin:$$PATH LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:deps/macos_toolchain/lib deps/macos_toolchain/bin/oa64-clang
$(filter $(BUILD_DIR)/macosrelease-x86_64/%,$(ALL_TARGETS)): CC = PATH=deps/macos_toolchain/bin:$$PATH LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:deps/macos_toolchain/lib deps/macos_toolchain/bin/o64-clang
$(filter $(BUILD_DIR)/macosrelease-arm/%,$(ALL_TARGETS)): CC = PATH=deps/macos_toolchain/bin:$$PATH LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:deps/macos_toolchain/lib deps/macos_toolchain/bin/oa64-clang
endif
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
$(ANDROID_X86_TARGETS): ANDROID_NDK_TARGET_TRIPLE := i686-linux-android
@ -214,25 +293,43 @@ $(ANDROID_TARGETS): AS = $(CC)
$(ANDROID_TARGETS): CFLAGS += \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
-Wno-unknown-warning-option
$(ANDROID_ARMV7A_TARGETS): CFLAGS += -Ideps/openssl/android/armeabi-v7a/usr/local/include
$(ANDROID_ARMV7A_TARGETS): LDFLAGS += -Ldeps/openssl/android/armeabi-v7a/usr/local/lib
$(ANDROID_ARM64_TARGETS): CFLAGS += -Ideps/openssl/android/arm64-v8a/usr/local/include
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Ldeps/openssl/android/arm64-v8a/usr/local/lib
$(ANDROID_X86_TARGETS): CFLAGS += -Ideps/openssl/android/x86/usr/local/include
$(ANDROID_ARMV7A_TARGETS): CFLAGS += -Iout/openssl/android/armeabi-v7a/usr/local/include
$(ANDROID_ARMV7A_TARGETS): LDFLAGS += -Lout/openssl/android/armeabi-v7a/usr/local/lib
$(ANDROID_ARM64_TARGETS): CFLAGS += -Iout/openssl/android/arm64-v8a/usr/local/include
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Lout/openssl/android/arm64-v8a/usr/local/lib
$(ANDROID_X86_TARGETS): CFLAGS += -Iout/openssl/android/x86/usr/local/include
$(ANDROID_X86_TARGETS): CFLAGS += -Wno-atomic-alignment
$(ANDROID_X86_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86/usr/local/lib
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
$(ANDROID_X86_TARGETS): LDFLAGS += -Lout/openssl/android/x86/usr/local/lib
$(ANDROID_X86_64_TARGETS): CFLAGS += -Iout/openssl/android/x86_64/usr/local/include
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Lout/openssl/android/x86_64/usr/local/lib
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
$(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
$(MACOS_TARGETS): LDFLAGS += -Wl,-dead_strip
$(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections -Wl,--as-needed
$(IOS_TARGETS): CFLAGS += -miphoneos-version-min=$(IPHONEOS_VERSION_MIN)
$(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=$(IPHONEOS_VERSION_MIN)
ifeq ($(UNAME_S),Darwin)
$(IOS_TARGETS): CFLAGS += -Iout/openssl/ios/ios64-xcrun/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Lout/openssl/ios/ios64-xcrun/usr/local/lib
else
$(IOS_TARGETS): CFLAGS += -Iout/openssl/$(UNAME_S)/ios64-cross/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Lout/openssl/$(UNAME_S)/ios64-cross/usr/local/lib
$(filter $(BUILD_DIR)/macosdebug-x86_64/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-x86_64/usr/local/include
$(filter $(BUILD_DIR)/macosdebug-arm/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-arm/usr/local/include
$(filter $(BUILD_DIR)/macosdebug-x86_64/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-x86_64/usr/local/lib
$(filter $(BUILD_DIR)/macosdebug-arm/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-arm/usr/local/lib
$(filter $(BUILD_DIR)/macosrelease-x86_64/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-x86_64/usr/local/include
$(filter $(BUILD_DIR)/macosrelease-arm/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-arm/usr/local/include
$(filter $(BUILD_DIR)/macosrelease-x86_64/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-x86_64/usr/local/lib
$(filter $(BUILD_DIR)/macosrelease-arm/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-arm/usr/local/lib
endif
$(IOSSIM_TARGETS): CFLAGS += -Iout/openssl/ios/iossimulator-xcrun/usr/local/include
$(IOSSIM_TARGETS): LDFLAGS += -Lout/openssl/ios/iossimulator-xcrun/usr/local/lib
$(HOST_TARGETS): CFLAGS += -Iout/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/include
$(HOST_TARGETS): LDFLAGS += -Lout/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib
ifeq ($(UNAME_M),x86_64)
ifeq ($(UNAME_S),Linux)
all: appimage
all: appimage out/release/tildefriends.standalone
endif
ifneq ($(UNAME_S),Haiku)
out/debug/tildefriends: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
@ -247,13 +344,14 @@ endif
get_objs = \
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
$(foreach build_type,windebug winrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_win))))) \
$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androiddebug-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androidrelease-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
$(foreach build_type,androiddebug androidrelease androiddebug-x86 androidrelease-x86 androiddebug-x86_64 androidrelease-x86_64 androiddebug-armv7a androidrelease-armv7a,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
$(foreach build_type,macosdebug macosrelease iosdebug iosrelease iossimdebug iossimrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_macos))))) \
$(foreach build_type,iosdebug iosrelease iossimdebug iossimrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_ios))))) \
$(foreach build_type,androiddebug-x86 androidrelease-x86,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_x86)))))
$(foreach build_type,iosdebug iosrelease iossimdebug iossimrelease macosdebug-arm macosrelease-arm macosdebug-x86_64 macosrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_macos))))) \
$(foreach build_type,androiddebug-x86 androidrelease-x86,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_x86))))) \
$(if $(findstring Darwin,$(UNAME_S)),$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_macos)))))) \
$(if $(findstring Darwin,$(UNAME_S)),,$(foreach build_type,debug release armdebug armrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))))
APP_SOURCES := $(wildcard src/*.c)
APP_SOURCES_ios := $(wildcard src/*.m)
@ -275,10 +373,12 @@ $(APP_OBJS): CFLAGS += \
-Ideps/valgrind \
-Wdouble-promotion \
-Werror
ifneq ($(UNAME_S),Darwin)
ifeq ($(UNAME_M),x86_64)
$(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/macos% $(BUILD_DIR)/ios%,$(APP_OBJS)): CFLAGS += \
$(filter-out $(BUILD_DIR)/android% $(BUILD_DIR)/ios% $(BUILD_DIR)/macos%,$(APP_OBJS)): CFLAGS += \
-fanalyzer
endif
endif
ARES_SOURCES := \
deps/c-ares/src/lib/ares_addrinfo2hostent.c \
@ -515,7 +615,6 @@ $(UV_OBJS): CFLAGS += \
-Wno-unused-result \
-Wno-unused-variable \
-Wno-nonnull
$(UV_OBJS): CFLAGS += -fno-lto
$(filter out/win%,$(UV_OBJS)): \
CFLAGS += \
-Wno-cast-function-type \
@ -712,12 +811,12 @@ $(MINIUNZIP_OBJS): CFLAGS += \
LDFLAGS += \
-pthread \
-lm
$(LINUX_TARGETS) $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
$(HOST_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS) $(AARCH64_TARGETS) $(filter-out $(HOST_TARGETS),$(MACOS_TARGETS)): LDFLAGS += \
-lssl \
-lcrypto
ifneq ($(UNAME_S),Haiku)
ifneq ($(UNAME_S),OpenBSD)
debug release $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
$(HOST_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
-ldl
endif
endif
@ -747,9 +846,16 @@ $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
-framework UIKit \
-framework WebKit
unix: debug release
win: windebug winrelease
all: $(BUILD_TYPES)
##
## Common targets:
##
all: $(BUILD_TYPES) ## Build all targets that appear possible to build on this machine.
debug: ## Build a debug executable for the current host platform.
release: ## Build a release executable for the current host platform.
armdebug: ## Cross-compile aarch64 debug on Linux.
armrelease: ## Cross-compile aarch64 release on Linux.
windebug: ## Cross-compile a debug win32 executable on Linux.
winrelease: ## Cross-compile a release win32 executable on Linux.
.PHONY: all win unix
ALL_APP_OBJS := \
@ -807,35 +913,58 @@ src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
-e 's/android:targetSdkVersion="[[:digit:]]*"/android:targetSdkVersion="$(ANDROID_TARGET_SDK_VERSION)"/' \
$@
# Android support.
src/ios/Info.plist : $(firstword $(MAKEFILE_LIST))
@echo "[ios_version] $@"
@cat $@ | \
tr '\n' '^' | \
sed -r \
-e 's@(<key>CFBundleShortVersionString</key>\^[[:space:]]*<string>)[0-9.]*(</string>)@\1$(VERSION_NUMBER:%-wip=%)\2@' \
-e 's@(<key>CFBundleVersion</key>\^[[:space:]]*<string>)[[:digit:]]+(</string>)@\1$(VERSION_CODE_IOS)\2@' \
-e 's@(<key>MinimumOSVersion</key>\^[[:space:]]*<string>)[0-9.]*(</string>)@\1$(IPHONEOS_VERSION_MIN)\2@' | \
tr '^' '\n' > \
$@.tmp && mv $@.tmp $@ || rm -f $@.tmp
##
## 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] $@"
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
out/res/drawable_%.xml.flat: src/android/res/drawable/%.xml
@mkdir -p $(dir $@)
@echo "[aapt2] $@"
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ $(<)
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat out/res/drawable_logo.xml.flat src/android/AndroidManifest.xml
@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 \
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat out/res/drawable_logo.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
out/apk/res.fdroid.apk out/gen_fdroid/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon_fdroid.xml.flat out/res/drawable_logo.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 \
@sed -e 's@drawable/icon@drawable/icon_fdroid@' src/android/AndroidManifest.xml > out/apk/AndroidManifest.fdroid.xml
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon_fdroid.xml.flat out/res/drawable_logo.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 \
--manifest out/apk/AndroidManifest.fdroid.xml \
-o out/apk/res.fdroid.apk \
--java out/gen_fdroid/
@ -852,11 +981,11 @@ out/apk/classes.dex: $(CLASS_FILES)
@$(ANDROID_BUILD_TOOLS)/d8 --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
PACKAGE_DIRS := \
apps/ \
core/ \
deps/codemirror/ \
deps/prettier/ \
deps/lit/
apps \
core \
deps/codemirror \
deps/prettier \
deps/lit
RAW_FILES := $(sort $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f -not -name '.*')))
@ -886,6 +1015,7 @@ out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGET
--manifest src/android/AndroidManifest.xml \
-R out/res/layout_activity_main.xml.flat \
-R out/res/drawable_icon.xml.flat \
-R out/res/drawable_logo.xml.flat \
--auto-add-overlay
@unzip out/aab/temporary.apk -d out/aab/staging/
@mkdir -p out/aab/staging/root/deps
@ -912,9 +1042,9 @@ out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGET
@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
@$(ANDROID_BUILD_TOOLS)/apksigner sign -ks .keys/android.jks --ks-key-alias androidKey -ks-pass pass:android --min-sdk-version=$(ANDROID_MIN_SDK_VERSION) $@
aab: out/TildeFriends.aab
aab: out/TildeFriends.aab ## Build an Android App Bundle.
.PHONY: aab
out/TildeFriends.apks: out/TildeFriends.aab $(BUNDLETOOL)
@ -981,20 +1111,32 @@ out/%.zopfli.apk: out/%.apk
$(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
release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk ## Build an Android release APK.
.PHONY: release-apk
apkgo: out/TildeFriends-arm-debug.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/.TildeFriendsActivity
.PHONY: apkgo
releaseapkgo: out/TildeFriends-arm-release.apk
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 $< $@
@ -1003,14 +1145,21 @@ out/%.app/tildefriends.png: src/ios/tildefriends.png
@cp -v $< $@
out/data.zip: $(RAW_FILES)
@echo [zip] $@
@zip -u $@ -q -9 $(RAW_FILES)
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip
out/zsign_build/zsign: $(wildcard deps/zsign/*.cpp deps/zsign/*.h deps/zsign/*.txt deps/zsign/common/*)
@+echo [cmake] $@
@cmake -B out/zsign_build deps/zsign
@cmake --build out/zsign_build -- COLOR=0 VERBOSE=0 MAKESILENT=-s
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip $(if $(HAVE_LINUX_IOS),out/zsign_build/zsign)
@mkdir -p $(dir $@)
@cp -v $< $@
@cp -v $(filter-out out/zsign%,$<) $@
@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 $@))
@mkdir -p $(realpath $(dir $@))/_CodeSignature
@out/zsign_build/zsign -q -k .keys/apple.p12 -f -m src/ios/embedded.mobileprovision $(realpath $(dir $@))
endif
.SECONDARY:
out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
@ -1031,58 +1180,120 @@ out/%/tildefriends.standalone.exe: out/%/tildefriends.exe out/data.zip
@cat $< out/data.zip > $@
@chmod +x $@
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
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
iosrelease-ipa: out/tildefriends-release.ipa
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
fetchdeps:
@echo "[fetch] sqlite"
@test -f out/deps/sqlite.zip && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p out/deps/ && curl -q $(SQLITE_URL) -o out/deps/sqlite.zip)
@test -d deps/sqlite/ && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p deps/sqlite/ && unzip -qDjo -d deps/sqlite/ out/deps/sqlite.zip)
@echo -n $(SQLITE_URL) > out/deps/sqlite.txt
@echo "[fetch] prettier"
@test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs
@test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs
@test -f deps/prettier/babel.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/babel.mjs
@test -f deps/prettier/estree.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/estree.mjs
.PHONY: fetchdeps
ANDROID_DEPS := deps/openssl/android/arm64-v8a/usr/local/lib/libssl.a
ANDROID_DEPS := out/openssl/android/arm64-v8a/usr/local/lib/libssl.a
$(ANDROID_DEPS):
+@ANDROID_NDK_ROOT=$(ANDROID_NDK) tools/ssl-android
+@export ANDROID_NDK_ROOT=$(ANDROID_NDK)
+@export BUILD_PLATFORM=android
+@export TOOLCHAIN=$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64
+@PATH="$$TOOLCHAIN/x86_64-linux-android/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=x86_64 SSL_TARGET=android-x86_64 OPTIONS="-D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
+@PATH="$$TOOLCHAIN/i686-linux-android/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=x86 SSL_TARGET=android-x86 OPTIONS="-D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
+@PATH="$$TOOLCHAIN/arm-linux-androideabi/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=armeabi-v7a SSL_TARGET=android-arm OPTIONS="--target=armv7a-linux-androideabi -Wl,--fix-cortex-a8 -D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
+@PATH="$$TOOLCHAIN/aarch64-linux-android/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=arm64-v8a SSL_TARGET=android-arm64 OPTIONS="-D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
ifeq ($(UNAME_S),Linux)
ifneq ($(USE_SYSTEM_SSL),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@tools/ssl-local
$(filter $(BUILD_DIR)/debug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/release/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_CROSS_AARCH64),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/aarch64/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@OPTIONS="--cross-compile-prefix=aarch64-linux-gnu-" BUILD_TARGET=aarch64 SSL_TARGET=linux-aarch64 tools/ssl-local
$(filter $(BUILD_DIR)/armdebug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/armrelease/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_LINUX_IOS),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/ios64-cross/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@PATH=deps/ios_toolchain/target/bin:$$PATH \
BUILD_TARGET=ios64-cross \
SSL_TARGET=ios64-cross \
CROSS_COMPILE=../../deps/ios_toolchain/target/bin/arm-apple-darwin11- \
CROSS_TOP=../../deps/ios_toolchain/target \
CROSS_SDK=iPhoneOS18.2.sdk \
CC=clang \
OPTIONS=-miphoneos-version-min=$(IPHONEOS_VERSION_MIN) \
tools/ssl-local
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_LINUX_MACOS),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/macos-arm/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@PATH=../../deps/macos_toolchain/bin:$$PATH \
BUILD_TARGET=macos-arm \
SSL_TARGET=darwin64-arm64 \
CC=../../deps/macos_toolchain/bin/oa64-clang \
RANLIB=../../deps/macos_toolchain/bin/x86_64-apple-darwin24-ranlib \
AR=../../deps/macos_toolchain/bin/arm64-apple-darwin24-ar \
tools/ssl-local
$(filter $(BUILD_DIR)/macosrelease-arm/% $(BUILD_DIR)/macosdebug-arm/%,$(APP_OBJS)): | $(LOCAL_DEPS)
LOCAL_DEPS := out/openssl/$(UNAME_S)/macos-x86_64/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@PATH=../../deps/macos_toolchain/bin:$$PATH \
BUILD_TARGET=macos-x86_64 \
SSL_TARGET=darwin64-x86_64 \
CC=../../deps/macos_toolchain/bin/o64-clang \
RANLIB=../../deps/macos_toolchain/bin/x86_64-apple-darwin24-ranlib \
AR=../../deps/macos_toolchain/bin/x86_64-apple-darwin24-ar \
tools/ssl-local
$(filter $(BUILD_DIR)/macosrelease-x86_64/% $(BUILD_DIR)/macosdebug-x86_64/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
endif
ifeq ($(UNAME_S),Darwin)
LOCAL_DEPS := out/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@tools/ssl-local
$(filter $(BUILD_DIR)/debug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/release/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_WIN),1)
WINDOWS_DEPS := deps/openssl/mingw64/usr/local/lib/libssl.a
WINDOWS_DEPS := out/openssl/$(UNAME_S)/mingw64/usr/local/lib/libssl.a
$(WINDOWS_DEPS):
+@tools/ssl-mingw64
+@BUILD_TARGET=mingw64 SSL_TARGET=mingw64 OPTIONS="--cross-compile-prefix=x86_64-w64-mingw32-" tools/ssl-local
$(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 := out/openssl/ios/ios64-xcrun/usr/local/lib/libssl.a
$(IOS_DEPS):
+@tools/ssl-ios
+@BUILD_PLATFORM=ios BUILD_TARGET=ios64-xcrun SSL_TARGET=ios64-xcrun OPTIONS="-fPIC -Wno-macro-redefined -miphoneos-version-min=$(IPHONEOS_VERSION_MIN)" tools/ssl-local
+@BUILD_PLATFORM=ios BUILD_TARGET=iossimulator-xcrun SSL_TARGET=iossimulator-xcrun OPTIONS="-fPIC -Wno-macro-redefined" tools/ssl-local
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
endif
out/macos%/tildefriends: out/macos%-arm/tildefriends out/macos%-x86_64/tildefriends
@echo [lipo] $@
@mkdir -p $(@D)
@deps/macos_toolchain/bin/lipo -create -output $@ $^
##
## Linux package targets:
##
out/tildefriends-x86_64.AppImage: out/release/tildefriends out/data.zip
@echo "[appimage] $$@"
@rm -rf out/tildefriends.AppDir
@ -1102,19 +1313,37 @@ out/tildefriends-x86_64.AppImage: out/release/tildefriends out/data.zip
@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
appimage: out/tildefriends-x86_64.AppImage ## Build an AppImage.
.PHONY: appimage
flatpak: out/
flatpak-builder --force-clean --user --install-deps-from=flathub --install --repo=out/flatpak-repo out/flatpak src/com.unprompted.tildefriends.yml
flatpak: out/ ## Build a flatpak.
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak-builder --user --disable-rofiles-fuse --install-deps-from=flathub --install --repo=out/flatpak-repo out/flatpak src/com.unprompted.tildefriends.yml
flatpak build-bundle out/flatpak-repo out/tildefriends.flatpak com.unprompted.tildefriends
.PHONY: flatpak
clean:
rm -rf $(BUILD_DIR)
.PHONY: clean
##
## Targets for release management:
##
tarball:
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 && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p out/deps/ && curl -q $(SQLITE_URL) -o out/deps/sqlite.zip)
@test -d deps/sqlite/ && test "$$(cat out/deps/sqlite.txt 2>/dev/null)" = $(SQLITE_URL) || (mkdir -p deps/sqlite/ && unzip -qDjo -d deps/sqlite/ out/deps/sqlite.zip)
@echo -n $(SQLITE_URL) > out/deps/sqlite.txt
@echo "[fetch] prettier"
@test -f deps/prettier/standalone.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/standalone.mjs
@test -f deps/prettier/html.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/html.mjs
@test -f deps/prettier/babel.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/babel.mjs
@test -f deps/prettier/estree.mjs || curl -q --create-dirs -O --output-dir deps/prettier/ https://cdn.jsdelivr.net/npm/prettier@3.2.5/plugins/estree.mjs
.PHONY: fetchdeps
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
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)
@ -1129,7 +1358,6 @@ tarball:
--exclude=deps/libsodium/test \
--exclude=deps/libuv/docs \
--exclude=deps/libuv/test \
--exclude=deps/openssl \
--exclude=deps/speedscope/*.map \
--exclude=deps/sqlite/shell.c \
--exclude=deps/zlib/contrib/vstudio \
@ -1139,7 +1367,21 @@ tarball:
tildefriends-$(VERSION_NUMBER)
.PHONY: tarball
dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefriends.standalone.exe) out/TildeFriends-release.fdroid.apk appimage tarball
dist: ## Build versions of all distributables for release.
dist: release-apk aab out/TildeFriends-release.fdroid.apk appimage tarball out/release/tildefriends.standalone
ifeq ($(HAVE_LINUX_IOS),1)
dist: iosrelease-ipa
endif
ifeq ($(HAVE_LINUX_MACOS),1)
dist: out/macosrelease/tildefriends.standalone
endif
ifeq ($(HAVE_WIN),1)
dist: out/winrelease/tildefriends.standalone.exe
endif
ifeq ($(HAVE_CROSS_AARCH64),1)
dist: out/armrelease/tildefriends.standalone
endif
dist:
@mkdir -p dist/
@echo "[cp] tildefriends-$(VERSION_NUMBER).tar.xz"
@cp out/tildefriends-$(VERSION_NUMBER).tar.xz dist/tildefriends-$(VERSION_NUMBER).tar.xz
@ -1147,8 +1389,10 @@ dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefrien
@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.zopfli.apk dist/TildeFriends-arm-$(VERSION_NUMBER).apk
@echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
@cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
@test $(HAVE_LINUX_IOS) && echo "[cp] TildeFriends-$(VERSION_NUMBER).ipa"
@test $(HAVE_LINUX_IOS) && cp out/tildefriends-release.ipa dist/TildeFriends-$(VERSION_NUMBER).ipa
@test $(HAVE_LINUX_MACOS) && echo "[cp] tildefriends-macos-$(VERSION_NUMBER)"
@test $(HAVE_LINUX_MACOS) && cp out/macosrelease/tildefriends.standalone dist/tildefriends-macos-$(VERSION_NUMBER)
@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"
@ -1157,26 +1401,70 @@ dist: release-apk iosrelease-ipa aab $(if $(HAVE_WIN), out/winrelease/tildefrien
@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
@echo "[cp] tildefriends-linux-$(UNAME_M)-$(VERSION_NUMBER)"
@cp out/release/tildefriends.standalone dist/tildefriends-linux-$(UNAME_M)-$(VERSION_NUMBER)
@test $(HAVE_CROSS_AARCH64) && echo "[cp] tildefriends-linux-aarch64-$(VERSION_NUMBER)"
@test $(HAVE_CROSS_AARCH64) && cp out/armrelease/tildefriends.standalone dist/tildefriends-linux-aarch64-$(VERSION_NUMBER)
.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:
dist-ios: iosrelease-app
rm -rfv out/Payload out/tildefriends.ipa
mkdir -p out/Payload/tildefriends.app
cp -avR out/tildefriends-iosrelease.app/* out/Payload/tildefriends.app/
cp src/ios/tildefriends.png out/Payload/tildefriends.app/
xcrun -sdk iphoneos actool --compile out/Payload/tildefriends.app/ --platform iphoneos --minimum-deployment-target $(IPHONEOS_VERSION_MIN) --app-icon AppIcon src/ios/icons/Assets.xcassets src/ios/icons/*.png --output-partial-info-plist out/actool.plist
cp src/ios/distribution.mobileprovision out/Payload/tildefriends.app/embedded.mobileprovision
xcrun -sdk iphoneos codesign -f -s 'Apple Distribution' --entitlements src/ios/Entitlements.plist --generate-entitlement-der out/Payload/tildefriends.app
cd out; zip -r tildefriends.ipa Payload; cd ..
xcrun -sdk iphoneos altool --upload-app -f out/tildefriends.ipa -t ios -u $$(cat .keys/altool-user) -p $$(cat .keys/altool-password)
##
## Targets for tidying up:
##
format: ## Standardize formatting of C source.
@clang-format -i $(wildcard src/*.c src/*.h src/*.m)
.PHONY: format
prettier:
prettier: ## Standardize formatting of JavaScript and Markdown source.
@npm run prettier
.PHONY: prettier
docs:
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%-21s%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
fdroid: out/apk/TildeFriends-release.fdroid.unsigned.apk
.PHONY: fdroid

@ -1,23 +1,18 @@
# Tilde Friends
Tilde Friends is a tool for making and sharing.
Tilde Friends participates in the Secure Scuttlebutt decentralized social
network while also functioning as a platform for making, sharing, and running
web applications.
A public instance lives at https://www.tildefriends.net/.
It is both a peer-to-peer social network client, participating in Secure
Scuttlebutt, as well as a platform for writing and running web applications.
## Goals
1. Make it easy and fun to run all sorts of web applications.
2. Provide security that is easy to understand and protects your data.
3. Make creating and sharing web applications accessible to anyone with a
browser.
1. Be the fanciest, best-maintained Secure Scuttlebutt client in town.
1. Make it easy to make, share, and run all sorts of applications while
respecting the privacy and safety of your data.
## 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
Tilde Friends uses git submodules, so either:
@ -35,29 +30,42 @@ git submodule update --init --recursive
The `.tar.xz` source releases are all-inclusive.
1. On Linux only, system OpenSSL libraries (`libssl-dev`, in debian-speak) are
assumed to be available.
2. To build, run `make debug` or `make release`. An executable will be
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.
## 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
System OpenSSL libraries are assumed to be available on Haiku and OpenSSL.
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 in the `admin` app 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>.
Docs live here: <https://docs.tildefriends.net/>.
## License

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

@ -72,7 +72,7 @@ ${description.value}</textarea
</button>
</li>
`;
} else {
} else if (description.type != 'hidden') {
return html`
<li class="w3-row">
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>

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

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

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

@ -19,10 +19,6 @@ async function fetch_info(apps) {
return result;
}
/**
*
*
*/
async function fetch_shared_apps() {
let messages = {};
@ -69,17 +65,17 @@ async function main() {
const stylesheet = `
body {
color: whitesmoke;
font-family: sans-serif;
margin: 16px;
margin: 8px;
}
.container {
.iconbox {
display: grid;
grid-template-columns: repeat(auto-fill, 64px);
gap: 1em;
justify-content: space-around;
background-color: #ffffff10;
border: 2px solid #073642;
border-radius: 8px;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
}
.iconbox::after {
content: "";
flex: auto;
}
.app {
@ -101,16 +97,28 @@ async function main() {
`;
const body = `
<h1 style="text-shadow: #808080 0 0 10px;">Welcome to Tilde Friends.</h1>
<h1>Welcome to Tilde Friends</h1>
<h2>your apps</h2>
<div id="apps" class="container"></div>
<div class="w3-card-4 w3-margin-top">
<header class="w3-container w3-light-blue">
<h2>Your Apps</h2>
</header>
<div id="apps" class="w3-indigo iconbox"></div>
</div>
<h2>shared apps</h2>
<div id="shared_apps" class="container"></div>
<div class="w3-card-4 w3-margin-top">
<header class="w3-container w3-light-blue">
<h2>Shared Apps</h2>
</header>
<div id="shared_apps" class="w3-indigo iconbox"></div>
</div>
<h2>core apps</h2>
<div id="core_apps" class="container"></div>
<div class="w3-card-4 w3-margin-top">
<header class="w3-container w3-light-blue">
<h2>Core Apps</h2>
</header>
<div id="core_apps" class="w3-indigo iconbox"></div>
</div>
`;
const script = `
@ -126,9 +134,13 @@ async function main() {
// For each app in the provided list
for (let app of Object.keys(apps).sort()) {
// Create the item
let div = list.appendChild(document.createElement('div'));
let inline = document.createElement('div');
inline.style.display = 'inline-block';
inline.classList.add('w3-button');
list.appendChild(inline);
let div = document.createElement('div');
inline.appendChild(div);
div.classList.add('app');
// The app's icon
@ -161,12 +173,13 @@ async function main() {
<!DOCTYPE html>
<html>
<head>
<link type="text/css" rel="stylesheet" href="w3.css"></link>
<style>
${stylesheet}
</style>
</head>
<body>
<body class="w3-darkgray">
${body}
</body>

235
apps/apps/w3.css Normal file

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

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

@ -52,8 +52,8 @@ export async function get_blog_message(id) {
}
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;

@ -1,4 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "➡️"
"emoji": "➡️",
"previous": "&YDDSzbRD8NFAykYlZnk4r4hAK5qXjT5LmKE6rhS1s+A=.sha256"
}

@ -14,7 +14,7 @@ async function contacts_internal(id, last_row_id, following, max_row_id) {
result.blocking = result.blocking || {};
let contacts = await query(
`
SELECT content FROM messages
SELECT json(content) AS content FROM messages
WHERE author = ? AND
rowid > ? AND
rowid <= ? AND
@ -189,50 +189,6 @@ async function fetch_about(db, ids, users) {
return Object.assign({}, users);
}
async function getAbout(db, id) {
if (g_about_cache[id]) {
return g_about_cache[id];
}
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',
[id, f.sequence],
function (row) {
f.sequence = row.sequence;
if (row.content) {
let about = {};
try {
about = JSON.parse(row.content);
} 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);
}
g_about_cache[id] = f.about;
return f.about;
}
async function getSize(db, id) {
let size = 0;
await ssb.sqlAsync(

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

@ -1,5 +1,11 @@
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' &&
@ -61,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);

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

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

@ -1,7 +1,9 @@
async function main() {
let host = core.url.match(/.*\/\/(.*?)\//)[1];
print(core.url);
let host = core.url.match(/.*?\/\/([^:/]*)/)[1];
let port = await ssb.port();
let id = (await ssb.getServerIdentity()).substring(1);
let room = `net:${host}:${ssb.port}~shs:${id}:SSB+Room+SK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
let room = `net:${host}:${port}~shs:${id}:SSB+Room+SK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
await app.setDocument(`
<body style="color: #fff">
<h1>Server</h1>

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🐌",
"previous": "&IH3DadMNF785idnMI/LuCpIJQxzvpg1PDp8BI7m1Nx0=.sha256"
"emoji": "🦀",
"previous": "&jAAzd36Nmpw0sRA1Dx9wLiIwGX+q//+S/Han+RLlEOw=.sha256"
}

@ -21,9 +21,6 @@ tfrpc.register(async function createIdentity() {
tfrpc.register(async function getServerIdentity() {
return ssb.getServerIdentity();
});
tfrpc.register(async function setServerFollowingMe(id, following) {
return ssb.setServerFollowingMe(id, following);
});
tfrpc.register(async function getIdentities() {
return ssb.getIdentities();
});
@ -106,6 +103,10 @@ tfrpc.register(async function getActiveIdentity() {
tfrpc.register(async function sync() {
return await ssb.sync();
});
tfrpc.register(async function url() {
return core.url;
});
core.register('onBroadcastsChanged', async function () {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
});

@ -7,7 +7,7 @@ function textNode(text) {
function linkNode(text, link) {
const linkNode = new commonmark.Node('link', undefined);
if (link.startsWith('#')) {
linkNode.destination = `#q=${encodeURIComponent(link)}`;
linkNode.destination = `#${encodeURIComponent(link)}`;
} else {
linkNode.destination = link;
}

@ -37,10 +37,12 @@ export async function picker(callback, anchor, author) {
div.style.color = '#000';
div.style.background = '#fff';
div.style.border = '1px solid #000';
div.style.display = 'block';
div.style.display = 'flex';
div.style.overflow = 'scroll';
div.style.fontWeight = 'bold';
div.style.fontSize = 'xx-large';
div.style.flex = '1 1';
div.style.flexDirection = 'column';
let input = document.createElement('input');
input.type = 'text';
input.style.display = 'block';
@ -50,6 +52,7 @@ export async function picker(callback, anchor, author) {
input.style.position = 'relative';
div.appendChild(input);
let list = document.createElement('div');
list.style.overflow = 'scroll';
div.appendChild(list);
div.addEventListener('mousedown', function (event) {
event.stopPropagation();
@ -142,21 +145,40 @@ export async function picker(callback, anchor, author) {
}
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);
}
let modal = html`
<style>
${styles}
</style>
<div
class="w3-modal"
style="display: block; box-sizing: border-box; z-index: 10"
>
<div class="w3-modal-content w3-card-4">
<div
class="w3-content w3-theme-d1"
style="display: flex; flex-direction: column; max-height: 80vh"
>
<header class="w3-container" style="flex: 0 0">
<h1>Choose a Reaction</h1>
<span class="w3-button w3-display-topright" @click=${cleanup}
>&times;</span
>
</header>
${div}
<footer class="w3-container w3-padding" style="flex: 0 0">
<button class="w3-button" @click=${cleanup}>Close</button>
</footer>
</div>
</div>
</div>
`;
document.body.appendChild(parent);
render(modal, parent);
input.focus();
document.body.addEventListener('mousedown', cleanup);

File diff suppressed because one or more lines are too long

@ -8,10 +8,16 @@ 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_reactions_modal from './tf-reactions-modal.js';
import * as tf_tab_mentions from './tf-tab-mentions.js';
import * as tf_tab_news from './tf-tab-news.js';
import * as tf_tab_news_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_styles from './tf-styles.js';
window.addEventListener('load', function () {
let style = document.createElement('style');
style.innerText = tf_styles.styles;
document.body.appendChild(style);
});

@ -7,7 +7,6 @@ class TfElement extends LitElement {
return {
whoami: {type: String},
hash: {type: String},
unread: {type: Array},
tab: {type: String},
broadcasts: {type: Array},
connections: {type: Array},
@ -16,7 +15,12 @@ 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},
guest: {type: Boolean},
url: {type: String},
private_messages: {type: Array},
};
}
@ -26,14 +30,17 @@ class TfElement extends LitElement {
super();
let self = this;
this.hash = '#';
this.unread = [];
this.tab = 'news';
this.broadcasts = [];
this.connections = [];
this.following = [];
this.users = {};
this.loaded = false;
this.tags = [];
this.channels = [];
this.channels_unread = {};
this.channels_latest = {};
this.loading_latest = 0;
this.loading_latest_scheduled = 0;
tfrpc.rpc.getBroadcasts().then((b) => {
self.broadcasts = b || [];
});
@ -62,18 +69,78 @@ class TfElement extends LitElement {
async initial_load() {
let whoami = await tfrpc.rpc.getActiveIdentity();
let ids = (await tfrpc.rpc.getIdentities()) || [];
this.url = await tfrpc.rpc.url();
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
this.guest = !this.whoami?.length;
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 = ['', '@', '🔐', ...this.channels.map((x) => '#' + x)];
let index = channel_names.indexOf(this.hash.substring(1));
index = index != -1 ? index + delta : 0;
tfrpc.rpc.setHash(
'#' +
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 {
@ -81,9 +148,11 @@ class TfElement extends LitElement {
}
}
async fetch_about(ids, users) {
async fetch_about(following, users) {
let ids = Object.keys(following).sort();
const k_cache_version = 1;
let cache = await tfrpc.rpc.databaseGet('about');
let original_cache = cache;
cache = cache ? JSON.parse(cache) : {};
if (cache.version !== k_cache_version) {
cache = {
@ -95,8 +164,8 @@ class TfElement extends LitElement {
let max_row_id = (
await tfrpc.rpc.query(
`
SELECT MAX(rowid) AS max_row_id FROM messages
`,
SELECT MAX(rowid) AS max_row_id FROM messages
`,
[]
)
)[0].max_row_id;
@ -109,7 +178,7 @@ class TfElement extends LitElement {
let abouts = await tfrpc.rpc.query(
`
SELECT
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
messages.author, json(messages.content) AS content, messages.sequence
FROM
messages,
json_each(?1) AS following
@ -120,7 +189,7 @@ class TfElement extends LitElement {
json_extract(messages.content, '$.type') = 'about'
UNION
SELECT
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
messages.author, json(messages.content) AS content, messages.sequence
FROM
messages,
json_each(?2) AS following
@ -149,10 +218,20 @@ class TfElement extends LitElement {
}
}
cache.last_row_id = max_row_id;
await tfrpc.rpc.databaseSet('about', JSON.stringify(cache));
let new_cache = JSON.stringify(cache);
if (new_cache !== original_cache) {
let start_time = new Date();
tfrpc.rpc.databaseSet('about', new_cache).then(function () {
console.log('saving about took', (new Date() - start_time) / 1000);
});
}
users = users || {};
for (let id of Object.keys(cache.about)) {
users[id] = Object.assign(users[id] || {}, cache.about[id]);
users[id] = Object.assign(
{follow_depth: following[id]?.d},
users[id] || {},
cache.about[id]
);
}
return Object.assign({}, users);
}
@ -167,10 +246,15 @@ class TfElement extends LitElement {
`,
[JSON.stringify(this.following), id]
);
if (messages && messages.length) {
this.unread = [...this.unread, ...messages];
this.unread = this.unread.slice(this.unread.length - 1024);
for (let message of messages) {
if (
message.author == this.whoami &&
JSON.parse(message.content)?.type == 'channel'
) {
this.load_channels();
}
}
this.schedule_load_latest();
}
async _handle_whoami_changed(event) {
@ -185,70 +269,234 @@ class TfElement extends LitElement {
}
}
async create_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) {
this.whoami = this.ids[0];
async get_latest_private(following) {
const k_version = 1;
// { "version": 1, "range": [1234, 5678], messages: [ "%1.sha256", "%2.sha256", ... ], latest: rowid }
let cache = JSON.parse(
(await tfrpc.rpc.databaseGet(`private:${this.whoami}`)) ?? '{}'
);
if (cache.version !== k_version) {
cache = {
version: k_version,
messages: [],
range: [],
};
}
let latest = (
await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages')
)[0].latest;
let ranges = [];
const k_chunk_size = 512;
if (cache.range.length) {
for (let i = cache.range[1]; i < latest; i += k_chunk_size) {
ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
}
for (let i = cache.range[0]; i >= 0; i -= k_chunk_size) {
ranges.push([
Math.max(i - k_chunk_size, 0),
Math.min(cache.range[0], i + k_chunk_size),
false,
]);
}
} else {
for (let i = 0; i < latest; i += k_chunk_size) {
ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
}
}
for (let range of ranges) {
let messages = await tfrpc.rpc.query(
`
SELECT messages.rowid, messages.id, json(content) AS content
FROM messages
WHERE
messages.rowid > ?1 AND
messages.rowid <= ?2 AND
json(messages.content) LIKE '"%'
ORDER BY sequence DESC
`,
[range[0], range[1]]
);
messages = (await this.decrypt(messages)).filter((x) => x.decrypted);
if (messages.length) {
cache.latest = Math.max(
cache.latest ?? 0,
...messages.map((x) => x.rowid)
);
if (range[2]) {
cache.messages = [...cache.messages, ...messages.map((x) => x.id)];
} else {
cache.messages = [...messages.map((x) => x.id), ...cache.messages];
}
}
cache.range[0] = Math.min(cache.range[0] ?? range[0], range[0]);
cache.range[1] = Math.max(cache.range[1] ?? range[1], range[1]);
await tfrpc.rpc.databaseSet(
`private:${this.whoami}`,
JSON.stringify(cache)
);
}
return [cache.latest, cache.messages];
}
async load_channels_latest(following) {
let start_time = new Date();
let latest_private = this.get_latest_private(following);
let channels = await 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 AND
messages.author != ?4
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
WHERE
messages.content ->> 'type' = 'post' AND
messages.content ->> 'root' IS NULL AND
messages.author != ?4
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
WHERE messages.author != ?4
`,
[
JSON.stringify(this.channels),
JSON.stringify(following),
'"' + this.whoami.replace('"', '""') + '"',
this.whoami,
]
);
this.channels_latest = Object.fromEntries(
channels.map((x) => [x.channel, x.rowid])
);
console.log('channels took', (new Date() - start_time) / 1000.0);
let self = this;
start_time = new Date();
latest_private.then(function (latest) {
self.channels_latest = Object.assign({}, self.channels_latest, {
'🔐': latest[0],
});
console.log('private took', (new Date() - start_time) / 1000.0);
console.log(latest);
self.private_messages = latest[1];
});
}
_schedule_load_latest_timer() {
--this.loading_latest_scheduled;
this.schedule_load_latest();
}
schedule_load_latest() {
if (!this.loading_latest) {
this.shadowRoot.getElementById('tf-tab-news')?.load_latest();
this.load();
} else if (!this.loading_latest_scheduled) {
this.loading_latest_scheduled++;
setTimeout(this._schedule_load_latest_timer.bind(this), 5000);
}
}
async load_recent_tags() {
let start = new Date();
this.tags = await tfrpc.rpc.query(
async fetch_user_info(users) {
let info = await tfrpc.rpc.query(
`
WITH
recent AS (SELECT id, json(content) AS content FROM messages
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
ORDER BY timestamp DESC LIMIT 1024),
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
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]
SELECT messages_stats.author, messages_stats.max_sequence, messages_stats.max_timestamp AS max_ts FROM messages_stats
JOIN json_each(?) AS following
ON messages_stats.author = following.value
`,
[JSON.stringify(Object.keys(users))]
);
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
for (let row of info) {
users[row.author].seq = row.max_seq;
users[row.author].ts = row.max_ts;
}
return users;
}
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 = [];
for (let [id, v] of Object.entries(following)) {
users[id] = {
following: v.of,
blocking: v.ob,
followed: v.if,
blocked: v.ib,
};
by_count.push({count: v.of, id: id});
this.loading_latest = true;
try {
let start_time = new Date();
let whoami = this.whoami;
let following = await tfrpc.rpc.following([whoami], 2);
let users = {};
let by_count = [];
for (let [id, v] of Object.entries(following)) {
users[id] = {
following: v.of,
blocking: v.ob,
followed: v.if,
blocked: v.ib,
};
by_count.push({count: v.of, id: id});
}
this.load_channels_latest(Object.keys(following));
this.channels_unread = JSON.parse(
(await tfrpc.rpc.databaseGet('unread')) ?? '{}'
);
this.following = Object.keys(following);
let about_start_time = new Date();
users = await this.fetch_about(following, users);
console.log(
'about took',
(new Date() - about_start_time) / 1000.0,
'seconds for',
Object.keys(users).length,
'users'
);
start_time = new Date();
users = await this.fetch_user_info(users);
console.log(
'user info took',
(new Date() - start_time) / 1000.0,
'seconds'
);
this.users = users;
console.log(
`load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}`
);
this.whoami = whoami;
this.loaded = whoami;
} finally {
this.loading_latest = false;
}
console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
let start_time = new Date();
users = await this.fetch_about(Object.keys(following).sort(), users);
console.log(
'about took',
(new Date() - start_time) / 1000.0,
'seconds for',
Object.keys(users).length,
'users'
}
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;
})
);
this.following = Object.keys(following);
this.users = users;
await tags;
console.log(`load finished ${whoami} => ${this.whoami}`);
this.whoami = whoami;
this.loaded = whoami;
}
render_tab() {
@ -262,9 +510,13 @@ class TfElement extends LitElement {
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}
.connections=${this.connections}
.private_messages=${this.private_messages}
></tf-tab-news>
`;
} else if (this.tab === 'connections') {
@ -275,14 +527,6 @@ class TfElement extends LitElement {
.broadcasts=${this.broadcasts}
></tf-tab-connections>
`;
} else if (this.tab === 'mentions') {
return html`
<tf-tab-mentions
.following=${this.following}
whoami=${this.whoami}
.users="${this.users}}"
></tf-tab-mentions>
`;
} else if (this.tab === 'search') {
return html`
<tf-tab-search
@ -314,13 +558,15 @@ 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;
@ -334,13 +580,23 @@ class TfElement extends LitElement {
const k_tabs = {
'📰': 'news',
'📡': 'connections',
'@': 'mentions',
'🔍': 'search',
'👩‍💻': 'query',
};
let tabs = html`
<div class="w3-bar w3-theme-l1">
<div
class="w3-bar w3-theme-l1"
style="position: static; top: 0; z-index: 10"
>
<button
class=${'w3-bar-item w3-button w3-circle w3-ripple' +
(this.connections?.some((x) => x.flags.one_shot) ? ' w3-spin' : '')}
style="width: 1.5em; height: 1.5em; padding: 8px"
@click=${this.refresh}
>
</button>
${Object.entries(k_tabs).map(
([k, v]) => html`
<button
@ -359,26 +615,34 @@ class TfElement extends LitElement {
)}
</div>
`;
let contents = !this.loaded
? this.loading
? html`<div
class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge"
let contents = this.guest
? html`<div
class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge w3-container"
>
<p>⚠️🦀 Must be logged in to Tilde Friends to scuttle here. 🦀⚠️</p>
<footer class="w3-center">
<a
class="w3-button w3-theme-d1"
href=${`/login?return=${encodeURIComponent(this.url)}`}
>Login</a
>
Loading...
</div>
${this.render_tab()}`
: html`<div>Select or create an identity.</div>`
: this.render_tab();
</footer>
</div>`
: !this.loaded || this.loading
? html`<div
class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge"
>
<span class="w3-spin" style="display: inline-block">🦀</span>
Loading...
</div>`
: this.render_tab();
return html`
<div
style="width: 100vw; min-height: 100vh; height: 100%"
style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
class="w3-theme-dark"
>
${tabs}
<div style="padding: 8px">
${this.tags.map(
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
)}
<div style="flex: 0 0">${tabs}</div>
<div style="flex: 1 1; overflow: auto; contain: layout">
${contents}
</div>
</div>

@ -14,6 +14,8 @@ class TfComposeElement extends LitElement {
apps: {type: Object},
drafts: {type: Object},
author: {type: String},
channel: {type: String},
new_thread: {type: Boolean},
};
}
@ -27,6 +29,7 @@ class TfComposeElement extends LitElement {
this.apps = undefined;
this.drafts = {};
this.author = undefined;
this.new_thread = false;
}
process_text(text) {
@ -196,11 +199,26 @@ class TfComposeElement extends LitElement {
let message = {
type: 'post',
text: edit.innerText,
channel: this.channel,
};
if (this.root || this.branch) {
message.root = this.root;
message.root = this.new_thread ? (this.branch ?? this.root) : this.root;
message.branch = this.branch;
}
let reply = Object.fromEntries(
(
await tfrpc.rpc.query(
`
SELECT messages.id, messages.author FROM messages
JOIN json_each(?) AS refs ON messages.id = refs.value
`,
[JSON.stringify([this.root, this.branch])]
)
).map((row) => [row.id, row.author])
);
if (Object.keys(reply).length) {
message.reply = reply;
}
if (Object.values(draft.mentions || {}).length) {
message.mentions = Object.values(draft.mentions);
}
@ -467,6 +485,20 @@ class TfComposeElement extends LitElement {
}
}
render_new_thread() {
let self = this;
if (
this.root !== undefined &&
this.branch !== undefined &&
this.root != this.branch
) {
return html`
<input type="checkbox" class="w3-check w3-theme-d1" id="new_thread" @change=${() => (self.new_thread = !self.new_thread)} ?checked=${self.new_thread}></input>
<label for="new_thread">New Thread</label>
`;
}
}
get_draft() {
return this.drafts[this.branch || ''] || {};
}
@ -531,11 +563,24 @@ class TfComposeElement extends LitElement {
🔐
</button>`;
let result = html`
<style>
.w3-input:empty::before {
content: attr(placeholder);
}
.w3-input:empty:focus::before {
content: '';
}
</style>
<div
class="w3-card-4 w3-theme-d4 w3-padding-small"
class="w3-card-4 w3-theme-d4 w3-padding w3-margin-top w3-margin-bottom"
style="box-sizing: border-box"
>
${this.render_encrypt()}
<header class="w3-container">
${this.channel !== undefined
? html`<p>To #${this.channel}:</p>`
: undefined}
${this.render_encrypt()}
</header>
<div class="w3-container w3-padding-small">
<div class="w3-half">
<span
@ -549,25 +594,32 @@ class TfComposeElement extends LitElement {
.innerText=${live(draft.text ?? '')}
></span>
</div>
<div class="w3-half w3-padding">
<div class="w3-half w3-container">
${content_warning}
<div id="preview"></div>
<p id="preview"></p>
</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-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>
<footer class="w3-container">
${this.render_attach_app()} ${this.render_content_warning()}
${this.render_new_thread()}
<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>
</footer>
</div>
`;
return result;

@ -1,4 +1,4 @@
import {LitElement, html, render, unsafeHTML} from './lit-all.min.js';
import {LitElement, html, repeat, 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,6 +30,7 @@ class TfMessageElement extends LitElement {
this.drafts = {};
this.format = 'message';
this.expanded = {};
this.channel_unread = -1;
}
show_reply() {
@ -94,6 +97,13 @@ class TfMessageElement extends LitElement {
}
}
render_json(value) {
let json = JSON.stringify(value, null, 2);
return html`
<pre style="white-space: pre-wrap; overflow-wrap: anywhere">${json}</pre>
`;
}
render_raw() {
let raw = {
id: this.message?.id,
@ -105,36 +115,24 @@ 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 this.render_json(raw);
}
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, {
type: 'vote',
vote: {
link: message,
value: 1,
expression: reaction,
},
})
.catch(function (error) {
alert(error?.message);
});
}
tfrpc.rpc
.appendMessage(this.whoami, {
type: 'vote',
vote: {
link: message,
value: 1,
expression: reaction,
},
})
.catch(function (error) {
alert(error?.message);
});
}
react(event) {
@ -187,7 +185,7 @@ class TfMessageElement extends LitElement {
render_mention(mention) {
if (!mention?.link || typeof mention.link != 'string') {
return html` <pre>${JSON.stringify(mention)}</pre>`;
return this.render_json(mention);
} else if (
mention?.link?.startsWith('&') &&
mention?.type?.startsWith('image/')
@ -228,7 +226,7 @@ class TfMessageElement extends LitElement {
>${mention.name}</a
>`;
} else if (mention.link?.startsWith('#')) {
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}
return html` <a href=${'#' + encodeURIComponent('#' + mention.link)}
>${mention.link}</a
>`;
} else if (
@ -238,16 +236,17 @@ class TfMessageElement extends LitElement {
) {
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 this.render_json(mention);
}
}
render_mentions() {
let mentions = this.message?.content?.mentions || [];
mentions = mentions.filter(
(x) => this.message?.content?.text?.indexOf(x.link) === -1
(x) =>
this.message?.content?.text?.indexOf(
typeof x === 'string' ? x : x.link
) === -1
);
if (mentions.length) {
let self = this;
@ -304,7 +303,9 @@ ${JSON.stringify(mention, null, 2)}</pre
@click=${() => self.set_expanded(false)}
>
Collapse</button
>${(this.message.child_messages || []).map(
>${repeat(
this.message.child_messages || [],
(x) => x.id,
(x) =>
html`<tf-message
.message=${x}
@ -312,12 +313,29 @@ ${JSON.stringify(mention, null, 2)}</pre
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>`
)}`;
}
} else {
return undefined;
}
}
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') {
@ -337,29 +355,38 @@ ${JSON.stringify(mention, null, 2)}</pre
return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
}
render() {
class_background() {
return this.message?.decrypted
? 'w3-pale-red'
: this.message?.rowid >= this.channel_unread
? 'w3-theme-d2'
: 'w3-theme-d4';
}
get_content() {
let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
}
let class_background = this.message?.decrypted
? 'w3-pale-red'
: 'w3-theme-d4';
let self = this;
return content;
}
render_raw_button() {
let content = this.get_content();
let raw_button;
switch (this.format) {
case 'raw':
if (content?.type == 'post' || content?.type == 'blog') {
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'md')}
@click=${() => (this.format = 'md')}
>
Markdown
</button>`;
} else {
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'message')}
@click=${() => (this.format = 'message')}
>
Message
</button>`;
@ -368,7 +395,7 @@ ${JSON.stringify(mention, null, 2)}</pre
case 'md':
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'message')}
@click=${() => (this.format = 'message')}
>
Message
</button>`;
@ -376,7 +403,7 @@ ${JSON.stringify(mention, null, 2)}</pre
case 'decrypted':
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'raw')}
@click=${() => (this.format = 'raw')}
>
Raw
</button>`;
@ -385,55 +412,136 @@ ${JSON.stringify(mention, null, 2)}</pre
if (this.message.decrypted) {
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'decrypted')}
@click=${() => (this.format = 'decrypted')}
>
Decrypted
</button>`;
} else {
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'raw')}
@click=${() => (this.format = 'raw')}
>
Raw
</button>`;
}
break;
}
function small_frame(inner) {
let body;
return html`
<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
return raw_button;
}
render_header() {
let is_encrypted = this.message?.decrypted
? html`<span class="w3-bar-item">🔓</span>`
: typeof this.message?.content == 'string'
? html`<span class="w3-bar-item">🔒</span>`
: undefined;
return html`
<header class="w3-bar">
<span class="w3-bar-item">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
</span>
${is_encrypted}
<span class="w3-bar-item w3-right">${this.render_raw_button()}</span>
<span class="w3-bar-item w3-right" style="text-wrap: nowrap"
><a target="_top" href=${'#' + encodeURIComponent(this.message.id)}
>%</a
>
${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}
></tf-message>
`
)}
</div>
`;
}
if (this.message?.type === 'contact_group') {
return html` <div
class="w3-card-4 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
${new Date(this.message.timestamp).toLocaleString()}</span
>
</header>
`;
}
render_frame(inner) {
return html`
<style>
code {
white-space: pre-wrap;
overflow-wrap: break-word;
}
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4 ${this.class_background()} w3-border-theme w3-margin-top"
style="overflow: auto; overflow-wrap: anywhere; display: block; max-width: 100%"
>
${this.message.messages.map(
${inner}
</div>
`;
}
render_small_frame(inner) {
let self = this;
return this.render_frame(html`
${self.render_header()}
${self.format == 'raw'
? html`<div class="w3-container">${self.render_raw()}</div>`
: 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>
`
)}
`);
}
render_actions() {
let content = this.get_content();
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`
<div class="w3-section w3-container">
${reply}
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
${this.render_children()}
</div>
`;
}
render() {
let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
}
let class_background = this.class_background();
let self = this;
if (this.message?.type === 'contact_group') {
return this.render_frame(
html` ${this.message.messages.map(
(x) =>
html`<tf-message
.message=${x}
@ -441,30 +549,37 @@ ${JSON.stringify(mention, null, 2)}</pre
.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 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
>
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
(placeholder)
<div>${this.render_votes()}</div>
${(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 this.render_frame(
html`<div class="w3-padding">
<p>
<a target="_top" href=${'#' + encodeURIComponent(this.message.id)}
>${this.message.id}</a
>
(placeholder)
</p>
<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;
@ -491,10 +606,14 @@ ${JSON.stringify(mention, null, 2)}</pre
Updated profile for
<tf-user id=${content.about} .users=${this.users}></tf-user>.
</div>`;
return small_frame(html` ${update} ${name} ${image} ${description} `);
return this.render_small_frame(html`
<div class="w3-container">
<p>${update} ${name} ${image} ${description}</p>
</div>
`);
} else if (content.type == 'contact') {
return html`
<div>
<div class="w3-padding">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
is
${content.blocking === true
@ -513,24 +632,6 @@ ${JSON.stringify(mention, null, 2)}</pre
</div>
`;
} else if (content.type == 'post') {
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) {
@ -547,11 +648,7 @@ ${JSON.stringify(mention, null, 2)}</pre
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 = this.render_json(content);
break;
}
let content_warning = html`
@ -573,90 +670,22 @@ ${JSON.stringify(content, null, 2)}</pre
? 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 {
white-space: pre-wrap;
overflow-wrap: break-word;
}
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4 ${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>${raw_button}</span>
</div>
${payload} ${this.render_votes()}
<p>
${reply}
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
</p>
${this.render_children()}
</div>
`;
return this.render_frame(html`
${this.render_header()}
<div class="w3-container">${payload}</div>
${this.render_votes()} ${this.render_actions()}
</div>
`);
} else if (content.type === 'issue') {
let is_encrypted = this.message?.decrypted
? html`<span style="align-self: center">🔓</span>`
: undefined;
return html`
<style>
code {
white-space: pre-wrap;
overflow-wrap: break-word;
}
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4 ${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>${raw_button}</span>
</div>
${content.text} ${this.render_votes()}
<p>
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
</p>
return this.render_frame(html`
${this.render_header()} ${content.text} ${this.render_votes()}
<footer class="w3-container">
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
${this.render_children()}
</div>
`;
</footer>
`);
} else if (content.type === 'blog') {
let self = this;
tfrpc.rpc.get_blob(content.blog).then(function (data) {
@ -692,72 +721,20 @@ ${JSON.stringify(content, null, 2)}</pre
`;
break;
}
let reply =
this.drafts[this.message?.id] !== undefined
? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
author=${this.message.author}
></tf-compose>
`
: html`
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
Reply
</button>
`;
return html`
<style>
code {
white-space: pre-wrap;
overflow-wrap: break-word;
}
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4 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>${raw_button}</span>
</div>
<div>${body}</div>
${this.render_mentions()}
<div>
${reply}
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
</div>
${this.render_votes()} ${this.render_children()}
</div>
`;
return this.render_frame(html`
${this.render_header()}
<div>${body}</div>
${this.render_mentions()} ${this.render_votes()}
${this.render_actions()}
`);
} else if (content.type === 'pub') {
return small_frame(
return this.render_small_frame(
html` <style>
span {
overflow-wrap: anywhere;
}
</style>
<span>
<div class="w3-padding">
<div>
🍻
<tf-user
@ -766,38 +743,47 @@ ${JSON.stringify(content, null, 2)}</pre
></tf-user>
</div>
<pre>${content.address.host}:${content.address.port}</pre>
</span>`
</div>`
);
} 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
>
return this.render_small_frame(html`
<div class="w3-container">
<p>
${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
<a href=${'#' + encodeURIComponent('#' + content.channel)}
>#${content.channel}</a
>
</p>
</div>
`);
} 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 this.render_small_frame(
html`<span class="w3-container">🔓</span> ${this.render_json(
this.message.decrypted
)}`
);
} else {
return small_frame(
html`<span>🔓</span>
<div>${this.message.decrypted.type}</div>`
return this.render_small_frame(
html`<span class="w3-container">🔓</span>
<div class="w3-container">${this.message.decrypted.type}</div>`
);
}
} else {
return small_frame(html`<span>🔒</span>`);
return this.render_small_frame();
}
} else {
return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
return this.render_small_frame(
html`<div class="w3-container">
<p><b>type</b>: ${content.type}</p>
</div>`
);
}
} else if (typeof this.message.content == 'string') {
return this.render_small_frame();
} else {
return small_frame(this.render_raw());
return this.render_small_frame(this.render_raw());
}
}
}

@ -1,4 +1,4 @@
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
import {LitElement, html, unsafeHTML, repeat, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
@ -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 = [];
}
@ -62,14 +66,14 @@ class TfNewsElement extends LitElement {
} else if (message.content.type == 'post') {
if (message.content.root) {
if (typeof message.content.root === 'string') {
let m = ensure_message(message.content.root);
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 = [];
}
@ -162,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,
});
@ -170,6 +175,13 @@ 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;
}
@ -178,18 +190,40 @@ class TfNewsElement extends LitElement {
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
<div>
${repeat(
final_messages,
(x) => x.id,
(x) => html`
<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
collapsed="true"
></tf-message>`
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>
${x.rowid == unread_rowid
? 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>
`;

@ -11,7 +11,6 @@ class TfProfileElement extends LitElement {
id: {type: String},
users: {type: Object},
size: {type: Number},
server_follows_me: {type: Boolean},
following: {type: Boolean},
blocking: {type: Boolean},
};
@ -27,7 +26,6 @@ class TfProfileElement extends LitElement {
this.id = null;
this.users = {};
this.size = 0;
this.server_follows_me = undefined;
}
async load() {
@ -63,27 +61,8 @@ 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(
`
SELECT json_extract(content, '$.following') AS following
FROM messages
WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
`,
[server_id, this.whoami]
);
let is_followed = false;
for (let row of followed) {
is_followed = row.following != 0;
}
this.server_follows_me = is_followed;
}
modify(change) {
let self = this;
tfrpc.rpc
.appendMessage(
this.whoami,
@ -95,6 +74,10 @@ class TfProfileElement extends LitElement {
change
)
)
.then(function () {
self._follow_whoami = undefined;
self.load();
})
.catch(function (error) {
alert(error?.message);
});
@ -175,31 +158,11 @@ class TfProfileElement extends LitElement {
input.click();
}
async server_follow_me(follow) {
try {
await tfrpc.rpc.setServerFollowingMe(this.whoami, follow);
} catch (e) {
console.log(e);
}
try {
await this.initial_load();
} catch (e) {
console.log(e);
}
}
copy_id() {
navigator.clipboard.writeText(this.id);
}
render() {
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] || {};
@ -216,33 +179,24 @@ class TfProfileElement extends LitElement {
let block;
if (this.id === this.whoami) {
if (this.editing) {
let server_follow;
if (this.server_follows_me === true) {
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-theme-d1"
@click=${() => this.server_follow_me(true)}
>
Server, Follow Me
</button>`;
}
edit = html`
<button class="w3-button w3-theme-d1" @click=${this.save_edits}>
<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-theme-d1" @click=${this.edit}>
edit = html`<button
id="edit_profile"
class="w3-button w3-theme-d1"
@click=${this.edit}
>
Edit Profile
</button>`;
}
@ -268,20 +222,18 @@ class TfProfileElement extends LitElement {
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-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-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-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-theme-d1" @click=${this.attach_image}>Attach Image</button>
</div>
<div>
<label for="name">Name:</label>
<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}))} placeholder="Choose a name"></input>
</div>
<div><label for="description">Description:</label></div>
<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}))} placeholder="Tell people a little bit about yourself here, if you like.">${this.editing.description}</textarea>
<div>
<label for="public_web_hosting">Public Web Hosting:</label>
<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-theme-d1" @click=${this.attach_image}>Attach Image</button>
</div>
</div>`
: null;
@ -289,34 +241,43 @@ class TfProfileElement extends LitElement {
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)})
<div class="w3-row">
<div class="w3-col s1 w3-container w3-right">
<button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button>
return html`<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box">
<header class="w3-container">
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})</p>
</header>
<div class="w3-container">
<div class="w3-margin-bottom" style="display: flex; flex-direction: row">
<input type="text" class="w3-input w3-border w3-theme-d1" style="display: flex 1 1" readonly value=${this.id}></input>
<button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button>
</div>
<div class="w3-rest w3-container">
<input type="text" class="w3-theme-d1" style="width: 100%; vertical-align: middle" readonly value=${this.id}></input>
<div style="display: flex; flex-direction: row; gap: 1em">
${edit_profile}
<div style="flex: 1 0 50%">
${
image
? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>`
: html`<div>
<div class="w3-jumbo">😎</div>
<div><i>Profile image not set.</i></div>
</div>`
}
<div>${unsafeHTML(tfutils.markdown(description))}</div>
</div>
</div>
<div>
Following ${profile.following} identities.
Followed by ${profile.followed} identities.
Blocking ${profile.blocking} identities.
Blocked by ${profile.blocked} identities.
</div>
</div>
<div style="display: flex; flex-direction: row; gap: 1em">
${edit_profile}
<div style="flex: 1 0 50%">
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
<div>${unsafeHTML(tfutils.markdown(description))}</div>
</div>
</div>
<div>
Following ${profile.following} identities.
Followed by ${profile.followed} identities.
Blocking ${profile.blocking} identities.
Blocked by ${profile.blocked} identities.
</div>
<div>
${edit}
${follow}
${block}
</div>
<footer class="w3-container">
<p>
${edit}
${follow}
${block}
</p>
</footer>
</div>`;
}
}

@ -26,7 +26,7 @@ class TfReactionsModalElement extends LitElement {
return this.votes?.length
? html` <div
class="w3-modal w3-animate-opacity"
style="display: block; box-sizing: border-box"
style="display: block; box-sizing: border-box; z-index: 10"
@click=${this.clear}
>
<div

@ -1,4 +1,4 @@
import {css} from './lit-all.min.js';
import {css, unsafeCSS} from './lit-all.min.js';
const tf = css`
img {
@ -285,30 +285,165 @@ 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}
`;
// prettier-ignore
const w3_2016_riverside = css`
.w3-theme-l5 {color:#000 !important; background-color:#f4f6f9 !important}
.w3-theme-l4 {color:#000 !important; background-color:#d9e1ec !important}
.w3-theme-l3 {color:#000 !important; background-color:#b4c3d8 !important}
.w3-theme-l2 {color:#fff !important; background-color:#8ea6c5 !important}
.w3-theme-l1 {color:#fff !important; background-color:#6888b1 !important}
.w3-theme-d1 {color:#fff !important; background-color:#456185 !important}
.w3-theme-d2 {color:#fff !important; background-color:#3d5676 !important}
.w3-theme-d3 {color:#fff !important; background-color:#354b68 !important}
.w3-theme-d4 {color:#fff !important; background-color:#2e4059 !important}
.w3-theme-d5 {color:#fff !important; background-color:#26364a !important}
function rgb_to_hsl(r, g, b) {
let min,
max,
i,
l,
s,
maxcolor,
h,
rgb = [];
rgb[0] = r / 255;
rgb[1] = g / 255;
rgb[2] = b / 255;
min = rgb[0];
max = rgb[0];
maxcolor = 0;
for (i = 0; i < rgb.length - 1; i++) {
if (rgb[i + 1] <= min) {
min = rgb[i + 1];
}
if (rgb[i + 1] >= max) {
max = rgb[i + 1];
maxcolor = i + 1;
}
}
if (maxcolor == 0) {
h = (rgb[1] - rgb[2]) / (max - min);
}
if (maxcolor == 1) {
h = 2 + (rgb[2] - rgb[0]) / (max - min);
}
if (maxcolor == 2) {
h = 4 + (rgb[0] - rgb[1]) / (max - min);
}
if (isNaN(h)) {
h = 0;
}
h = h * 60;
if (h < 0) {
h = h + 360;
}
l = (min + max) / 2;
if (min == max) {
s = 0;
} else {
if (l < 0.5) {
s = (max - min) / (max + min);
} else {
s = (max - min) / (2 - max - min);
}
}
s = s;
return [h, s, l];
}
.w3-theme-light {color:#000 !important; background-color:#f4f6f9 !important}
.w3-theme-dark {color:#fff !important; background-color:#26364a !important}
.w3-theme-action {color:#fff !important; background-color:#26364a !important}
function hex_to_rgb(hex) {
if (hex.charAt(0) == '#') {
hex = hex.substring(1);
}
return [
parseInt(hex.substring(0, 2), 16),
parseInt(hex.substring(2, 4), 16),
parseInt(hex.substring(4, 6), 16),
];
}
.w3-theme {color:#fff !important; background-color:#4c6a92 !important}
.w3-text-theme {color:#4c6a92 !important}
.w3-border-theme {border-color:#4c6a92 !important}
function hsl_to_rgb(hue, sat, light) {
let t2;
hue /= 60;
if (light <= 0.5) {
t2 = light * (sat + 1);
} else {
t2 = light + sat - light * sat;
}
let t1 = light * 2 - t2;
return [
hue_to_rgb(t1, t2, hue + 2) * 255,
hue_to_rgb(t1, t2, hue) * 255,
hue_to_rgb(t1, t2, hue - 2) * 255,
];
}
function hue_to_rgb(t1, t2, hue) {
if (hue < 0) {
hue += 6;
}
if (hue >= 6) {
hue -= 6;
}
if (hue < 1) {
return (t2 - t1) * hue + t1;
} else if (hue < 3) {
return t2;
} else if (hue < 4) {
return (t2 - t1) * (4 - hue) + t1;
} else {
return t1;
}
}
.w3-hover-theme:hover {color:#fff !important; background-color:#4c6a92 !important}
.w3-hover-text-theme:hover {color:#4c6a92 !important}
.w3-hover-border-theme:hover {border-color:#4c6a92 !important}
`;
function rgb_to_hex(rgb) {
const hex_pair = (x) => Math.floor(x).toString(16).padStart(2, '0');
return `#${hex_pair(rgb[0])}${hex_pair(rgb[1])}${hex_pair(rgb[2])}`;
}
export let styles = [tf, w3, w3_2016_riverside];
function is_dark(hex, value) {
let [r, g, b] = hex_to_rgb(hex);
return (r * 299 + g * 587 + b * 114) / 1000 < value;
}
function generated() {
let now = new Date();
let k_color = rgb_to_hex([
(now.getDay() * 128) / 6,
(now.getHours() * 128) / 23,
(now.getSeconds() * 128) / 59,
]);
//let k_color = '#034f84';
//let k_color = rgb_to_hex([Math.random() * 256, Math.random() * 256, Math.random() * 256]);
let [r, g, b] = hex_to_rgb(k_color);
let [h, s, l] = rgb_to_hsl(r, g, b);
let theme1 = {
l5: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 4.7)),
l4: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 4)),
l3: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 3)),
l2: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 2)),
l1: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 1)),
d0: rgb_to_hex(hsl_to_rgb(h, s, l)),
d1: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 0.5)),
d2: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 1)),
d3: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 1.5)),
d4: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 2)),
d5: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 2.5)),
};
for (let [k, v] of Object.entries(theme1)) {
theme1['t' + k] = is_dark(v, 165) ? '#fff' : '#000';
}
let result = `
.w3-theme-l5 {color: ${theme1.tl5} !important; background-color: ${theme1.l5} !important}
.w3-theme-l4 {color: ${theme1.tl4} !important; background-color: ${theme1.l4} !important}
.w3-theme-l3 {color: ${theme1.tl3} !important; background-color: ${theme1.l3} !important}
.w3-theme-l2 {color: ${theme1.tl2} !important; background-color: ${theme1.l2} !important}
.w3-theme-l1 {color: ${theme1.tl1} !important; background-color: ${theme1.l1} !important}
.w3-theme-d1 {color: ${theme1.td1} !important; background-color: ${theme1.d1} !important}
.w3-theme-d2 {color: ${theme1.td2} !important; background-color: ${theme1.d2} !important}
.w3-theme-d3 {color: ${theme1.td3} !important; background-color: ${theme1.d3} !important}
.w3-theme-d4 {color: ${theme1.td4} !important; background-color: ${theme1.d4} !important}
.w3-theme-d5 {color: ${theme1.td5} !important; background-color: ${theme1.d5} !important}
.w3-theme-light {color: ${theme1.tl5} !important; background-color: ${theme1.l5} !important}
.w3-theme-dark {color: ${theme1.td5} !important; background-color: ${theme1.d5} !important}
.w3-theme-action {color: ${theme1.td5} !important; background-color: ${theme1.d5} !important}
.w3-theme {color: ${theme1.td0} !important; background-color: ${theme1.d0} !important}
.w3-text-theme {color: ${theme1.d0} !important}
.w3-border-theme {border-color: ${theme1.d0} !important}
.w3-hover-theme:hover {color: ${theme1.td0} !important; background-color: ${theme1.d0} !important}
.w3-hover-text-theme:hover {color: ${theme1.d0} !important}
.w3-hover-border-theme:hover {border-color: ${theme1.d0} !important}
`;
return unsafeCSS(result);
}
export let styles = [tf, w3, generated()];

@ -12,6 +12,9 @@ class TfTabConnectionsElement extends LitElement {
stored_connections: {type: Array},
users: {type: Object},
server_identity: {type: String},
connect_attempt: {type: Object},
connect_message: {type: String},
connect_success: {type: Boolean},
};
}
@ -88,20 +91,36 @@ class TfTabConnectionsElement extends LitElement {
`;
}
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 class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap">
<button
class="w3-bar-item w3-button w3-theme-d1"
@click=${() => tfrpc.rpc.connect(connection)}
>
Connect
</button>
<div class="w3-bar-item">
${TfTabConnectionsElement.k_broadcast_emojis[connection.origin]}
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
${this.render_connection_summary(connection)}
<li>
<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>
`;
}
@ -123,12 +142,16 @@ class TfTabConnectionsElement extends LitElement {
}, {})
);
return html`
<button
class="w3-button w3-theme-d1"
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
>
Close
</button>
${connection.connected
? html`
<button
class="w3-button w3-theme-d1"
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
>
Close
</button>
`
: undefined}
${connection.flags.one_shot ? '🔃' : undefined}
<tf-user id=${connection.id} .users=${this.users}></tf-user>
${connection.tunnel !== undefined
@ -156,29 +179,44 @@ class TfTabConnectionsElement extends LitElement {
.map((x) => html`<li>${this.render_connection(x)}</li>`)}
${this.render_room_peers(connection.id)}
</ul>
<div ?hidden=${!connection.destroy_reason} class="w3-panel w3-red">
<p>${connection.destroy_reason}</p>
</div>
`;
}
refresh() {
tfrpc.rpc.sync();
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" style="box-sizing: border-box">
<button
class="w3-button w3-theme-l3 w3-circle w3-ripple w3-large"
@click=${this.refresh}
>
🔃
</button>
<h2>New Connection</h2>
<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=${() =>
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
self.connect(self.renderRoot.getElementById('code')?.value)}
>
Connect
</button>
@ -186,6 +224,9 @@ class TfTabConnectionsElement extends LitElement {
<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>
@ -202,46 +243,52 @@ class TfTabConnectionsElement extends LitElement {
<ul class="w3-ul w3-border">
${this.stored_connections.map(
(x) => html`
<li 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=${() => tfrpc.rpc.connect(x)}
>
Connect
</button>
<div class="w3-bar-item">
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
<div><small>${x.address}:${x.port}</small></div>
<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 class="w3-ul w3-border">
<div class="w3-container">
${this.identities.map(
(x) =>
html`<li class="w3-bar">
html`<div
class="w3-tag w3-round w3-theme-l3"
style="padding: 4px; margin: 2px; max-width: 100%; text-wrap: nowrap; overflow: hidden"
>
${x == this.server_identity
? html`<span class="w3-tag w3-medium w3-round w3-theme-l1"
>🖥 local server</span
>`
? html`<div class="w3-tag w3-medium w3-round w3-theme-l1">
🖥 local server
</div>`
: undefined}
${this.my_identities.indexOf(x) != -1
? html`<span class="w3-tag w3-medium w3-round w3-theme-d1"
>😎 you</span
>`
? html`<div class="w3-tag w3-medium w3-round w3-theme-d1">
😎 you
</div>`
: undefined}
<tf-user id=${x} .users=${this.users}></tf-user>
</li>`
</div>`
)}
</ul>
</div>
</div>
`;
}

@ -1,78 +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.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.author != ?
ORDER BY timestamp DESC limit 20
`,
[
'"' + this.whoami.replace('"', '""') + '"',
JSON.stringify(this.following),
this.whoami,
]
);
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);

@ -1,4 +1,4 @@
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
import {LitElement, cache, html, unsafeHTML, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
@ -12,6 +12,12 @@ 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},
private_messages: {type: Array},
};
}
@ -26,112 +32,200 @@ 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 id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
WHERE messages.author = ?
ORDER BY sequence DESC
LIMIT 20)
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM mine
JOIN messages_refs ON mine.id = messages_refs.ref
WITH mentions 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_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
(?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4
ORDER BY timestamp DESC limit 20)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM mentions
JOIN messages_refs ON mentions.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT * FROM mine
SELECT TRUE AS is_primary, * FROM mentions
`,
[this.hash.substring(1)]
[
'"' + this.whoami.replace('"', '""') + '"',
JSON.stringify(this.following),
start_time,
end_time,
]
);
return r;
} else if (this.hash.startsWith('#%')) {
return await tfrpc.rpc.query(
} else if (this.hash.startsWith('#@')) {
result = await tfrpc.rpc.query(
`
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
WHERE id = ?1
WITH
selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3
ORDER BY sequence DESC LIMIT 20
)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM selected
JOIN messages_refs ON selected.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
SELECT TRUE AS is_primary, * FROM selected
`,
[this.hash.substring(1), start_time, end_time]
);
} else if (this.hash.startsWith('#%')) {
result = await tfrpc.rpc.query(
`
SELECT TRUE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
WHERE messages.id = ?1
UNION
SELECT FALSE AS is_primary, 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 if (this.hash.startsWith('##')) {
result = await tfrpc.rpc.query(
`
WITH
all_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.content ->> 'channel' = ?4
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),
news AS (SELECT * FROM all_news
WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
ORDER BY all_news.timestamp DESC LIMIT 20)
SELECT FALSE AS is_primary, 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 FALSE AS is_primary, 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 TRUE AS is_primary, news.* FROM news
`,
[
JSON.stringify(this.following),
start_time,
end_time,
this.hash.substring(2),
'"#' + this.hash.substring(2).replace('"', '""') + '"',
]
);
} else if (this.hash == '#🔐') {
result = await tfrpc.rpc.query(
`
SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
JOIN json_each(?1) AS private_messages ON messages.id = private_messages.value
WHERE
(?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND
json(messages.content) LIKE '"%'
ORDER BY messages.sequence DESC LIMIT 20
`,
[JSON.stringify(this.private_messages), 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.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.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.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,
]
)
);
}
return [].concat(...(await Promise.all(promises)));
result = await tfrpc.rpc.query(
`
WITH
all_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 timestamp >= 0 AND timestamp < ?3),
news AS (
SELECT * FROM all_news
WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
ORDER BY timestamp DESC LIMIT 20
)
SELECT FALSE AS is_primary, 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 FALSE AS is_primary, 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 TRUE AS is_primary, news.* FROM news
`,
[JSON.stringify(this.following), start_time, end_time]
);
}
this.time_loading = undefined;
return result;
}
update_time_range_from_messages(messages) {
let only_primary = messages.filter((x) => x.is_primary);
this.time_range = [
only_primary.reduce(
(accumulator, current) => Math.min(accumulator, current.timestamp),
this.time_range[0]
),
only_primary.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.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.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.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM news
JOIN messages_refs ON news.id = messages_refs.message
JOIN messages ON messages_refs.ref = messages.id
UNION
SELECT news.* FROM news
`,
[JSON.stringify(this.following), this.start_time, last_start_time]
);
this.messages = await this.decrypt([...more, ...this.messages]);
this.loading++;
this.loading_canceled = false;
try {
let more = [];
let last_start_time = this.time_range[0];
more = await this.fetch_messages(null, last_start_time);
this.update_time_range_from_messages(
more.filter((x) => 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;
@ -156,44 +250,143 @@ class TfTabNewsFeedElement extends LitElement {
return result;
}
async add_messages(messages) {
this.messages = await this.decrypt([...messages, ...this.messages]);
merge_messages(old_messages, new_messages) {
let old_by_id = Object.fromEntries(old_messages.map((x) => [x.id, x]));
return new_messages.map((x) => (old_by_id[x.id] ? old_by_id[x.id] : x));
}
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[0], end_time);
messages = await this.decrypt(messages);
this.update_time_range_from_messages(
messages.filter(
(x) => x.timestamp >= this.time_range[0] && x.timestamp < end_time
)
);
} finally {
this.loading--;
}
this.messages = this.merge_messages(
this.messages,
Object.values(
Object.fromEntries(
[...this.messages, ...messages]
.sort((x, y) => x.timestamp - y.timestamp)
.slice(-1024)
.map((x) => [x.id, x])
)
)
);
console.log('done loading latest messages.');
}
async load_messages() {
let start_time = new Date();
let self = this;
this.loading++;
let messages = [];
try {
if (this._messages_hash !== this.hash) {
this.messages = [];
this._messages_hash = this.hash;
}
this._messages_following = this.following;
let now = new Date().valueOf();
let start_time = now - 24 * 60 * 60 * 1000;
this.start_time = start_time;
this.time_range = [now + 24 * 60 * 60 * 1000, now + 24 * 60 * 60 * 1000];
messages = await this.fetch_messages(null, this.time_range[1]);
this.update_time_range_from_messages(
messages.filter((x) => x.timestamp < this.time_range[1])
);
messages = await this.decrypt(messages);
} finally {
this.loading--;
}
this.messages = this.merge_messages(this.messages, messages);
this.time_loading = undefined;
console.log(
`loading messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s`
);
}
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
JSON.stringify(this._messages_following) !==
JSON.stringify(this.following)
) {
console.log(
`loading messages for ${this.whoami} (following ${this.following.length})`
);
let self = this;
this.messages = [];
this._messages_hash = this.hash;
this._messages_following = this.following;
this.fetch_messages()
.then(this.decrypt.bind(this))
.then(function (messages) {
self.messages = messages;
console.log(`loading mesages done for ${self.whoami}`);
})
.catch(function (error) {
alert(JSON.stringify(error, null, 2));
});
this.load_messages();
}
let more;
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
if (!this.hash.startsWith('#%')) {
more = html`
<p>
<button class="w3-button w3-theme-d1" @click=${this.load_more}>
<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`
return cache(html`
<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
Mark All Read
</button>
<tf-news
id="news"
whoami=${this.whoami}
@ -202,9 +395,11 @@ class TfTabNewsFeedElement extends LitElement {
.following=${this.following}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel()}
channel_unread=${this.channels_unread?.[this.channel()]}
></tf-news>
${more}
`;
`);
}
}

@ -1,4 +1,11 @@
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
import {
LitElement,
cache,
keyed,
html,
unsafeHTML,
until,
} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
@ -8,11 +15,15 @@ class TfTabNewsElement extends LitElement {
whoami: {type: String},
users: {type: Object},
hash: {type: String},
unread: {type: Array},
following: {type: Array},
drafts: {type: Object},
expanded: {type: Object},
loading: {type: Boolean},
channels: {type: Array},
channels_unread: {type: Object},
channels_latest: {type: Object},
connections: {type: Array},
private_messages: {type: Array},
};
}
@ -24,11 +35,14 @@ class TfTabNewsElement extends LitElement {
this.whoami = null;
this.users = {};
this.hash = '#';
this.unread = [];
this.following = [];
this.cache = {};
this.drafts = {};
this.expanded = {};
this.channels_unread = {};
this.channels_latest = {};
this.channels = [];
this.connections = [];
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}');
});
@ -44,39 +58,13 @@ class TfTabNewsElement extends LitElement {
document.body.removeEventListener('keypress', this.on_keypress.bind(this));
}
show_more() {
let unread = this.unread;
load_latest() {
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])))
);
this.dispatchEvent(new CustomEvent('refresh'));
news.load_latest();
}
}
new_messages_text() {
if (!this.unread?.length) {
return 'No new messages.';
}
let counts = {};
for (let message of this.unread) {
let type = 'private';
try {
type = JSON.parse(message.content).type || type;
} catch {}
counts[type] = (counts[type] || 0) + 1;
}
return (
'↻ Show New: ' +
Object.keys(counts)
.sort()
.map((x) => counts[x].toString() + ' ' + x + 's')
.join(', ')
);
}
draft(event) {
let id = event.detail.id || '';
let previous = this.drafts[id];
@ -106,14 +94,181 @@ class TfTabNewsElement extends LitElement {
}
}
unread_status(channel) {
if (
this.channels_latest[channel] &&
this.channels_latest[channel] > 0 &&
(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;
}
compare_follows() {
const now = new Date().valueOf();
return function (a, b) {
return (b[1].ts > now ? -1 : b[1].ts) - (a[1].ts > now ? -1 : a[1].ts);
};
}
suggested_follows() {
/*
** Filter out people who have used future timestamps so that they aren't
** pinned at the top.
*/
let self = this;
return Object.entries(this.users)
.filter((x) => x[1].follow_depth > 1)
.sort(self.compare_follows())
.slice(0, 8)
.map((x) => x[0]);
}
render_sidebar() {
return html`
<div
class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left"
style="width: 2in; left: 0; z-index: 5; box-sizing: border-box; top: 0"
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}
<h4 class="w3-bar-item w3-theme-d2">Channels</h4>
<a
href="#"
class="w3-bar-item w3-button"
style=${this.hash == '#' ? 'font-weight: bold' : undefined}
>${this.unread_status('')}general</a
>
<a
href="#@"
class="w3-bar-item w3-button"
style=${this.hash == '#@' ? 'font-weight: bold' : undefined}
>${this.unread_status('@')}@mentions</a
>
<a
href="#🔐"
class="w3-bar-item w3-button"
style=${this.hash == '#🔐' ? 'font-weight: bold' : undefined}
>${this.unread_status('🔐')}🔐private</a
>
${Object.keys(this.drafts)
.sort()
.map(
(x) => html`
<a
href=${'#' + encodeURIComponent(x)}
class="w3-bar-item w3-button"
style="text-wrap: nowrap; text-overflow: ellipsis"
>📝 ${this.drafts[x]?.text ?? x}</a
>
`
)}
${this.channels.map(
(x) => html`
<a
href=${'#' + encodeURIComponent('#' + x)}
class="w3-bar-item w3-button"
style=${this.hash == '##' + x ? 'font-weight: bold' : undefined}
>${this.unread_status(x)}#${x}</a
>
`
)}
<h4 class="w3-bar-item w3-theme-d2">Connections</h4>
${this.connections
.filter((x) => x.id && !x.destroy_reason)
.map(
(x) => html`
<tf-user
class="w3-bar-item"
style="max-width: 100%"
id=${x.id}
.users=${this.users}
></tf-user>
`
)}
<h4 class="w3-bar-item w3-theme-d2">Suggested Follows</h4>
${this.suggested_follows().map(
(x) => html`
<tf-user
class="w3-bar-item"
style="max-width: 100%"
id=${x}
.users=${this.users}
></tf-user>
`
)}
</div>
<div
class="w3-overlay"
id="sidebar_overlay"
@click=${this.hide_sidebar}
></div>
`;
}
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 != '#@'
? keyed(
this.hash.substring(1),
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 &&
@ -127,41 +282,67 @@ class TfTabNewsElement extends LitElement {
name.
</div>`;
}
return html`
<p class="w3-bar">
<button
class="w3-bar-item w3-button w3-theme-d1"
@click=${this.show_more}
>
${this.new_messages_text()}
</button>
</p>
<div class="w3-bar">
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
${edit_profile}
return cache(html`
${this.render_sidebar()}
<div
style="margin-left: 2in; padding: 0px; top: 0; max-height: 100%; overflow: auto"
id="main"
class="w3-main"
>
<div style="padding: 8px">
<p>
${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>
<div
id="show_sidebar"
class="w3-button w3-hide-large"
@click=${this.show_sidebar}
>
&#9776;
</div>
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
${edit_profile}
</div>
<div>
<tf-compose
id="tf-compose"
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
@tf-draft=${this.draft}
.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}
.private_messages=${this.private_messages}
></tf-tab-news-feed>
</div>
</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>
`;
`);
}
}

@ -18,10 +18,10 @@ 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"
href=${'#' + encodeURIComponent(this.tag)}
class="w3-tag w3-theme-d1 w3-round-4 w3-button"
>${this.tag}${number}</a
>`;
> `;
}
}

@ -19,30 +19,36 @@ class TfUserElement extends LitElement {
}
render() {
let user = this.users[this.id];
let shape =
user?.follow_depth === undefined || user.follow_depth >= 2
? 'w3-circle'
: 'w3-round';
let image = html`<span
class="w3-theme-light w3-circle"
class=${'w3-theme-l4 ' + shape}
style="display: inline-block; width: 2em; height: 2em; text-align: center; line-height: 2em"
>?</span
>😎</span
>`;
let name = this.users?.[this.id]?.name;
name =
name !== undefined
? html`<a target="_top" href=${'#' + this.id}>${name}</a>`
: html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
name = html`<a target="_top" href=${'#' + this.id}
>${name !== undefined ? name : this.id}</a
>`;
if (this.users[this.id]) {
let image_link = this.users[this.id].image;
if (user) {
let image_link = user.image;
image_link =
typeof image_link == 'string' ? image_link : image_link?.link;
if (image_link !== undefined) {
image = html`<img
class="w3-circle"
class=${'w3-theme-l4 ' + shape}
style="width: 2em; height: 2em; vertical-align: middle; object-fit: cover"
src="/${image_link}/view"
/>`;
}
}
return html` <div style="display: inline-block; font-weight: bold">
return html` <div
style="display: inline-block; vertical-align: middle; font-weight: bold; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis"
>
${image} ${name}
</div>`;
}

@ -2,6 +2,12 @@ import * as hashtagify from './commonmark-hashtag.js';
const k_code_classes = 'w3-theme-l4 w3-theme-border w3-round';
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' &&
@ -81,8 +87,8 @@ function attrs(node) {
}
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});
writer.image = image;
writer.code = code;
writer.attrs = attrs;

5
apps/storage.json Normal file

@ -0,0 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "💾",
"previous": "&mvGTlWKFR5QM/3nb4fJ2WQq0n/gNKvBmhGDkAvb8ki8=.sha256"
}

127
apps/storage/app.js Normal file

@ -0,0 +1,127 @@
async function query(sql, args) {
let rows = [];
await ssb.sqlAsync(sql, args ?? [], function (row) {
rows.push(row);
});
return rows;
}
async function get_biggest() {
return query(`
select author, sum(length(content)) as size from messages group by author order by size desc limit 10;
`);
}
async function get_total() {
return (
await query(`
select sum(length(content)) as size, count(distinct author) as count from messages;
`)
)[0];
}
async function get_names(identities) {
return query(
`
SELECT author, name FROM (
SELECT
messages.author,
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
messages.content ->> 'name' AS name
FROM messages
JOIN json_each(?) AS identities ON identities.value = messages.author
WHERE
json_extract(messages.content, '$.type') = 'about' AND
content ->> 'about' = messages.author AND name IS NOT NULL)
WHERE author_rank = 1
`,
[JSON.stringify(identities)]
);
}
async function get_most_follows() {
return query(`
select author, count(*) as count
from messages
where content ->> 'type' = 'contact' and content ->> 'following' = true
group by author
order by count desc
limit 10;
`);
}
function nice_size(bytes) {
let value = bytes;
let index = 0;
let units = ['B', 'kB', 'MB', 'GB'];
while (value > 1024 && index < units.length - 1) {
value /= 1024;
index++;
}
return `${Math.round(value * 10) / 10} ${units[index]}`;
}
async function main() {
await app.setDocument(
'<p style="color: #fff">Finding the top 10 largest feeds...</p>'
);
let most_follows = await get_most_follows();
let total = await get_total();
let identities = await ssb.getAllIdentities();
let following1 = await ssb.following(identities, 1);
let following2 = await ssb.following(identities, 2);
let biggest = await get_biggest();
let names = await get_names(
[].concat(
biggest.map((x) => x.author),
most_follows.map((x) => x.author)
)
);
names = Object.fromEntries(names.map((x) => [x.author, x.name]));
for (let item of biggest) {
item.name = names[item.author];
item.following =
identities.indexOf(item.author) != -1
? 0
: following1[item.author] !== undefined
? 1
: following2[item.author] !== undefined
? 2
: undefined;
}
for (let item of most_follows) {
item.name = names[item.author];
}
let html = `<body style="color: #000; background-color: #ddd">\n
<h1>Storage Summary</h1>
<h2>Top 10 Accounts by Size</h2>
<ol>`;
for (let item of biggest) {
html += `<li>
<span style="color: #888">${nice_size(item.size)}</span>
<a target="_top" href="/~core/ssb/#${encodeURI(item.author)}">${item.name ?? item.author}</a>
</li>
\n`;
}
html += `
</ol>
<h2>Top 10 Accounts by Follows</h2>
<ol>`;
for (let item of most_follows) {
html += `<li>
<span style="color: #888">${item.count}</span>
${following2[item.author] ? '✅' : '🚫'}
<a target="_top" href="/~core/ssb/#${encodeURI(item.author)}">${item.name ?? item.author}</a>
</li>
\n`;
}
html += `
</ol>
<p>Total <span style="color: #888">${nice_size(total.size)}</span> in ${total.count} accounts.</p>
`;
await app.setDocument(html);
}
main().catch(function (e) {
print(e);
});

4
apps/test.json Normal file

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

3
apps/test/app.js Normal file

@ -0,0 +1,3 @@
app.setDocument(
'<p style="color: #fff">Maybe one day this app will run tests, but for now there is nothing to see here.</p>'
);

1
apps/test/hello.txt Normal file

@ -0,0 +1 @@
Hello, world!

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "👋",
"previous": "&7gFmLW5zSMhmxWWY1+jeRcHdullgujSqGJg94lVgr1k=.sha256"
"previous": "&wAb7J6E35xEXpiXsQ6t1RaWTGIvlatUnyH8ipF6pVic=.sha256"
}

@ -10,17 +10,6 @@
<link rel="stylesheet" href="brands.min.css" />
<style>
body,
h1,
h2,
h3,
h4,
h5 {
font-family: 'Poppins', sans-serif;
}
body {
font-size: 16px;
}
img {
margin-bottom: -8px;
}
@ -39,11 +28,14 @@
<b>😎 Tilde Friends</b>
</h1>
<h1 class="w3-xxlarge w3-text-green">
<b>Make apps and friends from the comfort of your web browser.</b>
<b
>the Secure Scuttlebutt decentralized social network client that's
<i>fancy🎩</i></b
>
</h1>
<p>
Tilde Friends is a platform for building, running, and sharing web
applications.
In addition to participating in Secure Scuttlebutt, Tilde Friends is
a platform for building, running, and sharing applications.
</p>
<p>
Available for lots of devices:
@ -60,7 +52,7 @@
>
<a
class="w3-button w3-black w3-padding-large"
href="https://www.tildefriends.net/~cory/apps/"
href="https://www.tildefriends.net/~core/ssb/"
><i class="fa fa-link"></i> Try It</a
>
<a
@ -68,6 +60,11 @@
href="https://dev.tildefriends.net/"
><i class="fa fa-mug-hot"></i> Development</a
>
<a
class="w3-button w3-black w3-padding-large"
href="https://docs.tildefriends.net/"
><i class="fa fa-book"></i> Documentation</a
>
<p>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
@ -223,16 +220,15 @@
<!-- Technlology Section -->
<div class="w3-container w3-padding-64 w3-light-grey w3-center">
<h1 class="w3-jumbo"><b>Boring Technology</b></h1>
<h1 class="w3-jumbo"><b>Built the Old Fashioned Way</b></h1>
<p>
Tilde Friends is built using boring, trusted tech. Unless a better
reason presents itself, it strives to use only simple and widely adopted
dependencies in order to keep it easy to build for all sorts of
platforms and maintainable for a very long time.
Tilde Friends strives to use only simple and widely adopted dependencies
in order to keep it easy to build for all sorts of platforms and
maintainable for a very long time.
</p>
<p>
Though of course for building Tilde Friends apps, you are free to use
whatever fits.
whatever fits on top.
</p>
<div class="w3-row" style="margin-top: 64px">

@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "📝",
"previous": "&DaYqKHRBKhjFGaOzbKZ1+/pLspJeEkDJYTF2B50tH6k=.sha256"
"previous": "&4UHlsfQJvSh7L3D86uFtr7KUKCMRVBBTFxRIMqIc5as=.sha256"
}

File diff suppressed because one or more lines are too long

@ -2,8 +2,8 @@ import * as utils from './utils.js';
import * as commonmark from './commonmark.min.js';
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;

@ -20,8 +20,8 @@ class TfWikiDocElement extends LitElement {
}
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;

@ -81,9 +81,8 @@ App.prototype.send = function (message) {
* TODOC
* @param {*} request
* @param {*} response
* @param {*} client
*/
async function socket(request, response, client) {
exports.app_socket = async function socket(request, response) {
let process;
let options = {};
let credentials = await httpd.auth_query(request.headers);
@ -245,6 +244,6 @@ async function socket(request, response, client) {
};
response.upgrade(100, {});
}
};
export {socket, App};
export {App};

@ -56,7 +56,7 @@ class TfNavigationElement extends LitElement {
status: {type: Object},
spark_lines: {type: Object},
version: {type: Object},
show_version: {type: Boolean},
show_expanded: {type: Boolean},
identity: {type: String},
identities: {type: Array},
names: {type: Object},
@ -105,7 +105,6 @@ class TfNavigationElement extends LitElement {
let spark_line = document.createElement('tf-sparkline');
spark_line.title = key;
spark_line.classList.add('w3-bar-item');
spark_line.classList.add('w3-hide-small');
spark_line.style.paddingRight = '0';
if (options) {
if (options.max) {
@ -150,7 +149,7 @@ class TfNavigationElement extends LitElement {
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div class="w3-dropdown-click w3-right" style="max-width: 100%">
<button
class="w3-button w3-rest w3-cyan"
class="w3-button w3-rest"
style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%"
id="identity"
@click=${self.toggle_id_dropdown}
@ -162,6 +161,10 @@ class TfNavigationElement extends LitElement {
class="w3-dropdown-content w3-bar-block w3-card-4"
style="max-width: 100%; right: 0"
>
<div
style="position: fixed; left: 0; right: 0; top: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.25); z-index: -100"
@click=${self.toggle_id_dropdown}
></div>
<button
class="w3-bar-item w3-button w3-border"
@click=${() => (window.location.href = '/~core/identity')}
@ -169,6 +172,7 @@ class TfNavigationElement extends LitElement {
Manage Identities...
</button>
<button
id="edit_profile"
class="w3-bar-item w3-button w3-border"
@click=${self.edit_profile}
>
@ -311,13 +315,13 @@ class TfNavigationElement extends LitElement {
<span
class="w3-bar-item"
style="cursor: pointer"
@click=${() => (this.show_version = !this.show_version)}
@click=${() => (this.show_expanded = !this.show_expanded)}
>😎</span
>
<span
class="w3-bar-item"
style=${'white-space: nowrap' +
(this.show_version ? '' : '; display: none')}
(this.show_expanded ? '' : '; display: none')}
title=${this.version?.name +
' ' +
Object.entries(this.version || {})
@ -372,9 +376,11 @@ class TfNavigationElement extends LitElement {
</div>
`
: undefined}
${Object.keys(this.spark_lines)
.sort()
.map((x) => this.spark_lines[x])}
<span class=${this.show_expanded ? '' : 'w3-hide-small'}>
${Object.keys(this.spark_lines)
.sort()
.map((x) => this.spark_lines[x])}
</span>
${this.render_identity()}
</div>
${this.status?.is_error
@ -1174,7 +1180,7 @@ function api_requestPermission(permission, id) {
let div = document.createElement('div');
div.appendChild(
document.createTextNode('This app is requesting the following permission:')
document.createTextNode('This app is requesting the following permission: ')
);
let span = document.createElement('span');
span.style = 'font-weight: bold';
@ -1190,6 +1196,7 @@ function api_requestPermission(permission, id) {
check.classList.add('w3-check');
check.classList.add('w3-blue');
div.appendChild(check);
div.appendChild(document.createTextNode(' '));
let label = document.createElement('label');
label.htmlFor = check.id;
label.appendChild(document.createTextNode('Remember this decision.'));

@ -1,92 +1,8 @@
import * as app from './app.js';
import * as form from './form.js';
import * as http from './http.js';
let gProcesses = {};
let gStatsTimer = false;
const k_content_security_policy =
'sandbox allow-downloads allow-top-navigation-by-user-activation';
const k_global_settings = {
index: {
type: 'string',
default_value: '/~core/apps/',
description: 'Default path.',
},
index_map: {
type: 'textarea',
default_value: undefined,
description:
'Mappings from hostname to redirect path, one per line, as in: "www.tildefriends.net=/~core/index/"',
},
room: {
type: 'boolean',
default_value: true,
description: 'Enable peers to tunnel through this instance as a room.',
},
room_name: {
type: 'string',
default_value: 'tilde friends tunnel',
description: 'Name of the room.',
},
replicator: {
type: 'boolean',
default_value: true,
description: 'Enable message and blob replication.',
},
code_of_conduct: {
type: 'textarea',
default_value: undefined,
description: 'Code of conduct presented at sign-in.',
},
http_redirect: {
type: 'string',
default_value: undefined,
description:
'If connecting by HTTP and HTTPS is configured, Location header prefix (ie, "https://example.com")',
},
fetch_hosts: {
type: 'string',
default_value: undefined,
description:
'Comma-separated list of host names to which HTTP fetch requests are allowed. None if empty.',
},
blob_fetch_age_seconds: {
type: 'integer',
default_value:
platform() == 'android' || platform() == 'iphone'
? 0.5 * 365 * 24 * 60 * 60
: undefined,
description:
'Only blobs mentioned more recently than this age will be automatically fetched.',
},
blob_expire_age_seconds: {
type: 'integer',
default_value:
platform() == 'android' || platform() == 'iphone'
? 1.0 * 365 * 24 * 60 * 60
: undefined,
description: 'Blobs older than this will be automatically deleted.',
},
seeds_host: {
type: 'string',
default_value: 'seeds.tildefriends.net',
description: 'Hostname for seed connections.',
},
peer_exchange: {
type: 'boolean',
default_value: false,
description:
'Enable discovery of, sharing of, and connecting to internet peer strangers, including announcing this instance.',
},
account_registration: {
type: 'boolean',
default_value: true,
description: 'Allow registration of new accounts.',
},
};
let kPingInterval = 60 * 1000;
/**
@ -489,7 +405,7 @@ async function getProcessBlob(blobId, key, options) {
};
if (process.credentials?.permissions?.administration) {
imports.core.globalSettingsDescriptions = async function () {
let settings = Object.assign({}, k_global_settings);
let settings = Object.assign({}, defaultGlobalSettings());
for (let [key, value] of Object.entries(await loadSettings())) {
if (settings[key]) {
settings[key].value = value;
@ -502,6 +418,7 @@ async function getProcessBlob(blobId, key, options) {
return settings?.[key];
};
imports.core.globalSettingsSet = async function (key, value) {
await imports.core.permissionTest('set_global_setting');
print('Setting', key, value);
let settings = await loadSettings();
settings[key] = value;
@ -553,7 +470,6 @@ async function getProcessBlob(blobId, key, options) {
imports.ssb = Object.fromEntries(
Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
);
imports.ssb.port = tildefriends.ssb_port;
imports.ssb.createIdentity = () => process.createIdentity();
imports.ssb.addIdentity = function (id) {
if (
@ -659,19 +575,6 @@ async function getProcessBlob(blobId, key, options) {
);
}
};
imports.ssb.setServerFollowingMe = function (id, following) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.setServerFollowingMe(
process.credentials.session.name,
id,
following
);
}
};
imports.ssb.swapWithServerIdentity = function (id) {
if (
process.credentials &&
@ -812,206 +715,6 @@ async function getProcessBlob(blobId, key, options) {
return process;
}
/**
* TODOC
* @param {*} response
* @param {*} data
* @param {*} type
* @param {*} headers
* @param {*} status_code
*/
function sendData(response, data, type, headers, status_code) {
if (data) {
response.writeHead(
status_code ?? 200,
Object.assign(
{
'Content-Type':
type ||
httpd.mime_type_from_magic_bytes(data) ||
'application/binary',
'Content-Length': data.byteLength,
},
headers || {}
)
);
response.end(data);
} else {
response.writeHead(
status_code ?? 404,
Object.assign(
{
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': 'File not found'.length,
},
headers || {}
)
);
response.end('File not found');
}
}
let g_handler_index = 0;
/**
* TODOC
* @param {*} response
* @param {*} handler_blob_id
* @param {*} path
* @param {*} query
* @param {*} headers
* @param {*} packageOwner
* @param {*} packageName
* @returns
*/
async function useAppHandler(
response,
handler_blob_id,
path,
query,
headers,
packageOwner,
packageName
) {
print('useAppHandler', packageOwner, packageName);
let do_resolve;
let promise = new Promise(async function (resolve, reject) {
do_resolve = resolve;
});
let process;
let result;
try {
process = await getProcessBlob(
handler_blob_id,
'handler_' + g_handler_index++,
{
script: 'handler.js',
imports: {
request: {
path: path,
query: query,
},
respond: do_resolve,
},
credentials: await httpd.auth_query(headers),
packageOwner: packageOwner,
packageName: packageName,
}
);
await process.ready;
result = await promise;
} finally {
if (process?.task) {
await process.task.kill();
}
}
return result;
}
/**
* TODOC
* @param {*} request
* @param {*} response
* @param {*} blobId
* @param {*} uri
* @returns
*/
async function blobHandler(request, response, blobId, uri) {
if (!uri) {
response.writeHead(303, {
Location:
(request.client.tls ? 'https://' : 'http://') +
(request.headers['x-forwarded-host'] ?? request.headers.host) +
blobId +
'/',
'Content-Length': '0',
});
response.end();
return;
}
let process;
let data;
let match;
let id;
let app_id = blobId;
let packageOwner;
let packageName;
if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) {
packageOwner = match[1];
packageName = match[2];
let db = new Database(match[1]);
app_id = await db.get('path:' + match[2]);
}
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app_id)));
id = app_object?.files[uri.substring(1)];
if (!id && app_object?.files['handler.js']) {
let answer;
try {
answer = await useAppHandler(
response,
app_id,
uri.substring(1),
request.query ? form.decodeForm(request.query) : undefined,
request.headers,
packageOwner,
packageName
);
} catch (error) {
data = utf8Encode(
`Internal Server Error\n\n${error?.message}\n${error?.stack}`
);
response.writeHead(500, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': data.length,
});
response.end(data);
return;
}
if (answer && typeof answer.data == 'string') {
answer.data = utf8Encode(answer.data);
}
sendData(
response,
answer?.data,
answer?.content_type,
Object.assign(answer?.headers ?? {}, {
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': k_content_security_policy,
}),
answer.status_code
);
} else if (id) {
if (
request.headers['if-none-match'] &&
request.headers['if-none-match'] == '"' + id + '"'
) {
let headers = {
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': k_content_security_policy,
'Content-Length': '0',
};
response.writeHead(304, headers);
response.end();
} else {
let headers = {
ETag: '"' + id + '"',
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy': k_content_security_policy,
};
data = await ssb.blobGet(id);
let type =
httpd.mime_type_from_extension(uri) ||
httpd.mime_type_from_magic_bytes(data);
sendData(response, data, type, headers);
}
} else {
sendData(response, data, undefined, {});
}
}
ssb.addEventListener('message', function () {
broadcastEvent('onMessage', [...arguments]);
});
@ -1037,7 +740,7 @@ async function loadSettings() {
} catch (error) {
print('Settings not found in database:', error);
}
for (let [key, value] of Object.entries(k_global_settings)) {
for (let [key, value] of Object.entries(defaultGlobalSettings())) {
if (data[key] === undefined) {
data[key] = value.default_value;
}
@ -1063,69 +766,71 @@ function sendStats() {
}
}
/**
* TODOC
*/
loadSettings()
.then(function (settings) {
if (tildefriends.https_port && settings.http_redirect) {
httpd.set_http_redirect(settings.http_redirect);
}
httpd.all('/app/socket', app.socket);
httpd.all('', function default_http_handler(request, response) {
let match;
if ((match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri))) {
return blobHandler(request, response, match[1], match[2]);
} else if (
(match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri))
) {
return blobHandler(request, response, match[1], match[2]);
}
});
let port = httpd.start(tildefriends.http_port);
if (tildefriends.args.out_http_port_file) {
print('Writing the port file.');
File.writeFile(
tildefriends.args.out_http_port_file,
port.toString() + '\n'
)
.then(function (r) {
print(
'Wrote the port file:',
tildefriends.args.out_http_port_file,
r
);
})
.catch(function () {
print('Failed to write the port file.');
});
}
let g_handler_index = 0;
if (tildefriends.https_port) {
async function start_tls() {
const kCertificatePath = 'data/httpd/certificate.pem';
const kPrivateKeyPath = 'data/httpd/privatekey.pem';
let privateKey;
let certificate;
try {
privateKey = utf8Decode(await File.readFile(kPrivateKeyPath));
certificate = utf8Decode(await File.readFile(kCertificatePath));
} catch (e) {
print(`TLS disabled (${e.message}).`);
return;
exports.callAppHandler = async function callAppHandler(
response,
app_blob_id,
path,
query,
headers,
package_owner,
package_name
) {
let answer;
try {
let do_resolve;
let promise = new Promise(async function (resolve, reject) {
do_resolve = resolve;
});
let process;
try {
process = await getProcessBlob(
app_blob_id,
'handler_' + g_handler_index++,
{
script: 'handler.js',
imports: {
request: {
path: path,
query: query,
},
respond: do_resolve,
},
credentials: await httpd.auth_query(headers),
packageOwner: package_owner,
packageName: package_name,
}
let context = new TlsContext();
context.setPrivateKey(privateKey);
context.setCertificate(certificate);
httpd.start(tildefriends.https_port, context);
);
await process.ready;
answer = await promise;
} finally {
if (process?.task) {
await process.task.kill();
}
start_tls();
}
})
.catch(function (error) {
print('Failed to load settings.');
printError({print: print}, error);
exit(1);
} catch (error) {
let data = utf8Encode(
`Internal Server Error\n\n${error?.message}\n${error?.stack}`
);
response.writeHead(500, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Length': data.length,
});
response.end(data);
return;
}
if (typeof answer?.data == 'string') {
answer.data = utf8Encode(answer.data);
}
response.writeHead(answer?.status_code, {
'Content-Type': answer?.content_type,
'Content-Length': answer?.data?.length,
'Access-Control-Allow-Origin': '*',
'Content-Security-Policy':
'sandbox allow-downloads allow-top-navigation-by-user-activation',
});
response.end(answer?.data);
};
export {invoke, getProcessBlob};

@ -1,44 +0,0 @@
/**
* TODOC
* @param {*} encoded
* @returns
*/
function decode(encoded) {
let result = '';
for (let i = 0; i < encoded.length; i++) {
let c = encoded[i];
if (c == '+') {
result += ' ';
} else if (c == '%') {
result += String.fromCharCode(parseInt(encoded.slice(i + 1, i + 3), 16));
i += 2;
} else {
result += c;
}
}
return result;
}
/**
* TODOC
* @param {*} encoded
* @param {*} initial
* @returns
*/
function decodeForm(encoded, initial) {
let result = initial || {};
if (encoded) {
encoded = encoded.trim();
let items = encoded.split('&');
for (let i = 0; i < items.length; i++) {
let item = items[i];
let equals = item.indexOf('=');
let key = decode(item.slice(0, equals));
let value = decode(item.slice(equals + 1));
result[key] = value;
}
}
return result;
}
export {decodeForm};

@ -6,6 +6,26 @@
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<link type="image/svg+xml" rel="icon" href="/static/tildefriends.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="title"
content="Tilde Friends - Make friends and apps from your web browser."
/>
<meta
name="description"
content="Tilde Friends is a Secure Scuttlebutt client and a platform for building, running, and sharing web applications. "
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://metatags.io/" />
<meta
property="og:title"
content="Tilde Friends - Make friends and apps from your web browser."
/>
<meta
property="og:description"
content="Tilde Friends is a Secure Scuttlebutt client and a platform for building, running, and sharing web applications. "
/>
<meta property="og:image" content="/static/tildefriends.svg" />
<script>
function set_access_key_title(event) {
if (!event.srcElement.title) {
@ -18,8 +38,8 @@
style="
display: flex;
flex-flow: column;
width: 100vw;
height: 100vh;
width: 100%;
height: 100%;
position: absolute;
max-width: 100%;
max-height: 100%;

@ -7,8 +7,8 @@ html {
body {
font-family: monospace;
background-color: #002b36;
color: #eee8d5;
background-color: #444;
color: #fff;
width: 100%;
height: 100%;
padding: 0;

@ -1,4 +1,8 @@
# How to upgrade to a newer version
# - On the june and december release, you'll have to update nixpkgs to the current branch
# Change `nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";`
# to the latest release (see https://nixos.org/)
# - Run `$ nix flake update`
# - Comment `src.hash`
# - Change `version`
# - Run `$ nix build`
@ -21,14 +25,14 @@
}:
pkgs.stdenv.mkDerivation rec {
pname = "tildefriends";
version = "0.0.23";
version = "0.0.27.1";
src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net";
owner = "cory";
repo = "tildefriends";
rev = "v${version}";
hash = "sha256-ukZpi+BXRTFGbdvd5ApmctTo8bjtPJMHjqFPgVSyBWU=";
hash = "sha256-3t1m9ZomQF3DteWyALJWrnCq0EAROEK8shKXh6Ao38c=";
fetchSubmodules = true;
};
@ -46,7 +50,7 @@ pkgs.stdenv.mkDerivation rec {
];
buildPhase = ''
make -j $NIX_BUILD_CORES release
make -j $NIX_BUILD_CORES release USE_SYSTEM_SSL=1
'';
installPhase = ''

2
deps/c-ares vendored

File diff suppressed because one or more lines are too long

410
deps/codemirror_src/package-lock.json generated vendored

@ -19,28 +19,20 @@
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.18.1",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz",
"integrity": "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA==",
"license": "MIT",
"version": "6.18.6",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
"integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
},
"peerDependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz",
"integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==",
"license": "MIT",
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.0.tgz",
"integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
@ -49,10 +41,9 @@
}
},
"node_modules/@codemirror/lang-css": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz",
"integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==",
"license": "MIT",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
@ -65,7 +56,6 @@
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
"integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
@ -79,10 +69,9 @@
}
},
"node_modules/@codemirror/lang-javascript": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz",
"integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==",
"license": "MIT",
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.3.tgz",
"integrity": "sha512-8PR3vIWg7pSu7ur8A07pGiYHgy3hHj+mRYRCSG8q+mPIrl0F02rgpGv+DsQTHRTc30rydOsf5PZ7yjKFg2Ackw==",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
@ -97,17 +86,15 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz",
"integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==",
"license": "MIT",
"version": "6.10.8",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.8.tgz",
"integrity": "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
@ -118,21 +105,19 @@
}
},
"node_modules/@codemirror/lint": {
"version": "6.8.2",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz",
"integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==",
"license": "MIT",
"version": "6.8.4",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz",
"integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.5.6",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz",
"integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==",
"license": "MIT",
"version": "6.5.10",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.10.tgz",
"integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
@ -140,16 +125,17 @@
}
},
"node_modules/@codemirror/state": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
"integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==",
"license": "MIT"
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
"integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@ -158,22 +144,20 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.34.1",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.1.tgz",
"integrity": "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ==",
"license": "MIT",
"version": "6.36.3",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.3.tgz",
"integrity": "sha512-N2bilM47QWC8Hnx0rMdDxO2x2ImJ1FvZWXubwKgjeoOrWwEiFrtpA7SFHcuZ+o2Ze2VzbkgbzWVj4+V18LVkeg==",
"dependencies": {
"@codemirror/state": "^6.4.0",
"@codemirror/state": "^6.5.0",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@ -188,7 +172,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
@ -198,7 +181,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
@ -208,7 +190,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
@ -218,15 +199,13 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@ -235,14 +214,12 @@
"node_modules/@lezer/common": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
"license": "MIT"
"integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="
},
"node_modules/@lezer/css": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.9.tgz",
"integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==",
"license": "MIT",
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.10.tgz",
"integrity": "sha512-V5/89eDapjeAkWPBpWEfQjZ1Hag3aYUUJOL8213X0dFRuXJ4BXa5NKl9USzOnaLod4AOpmVCkduir2oKwZYZtg==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
@ -253,7 +230,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
@ -262,7 +238,6 @@
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
"integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
@ -270,10 +245,9 @@
}
},
"node_modules/@lezer/javascript": {
"version": "1.4.19",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.19.tgz",
"integrity": "sha512-j44kbR1QL26l6dMunZ1uhKBFteVGLVCBGNUD2sUaMnic+rbTviVuoK0CD1l9FTW31EueWvFFswCKMH7Z+M3JRA==",
"license": "MIT",
"version": "1.4.21",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz",
"integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
@ -281,10 +255,9 @@
}
},
"node_modules/@lezer/json": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz",
"integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==",
"license": "MIT",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
@ -295,16 +268,19 @@
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.3.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz",
"integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==",
"license": "MIT",
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
"integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
@ -329,7 +305,6 @@
"resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
"integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"serialize-javascript": "^6.0.1",
"smob": "^1.0.0",
@ -348,10 +323,9 @@
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz",
"integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==",
"license": "MIT",
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
"integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
@ -370,208 +344,228 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz",
"integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
"integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz",
"integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz",
"integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz",
"integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz",
"integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz",
"integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz",
"integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz",
"integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz",
"integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz",
"integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz",
"integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz",
"integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz",
"integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz",
"integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz",
"integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz",
"integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz",
"integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz",
"integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==",
"cpu": [
"loong64"
],
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz",
"integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz",
"integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz",
"integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz",
"integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz",
"integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz",
"integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz",
"integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz",
"integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz",
"integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz",
"integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz",
"integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz",
"integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz",
"integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz",
"integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz",
"integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz",
"integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
@ -580,21 +574,18 @@
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"license": "MIT"
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
},
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"license": "MIT"
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="
},
"node_modules/acorn": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz",
"integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==",
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@ -606,14 +597,12 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/codemirror": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
@ -628,20 +617,17 @@
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@ -649,15 +635,13 @@
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
@ -670,7 +654,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -679,7 +662,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
@ -688,10 +670,9 @@
}
},
"node_modules/is-core-module": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"license": "MIT",
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dependencies": {
"hasown": "^2.0.2"
},
@ -705,20 +686,17 @@
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"license": "MIT"
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
@ -731,33 +709,33 @@
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
}
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"license": "MIT",
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dependencies": {
"is-core-module": "^2.13.0",
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/rollup": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
"integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==",
"license": "MIT",
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz",
"integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==",
"dependencies": {
"@types/estree": "1.0.6"
},
@ -769,22 +747,25 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.24.0",
"@rollup/rollup-android-arm64": "4.24.0",
"@rollup/rollup-darwin-arm64": "4.24.0",
"@rollup/rollup-darwin-x64": "4.24.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.24.0",
"@rollup/rollup-linux-arm-musleabihf": "4.24.0",
"@rollup/rollup-linux-arm64-gnu": "4.24.0",
"@rollup/rollup-linux-arm64-musl": "4.24.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.24.0",
"@rollup/rollup-linux-riscv64-gnu": "4.24.0",
"@rollup/rollup-linux-s390x-gnu": "4.24.0",
"@rollup/rollup-linux-x64-gnu": "4.24.0",
"@rollup/rollup-linux-x64-musl": "4.24.0",
"@rollup/rollup-win32-arm64-msvc": "4.24.0",
"@rollup/rollup-win32-ia32-msvc": "4.24.0",
"@rollup/rollup-win32-x64-msvc": "4.24.0",
"@rollup/rollup-android-arm-eabi": "4.34.8",
"@rollup/rollup-android-arm64": "4.34.8",
"@rollup/rollup-darwin-arm64": "4.34.8",
"@rollup/rollup-darwin-x64": "4.34.8",
"@rollup/rollup-freebsd-arm64": "4.34.8",
"@rollup/rollup-freebsd-x64": "4.34.8",
"@rollup/rollup-linux-arm-gnueabihf": "4.34.8",
"@rollup/rollup-linux-arm-musleabihf": "4.34.8",
"@rollup/rollup-linux-arm64-gnu": "4.34.8",
"@rollup/rollup-linux-arm64-musl": "4.34.8",
"@rollup/rollup-linux-loongarch64-gnu": "4.34.8",
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.8",
"@rollup/rollup-linux-riscv64-gnu": "4.34.8",
"@rollup/rollup-linux-s390x-gnu": "4.34.8",
"@rollup/rollup-linux-x64-gnu": "4.34.8",
"@rollup/rollup-linux-x64-musl": "4.34.8",
"@rollup/rollup-win32-arm64-msvc": "4.34.8",
"@rollup/rollup-win32-ia32-msvc": "4.34.8",
"@rollup/rollup-win32-x64-msvc": "4.34.8",
"fsevents": "~2.3.2"
}
},
@ -806,15 +787,13 @@
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
]
},
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
}
@ -823,15 +802,13 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz",
"integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==",
"dev": true,
"license": "MIT"
"dev": true
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@ -841,7 +818,6 @@
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@ -850,14 +826,12 @@
"node_modules/style-mod": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"license": "MIT"
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
@ -866,11 +840,10 @@
}
},
"node_modules/terser": {
"version": "5.36.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz",
"integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==",
"version": "5.39.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@ -887,8 +860,7 @@
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
}
}
}

2
deps/libuv vendored

Submodule deps/libuv updated: e1095c7a43...8fb9cb9194

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

@ -0,0 +1,2 @@
(()=>{var D="./favicon-16x16-VSI62OPJ.png";})();
//# sourceMappingURL=favicon-16x16-V2DMIAZS.js.map

Before

(image error) Size: 679 B

After

(image error) Size: 679 B

Before

(image error) Size: 1.5 KiB

After

(image error) Size: 1.5 KiB

@ -0,0 +1,2 @@
(()=>{var T="./favicon-32x32-3EB2YCUY.png";})();
//# sourceMappingURL=favicon-32x32-THY3JDJL.js.map

BIN
deps/speedscope/favicon-FOKUP5Y5.ico vendored Normal file

Binary file not shown.

After

(image error) Size: 15 KiB

2
deps/speedscope/favicon-M34RF7BI.js vendored Normal file

@ -0,0 +1,2 @@
(()=>{var m="./favicon-FOKUP5Y5.ico";})();
//# sourceMappingURL=favicon-M34RF7BI.js.map

File diff suppressed because one or more lines are too long

@ -1,2 +1,19 @@
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>speedscope</title><link href="https://fonts.googleapis.com/css?family=Source+Code+Pro" rel="stylesheet"><script></script><link rel="stylesheet" href="reset.8c46b7a1.css"><link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.bc503437.png"><link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.f74b3187.png"></head><body> <script src="speedscope.80eb88d2.js"></script>
</body></html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>speedscope</title>
<link rel="stylesheet" href="speedscope-GHPHNKXC.css">
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32-3EB2YCUY.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16-VSI62OPJ.png">
<link rel="icon" type="image/x-icon" href="favicon-FOKUP5Y5.ico">
</head>
<body>
<script src="speedscope-VHEG2FVF.js"></script>
</body>
</html>

@ -1,3 +1,3 @@
speedscope@1.20.0
Fri Jan 12 09:57:49 PST 2024
68fd88ceaf93d89aa27f3f0a20a27c9cfdc015c5
speedscope@1.22.2
Sat Feb 15 13:02:38 PST 2025
1c254dcb3e2b4f6d921340d20e972d9d27b788f4

@ -1,2 +0,0 @@
a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:initial}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{overflow:hidden}body,html{height:100%}body{overflow:auto}
/*# sourceMappingURL=reset.8c46b7a1.css.map */

@ -0,0 +1,93 @@
Copyright 2010-2019 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

File diff suppressed because one or more lines are too long

@ -0,0 +1,2 @@
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{overflow:hidden;height:100%}body{height:100%;overflow:auto}@font-face{font-family:Source Code Pro;font-weight:400;font-style:normal;font-stretch:normal;src:url("./SourceCodePro-Regular.ttf-ILST5JV6.woff2") format("woff2")}
/*# sourceMappingURL=speedscope-GHPHNKXC.css.map */

189
deps/speedscope/speedscope-VHEG2FVF.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

675
deps/sqlite/shell.c vendored

File diff suppressed because it is too large Load Diff

6112
deps/sqlite/sqlite3.c vendored

File diff suppressed because it is too large Load Diff

209
deps/sqlite/sqlite3.h vendored

@ -146,9 +146,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.47.0"
#define SQLITE_VERSION_NUMBER 3047000
#define SQLITE_SOURCE_ID "2024-10-21 16:30:22 03a9703e27c44437c39363d0baf82db4ebc94538a0f28411c85dda156f82636e"
#define SQLITE_VERSION "3.49.1"
#define SQLITE_VERSION_NUMBER 3049001
#define SQLITE_SOURCE_ID "2025-02-18 13:38:58 873d4e274b4988d260ba8354a9718324a1c26187a4ab4c1cc0227c03d0f10e70"
/*
** CAPI3REF: Run-Time Library Version Numbers
@ -652,6 +652,13 @@ SQLITE_API int sqlite3_exec(
** filesystem supports doing multiple write operations atomically when those
** write operations are bracketed by [SQLITE_FCNTL_BEGIN_ATOMIC_WRITE] and
** [SQLITE_FCNTL_COMMIT_ATOMIC_WRITE].
**
** The SQLITE_IOCAP_SUBPAGE_READ property means that it is ok to read
** from the database file in amounts that are not a multiple of the
** page size and that do not begin at a page boundary. Without this
** property, SQLite is careful to only do full-page reads and write
** on aligned pages, with the one exception that it will do a sub-page
** read of the first page to access the database header.
*/
#define SQLITE_IOCAP_ATOMIC 0x00000001
#define SQLITE_IOCAP_ATOMIC512 0x00000002
@ -668,6 +675,7 @@ SQLITE_API int sqlite3_exec(
#define SQLITE_IOCAP_POWERSAFE_OVERWRITE 0x00001000
#define SQLITE_IOCAP_IMMUTABLE 0x00002000
#define SQLITE_IOCAP_BATCH_ATOMIC 0x00004000
#define SQLITE_IOCAP_SUBPAGE_READ 0x00008000
/*
** CAPI3REF: File Locking Levels
@ -814,6 +822,7 @@ struct sqlite3_file {
** <li> [SQLITE_IOCAP_POWERSAFE_OVERWRITE]
** <li> [SQLITE_IOCAP_IMMUTABLE]
** <li> [SQLITE_IOCAP_BATCH_ATOMIC]
** <li> [SQLITE_IOCAP_SUBPAGE_READ]
** </ul>
**
** The SQLITE_IOCAP_ATOMIC property means that all writes of
@ -1091,6 +1100,11 @@ struct sqlite3_io_methods {
** pointed to by the pArg argument. This capability is used during testing
** and only needs to be supported when SQLITE_TEST is defined.
**
** <li>[[SQLITE_FCNTL_NULL_IO]]
** The [SQLITE_FCNTL_NULL_IO] opcode sets the low-level file descriptor
** or file handle for the [sqlite3_file] object such that it will no longer
** read or write to the database file.
**
** <li>[[SQLITE_FCNTL_WAL_BLOCK]]
** The [SQLITE_FCNTL_WAL_BLOCK] is a signal to the VFS layer that it might
** be advantageous to block on the next WAL lock if the lock is not immediately
@ -1244,6 +1258,7 @@ struct sqlite3_io_methods {
#define SQLITE_FCNTL_EXTERNAL_READER 40
#define SQLITE_FCNTL_CKSM_FILE 41
#define SQLITE_FCNTL_RESET_CACHE 42
#define SQLITE_FCNTL_NULL_IO 43
/* deprecated names */
#define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE
@ -2196,7 +2211,15 @@ struct sqlite3_mem_methods {
** CAPI3REF: Database Connection Configuration Options
**
** These constants are the available integer configuration options that
** can be passed as the second argument to the [sqlite3_db_config()] interface.
** can be passed as the second parameter to the [sqlite3_db_config()] interface.
**
** The [sqlite3_db_config()] interface is a var-args functions. It takes a
** variable number of parameters, though always at least two. The number of
** parameters passed into sqlite3_db_config() depends on which of these
** constants is given as the second parameter. This documentation page
** refers to parameters beyond the second as "arguments". Thus, when this
** page says "the N-th argument" it means "the N-th parameter past the
** configuration option" or "the (N+2)-th parameter to sqlite3_db_config()".
**
** New configuration options may be added in future releases of SQLite.
** Existing configuration options might be discontinued. Applications
@ -2208,8 +2231,14 @@ struct sqlite3_mem_methods {
** <dl>
** [[SQLITE_DBCONFIG_LOOKASIDE]]
** <dt>SQLITE_DBCONFIG_LOOKASIDE</dt>
** <dd> ^This option takes three additional arguments that determine the
** [lookaside memory allocator] configuration for the [database connection].
** <dd> The SQLITE_DBCONFIG_LOOKASIDE option is used to adjust the
** configuration of the lookaside memory allocator within a database
** connection.
** The arguments to the SQLITE_DBCONFIG_LOOKASIDE option are <i>not</i>
** in the [DBCONFIG arguments|usual format].
** The SQLITE_DBCONFIG_LOOKASIDE option takes three arguments, not two,
** so that a call to [sqlite3_db_config()] that uses SQLITE_DBCONFIG_LOOKASIDE
** should have a total of five parameters.
** ^The first argument (the third parameter to [sqlite3_db_config()] is a
** pointer to a memory buffer to use for lookaside memory.
** ^The first argument after the SQLITE_DBCONFIG_LOOKASIDE verb
@ -2232,7 +2261,8 @@ struct sqlite3_mem_methods {
** [[SQLITE_DBCONFIG_ENABLE_FKEY]]
** <dt>SQLITE_DBCONFIG_ENABLE_FKEY</dt>
** <dd> ^This option is used to enable or disable the enforcement of
** [foreign key constraints]. There should be two additional arguments.
** [foreign key constraints]. This is the same setting that is
** enabled or disabled by the [PRAGMA foreign_keys] statement.
** The first argument is an integer which is 0 to disable FK enforcement,
** positive to enable FK enforcement or negative to leave FK enforcement
** unchanged. The second parameter is a pointer to an integer into which
@ -2254,13 +2284,13 @@ struct sqlite3_mem_methods {
** <p>Originally this option disabled all triggers. ^(However, since
** SQLite version 3.35.0, TEMP triggers are still allowed even if
** this option is off. So, in other words, this option now only disables
** triggers in the main database schema or in the schemas of ATTACH-ed
** triggers in the main database schema or in the schemas of [ATTACH]-ed
** databases.)^ </dd>
**
** [[SQLITE_DBCONFIG_ENABLE_VIEW]]
** <dt>SQLITE_DBCONFIG_ENABLE_VIEW</dt>
** <dd> ^This option is used to enable or disable [CREATE VIEW | views].
** There should be two additional arguments.
** There must be two additional arguments.
** The first argument is an integer which is 0 to disable views,
** positive to enable views or negative to leave the setting unchanged.
** The second parameter is a pointer to an integer into which
@ -2279,7 +2309,7 @@ struct sqlite3_mem_methods {
** <dd> ^This option is used to enable or disable the
** [fts3_tokenizer()] function which is part of the
** [FTS3] full-text search engine extension.
** There should be two additional arguments.
** There must be two additional arguments.
** The first argument is an integer which is 0 to disable fts3_tokenizer() or
** positive to enable fts3_tokenizer() or negative to leave the setting
** unchanged.
@ -2294,7 +2324,7 @@ struct sqlite3_mem_methods {
** interface independently of the [load_extension()] SQL function.
** The [sqlite3_enable_load_extension()] API enables or disables both the
** C-API [sqlite3_load_extension()] and the SQL function [load_extension()].
** There should be two additional arguments.
** There must be two additional arguments.
** When the first argument to this interface is 1, then only the C-API is
** enabled and the SQL function remains disabled. If the first argument to
** this interface is 0, then both the C-API and the SQL function are disabled.
@ -2308,23 +2338,30 @@ struct sqlite3_mem_methods {
**
** [[SQLITE_DBCONFIG_MAINDBNAME]] <dt>SQLITE_DBCONFIG_MAINDBNAME</dt>
** <dd> ^This option is used to change the name of the "main" database
** schema. ^The sole argument is a pointer to a constant UTF8 string
** which will become the new schema name in place of "main". ^SQLite
** does not make a copy of the new main schema name string, so the application
** must ensure that the argument passed into this DBCONFIG option is unchanged
** until after the database connection closes.
** schema. This option does not follow the
** [DBCONFIG arguments|usual SQLITE_DBCONFIG argument format].
** This option takes exactly one additional argument so that the
** [sqlite3_db_config()] call has a total of three parameters. The
** extra argument must be a pointer to a constant UTF8 string which
** will become the new schema name in place of "main". ^SQLite does
** not make a copy of the new main schema name string, so the application
** must ensure that the argument passed into SQLITE_DBCONFIG MAINDBNAME
** is unchanged until after the database connection closes.
** </dd>
**
** [[SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE]]
** <dt>SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE</dt>
** <dd> Usually, when a database in wal mode is closed or detached from a
** database handle, SQLite checks if this will mean that there are now no
** connections at all to the database. If so, it performs a checkpoint
** operation before closing the connection. This option may be used to
** override this behavior. The first parameter passed to this operation
** is an integer - positive to disable checkpoints-on-close, or zero (the
** default) to enable them, and negative to leave the setting unchanged.
** The second parameter is a pointer to an integer
** <dd> Usually, when a database in [WAL mode] is closed or detached from a
** database handle, SQLite checks if if there are other connections to the
** same database, and if there are no other database connection (if the
** connection being closed is the last open connection to the database),
** then SQLite performs a [checkpoint] before closing the connection and
** deletes the WAL file. The SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE option can
** be used to override that behavior. The first argument passed to this
** operation (the third parameter to [sqlite3_db_config()]) is an integer
** which is positive to disable checkpoints-on-close, or zero (the default)
** to enable them, and negative to leave the setting unchanged.
** The second argument (the fourth parameter) is a pointer to an integer
** into which is written 0 or 1 to indicate whether checkpoints-on-close
** have been disabled - 0 if they are not disabled, 1 if they are.
** </dd>
@ -2485,7 +2522,7 @@ struct sqlite3_mem_methods {
** statistics. For statistics to be collected, the flag must be set on
** the database handle both when the SQL statement is prepared and when it
** is stepped. The flag is set (collection of statistics is enabled)
** by default. This option takes two arguments: an integer and a pointer to
** by default. <p>This option takes two arguments: an integer and a pointer to
** an integer.. The first argument is 1, 0, or -1 to enable, disable, or
** leave unchanged the statement scanstatus option. If the second argument
** is not NULL, then the value of the statement scanstatus setting after
@ -2499,7 +2536,7 @@ struct sqlite3_mem_methods {
** in which tables and indexes are scanned so that the scans start at the end
** and work toward the beginning rather than starting at the beginning and
** working toward the end. Setting SQLITE_DBCONFIG_REVERSE_SCANORDER is the
** same as setting [PRAGMA reverse_unordered_selects]. This option takes
** same as setting [PRAGMA reverse_unordered_selects]. <p>This option takes
** two arguments which are an integer and a pointer to an integer. The first
** argument is 1, 0, or -1 to enable, disable, or leave unchanged the
** reverse scan order flag, respectively. If the second argument is not NULL,
@ -2508,7 +2545,76 @@ struct sqlite3_mem_methods {
** first argument.
** </dd>
**
** [[SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE]]
** <dt>SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE</dt>
** <dd>The SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE option enables or disables
** the ability of the [ATTACH DATABASE] SQL command to create a new database
** file if the database filed named in the ATTACH command does not already
** exist. This ability of ATTACH to create a new database is enabled by
** default. Applications can disable or reenable the ability for ATTACH to
** create new database files using this DBCONFIG option.<p>
** This option takes two arguments which are an integer and a pointer
** to an integer. The first argument is 1, 0, or -1 to enable, disable, or
** leave unchanged the attach-create flag, respectively. If the second
** argument is not NULL, then 0 or 1 is written into the integer that the
** second argument points to depending on if the attach-create flag is set
** after processing the first argument.
** </dd>
**
** [[SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE]]
** <dt>SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE</dt>
** <dd>The SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE option enables or disables the
** ability of the [ATTACH DATABASE] SQL command to open a database for writing.
** This capability is enabled by default. Applications can disable or
** reenable this capability using the current DBCONFIG option. If the
** the this capability is disabled, the [ATTACH] command will still work,
** but the database will be opened read-only. If this option is disabled,
** then the ability to create a new database using [ATTACH] is also disabled,
** regardless of the value of the [SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE]
** option.<p>
** This option takes two arguments which are an integer and a pointer
** to an integer. The first argument is 1, 0, or -1 to enable, disable, or
** leave unchanged the ability to ATTACH another database for writing,
** respectively. If the second argument is not NULL, then 0 or 1 is written
** into the integer to which the second argument points, depending on whether
** the ability to ATTACH a read/write database is enabled or disabled
** after processing the first argument.
** </dd>
**
** [[SQLITE_DBCONFIG_ENABLE_COMMENTS]]
** <dt>SQLITE_DBCONFIG_ENABLE_COMMENTS</dt>
** <dd>The SQLITE_DBCONFIG_ENABLE_COMMENTS option enables or disables the
** ability to include comments in SQL text. Comments are enabled by default.
** An application can disable or reenable comments in SQL text using this
** DBCONFIG option.<p>
** This option takes two arguments which are an integer and a pointer
** to an integer. The first argument is 1, 0, or -1 to enable, disable, or
** leave unchanged the ability to use comments in SQL text,
** respectively. If the second argument is not NULL, then 0 or 1 is written
** into the integer that the second argument points to depending on if
** comments are allowed in SQL text after processing the first argument.
** </dd>
**
** </dl>
**
** [[DBCONFIG arguments]] <h3>Arguments To SQLITE_DBCONFIG Options</h3>
**
** <p>Most of the SQLITE_DBCONFIG options take two arguments, so that the
** overall call to [sqlite3_db_config()] has a total of four parameters.
** The first argument (the third parameter to sqlite3_db_config()) is a integer.
** The second argument is a pointer to an integer. If the first argument is 1,
** then the option becomes enabled. If the first integer argument is 0, then the
** option is disabled. If the first argument is -1, then the option setting
** is unchanged. The second argument, the pointer to an integer, may be NULL.
** If the second argument is not NULL, then a value of 0 or 1 is written into
** the integer to which the second argument points, depending on whether the
** setting is disabled or enabled after applying any changes specified by
** the first argument.
**
** <p>While most SQLITE_DBCONFIG options use the argument format
** described in the previous paragraph, the [SQLITE_DBCONFIG_MAINDBNAME]
** and [SQLITE_DBCONFIG_LOOKASIDE] options are different. See the
** documentation of those exceptional options for details.
*/
#define SQLITE_DBCONFIG_MAINDBNAME 1000 /* const char* */
#define SQLITE_DBCONFIG_LOOKASIDE 1001 /* void* int int */
@ -2530,7 +2636,10 @@ struct sqlite3_mem_methods {
#define SQLITE_DBCONFIG_TRUSTED_SCHEMA 1017 /* int int* */
#define SQLITE_DBCONFIG_STMT_SCANSTATUS 1018 /* int int* */
#define SQLITE_DBCONFIG_REVERSE_SCANORDER 1019 /* int int* */
#define SQLITE_DBCONFIG_MAX 1019 /* Largest DBCONFIG */
#define SQLITE_DBCONFIG_ENABLE_ATTACH_CREATE 1020 /* int int* */
#define SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE 1021 /* int int* */
#define SQLITE_DBCONFIG_ENABLE_COMMENTS 1022 /* int int* */
#define SQLITE_DBCONFIG_MAX 1022 /* Largest DBCONFIG */
/*
** CAPI3REF: Enable Or Disable Extended Result Codes
@ -2622,10 +2731,14 @@ SQLITE_API void sqlite3_set_last_insert_rowid(sqlite3*,sqlite3_int64);
** deleted by the most recently completed INSERT, UPDATE or DELETE
** statement on the database connection specified by the only parameter.
** The two functions are identical except for the type of the return value
** and that if the number of rows modified by the most recent INSERT, UPDATE
** and that if the number of rows modified by the most recent INSERT, UPDATE,
** or DELETE is greater than the maximum value supported by type "int", then
** the return value of sqlite3_changes() is undefined. ^Executing any other
** type of SQL statement does not modify the value returned by these functions.
** For the purposes of this interface, a CREATE TABLE AS SELECT statement
** does not count as an INSERT, UPDATE or DELETE statement and hence the rows
** added to the new table by the CREATE TABLE AS SELECT statement are not
** counted.
**
** ^Only changes made directly by the INSERT, UPDATE or DELETE statement are
** considered - auxiliary changes caused by [CREATE TRIGGER | triggers],
@ -4185,11 +4298,22 @@ SQLITE_API int sqlite3_limit(sqlite3*, int id, int newVal);
** <dd>The SQLITE_PREPARE_NO_VTAB flag causes the SQL compiler
** to return an error (error code SQLITE_ERROR) if the statement uses
** any virtual tables.
**
** [[SQLITE_PREPARE_DONT_LOG]] <dt>SQLITE_PREPARE_DONT_LOG</dt>
** <dd>The SQLITE_PREPARE_DONT_LOG flag prevents SQL compiler
** errors from being sent to the error log defined by
** [SQLITE_CONFIG_LOG]. This can be used, for example, to do test
** compiles to see if some SQL syntax is well-formed, without generating
** messages on the global error log when it is not. If the test compile
** fails, the sqlite3_prepare_v3() call returns the same error indications
** with or without this flag; it just omits the call to [sqlite3_log()] that
** logs the error.
** </dl>
*/
#define SQLITE_PREPARE_PERSISTENT 0x01
#define SQLITE_PREPARE_NORMALIZE 0x02
#define SQLITE_PREPARE_NO_VTAB 0x04
#define SQLITE_PREPARE_DONT_LOG 0x10
/*
** CAPI3REF: Compiling An SQL Statement
@ -10718,8 +10842,9 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const c
/*
** CAPI3REF: Serialize a database
**
** The sqlite3_serialize(D,S,P,F) interface returns a pointer to memory
** that is a serialization of the S database on [database connection] D.
** The sqlite3_serialize(D,S,P,F) interface returns a pointer to
** memory that is a serialization of the S database on
** [database connection] D. If S is a NULL pointer, the main database is used.
** If P is not a NULL pointer, then the size of the database in bytes
** is written into *P.
**
@ -10880,7 +11005,7 @@ SQLITE_API int sqlite3_deserialize(
#ifdef __cplusplus
} /* End of the 'extern "C"' block */
#endif
#endif /* SQLITE3_H */
/* #endif for SQLITE3_H will be added by mksqlite3.tcl */
/******** Begin file sqlite3rtree.h *********/
/*
@ -13131,14 +13256,29 @@ struct Fts5PhraseIter {
** value returned by xInstCount(), SQLITE_RANGE is returned. Otherwise,
** output variable (*ppToken) is set to point to a buffer containing the
** matching document token, and (*pnToken) to the size of that buffer in
** bytes. This API is not available if the specified token matches a
** prefix query term. In that case both output variables are always set
** to 0.
** bytes.
**
** The output text is not a copy of the document text that was tokenized.
** It is the output of the tokenizer module. For tokendata=1 tables, this
** includes any embedded 0x00 and trailing data.
**
** This API may be slow in some cases if the token identified by parameters
** iIdx and iToken matched a prefix token in the query. In most cases, the
** first call to this API for each prefix token in the query is forced
** to scan the portion of the full-text index that matches the prefix
** token to collect the extra data required by this API. If the prefix
** token matches a large number of token instances in the document set,
** this may be a performance problem.
**
** If the user knows in advance that a query may use this API for a
** prefix token, FTS5 may be configured to collect all required data as part
** of the initial querying of the full-text index, avoiding the second scan
** entirely. This also causes prefix queries that do not use this API to
** run more slowly and use more memory. FTS5 may be configured in this way
** either on a per-table basis using the [FTS5 insttoken | 'insttoken']
** option, or on a per-query basis using the
** [fts5_insttoken | fts5_insttoken()] user function.
**
** This API can be quite slow if used with an FTS5 table created with the
** "detail=none" or "detail=column" option.
**
@ -13572,3 +13712,4 @@ struct fts5_api {
#endif /* _FTS5_H */
/******** End of fts5.h *********/
#endif /* SQLITE3_H */

1
deps/zsign vendored Submodule

Submodule deps/zsign added at d995d539ff

@ -1,4 +1,4 @@
# Tilde Friends Cheat Sheet
# App Development Cheat Sheet
Making apps for the impatient tilde friend.
@ -18,36 +18,36 @@ Making apps for the impatient tilde friend.
## Output
- **`app.setDocument(html)`** - send HTML to the browser
- **`print(...)`** - send values to the browser's developer console
- `app.setDocument(html)` - send HTML to the browser
- `print(...)` - send values to the browser's developer console
## Persistence
- **`app.localStorageGet(key)`** -> **`value`**
- **`app.localStorageSet(key, value)`**
- **`database()`**, **`shared_database(key)`**, **`my_shared_database(package, key)`**
- **`db.get(key)`** -> **`value`**
- **`db.set(key, value)`**
- **`db.exchange(key, expected, value)`** -> **`exchanged`**
- **`db.remove(key)`**
- **`db.getAll()`** -> **`[key1, ...]`**
- **`db.getLike(pattern)`** -> **`{key1: value1, ...}`**
- `app.localStorageGet(key)` -> `value`
- `app.localStorageSet(key, value)`
- `database()`, `shared_database(key)`, `my_shared_database(package, key)`
- `db.get(key)` -> `value`
- `db.set(key, value)`
- `db.exchange(key, expected, value)` -> `exchanged`
- `db.remove(key)`
- `db.getAll()` -> `[key1, ...]`
- `db.getLike(pattern)` -> `{key1: value1, ...}`
## SSB
- **`ssb.createIdentity()`** -> **`id`**
- **`ssb.getIdentities()`** -> **`[id1, ...]`**
- **`ssb.appendMessageWithIdentity(id, content)`** -> **`message_id`**
- **`ssb.blobStore(blob)`** -> **`blob_id`**
- **`ssb.blobGet(id)`** -> **`blob`**
- **`ssb.sqlAsync(query, args, row_callback)`**
- `ssb.createIdentity()` -> `id`
- `ssb.getIdentities()` -> `[id1, ...]`
- `ssb.appendMessageWithIdentity(id, content)` -> `message_id`
- `ssb.blobStore(blob)` -> `blob_id`
- `ssb.blobGet(id)` -> `blob`
- `ssb.sqlAsync(query, args, row_callback)`
## TF-RPC
Stock helper code for calling functions across the web server and browser boundary.
- on the server: `import * as tfrpc from '/tfrpc.js';`
- in the browser: `import * as tfrpc from '/static/tfrpc.js';`
- on the server: `import * as tfrpc from "/tfrpc.js";`
- in the browser: `import * as tfrpc from "/static/tfrpc.js";`
- either direction:
- register a function: `tfrpc.register(function my_function() {});`
- call a remote function: `let promise = tfrpc.rpc.my_function();`

@ -1,4 +1,4 @@
# Tilde Friends Developer's Guide
# App Development Guide
A Tilde Friends application starts with code that runs on a Tilde Friends server, possibly far away from where you wrote it, in a little JavaScript environment, in its own restricted process, with the only access to the outside world being the ability to send messages to the server. This document gives some recipes showing how that can be used to build a functional user-facing application in light of the unique constraints present.
@ -8,13 +8,13 @@ Of course we must start with a classic.
### app.js
```
```js
app.setDocument('<h1 style="color: #fff">Hello, world!</h1>');
```
### Output
<iframe srcdoc="&lt;h1 style=&quot;color: #fff&quot;&gt;Hello, world!&lt;/h1&gt;"></iframe>
<h1 style="color: #fff">Hello, world!</h1>
### Explanation
@ -35,7 +35,7 @@ Let's take advantage of code running on the server and create a little hit count
### app.js
```
```js
async function main() {
let db = await shared_database('visitors');
let count = parseInt((await db.get('visitors')) ?? '0') + 1;
@ -50,7 +50,7 @@ main();
### Output
<iframe srcdoc="&lt;h1 style=&quot;color: #fff&quot;&gt;Welcome, visitor #1!&lt;/h1&gt;"></iframe>
<h1 style="color: #fff">Welcome, visitor #1!</h1>
### Explanation
@ -66,7 +66,7 @@ Suppose you don't want to create your entire app in a single server-side file as
### app.js
```
```js
async function main() {
let html = utf8Decode(await getFile('index.html'));
app.setDocument(html);
@ -77,7 +77,7 @@ main();
### index.html
```
```html
<html>
<head>
<script type="module" src="script.js"></script>
@ -90,7 +90,7 @@ main();
### script.js
```
```js
window.addEventListener('load', function() {
document.body.appendChild(document.createTextNode('Hello, world');
});
@ -98,7 +98,7 @@ window.addEventListener('load', function() {
### Output
<iframe srcdoc="&lt;body style=&quot;color: #fff&quot;&gt;<h1>File Test</h1>Hello, world!&lt;/body&gt;"></iframe>
<h1>File Test</h1><p>Hello, world!</p>
### Explanation
@ -110,11 +110,11 @@ While making calls between the client and the server, it is possible to pass fun
### app.js
```
```js
import * as tf from '/tfrpc.js';
function sum() {
let s = 0
let s = 0;
for (let x of arguments) {
s += x;
}
@ -130,10 +130,10 @@ main();
### index.html
```
```html
<html>
<body>
<h1 id='result'>Calculating...</h1>
<h1 id="result">Calculating...</h1>
</body>
<script type="module" src="script.js"></script>
</html>
@ -141,17 +141,17 @@ main();
### script.js
```
```js
import * as tf from '/static/tfrpc.js';
window.addEventListener('load', async function() {
window.addEventListener('load', async function () {
document.getElementById('result').innerText = await tf.rpc.sum(1, 2, 3);
});
```
### Output
<iframe srcdoc="&lt;body style=&quot;color: #fff&quot;&gt;<h1>6</h1>&lt;/body&gt;"></iframe>
<h1>6</h1>
### Explanation

15
docs/inspiration.md Normal file

@ -0,0 +1,15 @@
# Inspiration
This is an ever-growing list of software that is similar to what Tilde Friends tries to be but as far as I can tell don't quite fit the same niche.
- Secure Scuttlebutt Clients
- [Manyverse](https://www.manyver.se/)
- [Patchwork](https://github.com/ssbc/patchwork)
- [Patchfox](https://patchfox.org/#/)
- [Habitat](https://gitlab.com/quickdudley/habitat)
- [Āhau](https://gitlab.com/ahau/ahau/)
- [erlbutt](https://github.com/cmoid/erlbutt/)
- Web Application Platforms
- [Glitch](https://glitch.com/)
- [Val Town](https://www.val.town/)
- [Clace](https://clace.io/)

@ -4,15 +4,20 @@
- run the tests
- format + prettier
- update metadata/en-US/changelogs
- git tag
- git tag v1.2.3
- git tag -f latest_release
- push
- make dist
- make a release on gitea
- upload the artifacts
- upload the AppImage and zsyncmake
- upload to Google
- upload to Apple with dist-ios on macos
- nix
- june and december: update release version
- run `nix flake update`
- comment out the hash in default.nix
- update the version
- run `nix build`
- run `nix-build`
- update the hash
- bump the versions in GNUmakefile for the next release
- make

@ -1,4 +1,4 @@
# Tilde Friends Vision
# Vision
Tilde Friends is a tool for making and sharing.

14
flake.lock generated

@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
@ -20,16 +20,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1717281328,
"narHash": "sha256-evZPzpf59oNcDUXxh2GHcxHkTEG4fjae2ytWP85jXRo=",
"lastModified": 1739758141,
"narHash": "sha256-uq6A2L7o1/tR6VfmYhZWoVAwb3gTy7j4Jx30MIrH0rE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b3b2b28c1daa04fe2ae47c21bb76fd226eac4ca1",
"rev": "c618e28f70257593de75a7044438efc1c1fc0791",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}

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