193 Commits

Author SHA1 Message Date
90b0c22f87 docs: Clean up changelog.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-29 12:03:11 -05:00
07e5188525 bookclub: Fix the bookclub app showing nothing for no bookclub messages.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m35s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-29 11:57:50 -05:00
bb1fcd8bb9 build: Let's build 0.2025.12.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m41s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-29 11:53:14 -05:00
83d9e5fd48 ssb: Fix an intermittent leak observed while running tests. 2025-12-29 11:52:59 -05:00
85bbb9c010 docs: Update build notes.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m29s
Build Tilde Friends / Build-All (push) Successful in 10m13s
2025-12-27 16:30:09 -05:00
82b0fe8d57 build: Actually dist the aarch64 appimage.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m40s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-27 16:27:08 -05:00
00b233dd95 core: A little more tracing. 2025-12-27 16:14:30 -05:00
7154212ddd ssb: Make editing your profile look a little more obvious.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 11m27s
2025-12-27 15:55:16 -05:00
364c4c04ac core: loadSettings() no longer needed.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m23s
Build Tilde Friends / Build-All (push) Successful in 10m56s
2025-12-27 14:51:33 -05:00
878c022934 ssb: Attempt to group related messages (bookclub + bookclubUpdates, wiki + wiki-doc, ...) 2025-12-27 14:35:58 -05:00
2c9654b480 android: Fix copying message ids based on: https://stackoverflow.com/questions/61401384/can-text-within-an-iframe-be-copied-to-clipboard.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m29s
Build Tilde Friends / Build-All (push) Successful in 12m21s
2025-12-27 11:55:29 -05:00
14e36308f9 ssb: Fix the messages_stats trigger.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m41s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-27 11:24:09 -05:00
cd8df2fe15 welcome: Routine re-wordsmithing.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m47s
Build Tilde Friends / Build-All (push) Successful in 10m36s
2025-12-27 11:04:48 -05:00
8abcdd1e7d core: Fix unauthenticated sessions.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m38s
Build Tilde Friends / Build-All (push) Successful in 9m52s
2025-12-26 21:53:54 -05:00
97aeff60cc build: Build an aarch64 .AppImage.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m28s
Build Tilde Friends / Build-All (push) Successful in 10m6s
2025-12-26 21:40:11 -05:00
86d6a5c049 update: CodeMirror. 2025-12-26 21:24:52 -05:00
f6f815eec1 ssb: Fix the oblong spinning refresh button. 2025-12-26 21:22:00 -05:00
73a1c1d978 core: Move ssb.getPrivateKey from JS => C.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m25s
Build Tilde Friends / Build-All (push) Successful in 10m35s
2025-12-26 17:27:36 -05:00
5445072d36 core: Move ssb.deleteIdentity from JS => C. 2025-12-26 17:07:07 -05:00
d9a2519e9b core: Move ssb.addItentity() from JS => C. 2025-12-26 16:48:46 -05:00
687a85dbd8 build: Disable -t auto again. Oh well.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m26s
Build Tilde Friends / Build-All (push) Successful in 10m26s
2025-12-26 16:16:33 -05:00
9e25aa1c54 build: Maybe on trixie?
Some checks failed
Build Tilde Friends / Build-Docs (push) Failing after 4m41s
Build Tilde Friends / Build-All (push) Successful in 11m3s
2025-12-26 10:13:08 -05:00
e309f519f2 build: Oops, actually test the thing.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Failing after 10m55s
2025-12-25 11:18:46 -05:00
5ccd9f16c3 build: Last try.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-25 11:14:10 -05:00
7d596ebd3b build: Maybe like this?
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Failing after 6s
2025-12-25 11:10:01 -05:00
938f728eb9 build: Just curious, can the CI worker run headless selenium tests?
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Failing after 12s
2025-12-25 11:03:22 -05:00
6e8a0031a8 bookclub: Handle both about and bookclubUpdate messages. 2025-12-25 10:38:18 -05:00
085f62aadf core: Move ssb.appendMessageWithIdentity() from JS => C, and fix a permission test lifetime issue along the way.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 9m53s
2025-12-23 17:12:38 -05:00
71d556143b update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m24s
2025-12-23 16:12:23 -05:00
081bff9a26 ssb: Load names and profile info for users we see who we're not following.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m26s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-23 16:09:47 -05:00
f2f4455c82 ssb: Replication fun. Don't request blobs until messages are up to date. Fix multiple issues with blob wants determinating. Fix issues resulting from message added callbacks being merged.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 12m54s
2025-12-23 12:54:01 -05:00
2d38b3bd61 update: appimagetool 1.9.1.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m23s
Build Tilde Friends / Build-All (push) Successful in 10m12s
2025-12-21 22:26:16 -05:00
3e73c9e00b update: bundletool 1.18.3. 2025-12-21 22:21:41 -05:00
9aa0e2eda4 ssb: Make opening and closing private chats work a little more consistently with the sidebar.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m29s
Build Tilde Friends / Build-All (push) Successful in 10m6s
2025-12-21 19:19:54 -05:00
076cc265f8 edit: Make the navigation bar more legible on mobile.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m46s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-21 16:02:29 -05:00
f0ee9808a9 build: nix flake update.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m50s
Build Tilde Friends / Build-All (push) Successful in 9m53s
2025-12-21 15:42:58 -05:00
27be6de208 core: Oh yeah, I just made this unused.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-21 15:28:00 -05:00
304bb82c74 prettier: Yuck.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m35s
Build Tilde Friends / Build-All (push) Successful in 9m52s
2025-12-21 15:14:56 -05:00
ec80f27434 core: Simplify/touch up some of the client-side error handling.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-21 15:08:17 -05:00
12733acd9f core: Slight tweaks to the permissions ui.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m32s
Build Tilde Friends / Build-All (push) Successful in 10m3s
2025-12-21 14:52:31 -05:00
7ba28a269a core: Oops, was terminating queries too early from my earlier error fix.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 10m10s
2025-12-21 14:18:47 -05:00
28e6004c91 edit: Reload static html in its iframe instead of the whole window to make the experience smoother.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m40s
Build Tilde Friends / Build-All (push) Successful in 10m3s
2025-12-21 14:05:25 -05:00
1a1d84e603 welcome: x86_64 is more relevant than 64-bit.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Successful in 9m53s
2025-12-21 13:42:16 -05:00
da1116220c core: Shutdown paranoia / trying to fix an intermittent test failure.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 11m19s
2025-12-21 10:34:14 -05:00
57c945e2cf core: Fix some longstanding exception plumbing issues.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m32s
Build Tilde Friends / Build-All (push) Failing after 10m48s
2025-12-21 10:08:34 -05:00
bef66329b2 apps: Use the indexes more consistently. 2025-12-21 09:01:03 -05:00
4f726ce502 bookclub: Use the index. 2025-12-21 08:46:55 -05:00
cc0266b5e8 intro: Make the buttons more consistent/less obnoxious.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m28s
Build Tilde Friends / Build-All (push) Successful in 10m16s
2025-12-20 17:40:24 -05:00
dad14d1754 bookclub: Load and display reviews.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Successful in 9m57s
2025-12-20 15:58:24 -05:00
c867204233 core: allPermissionsGranted() JS => C. 2025-12-20 14:04:25 -05:00
68eb53b1ff core: Reestablish the websocket connection when disconnected as the mobile app is brought to the front.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 10m31s
2025-12-20 10:34:47 -05:00
403c5fcfe6 ssb: Fixed some private message types not rendering anything at all.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m25s
Build Tilde Friends / Build-All (push) Successful in 10m37s
2025-12-18 22:29:13 -05:00
c0ed9fda01 test: Post and view a private message in the test.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 11m20s
2025-12-18 12:39:48 -05:00
95d263e139 core: Merge App into the process object.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m35s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-17 20:44:27 -05:00
782013f3a3 docs: Prepare some release notes.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 3m4s
Build Tilde Friends / Build-All (push) Successful in 9m58s
2025-12-17 20:16:57 -05:00
a5ed64f866 ssb: Fixing private messaging yourself, and delete a failing not-quite-relevant private message test.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m24s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-17 20:05:14 -05:00
88e3494dcf ssb: Finish moving private message encrypt/decrypt to C. 2025-12-17 19:46:26 -05:00
abe16dcf66 linux: Request no new privileges: https://docs.kernel.org/userspace-api/no_new_privs.html.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-17 19:05:43 -05:00
03a32ca371 ssb: Stop following yourself.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m24s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-17 18:58:09 -05:00
09a4fae432 ssb: Consolidate/fix following and blocking messaging. 2025-12-17 18:55:07 -05:00
5c173b2695 prettier 2025-12-17 18:48:38 -05:00
71493aac51 bookclub: Initial commit. Very simple view of all local bookclub messages for now. 2025-12-17 18:47:30 -05:00
8fb1850044 update: CodeMirror. 2025-12-17 18:13:56 -05:00
bbfcbfcae6 ssb: Avoid one last superfluous reload.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m49s
Build Tilde Friends / Build-All (push) Successful in 10m35s
2025-12-17 12:38:24 -05:00
cd2903c0df ssb: Embiggen the search input box when focused. 2025-12-17 12:24:58 -05:00
d873d99b23 ssb: Handful of URL encoding issues.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m25s
Build Tilde Friends / Build-All (push) Successful in 10m9s
2025-12-15 20:42:57 -05:00
1a5392d942 ssb: Avoid an unnecessary messages load.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m54s
2025-12-15 12:30:27 -05:00
ef80c0910c intro: Scroll to top when switching pages.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 15m36s
2025-12-13 09:01:17 -05:00
6c641acdd3 ssb: Put the hamburger menu on the same line as the welcome text. 2025-12-13 08:57:06 -05:00
f0babc6f95 core: Fix a recently introduced use after free.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Successful in 11m44s
2025-12-12 18:24:23 -05:00
1382eac7e5 ssb: Paranoia around trying to avoid showing stale/irrelevant messages. Need to rethink this approach entirely sometime.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m27s
Build Tilde Friends / Build-All (push) Successful in 9m55s
2025-12-11 22:05:06 -05:00
79b7252a27 ssb: Be much more generous about what's allowed in a hashtag ref. Fixes #dev-diary not behaving correctly as a channel.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m32s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-11 22:01:38 -05:00
2e8402d11d update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Successful in 11m8s
2025-12-11 12:48:44 -05:00
c34065795c build: Get iOS and Android on the same versionCode/CFBundleVersion.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m2s
2025-12-10 12:34:57 -05:00
1463c18c12 core: Unused.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 12m29s
2025-12-10 12:28:44 -05:00
f39b0977b7 build: Fix.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m40s
Build Tilde Friends / Build-All (push) Successful in 10m1s
2025-12-09 21:33:03 -05:00
8f9824e9b7 core: Minor simplification around getting account name.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Failing after 3m24s
2025-12-09 20:30:09 -05:00
33392e7c55 core: Move ssb.swapWithServerIdentity() to C.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m26s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-09 20:21:02 -05:00
b4c014fd27 core: Remove some ancient unused resizeMe, setHash, and storeBlob message handlers.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m31s
Build Tilde Friends / Build-All (push) Successful in 10m4s
2025-12-09 19:05:07 -05:00
81353b4da9 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m37s
Build Tilde Friends / Build-All (push) Successful in 10m8s
2025-12-09 18:48:08 -05:00
d67297c35b ssb: Better search feedback. 2025-12-09 18:43:52 -05:00
192e9e0955 core: Better error handling for deleting users.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m36s
Build Tilde Friends / Build-All (push) Successful in 10m5s
2025-12-09 18:10:47 -05:00
2449202b5d core: Move core.deleteUser() to C.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m26s
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-09 18:02:47 -05:00
f1876a34ec core: Move core.globalSettingSet to C.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m30s
Build Tilde Friends / Build-All (push) Successful in 10m46s
2025-12-09 13:02:25 -05:00
3c6eeb9cd3 core: Move invoking the permission test to C, at least for adding/removing blocks.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m56s
Build Tilde Friends / Build-All (push) Successful in 10m15s
2025-12-08 21:51:10 -05:00
c29ab66073 update: c-ares 1.34.6.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m32s
Build Tilde Friends / Build-All (push) Successful in 10m19s
2025-12-08 12:17:50 -05:00
d7782d53a1 build: Nope, guess we needed those deps.
All checks were successful
Build Tilde Friends / Build-Docs (push) Successful in 2m34s
Build Tilde Friends / Build-All (push) Successful in 9m46s
2025-12-07 14:41:02 -05:00
ce3a8c53c6 build: Build docs separately and on a later image. Also remove some build dependencies I don't think we need.
Some checks failed
Build Tilde Friends / Build-Docs (push) Successful in 2m33s
Build Tilde Friends / Build-All (push) Failing after 8m9s
2025-12-07 14:22:09 -05:00
0af54edac1 docs: Add some slight organization.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m4s
2025-12-07 08:42:34 -05:00
2086075f7b core: Minor cleanup.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m46s
2025-12-07 08:25:55 -05:00
14955fa421 build: #buildfix.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m7s
2025-12-06 23:14:23 -05:00
1e1059489b test: Disable -t=auto on CI.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 9m17s
2025-12-06 23:01:07 -05:00
68dc5129c8 build: Let's see what happens if CI tries to run tests.
Some checks failed
Build Tilde Friends / Build-All (push) Failing after 9m23s
2025-12-06 22:43:38 -05:00
690b027c0c core: Remove app.js.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m3s
2025-12-06 20:55:02 -05:00
2f0c379a69 core: Implement websocket timeout in C.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m6s
2025-12-06 19:13:06 -05:00
7c1931f529 core: Only the timeout remaing for the websocket handler in C?
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m2s
2025-12-06 18:21:42 -05:00
69f9646955 core: Enough websocket in C to run an app. 2025-12-06 18:03:52 -05:00
c4ff00dec1 core: Handle the async process start from the C websocket.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m5s
2025-12-06 15:36:58 -05:00
9ce08f79fb core: Going through the motions of starting a task from C.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m59s
2025-12-06 15:17:22 -05:00
0fa9c90ab9 core: Respond with a session message in the C websocket handler. 2025-12-06 14:54:30 -05:00
2b191a5345 core: parentApp hasn't been a thing in a long while. 2025-12-06 14:30:30 -05:00
6381ba6785 core: Need to be able to parse the app path more differently. 2025-12-06 14:26:07 -05:00
d84b06f814 core: Add a helper for getting a property by string as a string. I've typed this too much.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m15s
2025-12-06 14:04:27 -05:00
3a3b889196 core: Inconsistent ready message in editonly mode. 2025-12-06 13:21:45 -05:00
78474e0bea ios: Fix crashes transitioning between apps in one process mode.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m56s
2025-12-06 13:01:23 -05:00
759d5849ba docs: Add rough notes about moving accounts around.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m14s
2025-12-06 12:34:17 -05:00
0df9796fb8 core: Disable some the javascript autocomplete. Breaking tests and my brain.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m3s
2025-12-06 12:14:58 -05:00
95483b3e55 core: More slight C/websocket progress. 2025-12-06 12:01:26 -05:00
80c0394ec0 core: Miniscule incremental progress on websocket message handling in C.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m36s
2025-12-05 12:57:58 -05:00
1972ce7091 core: Beginnings of websocket handling in C.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m45s
2025-12-04 12:49:35 -05:00
f756d1e5b2 update: speedscope 1.25.0.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m19s
2025-12-03 19:21:18 -05:00
4ebc3b0ccc ssb: Make the search box behave better on mobile.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-03 19:14:49 -05:00
a7099d00b9 ssb: Remove some log output that is breaking tests.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m3s
2025-12-02 19:59:56 -05:00
0ec9010bd4 ssb: Rearrange the navigation bar a bit. 2025-12-02 19:59:43 -05:00
1381696f9b editor: File extension-based language detection.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m42s
2025-12-02 19:26:13 -05:00
19b346cc6d ssb: Move the search to an ever-present textbox on the menu bar. 2025-12-02 18:47:58 -05:00
ec3064e0a1 core: Make a place to implement the server-side websocket handling in C, conditionally.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m18s
2025-12-01 12:28:09 -05:00
af8e60f8c3 cleanup: Stray/stale typedefs. 2025-12-01 12:28:09 -05:00
d3e06434e6 welcome: Replace TestFlight link with iOS App Store.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-12-01 12:24:09 -05:00
83fd005ded ssb: Remove log noise.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m43s
2025-11-29 21:34:25 -05:00
c0dd47ba28 ssb: Oh golly you're supposed to request a pragma analyze periodically this way.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m11s
2025-11-29 16:17:54 -05:00
e44a85c92b ssb: Log noise. 2025-11-29 16:13:02 -05:00
89d3e9b4fe ssb: Mentions have become too slow. Using refs instead of fts seems better and faster, again? 2025-11-29 15:37:17 -05:00
f4c6e2db1f ssb: Plug a leak.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m43s
2025-11-29 13:38:00 -05:00
48406bfe38 docs: Wordsmith this iOS doc blindly some more.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-11-29 13:35:10 -05:00
07c879f5f5 test: Exercise the flag menu option slightly.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m19s
2025-11-29 13:25:48 -05:00
cd34c127d1 ssb: Use the same flag in all the places.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m36s
2025-11-29 12:50:25 -05:00
fb6e554e59 ssb: Respect blocks when getting blocks and accounts at the db level.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m59s
2025-11-29 12:14:34 -05:00
d200e361f7 docs: Mention flagging in the iOS agreement. 2025-11-29 11:47:59 -05:00
bc3fd57d7a ssb: The block list can be crudely managed through the admin app.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m54s
2025-11-29 11:33:16 -05:00
fa4ef3b082 ssb: Show flags on more message type. 2025-11-29 10:51:00 -05:00
0827718d68 ssb: Un-clobber the flagged message UI. Whoops. 2025-11-29 10:41:08 -05:00
0ec862eaac ssb: Fight blog post CSS a bit more.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 12m32s
2025-11-29 10:28:10 -05:00
7e1621dfb4 ssb: Slight improvements to blog header display. 2025-11-29 10:17:13 -05:00
c4d4e3822d update: CodeMirror. 2025-11-29 10:01:20 -05:00
d2e5015eac test: Wait for alerts harder. 2025-11-29 10:01:12 -05:00
510c2f81bd update: sqlite 3.51.1. 2025-11-28 18:42:48 -05:00
4f2e0245d3 ssb: Make blocks begin to do something.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m21s
2025-11-27 16:43:15 -05:00
eecdbf6852 ssb: Exercise at least calling adding/removing blocks in a test.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m12s
2025-11-27 16:06:33 -05:00
ddc4603f13 ssb: Add some plausible API and a table for storing instance-wide blocks. 2025-11-27 14:33:57 -05:00
759b522cd1 ssb: Preliminary view of flagged messages. Seems a bit counter-productive, but here we are.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m3s
2025-11-27 13:32:37 -05:00
7ecb4a192d buttfeed: Add SoapDog's PatchWork.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 11m18s
2025-11-27 13:05:19 -05:00
d84626ac31 ssb: Fix showing flags if we see the messages in the other order.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-11-26 12:16:23 -05:00
9c36e0db7b ssb: Show flagged messages similar to a message with a content warning.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m19s
2025-11-25 19:20:49 -05:00
fcd26bac1c ssb: Add some UI for posting 'flag' messages.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m8s
2025-11-25 19:04:52 -05:00
e8e7c98705 build: wip.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m14s
2025-11-25 18:31:25 -05:00
b5af5cc223 build: Let's start work on the December build. 2025-11-25 18:29:50 -05:00
ba8253fa30 docs: Update change notes.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m50s
2025-11-25 18:08:01 -05:00
f5bd389183 build: This will probably be the November release.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 20m28s
2025-11-25 12:51:13 -05:00
0c34a38e15 ssb: Try not very successfully to make the welcome line format less awkwardly. 2025-11-25 12:50:33 -05:00
7c7857a6cd core: Move speedscope into a trace app. Isolate all the things.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m11s
2025-11-23 20:08:51 -05:00
716bce2bb0 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m10s
2025-11-23 16:37:00 -05:00
33fb96b120 core: Fix a disagreement determining the active identity.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m34s
2025-11-22 09:28:34 -05:00
28a4accabf build: Bump ios => 25.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m35s
2025-11-21 19:42:53 -05:00
31c7394c17 core: Make the navigation bar feel slightly less web-y.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m8s
2025-11-20 12:35:40 -05:00
e2974d34e2 update: bundletool 1.18.2.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m2s
2025-11-19 20:32:50 -05:00
4a06c84511 docs: Prepare some release notes.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m17s
2025-11-19 19:56:14 -05:00
4960a1d9d6 build: Bump iOS.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m1s
2025-11-19 19:20:51 -05:00
75dd8889e9 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m49s
2025-11-19 19:08:55 -05:00
111a6c3c6e ssb: Follow + block = block.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m4s
2025-11-19 18:49:08 -05:00
775fdafa63 core: Fix updating identity info muliple times.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m16s
2025-11-19 18:19:19 -05:00
dae38bbd83 login: Don't show an empty / undefined code of conduct.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m26s
2025-11-19 12:39:06 -05:00
35f374047a ssb: Faster loads around the profile page. An experiment with caching SQL queries, and make one query just plain faster.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 8m53s
2025-11-16 14:20:26 -05:00
aea4a14a62 ssb: Emoji 17.0.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m8s
2025-11-16 13:53:46 -05:00
98f7504a4c core: Refresh identity info so that you see your name at the top right when editing your profile in ssb.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m34s
2025-11-16 13:40:50 -05:00
bb52cdd7c2 ssb: Reduce redundant queries.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m1s
2025-11-16 12:36:12 -05:00
07b660a0d6 core: Explain what global setting we're setting when we ask permission.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 10m35s
2025-11-16 12:01:35 -05:00
2b9d712d48 ssb: Remove now-duplicate logs.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-11-16 12:01:23 -05:00
3c1f60b62d ssb: Log all the query timing.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-11-16 11:59:18 -05:00
bb75edfd42 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m13s
2025-11-15 18:45:06 -05:00
c2b61cec2c ios: Bump.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m2s
2025-11-15 07:32:11 -05:00
05c3107b27 prettier
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m40s
2025-11-13 12:42:28 -05:00
bb67df7846 core: Fix some grammar and style issues with the permission prompt.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-11-13 12:30:42 -05:00
89ec523ea2 format
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m24s
2025-11-12 20:09:10 -05:00
f30458d953 ios: Bump.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-11-12 20:01:07 -05:00
42df0d830e ios: Replace the browser navigation buttons with gestures, disable pinch to zoom, and round a button to make it feel more like a native app.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m30s
2025-11-12 19:31:33 -05:00
50b2c0c7f4 ios: Expose post text to Core Spotlight search.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m54s
2025-11-11 21:40:17 -05:00
0edb76b678 build: Still trying to do an ios thing as 0.2025.10.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m53s
2025-11-11 18:16:13 -05:00
2d71af3243 ssb: Shore more context when presenting a request for permissions to post a message.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m38s
2025-11-10 12:50:05 -05:00
b571cd213b update: libbacktrace.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m38s
2025-11-09 19:25:12 -05:00
b52c79ac4e build: Oops, correct the year.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m46s
2025-11-09 12:16:19 -05:00
63c6a5ab07 format: I don't know hot to configure clang-format to make Objective-C look remotely OK.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-11-09 12:00:54 -05:00
4447ea63e2 ios: Fix exporting/downloading to file.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-11-09 11:54:37 -05:00
61200c4a7d ios: Declare reasons we might use some permissions to avoid crashes.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m57s
2025-11-08 17:10:02 -05:00
62dc9d6cc0 docs: Add a little diagram of how I think about Tilde Friends.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m50s
2025-11-05 20:00:20 -05:00
a28d41e1ee docs: Usage.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 16m54s
2025-11-05 18:54:48 -05:00
6a05e3770b update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m48s
2025-11-05 12:27:00 -05:00
53a93e510c update: sqlite 3.51.0.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-11-05 12:25:07 -05:00
1cb3ecf1ea ios: Just kidding, iOS doesn't allow four number versions.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m53s
2025-11-02 20:32:17 -05:00
687665cd6b ios: Revise the iOS agreement somewhat.
Some checks failed
Build Tilde Friends / Build-All (push) Has been cancelled
2025-11-02 20:30:17 -05:00
7879ab1d50 ios: Add a EULA to try to appease Apple's Guideline 1.2 - Safety - User-Generated Content.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 22m0s
2025-11-02 14:30:25 -05:00
24f0cdb398 update: CodeMirror.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 18m35s
2025-10-30 12:43:05 -04:00
6d5555e596 build: Let's start building 0.2025.11.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 17m45s
2025-10-29 20:26:56 -04:00
126 changed files with 10990 additions and 5245 deletions

View File

@@ -3,6 +3,36 @@ run-name: ${{ gitea.actor }} running 🚀
on: [push]
jobs:
Build-Docs:
runs-on: ubuntu-latest
container:
image: node:trixie-slim
valid_volumes:
- '/opt/keys'
volumes:
- /opt/keys:/opt/keys
steps:
- name: Install build dependencies
run: >
apt update && apt install -y \
build-essential \
doxygen \
file \
git \
graphviz \
rsync \
unzip \
zip
- name: Get code
uses: actions/checkout@v4
with:
submodules: true
- name: Build documentation
run: |
mkdir -p out/html/ ~/.ssh/
make -j`nproc` 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/
Build-All:
runs-on: ubuntu-latest
container:
@@ -50,7 +80,6 @@ jobs:
mkdir -p out/html/ ~/.ssh/
make -j`nproc` 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:
@@ -60,10 +89,12 @@ jobs:
uses: android-actions/setup-android@v3
with:
packages: 'tools platform-tools build-tools;35.0.0 platforms;android-35 ndk;27.2.12479018'
- name: Docker build
run: DOCKER_BUILDKIT=1 docker build .
- name: Build
run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist
- name: Test Debug
run: TF_TEST_auto=0 out/debug/tildefriends test
- name: Docker build
run: DOCKER_BUILDKIT=1 docker build .
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:

View File

@@ -3,6 +3,7 @@ src
deps
.clang-format
flake.lock
apps/trace/speedscope/**
# Minified files
**/*.min.css

View File

@@ -907,7 +907,6 @@ WARN_LOGFILE =
# Note: If this tag is empty the current directory is searched.
INPUT = README.md \
core/app.js \
core/client.js \
core/core.js \
core/tfrpc.js \

View File

@@ -16,17 +16,16 @@ MAKEFLAGS += --no-builtin-rules
## LD := Linker.
## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 44
VERSION_CODE_IOS := 18
VERSION_NUMBER := 0.2025.10
VERSION_CODE := 49
VERSION_NUMBER := 0.2025.12
VERSION_NAME := This program kills fascists.
IPHONEOS_VERSION_MIN=14.0
IPHONEOS_VERSION_MIN=14.5
SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3500400.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
SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3510100.zip
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.18.3/bundletool-all-1.18.3.jar
APPIMAGETOOL_URL := https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := 43264887ffe43cdc02171b3463912168 out/appimagetool
PROJECT = tildefriends
BUILD_DIR ?= out
@@ -815,7 +814,9 @@ $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): CFLAGS += \
$(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
-framework Foundation \
-framework CoreFoundation \
-framework CoreSpotlight \
-framework UIKit \
-framework UniformTypeIdentifiers \
-framework WebKit
##
@@ -891,7 +892,7 @@ src/ios/Info.plist : $(firstword $(MAKEFILE_LIST))
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>CFBundleVersion</key>\^[[:space:]]*<string>)[[:digit:]]+(</string>)@\1$(VERSION_CODE)\2@' \
-e 's@(<key>MinimumOSVersion</key>\^[[:space:]]*<string>)[0-9.]*(</string>)@\1$(IPHONEOS_VERSION_MIN)\2@' | \
tr '^' '\n' > \
$@.tmp && mv $@.tmp $@ || rm -f $@.tmp
@@ -957,8 +958,7 @@ PACKAGE_DIRS := \
core \
deps/codemirror \
deps/prettier \
deps/lit \
deps/speedscope
deps/lit
RAW_FILES := $(sort $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f -not -name '.*')))
@@ -1180,8 +1180,9 @@ ios%go: out/tildefriends-ios%.app/tildefriends
ideviceinstaller -i $(realpath $(dir $<))
iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends ## Build, install, and run an iOS debug build.
xcrun -sdk iphoneos codesign -f -s 'Apple Distribution' --entitlements src/ios/Entitlements.plist --generate-entitlement-der out/tildefriends-iossimdebug.app
xcrun simctl install booted out/tildefriends-iossimdebug.app/
xcrun simctl launch booted com.unprompted.tildefriends
xcrun simctl launch --console booted com.unprompted.tildefriends
.PHONY: iossimdebuggo
out/macos%/tildefriends: out/macos%-arm/tildefriends out/macos%-x86_64/tildefriends
@@ -1212,7 +1213,26 @@ 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 ## Build an AppImage.
out/tildefriends-aarch64.AppImage: out/armrelease/tildefriends out/data.zip
@echo "[appimage] $$@"
@rm -rf out/tildefriends_aarch64.AppDir
@mkdir -p out/tildefriends_aarch64.AppDir/usr/bin
@mkdir -p out/tildefriends_aarch64.AppDir/usr/share/applications
@mkdir -p out/tildefriends_aarch64.AppDir/usr/share/icons/hicolor/scalable/apps
@mkdir -p out/tildefriends_aarch64.AppDir/usr/share/tildefriends
@echo $(APPIMAGETOOL_MD5) > out/appimagetool.md5
@test -x out/appimagetool || curl -q -L -o out/appimagetool $(APPIMAGETOOL_URL) && md5sum -c out/appimagetool.md5 && chmod +x out/appimagetool
@echo "[Desktop Entry]\nName=tildefriends\nExec=/usr/bin/tildefriends\nIcon=/usr/share/icons/hicolor/scalable/apps/tildefriends\nType=Application\nCategories=Network" > out/tildefriends_aarch64.AppDir/tildefriends.desktop
@cp src/ios/tildefriends.svg out/tildefriends_aarch64.AppDir/usr/share/icons/hicolor/scalable/apps/
@cp src/ios/tildefriends.svg out/tildefriends_aarch64.AppDir/
@cp out/armrelease/tildefriends out/tildefriends_aarch64.AppDir/usr/bin/
@cp out/data.zip out/tildefriends_aarch64.AppDir/usr/share/tildefriends/data.zip
@echo "#!/bin/sh\n\$${APPDIR}/usr/bin/tildefriends run -z \$$APPDIR/usr/share/tildefriends/data.zip" > out/tildefriends_aarch64.AppDir/AppRun
@chmod +x out/tildefriends_aarch64.AppDir/AppRun
@cd out; ./appimagetool --appimage-extract; cd ..
@cd out; unset SOURCE_DATE_EPOCH; PATH=$$PATH:squashfs-root/usr/bin ARCH=arm_aarch64 squashfs-root/usr/bin/appimagetool -u 'zsync|https://dev.tildefriends.net/releases/tildefriends-aarch64.AppImage.zsync' tildefriends_aarch64.AppDir tildefriends-aarch64.AppImage; cd ..
appimage: out/tildefriends-x86_64.AppImage out/tildefriends-aarch64.AppImage ## Build AppImages.
.PHONY: appimage
flatpak: out/ ## Build a flatpak.
@@ -1257,7 +1277,6 @@ tarball: ## Build an all-inclusive source tarball (.tar.xz).
--exclude=deps/libsodium/test \
--exclude=deps/libuv/docs \
--exclude=deps/libuv/test \
--exclude=deps/speedscope/*.map \
--exclude=deps/sqlite/shell.c \
--exclude=deps/zlib/contrib/vstudio \
--exclude=deps/zlib/doc \
@@ -1300,6 +1319,8 @@ dist:
@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-aarch64-$(VERSION_NUMBER).AppImage"
@cp out/tildefriends-aarch64.AppImage dist/TildeFriends-aarch64-$(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)"

View File

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

View File

@@ -8,12 +8,20 @@ tfrpc.register(function global_settings_set(key, value) {
return core.globalSettingsSet(key, value);
});
tfrpc.register(function addBlock(id) {
return ssb.addBlock(id);
});
tfrpc.register(function removeBlock(id) {
return ssb.removeBlock(id);
});
async function main() {
try {
let data = {
users: {},
granted: await core.allPermissionsGranted(),
settings: await core.globalSettingsDescriptions(),
blocks: await ssb.getBlocks(),
};
for (let user of await core.users()) {
data.users[user] = await core.permissionsForUser(user);

View File

@@ -16,6 +16,14 @@ function delete_user(user) {
}
}
async function add_block() {
await tfrpc.rpc.addBlock(document.getElementById('add_block').value);
}
async function remove_block(id) {
await tfrpc.rpc.removeBlock(id);
}
function global_settings_set(key, value) {
tfrpc.rpc
.global_settings_set(key, value)
@@ -94,11 +102,32 @@ ${description.value}</textarea
${user}: ${permissions.map((x) => permission_template(x))}
</li>
`;
const block_template = (block) => html`
<li class="w3-card w3-margin">
<button
class="w3-button w3-theme-action"
@click=${(e) => remove_block(block.id)}
>
Delete
</button>
<code>${block.id}</code>
${new Date(block.timestamp)}
</li>
`;
const users_template = (users) =>
html` <header class="w3-container w3-theme-l2"><h2>Users</h2></header>
<ul class="w3-ul">
${Object.entries(users).map((u) => user_template(u[0], u[1]))}
</ul>`;
const blocks_template = (blocks) =>
html` <header class="w3-container w3-theme-l2"><h2>Blocks</h2></header>
<div class="w3-row w3-margin">
<input type="text" class="w3-threequarter w3-input" id="add_block"></input>
<button class="w3-quarter w3-button w3-theme-action" @click=${add_block}>Add</button>
</div>
<ul class="w3-ul">
${blocks.map((b) => block_template(b))}
</ul>`;
const page_template = (data) =>
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header>
@@ -109,7 +138,7 @@ ${description.value}</textarea
.map((x) => html`${input_template(x, data.settings[x])}`)}
</ul>
</div>
${users_template(data.users)}
${users_template(data.users)} ${blocks_template(data.blocks)}
</div> `;
render(page_template(g_data), document.body);
});

5
apps/bookclub.json Normal file
View File

@@ -0,0 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "📖",
"previous": "&Vt7LWYGm9HpVM50+aBJv9Q1FnEf12Gd1+Uyft+IYWGo=.sha256"
}

84
apps/bookclub/app.js Normal file
View File

@@ -0,0 +1,84 @@
async function query(sql, args) {
let result = [];
await ssb.sqlAsync(sql, args, function (row) {
result.push(row);
});
return result;
}
async function main() {
let whoami = await ssb.getActiveIdentity();
let following = Object.keys(await ssb.following([whoami], 2));
let data = await query(
`
SELECT
messages.id,
content ->> 'title' AS title,
content ->> '$.image.link' AS image,
content ->> 'description' AS description,
content ->> 'authors' AS authors
FROM messages, json_each(?) AS following
ON messages.author = following.value
WHERE
content ->> 'type' = 'bookclub' AND
title IS NOT NULL AND
image IS NOT NULL AND
description IS NOT NULL
`,
[JSON.stringify(following)]
);
let books = Object.fromEntries(data.map((x) => [x.id, x]));
for (let book of Object.values(books)) {
try {
book.authors = JSON.parse(book.authors);
} catch {
book.authors = [book.authors];
}
book.reviews = [];
}
let reviews = await query(
`
SELECT author, about, json(json_group_object(key, value)) AS content
FROM (
SELECT
messages.author,
messages.content ->> '$.about' AS about,
fields.key,
RANK() OVER (PARTITION BY messages.author, messages.content ->> '$.about', fields.key ORDER BY messages.sequence DESC) AS rank,
fields.value
FROM messages, json_each(messages.content) AS fields, json_each(?1) AS book, json_each(?2) AS following
ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'about'
AND messages.content ->> '$.about' = book.value
AND NOT fields.key IN ('about', 'type')
UNION
SELECT
messages.author,
messages.content ->> '$.updates' AS about,
fields.key,
RANK() OVER (PARTITION BY messages.author, messages.content ->> '$.updates', fields.key ORDER BY messages.sequence DESC) AS rank,
fields.value
FROM messages, json_each(messages.content) AS fields, json_each(?1) AS book, json_each(?2) AS following
ON messages.author = following.value
WHERE
messages.content ->> 'type' = 'bookclubUpdate'
AND messages.content ->> '$.updates' = book.value
AND NOT fields.key IN ('about', 'updates', 'type')
) WHERE rank = 1
GROUP BY author, about
`,
[JSON.stringify(Object.keys(books)), JSON.stringify(following)]
);
for (let review of reviews) {
review.content = JSON.parse(review.content);
books[review.about].reviews.push(review);
}
await app.setDocument(
utf8Decode(getFile('index.html')).replace('G_DATA', JSON.stringify(data))
);
}
main().catch(function (e) {
throw new Error(e.message);
});

51
apps/bookclub/bc-app.js Normal file
View File

@@ -0,0 +1,51 @@
import {LitElement, html, map, unsafeHTML} from './lit-all.min.js';
import {markdown} from './markdown.js';
class BookClubElement extends LitElement {
render() {
if (!g_data?.length) {
return html`<h1>No bookclub messages to display.</h1>`;
}
return html`
<link rel="stylesheet" href="w3.css"></link>
<div class="w3-grid" style="background-color: #fff; gap:8px; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr))">
${map(
g_data,
(x) => html`
<div class="w3-card-4">
<header class="w3-container w3-center">
<h1>${markdown(x.title)}</h1>
<p>${map(x.authors, (author) => markdown(author))}</p>
</header>
<div class="w3-container w3-center">
<img
src="/${x.image}/view"
style="max-height: 2in; max-width: 2in"
/>
</div>
<div class="w3-container">
<p>${markdown(x.description)}</p>
</div>
<ul class="w3-container w3-list">
${map(
x.reviews.filter(
(x) => x.content?.rating || x.content?.review
),
(review) => html`
<li>
${review.content.rating} /
${review.content.ratingMax ?? 5}
${review.content.ratingType ?? 'stars'}
<div>${review.content.review}</div>
</li>
`
)}
</ul>
</div>
`
)}
</div>`;
}
}
customElements.define('bc-app', BookClubElement);

1
apps/bookclub/commonmark.min.js vendored Normal file

File diff suppressed because one or more lines are too long

13
apps/bookclub/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="w3.css" />
<script>
const g_data = G_DATA;
</script>
</head>
<body style="background-color: #fff">
<bc-app />
</body>
<script type="module" src="bc-app.js"></script>
</html>

120
apps/bookclub/lit-all.min.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

119
apps/bookclub/markdown.js Normal file
View File

@@ -0,0 +1,119 @@
import * as commonmark from './commonmark.min.js';
import {unsafeHTML} from './lit-all.min.js';
function image(node, entering) {
if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')
) {
if (entering) {
this.lit(
'<video style="max-width: 100%; max-height: 480px" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</video>');
}
} else if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')
) {
if (entering) {
this.lit(
'<audio style="height: 32px; max-width: 100%" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</audio>');
}
} else {
if (entering) {
if (this.disableTags === 0) {
this.lit(
'<div class="img_caption">' +
this.esc(node.firstChild?.literal || node.destination) +
'</div>'
);
if (this.options.safe && potentiallyUnsafe(node.destination)) {
this.lit('<img src="" title="');
} else {
this.lit('<img src="' + this.esc(node.destination) + '" title="');
}
}
this.disableTags += 1;
} else {
this.disableTags -= 1;
if (this.disableTags === 0) {
if (node.title) {
this.lit('" title="' + this.esc(node.title));
}
this.lit('" />');
}
}
}
}
function code(node) {
let attrs = this.attrs(node);
attrs.push(['class', k_code_classes]);
this.tag('code', attrs);
this.out(node.literal);
this.tag('/code');
}
function attrs(node) {
let result = commonmark.HtmlRenderer.prototype.attrs.bind(this)(node);
if (node.type == 'block_quote') {
result.push(['class', 'w3-theme-d1']);
} else if (node.type == 'code_block') {
result.push(['class', k_code_classes]);
}
return result;
}
export function markdown(md) {
let reader = new commonmark.Parser();
let writer = new commonmark.HtmlRenderer({safe: true});
writer.image = image;
writer.code = code;
writer.attrs = attrs;
let parsed = reader.parse(md || '');
let walker = parsed.walker();
let event, node;
while ((event = walker.next())) {
node = event.node;
if (event.entering) {
if (node.type == 'link') {
if (
node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')
) {
node.destination = '#' + encodeURIComponent(node.destination);
} else if (
node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')
) {
node.destination = '#' + encodeURIComponent(node.destination);
} else if (
node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')
) {
node.destination = '/' + node.destination + '/view';
}
} else if (node.type == 'image') {
if (node.destination.startsWith('&')) {
node.destination = '/' + node.destination + '/view';
}
}
}
}
return unsafeHTML(writer.render(parsed));
}

251
apps/bookclub/w3.css Normal file
View File

@@ -0,0 +1,251 @@
/* W3.CSS 5.02 March 31 2025 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-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
.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}
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
/* 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-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
.w3-danger{color:#fff!important;background-color:#dd0000!important}
.w3-note{color:#000!important;background-color:#fff599!important}
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
.w3-warning{color:#000!important;background-color:#ffb305!important}
.w3-success{color:#fff!important;background-color:#008a00!important}
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}

View File

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

View File

@@ -18,7 +18,7 @@ async function contacts_internal(id, last_row_id, following, max_row_id) {
WHERE author = ? AND
rowid > ? AND
rowid <= ? AND
json_extract(content, '$.type') = 'contact'
content ->> 'type' = 'contact'
ORDER BY sequence
`,
[id, last_row_id, max_row_id]
@@ -149,7 +149,7 @@ async function fetch_about(db, ids, users) {
messages.author = following.value AND
messages.rowid > ?3 AND
messages.rowid <= ?4 AND
json_extract(messages.content, '$.type') = 'about'
messages.content ->> 'type' = 'about'
UNION
SELECT
messages.*
@@ -159,7 +159,7 @@ async function fetch_about(db, ids, users) {
WHERE
messages.author = following.value AND
messages.rowid <= ?4 AND
json_extract(messages.content, '$.type') = 'about'
messages.content ->> 'type' = 'about'
ORDER BY messages.author, messages.sequence
`,
[

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "💡",
"previous": "&eN6DNPpQUNhGvxneLuLPgsOXR6qyFZ7u+MAz0b4fa7k=.sha256"
"previous": "&dez1mAjzd4X9Z6ss0cBJO8EJDP+g3GtyYDNiasrw2pM=.sha256"
}

View File

@@ -34,6 +34,7 @@
class="w3-flex w3-dark-gray w3-center"
>
<div
id="scrollbox"
style="
flex: 1 1 auto;
overflow: auto;
@@ -52,7 +53,7 @@
<div>~😎 Tilde Friends.</div>
</div>
<footer>
<button class="w3-button w3-yellow proceed">Next</button>
<button class="w3-button w3-yellow proceed" id="next0">Next</button>
</footer>
</div>
</div>
@@ -71,7 +72,7 @@
</li>
</ul>
<footer class="w3-center w3-xlarge w3-padding">
<button class="w3-button w3-yellow proceed">Onward</button>
<button class="w3-button w3-yellow proceed" id="next1">Next</button>
</footer>
</div>
<div class="slide w3-gray" style="width: 90%">
@@ -119,7 +120,7 @@
target="_blank"
>See scuttlebutt.nz</a
>
<button class="w3-button w3-yellow proceed">Got It</button>
<button class="w3-button w3-yellow proceed" id="next2">Next</button>
</footer>
</div>
</div>
@@ -158,7 +159,7 @@
</ul>
</div>
<footer class="w3-center w3-xlarge w3-padding">
<button class="w3-button w3-yellow proceed">Okay</button>
<button class="w3-button w3-yellow proceed" id="next3">Next</button>
</footer>
</div>
</div>
@@ -193,12 +194,14 @@
</li>
<li>
To see this tutorial again later, select <b>apps</b> -&gt;
<b>Core Apps</b> -&gt; <b>intro</b>.
<b>Core Apps</b> -&gt; <b>intro</b>. When you continue, you will
be prompted to save a setting so that you don't see this every
time.
</li>
</ul>
</div>
<footer class="w3-center w3-xlarge w3-padding">
<button class="w3-button w3-yellow" id="complete">Let's Go!</button>
<button class="w3-button w3-yellow" id="complete">Continue</button>
</footer>
</div>
</div>
@@ -251,6 +254,7 @@
index == 0 ? 'hidden' : 'visible';
document.getElementById('right').style.visibility =
index == slides.length - 1 ? 'hidden' : 'visible';
document.getElementById('scrollbox').scrollTo(0, 0);
}
let dots = [...document.getElementsByClassName('dot')];

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "🦀",
"previous": "&Gic1e3jOZ7z5131jSCclbFXRpjyu8JlWJrjE7Fvn5dc=.sha256"
"previous": "&PrgfYYKpL2tedApJXokIIk+h9/yRYo0aUliNF3QsnZ4=.sha256"
}

View File

@@ -2,6 +2,7 @@ import * as tfrpc from '/tfrpc.js';
let g_database;
let g_hash;
let g_sql_cache = {};
tfrpc.register(async function localStorageGet(key) {
return app.localStorageGet(key);
@@ -51,11 +52,40 @@ tfrpc.register(async function connect(token) {
tfrpc.register(async function closeConnection(id) {
await ssb.closeConnection(id);
});
tfrpc.register(async function query(sql, args) {
tfrpc.register(async function query(sql, args, options) {
let start = new Date();
let result = [];
await ssb.sqlAsync(sql, args, function callback(row) {
result.push(row);
});
let key = options?.cacheable ? JSON.stringify([sql, args]) : undefined;
let entry = key ? g_sql_cache[key] : undefined;
const k_ideal_count = 64;
if (entry) {
result = entry.result;
} else {
await ssb.sqlAsync(sql, args, function callback(row) {
result.push(row);
});
if (key) {
g_sql_cache[key] = {
result: result,
time: new Date().valueOf(),
};
if (Object.keys(g_sql_cache).length > k_ideal_count * 2) {
let aged = Object.entries(g_sql_cache).map(([k, v]) => [v.time, k]);
aged.sort();
for (let i = 0; i < aged.length / 2; i++) {
delete g_sql_cache[aged[i][1]];
}
}
}
}
let end = new Date();
if (end - start > 1000) {
print(
(end - start) / 1000,
entry ? 'from cache' : 'from db',
sql.replaceAll(/\s+/g, ' ').trim()
);
}
return result;
});
tfrpc.register(async function appendMessage(id, message) {

File diff suppressed because one or more lines are too long

View File

@@ -105,10 +105,24 @@ class TfElement extends LitElement {
await this.load_channels();
}
async open_private_chat(event) {
let update = {};
update[event.detail.key] = false;
this.private_closed = Object.assign(this.private_closed, update);
await tfrpc.rpc.databaseSet(
'private_closed',
JSON.stringify(this.private_closed)
);
}
async close_private_chat(event) {
let update = {};
update[event.detail.key] = true;
this.private_closed = Object.assign(update, this.private_closed);
update[
event.detail.key == '[]'
? JSON.stringify([this.whoami])
: event.detail.key
] = true;
this.private_closed = Object.assign(this.private_closed, update);
await tfrpc.rpc.databaseSet(
'private_closed',
JSON.stringify(this.private_closed)
@@ -167,16 +181,22 @@ class TfElement extends LitElement {
return [];
}
let self = this;
let self_key = JSON.stringify([this.whoami]);
let opened = Object.entries(this.private_closed)
.filter(([key, value]) => !value)
.map(([key, value]) => [key, []]);
return Object.fromEntries(
Object.entries(this.grouped_private_messages).filter(([key, value]) => {
let channel = '🔐' + [...new Set(JSON.parse(key))].sort().join(',');
let grouped_latest = Math.max(...value.map((x) => x.rowid));
return (
!self.private_closed[key] ||
self.channels_unread[channel] === undefined ||
grouped_latest > self.channels_unread[channel]
);
})
[...Object.entries(this.grouped_private_messages), ...opened].filter(
([key, value]) => {
let channel = '🔐' + [...new Set(JSON.parse(key))].sort().join(',');
let grouped_latest = Math.max(...value.map((x) => x.rowid));
return (
!self.private_closed[key] ||
self.channels_unread[channel] === undefined ||
grouped_latest > self.channels_unread[channel]
);
}
)
);
}
@@ -185,19 +205,24 @@ class TfElement extends LitElement {
'',
'@',
'👍',
'🚩',
...Object.keys(this.visible_private())
.sort()
.map((x) => '🔐' + JSON.parse(x).join(',')),
...this.channels.map((x) => '#' + x),
];
let index = channel_names.indexOf(this.hash.substring(1));
let lookup = this.hash.substring(1);
if (lookup == '🔐') {
lookup = '🔐' + this.whoami;
}
let index = channel_names.indexOf(lookup);
index = index != -1 ? index + delta : 0;
tfrpc.rpc.setHash(
'#' +
encodeURIComponent(
channel_names[(index + channel_names.length) % channel_names.length]
)
);
let name =
channel_names[(index + channel_names.length) % channel_names.length];
if (name == '🔐' + this.whoami) {
name = '🔐';
}
tfrpc.rpc.setHash('#' + encodeURIComponent(name));
}
set_hash(hash) {
@@ -211,7 +236,7 @@ class TfElement extends LitElement {
}
}
async fetch_about(following, users) {
async fetch_about(following, users, transient) {
this.loading_about++;
let ids = Object.keys(following).sort();
const k_cache_version = 3;
@@ -259,7 +284,7 @@ class TfElement extends LitElement {
fields.value
FROM messages JOIN json_each(messages.content) AS fields
WHERE
messages.content ->> '$.type' = 'about' AND
messages.content ->> 'type' = 'about' AND
messages.content ->> '$.about' = messages.author AND
NOT fields.key IN ('about', 'type')) all_abouts
JOIN json_each(?) AS following ON all_abouts.author = following.value
@@ -295,7 +320,7 @@ class TfElement extends LitElement {
this.loading_about--;
let new_cache = JSON.stringify(cache);
if (new_cache != original_cache) {
if (!transient && 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);
@@ -403,14 +428,6 @@ class TfElement extends LitElement {
return [cache.latest, cache.messages];
}
async query_timed(sql, args) {
let start = new Date();
let result = await tfrpc.rpc.query(sql, args);
let end = new Date();
console.log((end - start) / 1000, sql.replaceAll(/\s+/g, ' ').trim());
return result;
}
async group_private_messages(messages) {
let groups = {};
let result = await this.decrypt(
@@ -442,7 +459,6 @@ class TfElement extends LitElement {
}
async load_channels_latest(following) {
let start_time = new Date();
let latest_private = this.get_latest_private(following);
const k_args = [
JSON.stringify(this.channels),
@@ -452,7 +468,7 @@ class TfElement extends LitElement {
];
let channels = (
await Promise.all([
this.query_timed(
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
@@ -465,7 +481,7 @@ class TfElement extends LitElement {
`,
k_args
),
this.query_timed(
tfrpc.rpc.query(
`
SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN messages_refs ON messages.id = messages_refs.message
@@ -479,7 +495,7 @@ class TfElement extends LitElement {
`,
k_args
),
this.query_timed(
tfrpc.rpc.query(
`
SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?2) AS following ON messages.author = following.value
@@ -490,7 +506,7 @@ class TfElement extends LitElement {
`,
k_args
),
this.query_timed(
tfrpc.rpc.query(
`
SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
JOIN messages ON messages.rowid = messages_fts.rowid
@@ -499,9 +515,16 @@ class TfElement extends LitElement {
`,
k_args
),
tfrpc.rpc.query(
`
SELECT '🚩' AS channel, MAX(messages.rowid) AS rowid FROM messages
WHERE messages.content ->> 'type' = 'flag'
`,
k_args
),
])
).flat();
let latest = {};
let latest = {'🔐': undefined};
for (let row of channels) {
if (!latest[row.channel]) {
latest[row.channel] = row.rowid;
@@ -510,18 +533,14 @@ class TfElement extends LitElement {
}
}
this.channels_latest = latest;
console.log('channels took', (new Date() - start_time) / 1000.0);
let self = this;
start_time = new Date();
latest_private.then(async function (latest) {
let grouped = await self.group_private_messages(latest[1]);
self.channels_latest = Object.assign({}, self.channels_latest, {
'🔐': latest[0],
});
console.log('private took', (new Date() - start_time) / 1000.0);
self.private_messages = latest[1];
self.grouped_private_messages = await self.group_private_messages(
latest[1]
);
self.grouped_private_messages = grouped;
});
}
@@ -586,7 +605,7 @@ class TfElement extends LitElement {
SELECT DISTINCT content ->> '$.vote.expression' AS value
FROM messages
WHERE author = ? AND
content ->> '$.type' = 'vote'
content ->> 'type' = 'vote'
ORDER BY timestamp DESC LIMIT 10
`,
[this.whoami]
@@ -598,7 +617,6 @@ class TfElement extends LitElement {
this.loading_latest = true;
this.reset_progress();
try {
let start_time = new Date();
let whoami = this.whoami;
let following = await tfrpc.rpc.following([whoami], 2);
let old_users = this.users ?? {};
@@ -618,36 +636,20 @@ class TfElement extends LitElement {
by_count.push({count: v.of, id: id});
}
let reactions = this.load_recent_reactions();
this.load_channels_latest(Object.keys(following));
let channels = 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();
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;
let self = this;
this.fetch_about(following, users).then(function (result) {
self.users = result;
console.log(
'about took',
(new Date() - about_start_time) / 1000.0,
'seconds for',
Object.keys(users).length,
'users'
);
});
console.log(
`load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}`
);
await reactions;
await channels;
this.whoami = whoami;
this.loaded = whoami;
} finally {
@@ -706,6 +708,7 @@ class TfElement extends LitElement {
@refresh=${this.refresh}
@toggle_stay_connected=${this.toggle_stay_connected}
@loadmessages=${this.reset_progress}
@openprivatechat=${this.open_private_chat}
@closeprivatechat=${this.close_private_chat}
.connections=${this.connections}
.private_messages=${this.private_messages}
@@ -730,9 +733,7 @@ class TfElement extends LitElement {
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
query=${this.hash?.startsWith('#q=')
? decodeURIComponent(this.hash.substring(3))
: null}
query=${this.search_text()}
></tf-tab-search>
`;
}
@@ -775,6 +776,44 @@ class TfElement extends LitElement {
input.click();
}
search() {
let search_text = this.renderRoot.getElementById('search_text');
if (!search_text.value.length) {
search_text.focus();
this.set_tab('search');
} else {
this.set_hash('#q=' + encodeURIComponent(search_text.value));
}
}
search_keydown(event) {
if (event.keyCode == 13) {
this.search();
}
}
search_text() {
if (this.hash.startsWith('#q=')) {
try {
return decodeURIComponent(this.hash.substring('#q='.length));
} catch {
return this.hash.substring('#q='.length);
}
}
}
async request_user(event) {
let users = {};
users[event.detail.id] = {};
users = await this.fetch_user_info(users);
if (this.users[event.detail.id]?.seq !== users[event.detail.id]?.seq) {
let self = this;
this.fetch_about(users, users, true).then(function (result) {
self.users = Object.assign({}, self.users, users);
});
}
}
render() {
let self = this;
@@ -788,33 +827,19 @@ class TfElement extends LitElement {
const k_tabs = {
'📰': 'news',
'📡': 'connections',
'🔍': 'search',
};
let tabs = html`
<style>
#search_text:focus {
float: none !important;
width: 100%;
}
</style>
<div
class="w3-bar w3-theme-l1"
style="position: static; top: 0; z-index: 10"
>
${this.is_administrator
? html`
<button
class=${'w3-bar-item w3-button w3-circle w3-ripple' +
(this.connections?.some((x) => x.flags.one_shot)
? ' w3-spin'
: '')}
@click=${this.refresh}
>
</button>
<button
class="w3-bar-item w3-button w3-ripple"
@click=${this.toggle_stay_connected}
>
${this.stay_connected ? '🔗' : '⛓️‍💥'}
</button>
`
: undefined}
${Object.entries(k_tabs).map(
([k, v]) => html`
<button
@@ -837,6 +862,33 @@ class TfElement extends LitElement {
>
🎨<span class="w3-hide-small">Color</span>
</button>
${
this.is_administrator
? html`
<button
class="w3-bar-item w3-button w3-circle w3-right"
@click=${this.refresh}
>
<span
style="display: inline-block"
class=${this.connections?.some((x) => x.flags.one_shot)
? ' w3-spin'
: ''}
>
</span>
</button>
<button
class="w3-bar-item w3-button w3-right"
@click=${this.toggle_stay_connected}
>
${this.stay_connected ? '🔗' : '⛓️‍💥'}
</button>
`
: undefined
}
<button class="w3-bar-item w3-button w3-right" @click=${this.search}>🔍<span class="w3-hide-small">Search</span></button>
<input type="text" class=${'w3-input w3-bar-item w3-right w3-theme-d1' + (this.tab == 'search' ? ' w3-mobile' : ' w3-hide-small')} placeholder="keywords, @id, #channel" id="search_text" @keydown=${this.search_keydown} value=${this.search_text()}></input>
</div>
`;
let contents = this.guest
@@ -878,6 +930,7 @@ class TfElement extends LitElement {
<div
style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
class="w3-theme-dark"
@tf-request-user=${this.request_user}
>
${progress}
<div style="flex: 0 0">${tabs}</div>

View File

@@ -2,6 +2,7 @@ import {
LitElement,
css,
html,
map,
repeat,
render,
unsafeCSS,
@@ -196,6 +197,26 @@ class TfMessageElement extends LitElement {
);
}
flag(event) {
let reason = prompt(
'What is the reason for reporting this content (spam, nsfw, ...)?',
'offensive'
);
if (reason !== undefined) {
tfrpc.rpc
.appendMessage(this.whoami, {
type: 'flag',
flag: {
link: this.message.id,
reason: reason.length ? reason : undefined,
},
})
.catch(function (error) {
alert(error?.message);
});
}
}
show_image(link) {
let div = document.createElement('div');
div.style.left = 0;
@@ -452,7 +473,9 @@ class TfMessageElement extends LitElement {
}
copy_id(event) {
navigator.clipboard.writeText(this.message?.id);
navigator.clipboard.writeText(this.message?.id).catch(function (e) {
console.log(e);
});
}
toggle_menu(event) {
@@ -499,12 +522,16 @@ class TfMessageElement extends LitElement {
</button>
`
: undefined}
<button
class="w3-button w3-bar-item w3-border-bottom"
@click=${this.react}
>
<button class="w3-button w3-bar-item" @click=${this.react}>
👍 React
</button>
<button
id="button_flag"
class="w3-button w3-bar-item w3-border-bottom"
@click=${this.flag}
>
🚩 Flag
</button>
${formats.map(
([format, name]) => html`
<button
@@ -571,14 +598,41 @@ class TfMessageElement extends LitElement {
`;
}
render_refs() {
if (this.message?.refs) {
let self = this;
return html`<div class="w3-container">
<h3>Referring messages</h3>
${map(
this.message.refs,
(ref) => html`
<tf-message
.message=${ref}
whoami=${self.whoami}
.users=${self.users}
.drafts=${self.drafts}
.expanded=${self.expanded}
channel=${self.channel}
channel_unread=${self.channel_unread}
.recent_reactions=${self.recent_reactions}
depth=${self.depth + 1}
></tf-message>
`
)}
</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()}
<div class="w3-container">
${self.format == 'raw'
? html`${self.render_raw()}`
: self.render_flagged(inner)}
</div>
${self.render_votes()} ${self.render_refs()}
${(self.message.child_messages || []).map(
(x) => html`
<tf-message
@@ -622,21 +676,25 @@ class TfMessageElement extends LitElement {
`;
}
contact_description(content) {
return content.following && content.blocking
? 'following and blocking'
: content.following
? 'following'
: content.blocking
? 'blocking'
: content.blocking !== undefined
? 'no longer blocking'
: content.following !== undefined
? 'no longer following'
: '';
}
content_group_by_author() {
let sorted = this.message.messages
.map((x) => [
x.author,
x.content.following && x.content.blocking
? 'is following and blocking'
: x.content.following
? 'is following'
: x.content.blocking
? 'is blocking'
: x.content.blocking !== undefined
? 'is no longer blocking'
: x.content.following !== undefined
? 'is no longer following'
: '',
this.contact_description(x.content),
x.content.contact,
x,
])
@@ -703,6 +761,38 @@ class TfMessageElement extends LitElement {
: undefined;
}
render_flagged(inner) {
if (this.message.flags) {
return html`
<div
class="w3-panel w3-round-xlarge w3-theme-l4 w3"
style="cursor: pointer"
@click=${(x) => this.toggle_expanded(':cw')}
>
<p>
${this.message.flags
? html`<p>
Caution: This message has been flagged
${this.message.flags.length}
time${this.message.flags.length == 1 ? '' : 's'}.
</p>`
: undefined}
</p>
<p class="w3-small">
${inner !== undefined
? this.is_expanded(':cw')
? 'Show less'
: 'Show more'
: undefined}
</p>
</div>
${this.is_expanded(':cw') ? inner : undefined}
`;
} else {
return inner;
}
}
_render() {
let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') {
@@ -850,7 +940,9 @@ class TfMessageElement extends LitElement {
</div>
</div>
</div>
<div class="w3-container">${this.render_flagged(undefined)}</div>
<div>${this.render_votes()}</div>
${this.render_refs()}
${(this.message.child_messages || []).map(
(x) => html`
<tf-message
@@ -910,23 +1002,14 @@ class TfMessageElement extends LitElement {
id=${this.message.author}
.users=${this.users}
></tf-user>
is
${content.blocking === true
? 'blocking'
: content.blocking === false
? 'no longer blocking'
: content.following === true
? 'following'
: content.following === false
? 'no longer following'
: '?'}
is ${this.contact_description(content)}
<tf-user
id=${this.message.content.contact}
.users=${this.users}
></tf-user>
</div>
${this.render_menu()} ${this.render_votes()}
${this.render_actions()}
${this.render_refs()} ${this.render_actions()}
</div>
`);
break;
@@ -934,7 +1017,9 @@ class TfMessageElement extends LitElement {
return this.render_frame(html`
${this.render_header()}
<div class="w3-container">${this.render_raw()}</div>
${this.render_votes()} ${this.render_actions()}
${this.render_votes()}
${this.render_refs()}
${this.render_actions()}
</div>
`);
break;
@@ -965,7 +1050,19 @@ class TfMessageElement extends LitElement {
style="cursor: pointer"
@click=${(x) => this.toggle_expanded(':cw')}
>
<p>${content.contentWarning}</p>
<p>
${this.message.flags
? html`<p>
Caution: This message has been flagged
${this.message.flags.length}
time${this.message.flags.length == 1 ? '' : 's'}.
</p>`
: undefined}
${content.contentWarning
? html`<p>${content.contentWarning}</p>`
: undefined}
</p>
<p class="w3-small">
${this.is_expanded(':cw') ? 'Show less' : 'Show more'}
</p>
@@ -976,20 +1073,24 @@ class TfMessageElement extends LitElement {
<div @click=${this.body_click}>${body}</div>
${this.render_mentions()}
`;
let payload = content.contentWarning
? self.expanded[(this.message.id || '') + ':cw']
? html` ${content_warning} ${content_html} `
: content_warning
: content_html;
let payload =
this.message.flags || content.contentWarning
? self.expanded[(this.message.id || '') + ':cw']
? html` ${content_warning} ${content_html} `
: content_warning
: content_html;
return this.render_frame(html`
${this.render_header()}
<div class="w3-container">${payload}</div>
${this.render_votes()} ${this.render_actions()}
${this.render_votes()}
${this.render_refs()}
${this.render_actions()}
</div>
`);
} else if (content.type === 'issue') {
return this.render_frame(html`
${this.render_header()} ${content.text} ${this.render_votes()}
${this.render_refs()}
<footer class="w3-container">
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
@@ -999,15 +1100,13 @@ class TfMessageElement extends LitElement {
`);
} else if (content.type === 'blog') {
let self = this;
tfrpc.rpc.get_blob(content.blog).then(function (data) {
self.blog_data = data;
self.blog_data = tfrpc.rpc.get_blob(content.blog).then(function (data) {
return data
? unsafeHTML(tfutils.markdown(data))
: html`Blog post content unavailable.`;
});
let payload = this.expanded[(this.message.id || '') + ':blog']
? html`<div>
${this.blog_data
? unsafeHTML(tfutils.markdown(this.blog_data))
: 'Loading...'}
</div>`
? until(this.blog_data, 'Loading...')
: undefined;
let body;
switch (this.format) {
@@ -1020,22 +1119,31 @@ class TfMessageElement extends LitElement {
case 'message':
body = html`
<div
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
@click=${(x) => self.toggle_expanded(':blog')}>
class="w3-border w3-theme-l4 w3-round-xlarge"
style="padding: 8px; margin: 4px; cursor: pointer"
@click=${(x) => self.toggle_expanded(':blog')}
>
<h2>${content.title}</h2>
<div style="display: flex; flex-direction: row">
<img src=/${content.thumbnail}/view></img>
<div style="display: flex; flex-direction: row; gap: 8px">
${content.thumbnail
? html`<img src=/${content.thumbnail}/view style="max-width: 25vw; max-height: 25vw"></img>`
: undefined}
<span>${content.summary}</span>
</div>
<p class="w3-small">
${this.expanded[(this.message.id || '') + ':blog']
? 'Show less'
: 'Show more'}
</p>
</div>
${payload}
<div class="w3-container">${payload}</div>
`;
break;
}
return this.render_frame(html`
${this.render_header()}
<div>${body}</div>
${this.render_mentions()} ${this.render_votes()}
${this.render_mentions()} ${this.render_votes()} ${this.render_refs()}
${this.render_actions()}
`);
} else if (content.type === 'pub') {
@@ -1086,13 +1194,21 @@ class TfMessageElement extends LitElement {
}
} else {
return this.render_small_frame(
html`<div class="w3-container">
<p><b>type</b>: ${content.type}</p>
</div>`
html`<p><b>type</b>: ${content.type}</p>`
);
}
} else if (typeof this.message.content == 'string') {
return this.render_small_frame();
if (this.message?.decrypted) {
if (this.format == 'decrypted') {
return this.render_small_frame(
this.render_json(this.message.decrypted)
);
} else {
return this.render_small_frame(this.message.decrypted.type);
}
} else {
return this.render_small_frame();
}
} else {
return this.render_small_frame(this.render_raw());
}

View File

@@ -37,8 +37,6 @@ class TfNewsElement extends LitElement {
let self = this;
let messages_by_id = {};
console.log('processing', messages.length, 'messages');
function ensure_message(id, rowid) {
let found = messages_by_id[id];
if (found) {
@@ -59,6 +57,9 @@ class TfNewsElement extends LitElement {
}
function link_message(message) {
if (!message.content) {
return;
}
if (message.content.type === 'vote') {
let parent = ensure_message(message.content.vote.link, message.rowid);
if (!parent.votes) {
@@ -66,6 +67,16 @@ class TfNewsElement extends LitElement {
}
parent.votes.push(message);
message.parent_message = message.content.vote.link;
} else if (message.content.type == 'flag') {
let parent = ensure_message(message.content.flag.link, message.rowid);
if (!parent.flags) {
parent.flags = [];
}
parent.flags.push(message);
parent.flags = Object.values(
Object.fromEntries(parent.flags.map((x) => [x.id, x]))
);
message.parent_message = message.content.flag.link;
} else if (message.content.type == 'post') {
if (message.content.root) {
if (typeof message.content.root === 'string') {
@@ -84,6 +95,22 @@ class TfNewsElement extends LitElement {
message.parent_message = message.content.root[0];
}
}
} else {
let parent_id = message.content.about?.startswith?.('%')
? message.content.about
: message.content.updates?.startsWith?.('%')
? message.content.updates
: message.content.parent?.startsWith?.('%')
? message.content.parent
: undefined;
if (parent_id) {
let parent = ensure_message(parent_id, message.rowid);
if (!parent?.refs) {
parent.refs = [];
}
parent.refs.push(message);
message.parent_message = parent_id;
}
}
}
@@ -106,6 +133,7 @@ class TfNewsElement extends LitElement {
message.parent_message = placeholder.parent_message;
message.child_messages = placeholder.child_messages;
message.votes = placeholder.votes;
message.flags = placeholder.flags;
if (
placeholder.parent_message &&
messages_by_id[placeholder.parent_message]

View File

@@ -37,16 +37,22 @@ class TfProfileElement extends LitElement {
this.following = undefined;
this.blocking = undefined;
let latest = (
await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages')
)[0].latest;
let result = await tfrpc.rpc.query(
`
SELECT json_extract(content, '$.following') AS following
FROM messages WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? AND
following IS NOT NULL
following IS NOT NULL AND
messages.rowid <= ?
ORDER BY sequence DESC LIMIT 1
`,
[this.whoami, this.id]
[this.whoami, this.id, latest],
{cacheable: true}
);
this.following = result?.[0]?.following ?? false;
result = await tfrpc.rpc.query(
@@ -55,10 +61,12 @@ class TfProfileElement extends LitElement {
FROM messages WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? AND
blocking IS NOT NULL
blocking IS NOT NULL AND
messages.rowid <= ?
ORDER BY sequence DESC LIMIT 1
`,
[this.whoami, this.id]
[this.whoami, this.id, latest],
{cacheable: true}
);
this.blocking = result?.[0]?.blocking ?? false;
}
@@ -208,6 +216,7 @@ class TfProfileElement extends LitElement {
async load_follows() {
let accounts = await tfrpc.rpc.following([this.id], 1);
delete accounts[this.id];
return html`
<div class="w3-container">
<button
@@ -232,13 +241,27 @@ class TfProfileElement extends LitElement {
`;
}
open_private_chat() {
let hash = '#🔐' + (this.id != this.whoami ? this.id : '');
this.dispatchEvent(
new CustomEvent('openprivatechat', {
bubbles: true,
composed: true,
detail: {
key: JSON.stringify([this.id]),
},
})
);
tfrpc.rpc.setHash(hash);
}
render() {
this.load();
let self = this;
let profile = this.users[this.id] || {};
tfrpc.rpc
.query(
`SELECT SUM(LENGTH(content)) AS size, MAX(sequence) AS sequence FROM messages WHERE author = ?`,
`SELECT size AS size, max_sequence AS sequence FROM messages_stats WHERE author = ?`,
[this.id]
)
.then(function (result) {
@@ -251,16 +274,18 @@ class TfProfileElement extends LitElement {
if (this.id === this.whoami) {
if (this.editing) {
edit = html`
<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>
<div style="margin-top: 8px">
<button
id="save_profile"
class="w3-button w3-theme-l1"
@click=${this.save_edits}
>
Save Profile
</button>
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
Discard
</button>
</div>
`;
} else {
edit = html`<button
@@ -306,7 +331,7 @@ class TfProfileElement extends LitElement {
<div>
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
</div>
</div>`
</div>`
: null;
let image = profile.image;
if (typeof image == 'string' && !image.startsWith('&')) {
@@ -318,7 +343,7 @@ class TfProfileElement extends LitElement {
let description = this.editing?.description ?? profile.description;
return html`
<style>${generate_theme()}</style>
<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box">
<div class="w3-card-4 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)} in ${this.sequence} messages)</p>
</header>
@@ -327,19 +352,23 @@ class TfProfileElement extends LitElement {
<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 style="display: flex; flex-direction: row; gap: 1em">
${edit_profile}
<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal">
${
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 class=${this.editing ? 'w3-card' : ''}>
${this.editing ? html`<header class="w3-container w3-theme-l2"><h2>Editing Your Profile</h2></header>` : undefined}
<div style="display: flex; flex-direction: row; gap: 1em" class="w3-margin">
${edit_profile}
<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal">
${
image
? html`<div><img src=${'/' + image + '/view'} style="width: min(256px, 100%); 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>
${this.editing ? html`<footer class="w3-container w3-theme-l2"><p>${edit}</p></footer>` : undefined}
</div>
<div>
Following ${profile.following} identities.
@@ -351,10 +380,10 @@ class TfProfileElement extends LitElement {
${until(this.load_follows(), html`<p>Loading accounts followed...</p>`)}
<footer class="w3-container">
<p>
<a class="w3-button w3-theme-d1" href=${'#🔐' + (this.id != this.whoami ? this.id : '')}>
<button class="w3-button w3-theme-d1" @click=${this.open_private_chat} id="open_private_chat">
Open Private Chat
</a>
${edit}
</button>
${this.editing ? undefined : edit}
${follow}
${block}
</p>

View File

@@ -84,7 +84,6 @@ class TfTabNewsFeedElement extends LitElement {
`,
[JSON.stringify(combined.map((x) => x.id))]
);
let t0 = new Date();
let result = [].concat(
combined,
await tfrpc.rpc.query(
@@ -101,8 +100,6 @@ class TfTabNewsFeedElement extends LitElement {
]
)
);
let t1 = new Date();
console.log((t1 - t0) / 1000);
return result;
}
@@ -120,10 +117,11 @@ class TfTabNewsFeedElement extends LitElement {
result = await tfrpc.rpc.query(
`
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
FROM messages_refs
JOIN messages ON messages.id = messages_refs.message
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE
messages_refs.ref = ?1 AND
messages.author != ?1 AND
(?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4
ORDER BY timestamp DESC limit ?5)
@@ -135,7 +133,7 @@ class TfTabNewsFeedElement extends LitElement {
SELECT TRUE AS is_primary, * FROM mentions
`,
[
'"' + this.whoami.replace('"', '""') + '"',
this.whoami,
JSON.stringify(this.following),
start_time,
end_time,
@@ -175,7 +173,6 @@ class TfTabNewsFeedElement extends LitElement {
[this.hash.substring(1)]
);
} else if (this.hash.startsWith('##')) {
let t0 = new Date();
let initial_messages = await tfrpc.rpc.query(
`
WITH
@@ -203,12 +200,7 @@ class TfTabNewsFeedElement extends LitElement {
k_max_results,
]
);
let t1 = new Date();
result = await this._fetch_related_messages(initial_messages);
let t2 = new Date();
console.log(
`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
);
} else if (this.hash.startsWith('#🔐')) {
let ids =
this.hash == '#🔐' ? [] : this.hash.substring('#🔐'.length).split(',');
@@ -233,7 +225,8 @@ class TfTabNewsFeedElement extends LitElement {
k_max_results,
]
);
result = (await this.decrypt(result)).filter((x) => x.decrypted);
let decrypted = (await this.decrypt(result)).filter((x) => x.decrypted);
result = await this._fetch_related_messages(decrypted);
} else if (this.hash == '#👍') {
result = await tfrpc.rpc.query(
`
@@ -252,8 +245,24 @@ class TfTabNewsFeedElement extends LitElement {
`,
[JSON.stringify(this.following), start_time, end_time, k_max_results]
);
} else if (this.hash == '#🚩') {
result = await tfrpc.rpc.query(
`
WITH flags 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
WHERE
messages.content ->> 'type' = 'flag' AND
(?1 IS NULL OR messages.timestamp >= ?1) AND messages.timestamp < ?2
ORDER BY timestamp DESC limit ?3)
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 flags
JOIN messages ON messages.id = flags.content ->> '$.flag.link'
UNION
SELECT TRUE AS is_primary, * FROM flags
`,
[start_time, end_time, k_max_results]
);
} else {
let t0 = new Date();
let initial_messages = await tfrpc.rpc.query(
`
WITH
@@ -279,12 +288,7 @@ class TfTabNewsFeedElement extends LitElement {
JSON.stringify(Object.keys(this.channels_latest)),
]
);
let t1 = new Date();
result = await this._fetch_related_messages(initial_messages);
let t2 = new Date();
console.log(
`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
);
}
this.time_loading = undefined;
return result;
@@ -392,7 +396,13 @@ class TfTabNewsFeedElement extends LitElement {
)
)
);
console.log('done loading latest messages.');
}
make_messages_key() {
return JSON.stringify([
this.hash,
Object.keys(this.channels_latest ?? {}).filter((x) => x != '🔐'),
]);
}
async load_messages() {
@@ -400,16 +410,17 @@ class TfTabNewsFeedElement extends LitElement {
let self = this;
this.loading++;
let messages = [];
let original_hash = this.hash;
let original_key = this.make_messages_key();
try {
if (this._messages_hash !== this.hash) {
if (this._messages_key !== original_key) {
this.messages = [];
this._messages_hash = this.hash;
this._messages_key = original_key;
}
this._messages_following = JSON.stringify(this.following);
this._private_messages =
JSON.stringify(this.private_messages) +
JSON.stringify(this.grouped_private_messages);
this._private_messages = JSON.stringify([
this.private_messages,
this.grouped_private_messages,
]);
this._channels_latest = JSON.stringify(
Object.keys(this.channels_latest ?? {})
);
@@ -425,13 +436,11 @@ class TfTabNewsFeedElement extends LitElement {
} finally {
this.loading--;
}
if (this.hash == original_hash) {
let current_key = this.make_messages_key();
if (current_key === original_key) {
this.messages = this.merge_messages(this.messages, messages);
}
this.time_loading = undefined;
console.log(
`loading ${messages.length} messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s`
);
}
mark_all_read() {
@@ -484,16 +493,17 @@ class TfTabNewsFeedElement extends LitElement {
render() {
if (
!this.messages ||
this._messages_hash !== this.hash ||
this._messages_key !== this.make_messages_key() ||
this._messages_following !== JSON.stringify(this.following) ||
this._private_messages !==
JSON.stringify(this.private_messages) +
JSON.stringify(this.grouped_private_messages) ||
this._channels_latest !==
JSON.stringify(Object.keys(this.channels_latest))
(this.hash.startsWith('#🔐') &&
this._private_messages !==
JSON.stringify([
this.private_messages,
this.grouped_private_messages,
]))
) {
console.log(
`loading messages for ${this.whoami} (following ${this.following.length})`
`loading messages for ${this.whoami} (messages=${!this.messages},${this._messages_key != this.make_messages_key()} following=${this._messages_following !== JSON.stringify(this.following)}, private=${this._private_messages !== JSON.stringify([this.private_messages, this.grouped_private_messages])},${this.private_messages?.length},${Object.keys(this.grouped_private_messages ?? {}).length})`
);
this.load_messages();
}

View File

@@ -200,6 +200,7 @@ class TfTabNewsElement extends LitElement {
}
render_sidebar() {
let self_key = JSON.stringify([this.whoami]);
return html`
<div
class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left"
@@ -243,17 +244,27 @@ class TfTabNewsElement extends LitElement {
style=${this.hash == '#👍' ? 'font-weight: bold' : undefined}
>${this.unread_status('👍')}👍votes</a
>
<a
href="#🚩"
class="w3-bar-item w3-button"
style=${this.hash == '#🚩' ? 'font-weight: bold' : undefined}
>${this.unread_status('🚩')}🚩flagged</a
>
${Object.keys(this?.visible_private_messages ?? [])
?.sort()
?.map(
(key) => html`
<a
href=${'#🔐' + JSON.parse(key).join(',')}
href=${'#🔐' +
(key == self_key ? '' : JSON.parse(key).join(','))}
class="w3-bar-item w3-button"
style=${this.hash == '#🔐' + JSON.parse(key).join(',')
style=${this.hash ==
'#🔐' + (key == self_key ? '' : JSON.parse(key).join(','))
? 'font-weight: bold'
: undefined}
>${this.unread_status('🔐' + JSON.parse(key).join(','))}
>${this.unread_status(
'🔐' + (key == self_key ? '' : JSON.parse(key).join(','))
)}
${(key != '[]' ? JSON.parse(key) : [this.whoami]).map(
(id) => html`
<tf-user
@@ -369,6 +380,14 @@ class TfTabNewsElement extends LitElement {
`;
}
recipients() {
if (this.hash == '#🔐') {
return [this.whoami];
} else if (this.hash.startsWith('#🔐')) {
return this.hash.substring('#🔐'.length).split(',');
}
}
render() {
let profile =
this.hash.startsWith('#@') && this.hash != '#@'
@@ -422,13 +441,18 @@ class TfTabNewsElement extends LitElement {
</p>
<div>
<div
id="show_sidebar"
class="w3-button w3-hide-large"
@click=${this.show_sidebar}
style="width: 100%; max-width: 100%; white-space: nowrap; overflow: hidden"
>
${this.unread_status()}&#9776;
<button
id="show_sidebar"
class="w3-button w3-hide-large"
@click=${this.show_sidebar}
>
${this.unread_status()}&#9776;
</button>
Welcome,
<tf-user id=${this.whoami} .users=${this.users}></tf-user>!
</div>
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
${edit_profile}
</div>
<div>
@@ -439,9 +463,7 @@ class TfTabNewsElement extends LitElement {
.drafts=${this.drafts}
@tf-draft=${this.draft}
.channel=${this.channel()}
.recipients=${this.hash.startsWith('#🔐')
? this.hash.substring('#🔐'.length).split(',')
: undefined}
.recipients=${this.recipients()}
></tf-compose>
</div>
${profile}

View File

@@ -1,4 +1,4 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles, generate_theme} from './tf-styles.js';
@@ -44,42 +44,37 @@ class TfTabSearchElement extends LitElement {
this.error = undefined;
this.results = [];
this.messages = [];
if (query.startsWith('sql:')) {
this.messages = [];
try {
try {
if (query.startsWith('sql:')) {
this.messages = [];
this.results = await tfrpc.rpc.query(
query.substring('sql:'.length),
[]
);
} catch (e) {
this.results = [];
this.error = e;
} else {
let results = await tfrpc.rpc.query(
`
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value
ORDER BY timestamp DESC limit 100
`,
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
);
search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
search.select();
}
this.messages = results;
}
} else {
let results = await tfrpc.rpc.query(
`
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value
ORDER BY timestamp DESC limit 100
`,
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
);
console.log('Done.');
search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
search.select();
}
this.messages = results;
}
}
search_keydown(event) {
if (event.keyCode == 13) {
this.query = this.renderRoot.getElementById('search').value;
} catch (e) {
this.messages = [];
this.results = [];
this.error = e;
console.log(e);
}
}
@@ -139,20 +134,24 @@ class TfTabSearchElement extends LitElement {
}
}
render() {
async query_results() {
if (this.query !== this.last_query) {
this.last_query = this.query;
this.search(this.query);
this._query = this.search(this.query);
}
let self = this;
await this._query;
}
render() {
return html`
<style>${generate_theme()}</style>
<style>
${generate_theme()}
</style>
<div class="w3-padding">
<div style="display: flex; flex-direction: row; gap: 4px">
<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
</div>
${this.render_results()}
${until(
this.query_results().then(this.render_results.bind(this)),
html`<p>Searching...<span class="w3-animate-fading">🦀</span></p>`
)}
</div>
`;
}

View File

@@ -25,6 +25,15 @@ class TfUserElement extends LitElement {
render() {
let user = this.users[this.id];
if (!this.users[this.id]) {
this.dispatchEvent(
new CustomEvent('tf-request-user', {
bubbles: true,
composed: true,
detail: {id: this.id},
})
);
}
let shape =
user?.follow_depth === undefined || user.follow_depth >= 2
? 'w3-circle'
@@ -39,7 +48,9 @@ class TfUserElement extends LitElement {
name = this.icon_only
? undefined
: !this.nolink
? html`<a target="_top" href=${'#' + this.id}>${name_string}</a>`
? html`<a target="_top" href=${'#' + encodeURIComponent(this.id)}
>${name_string}</a
>`
: html`<span>${name_string}</span>`;
if (user) {

5
apps/trace.json Normal file
View File

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

27
apps/trace/app.js Normal file
View File

@@ -0,0 +1,27 @@
async function main() {
let speedscope_js = await utf8Decode(
getFile('speedscope/speedscope-Y2522XSH.js')
);
speedscope_js = speedscope_js.replace(/alert\(`Cannot load.*?return/, '');
app.setDocument(`
<!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/speedscope-GHPHNKXC.css">
</head>
<body>
<script>
delete window.localStorage;
window.location.hash = '#profileURL=${core.url}../../trace';
</script>
<script>${speedscope_js}</script>
</body>
</html>
`);
}
main();

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 679 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

@@ -12,9 +12,7 @@
"type": "number"
},
"type": {
"enum": [
"C"
],
"const": "C",
"title": "type",
"type": "string"
}
@@ -29,8 +27,8 @@
},
"FileFormat.EventType": {
"enum": [
"C",
"O"
"O",
"C"
],
"title": "FileFormat.EventType",
"type": "string"
@@ -64,9 +62,7 @@
"type": "number"
},
"type": {
"enum": [
"evented"
],
"const": "evented",
"title": "type",
"type": "string"
},
@@ -89,9 +85,7 @@
"FileFormat.File": {
"properties": {
"$schema": {
"enum": [
"https://www.speedscope.app/file-format-schema.json"
],
"const": "https://www.speedscope.app/file-format-schema.json",
"title": "$schema",
"type": "string"
},
@@ -109,14 +103,7 @@
},
"profiles": {
"items": {
"anyOf": [
{
"$ref": "#/definitions/FileFormat.EventedProfile"
},
{
"$ref": "#/definitions/FileFormat.SampledProfile"
}
]
"$ref": "#/definitions/FileFormat.Profile"
},
"title": "profiles",
"type": "array"
@@ -192,7 +179,8 @@
{
"$ref": "#/definitions/FileFormat.SampledProfile"
}
]
],
"title": "FileFormat.Profile"
},
"FileFormat.ProfileType": {
"enum": [
@@ -227,9 +215,7 @@
"type": "number"
},
"type": {
"enum": [
"sampled"
],
"const": "sampled",
"title": "type",
"type": "string"
},
@@ -298,9 +284,7 @@
"type": "number"
},
"type": {
"enum": [
"O"
],
"const": "O",
"title": "type",
"type": "string"
}

View File

@@ -11,7 +11,7 @@
<link rel="icon" type="image/x-icon" href="favicon-FOKUP5Y5.ico">
</head>
<body>
<script src="speedscope-432XE7GS.js"></script>
<script src="speedscope-Y2522XSH.js"></script>

Binary file not shown.

View File

@@ -0,0 +1,3 @@
speedscope@1.25.0
Wed Dec 3 07:18:39 PM EST 2025
810efdf0a4868bb28f1300f4daacd0db6b8a95bd

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../assets/reset.css", "../../assets/source-code-pro.css"],
"sourcesContent": ["/* http://meyerweb.com/eric/tools/css/reset/\n v2.0 | 20110126\n License: none (public domain)\n*/\nhtml, body, div, span, applet, object, iframe,\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\na, abbr, acronym, address, big, cite, code,\ndel, dfn, em, img, ins, kbd, q, s, samp,\nsmall, strike, strong, sub, sup, tt, var,\nb, u, i, center,\ndl, dt, dd, ol, ul, li,\nfieldset, form, label, legend,\ntable, caption, tbody, tfoot, thead, tr, th, td,\narticle, aside, canvas, details, embed,\nfigure, figcaption, footer, header, hgroup,\nmenu, nav, output, ruby, section, summary,\ntime, mark, audio, video {\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tfont-size: 100%;\n\tfont: inherit;\n\tvertical-align: baseline;\n}\n/* HTML5 display-role reset for older browsers */\narticle, aside, details, figcaption, figure,\nfooter, header, hgroup, menu, nav, section {\n\tdisplay: block;\n}\nbody {\n\tline-height: 1;\n}\nol, ul {\n\tlist-style: none;\n}\nblockquote, q {\n\tquotes: none;\n}\nblockquote:before, blockquote:after,\nq:before, q:after {\n\tcontent: '';\n\tcontent: none;\n}\ntable {\n\tborder-collapse: collapse;\n\tborder-spacing: 0;\n}\n\n/* Prevent overscrolling */\n/* https://stackoverflow.com/a/17899813 */\nhtml {\n overflow: hidden;\n height: 100%;\n}\nbody {\n height: 100%;\n overflow: auto;\n}", "@font-face{\n\tfont-family: 'Source Code Pro';\n\tfont-weight: 400;\n\tfont-style: normal;\n\tfont-stretch: normal;\n\tsrc: url('./source-code-pro/SourceCodePro-Regular.ttf.woff2') format('woff2');\n}\n"],
"mappings": "AAIA,KAAM,KAAM,IAAK,KAAM,OAAQ,OAAQ,OACvC,GAAI,GAAI,GAAI,GAAI,GAAI,GAAI,EAAG,WAAY,IACvC,EAAG,KAAM,QAAS,QAAS,IAAK,KAAM,KACtC,IAAK,IAAK,GAAI,IAAK,IAAK,IAAK,EAAG,EAAG,KACnC,MAAO,OAAQ,OAAQ,IAAK,IAAK,GAAI,IACrC,EAAG,EAAG,EAAG,OACT,GAAI,GAAI,GAAI,GAAI,GAAI,GACpB,SAAU,KAAM,MAAO,OACvB,MAAO,QAAS,MAAO,MAAO,MAAO,GAAI,GAAI,GAC7C,QAAS,MAAO,OAAQ,QAAS,MACjC,OAAQ,WAAY,OAAQ,OAAQ,OACpC,KAAM,IAAK,OAAQ,KAAM,QAAS,QAClC,KAAM,KAAM,MAAO,MAhBnB,OAiBS,EAjBT,QAkBU,EACT,OAAQ,EACR,UAAW,KACX,KAAM,QACN,eAAgB,QACjB,CAEA,QAAS,MAAO,QAAS,WAAY,OACrC,OAAQ,OAAQ,OAAQ,KAAM,IAAK,QAClC,QAAS,KACV,CACA,KACC,YAAa,CACd,CACA,GAAI,GACH,WAAY,IACb,CACA,WAAY,EACX,OAAQ,IACT,CACA,UAAU,QAAS,UAAU,OAC7B,CAAC,QAAS,CAAC,OACV,QAAS,GACT,QAAS,IACV,CACA,MACC,gBAAiB,SACjB,eAAgB,CACjB,CAIA,KACI,SAAU,OACV,OAAQ,IACZ,CACA,KACI,OAAQ,KACR,SAAU,IACd,CCzDA,WACC,YAAa,gBACb,YAAa,IACb,WAAY,OACZ,aAAc,OACd,IAAK,kDAAyD,OAAO,QACtE",
"names": []
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
{
"type": "tildefriends-app",
"emoji": "👋",
"previous": "&n1QkPkB5JoduFSx8UKOY3IlZqS2GwLiTUZv4ZrEOthQ=.sha256"
"previous": "&sVSmI40DUgnS4TUa2AiKrlNj+qN3WDeXII3364OSMIo=.sha256"
}

View File

@@ -8,14 +8,10 @@
<link rel="stylesheet" href="regular.min.css" />
<link rel="stylesheet" href="solid.min.css" />
<link rel="stylesheet" href="brands.min.css" />
<style>
img {
margin-bottom: -8px;
}
.mySlides {
display: none;
}
</style>
<base target="_top" />
</head>
@@ -108,10 +104,10 @@
</a>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://testflight.apple.com/join/tXxgtSpE"
href="https://apps.apple.com/us/app/tilde-friends/id6742085604"
>
<img src="ios.svg" style="height: 2em; margin: 0" />
Get it on iOS (TestFlight)
Get it on iOS
</a>
</p>
<p>Just launch the app.</p>
@@ -156,7 +152,7 @@
href="https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage"
>
<img src="appimage.svg" style="height: 2em; margin: 0" />
Get Linux 64-bit AppImage
Get Linux x86_64 AppImage
</a>
</p>
<p>
@@ -272,11 +268,11 @@
</div>
<!-- Sandbox Section -->
<div class="w3-padding-64 w3-grey">
<div class="w3-padding-64 w3-pale-blue">
<div class="w3-row-padding">
<div class="w3-col">
<h1 class="w3-jumbo" style="text-align: right">
<b>Sandbox Security</b>
<b>App Sandboxes</b>
</h1>
<i
class="fa fa-road-barrier w3-right w3-jumbo w3-text-yellow"
@@ -284,7 +280,7 @@
></i>
<p>
Tilde Friends tries to make sure apps can be trusted using similar
techniques to how web browsers and operating systems do it.
techniques to web browsers and operating systems.
</p>
<p>
This is all a work in progress, and it varies by platform, so don't
@@ -298,16 +294,24 @@
<!-- Technlology Section -->
<div class="w3-container w3-padding-64 w3-light-grey w3-center">
<h1 class="w3-jumbo"><b>Built to Last</b></h1>
<p>
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 on top.
</p>
<h1 class="w3-jumbo"><b>One Pile of Code</b></h1>
<div class="w3-left-align">
<p>
Tilde Friends diverges from the Node.js web of modules from which
Secure Scuttlebutt was first developed. Here we strive to maintain a
single C program that works as a foundation for building a wide
variety of social and other applications.
</p>
<p>
Tilde Friends uses only a handful of 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 works.
</p>
</div>
<div class="w3-row" style="margin-top: 64px">
<a
@@ -377,7 +381,7 @@
<footer class="w3-container w3-padding-32 w3-blue-grey w3-center w3-xlarge">
<p class="w3-medium">
This page and Tilde Friends itself was made by Cory mostly in coffee
shops and a local pizza place.
shops and a local pizza place while listening to Gary's Bangers.
</p>
</footer>
</body>

View File

@@ -1,252 +0,0 @@
/**
* \file
* \defgroup tfapp Tilde Friends App JS
* Tilde Friends server-side app wrapper.
* @{
*/
/** \cond */
import * as core from './core.js';
export {App};
/** \endcond */
/** A sequence number of apps. */
let g_session_index = 0;
/**
** App constructor.
** @return An app instance.
*/
function App() {
this._send_queue = [];
this.calls = {};
this._next_call_id = 1;
return this;
}
/**
** Create a function wrapper that when called invokes a function on the app
** itself.
** @param api The function and argument names.
** @return A function.
*/
App.prototype.makeFunction = function (api) {
let self = this;
let result = function () {
let id = self._next_call_id++;
while (!id || self.calls[id]) {
id = self._next_call_id++;
}
let promise = new Promise(function (resolve, reject) {
self.calls[id] = {resolve: resolve, reject: reject};
});
let message = {
action: 'tfrpc',
method: api[0],
params: [...arguments],
id: id,
};
self.send(message);
return promise;
};
Object.defineProperty(result, 'name', {value: api[0], writable: false});
return result;
};
/**
** Send a message to the app.
** @param message The message to send.
*/
App.prototype.send = function (message) {
if (this._send_queue) {
if (this._on_output) {
this._send_queue.forEach((x) => this._on_output(x));
this._send_queue = null;
} else if (message) {
this._send_queue.push(message);
}
}
if (message && this._on_output) {
this._on_output(message);
}
};
/**
** App socket handler.
** @param request The HTTP request of the WebSocket connection.
** @param response The HTTP response.
*/
exports.app_socket = async function socket(request, response) {
let process;
let options = {};
let credentials = await httpd.auth_query(request.headers);
response.onClose = async function () {
if (process && process.task) {
process.task.kill();
}
if (process) {
process.timeout = 0;
}
};
response.onMessage = async function (event) {
if (event.opCode == 0x1 || event.opCode == 0x2) {
let message;
try {
message = JSON.parse(event.data);
} catch (error) {
print(
'WebSocket error:',
error,
event.data,
event.data.length,
event.opCode
);
return;
}
if (!process && message.action == 'hello') {
let packageOwner;
let packageName;
let blobId;
let match;
let parentApp;
if (
(match = /^\/([&%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(message.path))
) {
blobId = match[1];
} else if ((match = /^\/\~([^\/]+)\/([^\/]+)\/$/.exec(message.path))) {
packageOwner = match[1];
packageName = match[2];
blobId = await new Database(packageOwner).get('path:' + packageName);
if (!blobId) {
response.send(
JSON.stringify({
action: 'tfrpc',
method: 'error',
params: [message.path + ' not found'],
id: -1,
}),
0x1
);
return;
}
if (packageOwner != 'core') {
let coreId = await new Database('core').get('path:' + packageName);
parentApp = {
path: '/~core/' + packageName + '/',
id: coreId,
};
}
}
response.send(
JSON.stringify(
Object.assign(
{
action: 'session',
credentials: credentials,
parentApp: parentApp,
id: blobId,
},
await ssb_internal.getIdentityInfo(
credentials?.session?.name,
packageOwner,
packageName
)
)
),
0x1
);
options.api = message.api || [];
options.credentials = credentials;
options.packageOwner = packageOwner;
options.packageName = packageName;
options.url = message.url;
let sessionId = 'session_' + (g_session_index++).toString();
if (blobId) {
if (message.edit_only) {
response.send(
JSON.stringify({action: 'ready', edit_only: true}),
0x1
);
} else {
process = await core.getProcessBlob(blobId, sessionId, options);
}
}
if (process) {
process.client_api.tfrpc = function (message) {
if (message.id) {
let calls = process?.app?.calls;
if (calls) {
let call = calls[message.id];
if (call) {
if (message.error !== undefined) {
call.reject(message.error);
} else {
call.resolve(message.result);
}
delete calls[message.id];
}
}
}
};
process.app._on_output = (message) =>
response.send(JSON.stringify(message), 0x1);
process.app.send();
}
let ping = function () {
let now = Date.now();
let again = true;
if (now - process.lastActive < process.timeout) {
// Active.
} else if (process.lastPing > process.lastActive) {
// We lost them.
if (process.task) {
process.task.kill();
}
again = false;
} else {
// Idle. Ping them.
response.send('', 0x9);
process.lastPing = now;
}
if (again && process.timeout) {
setTimeout(ping, process.timeout);
}
};
if (process && process.timeout > 0) {
setTimeout(ping, process.timeout);
}
} else {
if (process) {
if (process.client_api[message.action]) {
process.client_api[message.action](message);
} else if (process.eventHandlers['message']) {
await core.invoke(process.eventHandlers['message'], [message]);
}
}
}
} else if (event.opCode == 0x8) {
// Close.
if (process && process.task) {
process.task.kill();
}
response.send(event.data, 0x8);
} else if (event.opCode == 0xa) {
// PONG
}
if (process) {
process.lastActive = Date.now();
}
};
response.upgrade(100, {});
};
/** @} */

View File

@@ -75,6 +75,10 @@
margin-bottom: 1em;
padding: 1em;
}
#code_of_conduct:has(>textarea:empty) {
display: none;
width: 100%;
}
</style>
<div style="display: flex; flex-direction: column; max-width: 1280px; margin: auto">
<h1 ?hidden=${this.name}>Welcome.</h1>
@@ -126,8 +130,10 @@
There is currently no administrator. You will be made administrator.
</div>
<h2>Code of Conduct</h2>
<textarea readonly rows="20" cols="80" style="resize: none">${this.code_of_conduct}</textarea>
<div id="code_of_conduct">
<h2>Code of Conduct</h2>
<textarea readonly rows="20" style="resize: none; width: 100%">${this.code_of_conduct}</textarea>
</div>
</div>
`;
}

View File

@@ -29,7 +29,10 @@ const k_api = {
error: {args: ['error'], func: api_error},
localStorageSet: {args: ['key', 'value'], func: api_localStorageSet},
localStorageGet: {args: ['key'], func: api_localStorageGet},
requestPermission: {args: ['permission', 'id'], func: api_requestPermission},
requestPermission: {
args: ['permission', 'id', 'description'],
func: api_requestPermission,
},
print: {args: ['...'], func: api_print},
setHash: {args: ['hash'], func: api_setHash},
};
@@ -269,12 +272,20 @@ class TfNavigationElement extends LitElement {
return html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div
style="position: absolute; top: 0; padding: 0; margin: 0; z-index: 100; display: flex; justify-content: center; width: 100%"
style="position: absolute; top: 0; padding: 0; margin: 0; z-index: 100; display: flex; justify-content: center; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.4)"
@click=${() => (this.show_permissions = false)}
>
<div
style="background-color: #444; padding: 1em; margin: 0 auto; border-left: 4px solid #fff; border-right: 4px solid #fff; border-bottom: 4px solid #fff"
style="position: absolute; background-color: #444; padding: 1em; margin: auto; border-left: 4px solid #fff; border-right: 4px solid #fff; border-bottom: 4px solid #fff"
@click=${(event) => event.stopPropagation()}
>
<div>This app has the following permissions:</div>
<div>
This app at <code>${window.location.pathname}</code> has the
following permissions:
</div>
${Object.keys(this.permissions).length == 0
? html`<p class="w3-container">(no permissions)</p>`
: undefined}
${Object.keys(this.permissions).map(
(key) => html`
<div>
@@ -369,16 +380,15 @@ class TfNavigationElement extends LitElement {
>${this.version?.number}</span
>
<a
class="w3-bar-item"
class="w3-bar-item w3-button w3-text-white"
accesskey="h"
@mouseover=${set_access_key_title}
data-tip="Open home app."
href="/"
style="color: #fff; white-space: nowrap"
>TF</a
>
<a
class="w3-bar-item"
class="w3-bar-item w3-button w3-text-light-gray"
accesskey="a"
@mouseover=${set_access_key_title}
data-tip="Open apps list."
@@ -386,7 +396,7 @@ class TfNavigationElement extends LitElement {
>apps</a
>
<a
class="w3-bar-item"
class="w3-bar-item w3-button w3-text-light-gray"
accesskey="e"
@mouseover=${set_access_key_title}
data-tip="Toggle the app editor."
@@ -395,7 +405,7 @@ class TfNavigationElement extends LitElement {
>edit</a
>
<a
class="w3-bar-item"
class="w3-bar-item w3-button"
accesskey="p"
@mouseover=${set_access_key_title}
data-tip="View and change permissions."
@@ -424,13 +434,35 @@ class TfNavigationElement extends LitElement {
</div>
${this.status?.is_error
? html`
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div class="w3-model w3-animate-top" style="position: absolute; left: 50%; transform: translate(-50%); z-index: 1">
<dijv class="w3-modal-content w3-card-4" style="display: block; padding: 1em">
<span id="close_error" @click=${self.clear_error} class="w3-button w3-display-topright">&times;</span>
<div style="color: ${this.status.color ?? k_color_error}"><b>ERROR:</b><p id="error" style="white-space: pre">${this.status.message}</p></div>
<style>
#error {
white-space: pre;
}
</style>
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<div
class="w3-modal"
style="position: absolute; left: 50%; transform: translate(-50%); z-index: 1; display: flex; justify-content: center"
>
<div
class="w3-modal-content w3-card-4 w3-animate-top"
style="display: block; position: absolute; padding: 1em; top: 0"
>
<span
id="close_error"
@click=${self.clear_error}
class="w3-button w3-display-topright w3-red"
>&times;</span
>
<div
style="color: ${this.status.color ??
k_color_error}; display: block"
>
<b>ERROR:</b>
<p id="error">${this.status.message}</p>
</div>
</div>
</div>
</div>
`
: undefined}
`;
@@ -910,7 +942,7 @@ async function edit() {
* Open a performance trace.
*/
function trace() {
window.open(`/speedscope/#profileURL=${encodeURIComponent('/trace')}`);
window.open(`/~core/trace/`);
}
/**
@@ -993,6 +1025,34 @@ function closeEditor() {
document.getElementById('viewPane').style.display = 'flex';
}
/**
* Reload any static HTML content in the iframe.
*/
async function update_html() {
try {
let response = await fetch(window.location.href);
let text = await response.text();
let parser = new DOMParser();
let new_doc = parser.parseFromString(text, 'text/html');
let new_html = new_doc.getElementById('document').attributes.srcdoc.value;
let iframe = document.getElementById('document');
let sandbox = iframe.sandbox;
let allow = iframe.allow;
let iframe_parent = iframe.parentNode;
iframe_parent.removeChild(iframe);
let new_iframe = document.createElement('iframe');
new_iframe.sandbox = sandbox;
new_iframe.id = 'document';
new_iframe.srcdoc = new_html;
new_iframe.allow = allow;
iframe_parent.appendChild(new_iframe);
} catch (e) {
alert(error);
}
}
/**
* Save the app.
* @param save_to An optional path to which to save the app.
@@ -1088,7 +1148,7 @@ function save(save_to) {
if (save_path != window.location.pathname) {
alert('Saved to ' + save_path + '.');
} else if (!g_files['app.js']) {
window.location.reload();
update_html();
} else {
reconnect(save_path);
}
@@ -1222,10 +1282,10 @@ function api_localStorageGet(key) {
/**
* Request a permission
* @param permission The permission to request.
* @param id The id requeesting the permission.
* @param description An optional human-readable description of the action for which the permission is being requested.
* @return A promise fulfilled if the permission was granted.
*/
function api_requestPermission(permission, id) {
function api_requestPermission(permission, description) {
let outer = document.createElement('div');
outer.classList.add('permissions');
@@ -1242,6 +1302,18 @@ function api_requestPermission(permission, id) {
div.appendChild(span);
container.appendChild(div);
if (description) {
container.appendChild(document.createTextNode('for the action:'));
let description_div = document.createElement('div');
description_div.classList.add('w3-border');
description_div.classList.add('w3-padding');
description_div.style.backgroundColor = '#666';
description_div.style.maxHeight = '3em';
description_div.style.overflow = 'auto';
description_div.appendChild(document.createTextNode(description));
container.appendChild(description_div);
}
div = document.createElement('div');
div.style = 'padding: 1em';
let check = document.createElement('input');
@@ -1455,53 +1527,24 @@ function blur() {
send({event: 'blur'});
}
/**
* Notify the app of visibility change. Seems to work when changing apps/tabs
* where focus/blur doesn't on mobile.
*/
function visibilitychange() {
if (!document.hidden) {
if (g_socket && g_socket.readyState == g_socket.CLOSED) {
connectSocket();
}
}
}
/**
* Handle a message.
* @param event The message.
*/
function message(event) {
if (
event.data &&
event.data.event == 'resizeMe' &&
event.data.width &&
event.data.height
) {
let iframe = document.getElementById('iframe_' + event.data.name);
iframe.setAttribute('width', event.data.width);
iframe.setAttribute('height', event.data.height);
} else if (event.data && event.data.action == 'setHash') {
window.location.hash = event.data.hash;
} else if (event.data && event.data.action == 'storeBlob') {
fetch('/save', {
method: 'POST',
headers: {
'Content-Type': 'application/binary',
},
body: event.data.blob.buffer,
})
.then(function (response) {
if (!response.ok) {
throw new Error(response.status + ' ' + response.statusText);
}
return response.text();
})
.then(function (text) {
let iframe = document.getElementById('document');
iframe.contentWindow.postMessage(
{
storeBlobComplete: {
name: event.data.blob.name,
path: text,
type: event.data.blob.type,
context: event.data.context,
},
},
'*'
);
});
} else {
send({event: 'message', message: event.data});
}
send({event: 'message', message: event.data});
}
/**
@@ -1587,6 +1630,25 @@ function connectSocket(path) {
}
}
/**
* Determine a CodeMirror language mode from filename.
* @param name Filename.
* @return The mode name.
*/
function modeFromName(name) {
switch (name.split('.').pop()) {
case 'md':
return 'markdown';
case 'css':
return 'css';
case 'js':
return 'javascript';
case 'xml':
case 'svg':
return 'xml';
}
}
/**
* Open a file by name.
* @param name The file to open.
@@ -1598,6 +1660,7 @@ function openFile(name) {
: cm6.EditorState.create({doc: '', extensions: cm6.extensions});
let oldDoc = g_editor.state;
g_editor.setState(newDoc);
cm6.setEditorMode(g_editor, modeFromName(name));
if (g_files[g_current_file]) {
g_files[g_current_file].doc = oldDoc;
@@ -1864,6 +1927,7 @@ window.addEventListener('load', function () {
window.addEventListener('beforeunload', function () {
g_unloading = true;
});
document.addEventListener('visibilitychange', visibilitychange);
document.getElementById('name').value = window.location.pathname;
document
.getElementById('closeEditor')

View File

@@ -5,36 +5,14 @@
* @{
*/
/** \cond */
import * as app from './app.js';
export {invoke, getProcessBlob};
/** \endcond */
/** All running processes. */
let gProcesses = {};
/** Whether stats are currently being sent. */
let gStatsTimer = false;
/** Effectively a process ID. */
let g_handler_index = 0;
/** Time between pings, in milliseconds. */
const k_ping_interval = 60 * 1000;
/**
* Print an error.
* @param error The error.
*/
function printError(error) {
if (error.stackTrace) {
print(error.fileName + ':' + error.lineNumber + ': ' + error.message);
print(error.stackTrace);
} else {
for (let [k, v] of Object.entries(error)) {
print(k, v);
}
print(error.toString());
}
}
/** Whether updating accounts information is currently scheduled. */
let g_update_accounts_scheduled;
/**
* Invoke a handler.
@@ -165,7 +143,7 @@ function postMessageInternal(from, to, message) {
* @param options Other options.
* @return The process.
*/
async function getProcessBlob(blobId, key, options) {
exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
let process = gProcesses[key];
if (!process && !(options && 'create' in options && !options.create)) {
let resolveReady;
@@ -181,11 +159,63 @@ async function getProcessBlob(blobId, key, options) {
process.url = options?.url;
process.eventHandlers = {};
if (!options?.script || options?.script === 'app.js') {
process.app = new app.App();
process._send_queue = [];
process._calls = {};
process._next_call_id = 1;
/**
** Create a function wrapper that when called invokes a function on the app
** itself.
** @param api The function and argument names.
** @return A function.
*/
process.makeFunction = function (api) {
let result = function () {
let id = process._next_call_id++;
while (!id || process._calls[id]) {
id = process._next_call_id++;
}
let promise = new Promise(function (resolve, reject) {
process._calls[id] = {resolve: resolve, reject: reject};
});
let message = {
action: 'tfrpc',
method: api[0],
params: [...arguments],
id: id,
};
process.send(message);
return promise;
};
Object.defineProperty(result, 'name', {
value: api[0],
writable: false,
});
return result;
};
/**
** Send a message to the app.
** @param message The message to send.
*/
process.send = function (message) {
if (process._send_queue) {
if (process._on_output) {
process._send_queue.forEach((x) => process._on_output(x));
process._send_queue = null;
} else if (message) {
process._send_queue.push(message);
}
}
if (message && process._on_output) {
process._on_output(message);
}
};
} else {
process.makeFunction = function (api) {
return function () {};
};
}
process.lastActive = Date.now();
process.lastPing = null;
process.timeout = k_ping_interval;
process.ready = new Promise(function (resolve, reject) {
resolveReady = resolve;
rejectReady = reject;
@@ -199,47 +229,18 @@ async function getProcessBlob(blobId, key, options) {
core: {
broadcast: broadcast.bind(process),
user: getUser(process, process),
allPermissionsGranted: async function () {
permissionTest: async function (permission, description) {
let user = process?.credentials?.session?.name;
let settings = await loadSettings();
if (
user &&
options?.packageOwner &&
options?.packageName &&
settings.userPermissions &&
settings.userPermissions[user]
) {
return settings.userPermissions[user];
}
},
permissionTest: async function (permission) {
let user = process?.credentials?.session?.name;
let settings = await loadSettings();
if (!user || !options?.packageOwner || !options?.packageName) {
return;
} else if (
settings.userPermissions &&
settings.userPermissions[user] &&
settings.userPermissions[user][options.packageOwner] &&
settings.userPermissions[user][options.packageOwner][
options.packageName
] &&
settings.userPermissions[user][options.packageOwner][
options.packageName
][permission] !== undefined
) {
if (
settings.userPermissions[user][options.packageOwner][
options.packageName
][permission]
) {
let permissions = await imports.core.permissionsGranted();
if (permissions && permissions[permission] !== undefined) {
if (permissions[permission]) {
return true;
} else {
throw Error(`Permission denied: ${permission}.`);
}
} else if (process.app) {
return process.app
.makeFunction(['requestPermission'])(permission)
} else {
return process
.makeFunction(['requestPermission'])(permission, description)
.then(async function (value) {
if (value == 'allow') {
await ssb.setUserPermission(
@@ -268,25 +269,28 @@ async function getProcessBlob(blobId, key, options) {
}
throw Error(`Permission denied: ${permission}.`);
});
} else {
throw Error(`Permission denied: ${permission}.`);
}
},
},
};
process.sendIdentities = async function () {
process.app.send(
Object.assign(
{
action: 'identities',
},
await ssb_internal.getIdentityInfo(
process?.credentials?.session?.name,
options?.packageOwner,
options?.packageName
)
)
let identities = await ssb_internal.getIdentityInfo(
process?.credentials?.session?.name,
options?.packageOwner,
options?.packageName
);
let json = JSON.stringify(identities);
if (process._last_sent_identities !== json) {
process.send(
Object.assign(
{
action: 'identities',
},
identities
)
);
process._last_sent_identities = json;
}
};
process.setActiveIdentity = async function (identity) {
if (
@@ -335,36 +339,11 @@ async function getProcessBlob(blobId, key, options) {
throw new Error('Must be signed-in to create an account.');
}
};
if (process.credentials?.permissions?.administration) {
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;
await new Database('core').set('settings', JSON.stringify(settings));
print('Done.');
};
imports.core.deleteUser = async function (user) {
await imports.core.permissionTest('delete_user');
let db = new Database('auth');
db.remove('user:' + user);
let users = new Set();
let users_original = await db.get('users');
try {
users = new Set(JSON.parse(users_original));
} catch {}
users.delete(user);
users = JSON.stringify([...users].sort());
if (users !== users_original) {
await db.set('users', users);
}
};
}
if (options.api) {
imports.app = {};
for (let i in options.api) {
let api = options.api[i];
imports.app[api[0]] = process.app.makeFunction(api);
imports.app[api[0]] = process.makeFunction(api);
}
}
for (let [name, f] of Object.entries(options?.imports || {})) {
@@ -375,118 +354,12 @@ async function getProcessBlob(blobId, key, options) {
imports.app.print(...args);
}
};
process.task.onError = function (error) {
try {
if (process.app) {
process.app.makeFunction(['error'])(error);
} else {
printError(error);
}
} catch (e) {
printError(error);
}
};
process.task.onError = process.makeFunction(['error']);
imports.ssb = Object.fromEntries(
Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
);
imports.ssb.createIdentity = () => process.createIdentity();
imports.ssb.addIdentity = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_id_add')
).then(function () {
return ssb.addIdentity(process.credentials.session.name, id);
});
}
};
imports.ssb.deleteIdentity = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_id_delete')
).then(function () {
return ssb.deleteIdentity(process.credentials.session.name, id);
});
}
};
imports.ssb.setActiveIdentity = (id) => process.setActiveIdentity(id);
imports.ssb.getPrivateKey = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_id_export')
).then(function () {
return ssb.getPrivateKey(process.credentials.session.name, id);
});
}
};
imports.ssb.appendMessageWithIdentity = function (id, message) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_append')
).then(function () {
return ssb.appendMessageWithIdentity(
process.credentials.session.name,
id,
message
);
});
}
};
imports.ssb.privateMessageEncrypt = function (id, recipients, message) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.privateMessageEncrypt(
process.credentials.session.name,
id,
recipients,
message
);
}
};
imports.ssb.privateMessageDecrypt = function (id, message) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.privateMessageDecrypt(
process.credentials.session.name,
id,
message
);
}
};
imports.ssb.swapWithServerIdentity = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.swapWithServerIdentity(
process.credentials.session.name,
id
);
}
};
if (
process.credentials &&
process.credentials.session &&
@@ -536,7 +409,7 @@ async function getProcessBlob(blobId, key, options) {
};
}
process.sendPermissions = async function sendPermissions() {
process.app.send({
process.send({
action: 'permissions',
permissions: await imports.core.permissionsGranted(),
});
@@ -561,36 +434,33 @@ async function getProcessBlob(blobId, key, options) {
},
};
ssb.registerImports(imports, process);
process.imports = imports;
process.task.setImports(imports);
process.task.activate();
let source = await ssb.blobGet(blobId);
let appSourceName = blobId;
let appSource = utf8Decode(source);
try {
let appObject = JSON.parse(appSource);
if (appObject.type == 'tildefriends-app') {
appSourceName = options?.script ?? 'app.js';
let id = appObject.files[appSourceName];
let blob = await ssb.blobGet(id);
appSource = utf8Decode(blob);
await process.task.loadFile([
'/tfrpc.js',
await File.readFile('core/tfrpc.js'),
]);
await Promise.all(
Object.keys(appObject.files).map(async function (f) {
await process.task.loadFile([
f,
await ssb.blobGet(appObject.files[f]),
]);
})
);
}
} catch (e) {
printError(e);
let appObject = JSON.parse(appSource);
if (appObject.type == 'tildefriends-app') {
appSourceName = options?.script ?? 'app.js';
let id = appObject.files[appSourceName];
let blob = await ssb.blobGet(id);
appSource = utf8Decode(blob);
await process.task.loadFile([
'/tfrpc.js',
await File.readFile('core/tfrpc.js'),
]);
await Promise.all(
Object.keys(appObject.files).map(async function (f) {
await process.task.loadFile([
f,
await ssb.blobGet(appObject.files[f]),
]);
})
);
}
if (process.app) {
process.app.send({action: 'ready', version: version()});
if (process.send) {
process.send({action: 'ready', version: version()});
await process.sendPermissions();
}
await process.task.execute({name: appSourceName, source: appSource});
@@ -600,15 +470,27 @@ async function getProcessBlob(blobId, key, options) {
sendStats();
}
} catch (error) {
if (process?.app && process?.task?.onError) {
if (process?.task?.onError) {
process.task.onError(error);
} else {
printError(error);
}
rejectReady(error);
if (rejectReady) {
rejectReady(error);
}
}
}
return process;
};
/**
* Send any changed account information.
*/
function updateAccounts() {
g_update_accounts_scheduled = false;
let promises = [];
for (let process of Object.values(gProcesses)) {
promises.push(process.sendIdentities());
}
return Promise.all(promises);
}
/**
@@ -616,6 +498,11 @@ async function getProcessBlob(blobId, key, options) {
*/
ssb_internal.addEventListener('message', function () {
broadcastEvent('onMessage', [...arguments]);
if (!g_update_accounts_scheduled) {
setTimeout(updateAccounts, 1000);
g_update_accounts_scheduled = true;
}
});
ssb_internal.addEventListener('blob', function () {
@@ -630,39 +517,15 @@ ssb_internal.addEventListener('connections', function () {
broadcastEvent('onConnectionsChanged', []);
});
/**
* Load settings from the database.
* @return The settings as a key value pairs object.
*/
async function loadSettings() {
let data = {};
try {
let settings = await new Database('core').get('settings');
if (settings) {
data = JSON.parse(settings);
}
} catch (error) {
print('Settings not found in database:', error);
}
for (let [key, value] of Object.entries(defaultGlobalSettings())) {
if (data[key] === undefined) {
data[key] = value.default_value;
}
}
return data;
}
/**
* Send periodic stats to all clients.
*/
function sendStats() {
let apps = Object.values(gProcesses)
.filter((process) => process.app)
.map((process) => process.app);
let apps = Object.values(gProcesses).filter((process) => process.send);
if (apps.length) {
let stats = getStats();
for (let app of apps) {
app.send({action: 'stats', stats: stats});
for (let process of apps) {
process.send({action: 'stats', stats: stats});
}
setTimeout(sendStats, 1000);
} else {

67
core/eula.html Normal file
View File

@@ -0,0 +1,67 @@
<!doctype html>
<html>
<head>
<title>Tilde Friends Usage Agreement</title>
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body class="w3-container">
<h1>Tilde Friends Usage Agreement</h1>
<p>
Tilde Friends is an app that enables communication with other users
through the
<a href="https://ssbc.github.io/scuttlebutt-protocol-guide/"
>Secure Scuttlebutt</a
>
protocol.
</p>
<h2>Your actions are your responsibility</h2>
<p>
Apple tolerates no objectionable content or abusive users on their
platforms.
</p>
<p>
You are responsible for your own actions within this app. You are
responsible for complying with all applicable rules and laws.
</p>
<h2>You choose what you see</h2>
<p>
You are in full control of the content you see in Tilde Friends. The peers
to which you choose to connect and the users you choose to follow directly
determine the content presented to you. Initially you will be following no
one with no connections and as a result see no user-generated content.
</p>
<p>
If you encounter objectionable content, you can filter it from your view
by blocking the user who posted it. This also makes it so that users
following you will not see it as a consequence of following you. You
moderate for your friends and vice versa.
</p>
<p>
You can also flag a message to report to the operators of services with
which you interact that the it should be considered for removal.
</p>
<p>
The <code>admin</code> app contains a variety of settings that control the
types of connections Tilde Friends will make or accept, including whether
the app will even accept or make connections at all. This is also where a
local blocklist can be managed.
</p>
<h2>This app is not a service</h2>
<p>
Tilde Friends is an app. It relies on no servers. The author of this app
has no more ability to see or filter what you post or read than any other
user of the network.
</p>
<h2>Agreement</h2>
<p>
If you do not accept these terms, do not use this app. You may close and
delete it now.
</p>
<div class="w3-center w3-margin">
<a class="w3-button w3-blue w3-round-large" href="/eula/accept"
>Accept Agreement</a
>
</div>
</body>
</html>

View File

@@ -5,7 +5,10 @@
<link type="text/css" rel="stylesheet" href="/static/style.css" />
<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="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
<meta
name="title"
content="Tilde Friends - Make friends and apps from your web browser."
@@ -63,7 +66,7 @@
class="vbox"
style="flex: 0 1 100%; display: none; overflow: auto"
>
<div class="navigation w3-bar" style="display: flex">
<div class="w3-bar w3-blue">
<button
class="w3-bar-item w3-button w3-blue"
id="closeEditor"
@@ -74,16 +77,6 @@
>
Close
</button>
<button
class="w3-bar-item w3-button w3-blue"
id="save"
name="save"
accesskey="s"
onmouseover="set_access_key_title(event)"
data-tip="Save the app under the given path"
>
Save
</button>
<button
class="w3-bar-item w3-button w3-blue"
id="icon"
@@ -134,13 +127,6 @@
>
</button>
<input
class="w3-bar-item w3-input w3-border w3-blue"
type="text"
id="name"
name="name"
style="flex: 1 1; min-width: 1em"
/>
<button
class="w3-bar-item w3-button w3-blue"
id="delete"
@@ -160,6 +146,23 @@
>
Trace
</button>
<button
class="w3-bar-item w3-button w3-blue w3-right"
id="save"
name="save"
accesskey="s"
onmouseover="set_access_key_title(event)"
data-tip="Save the app under the given path"
>
Save
</button>
<input
class="w3-bar-item w3-input w3-cobalt w3-right"
type="text"
id="name"
name="name"
style="min-width: 1em"
/>
</div>
<div class="hbox" style="flex: 1 1; overflow: auto">
<div style="overflow: auto">
@@ -175,7 +178,7 @@
<iframe
id="document"
sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-popups allow-downloads"
style="width: 100%; height: 100%; border: 0"
allow="clipboard-write"
></iframe>
</div>
</div>

View File

@@ -152,4 +152,11 @@ body {
border-bottom: 4px solid #fff;
padding: 1em;
margin: 0 auto;
max-width: 80%;
}
#document {
width: 100%;
height: 100%;
border: 0;
}

View File

@@ -25,14 +25,14 @@
}:
pkgs.stdenv.mkDerivation rec {
pname = "tildefriends";
version = "0.2025.9";
version = "0.2025.11";
src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net";
owner = "cory";
repo = "tildefriends";
rev = "v${version}";
hash = "sha256-1nhsfhdOO5HIiiTMb+uROB8nDPL/UpOYm52hZ/OpPyk=";
hash = "sha256-z4v4ghKOBTMv+agTUKg+HU8zfE4imluXFsozQCT4qX8=";
fetchSubmodules = true;
};

2
deps/c-ares vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,14 @@
import {EditorState} from "@codemirror/state"
import {EditorState, Compartment} from "@codemirror/state"
import {EditorView} from '@codemirror/view';
import {javascript} from "@codemirror/lang-javascript"
import {html} from "@codemirror/lang-html"
import {javascriptLanguage} from "@codemirror/lang-javascript"
import {htmlLanguage, html} from "@codemirror/lang-html"
import {css} from "@codemirror/lang-css"
import {markdown} from "@codemirror/lang-markdown"
import {xml} from "@codemirror/lang-xml"
import {search} from "@codemirror/search"
import {oneDark} from "./theme-tf-dark.js"
import {lineNumbers, highlightActiveLineGutter, highlightSpecialChars, highlightTrailingWhitespace, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, keymap, highlightWhitespace} from '@codemirror/view';
import {foldGutter, indentUnit, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap} from '@codemirror/language';
import {language, foldGutter, indentUnit, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap} from '@codemirror/language';
import {history, defaultKeymap, historyKeymap, indentWithTab} from '@codemirror/commands';
import {highlightSelectionMatches, searchKeymap} from '@codemirror/search';
import {autocompletion, closeBracketsKeymap, completionKeymap} from '@codemirror/autocomplete';
@@ -18,6 +20,9 @@ let updateListenerExtension = EditorView.updateListener.of((update) => {
}
});
/* https://codemirror.net/examples/config/ */
const languageConfig = new Compartment();
const extensions = [
lineNumbers(),
highlightActiveLineGutter(),
@@ -47,9 +52,7 @@ const extensions = [
...lintKeymap,
indentWithTab,
]),
javascript(),
html(),
css(),
languageConfig.of(javascriptLanguage),
search(),
oneDark,
updateListenerExtension,
@@ -60,11 +63,25 @@ function TildeFriendsEditorView(parent) {
extensions: extensions,
parent: parent,
});
};
}
function setEditorMode(view, mode) {
const k_modes = {
'css': css(),
'html': html(),
'javascript': javascriptLanguage,
'markdown': markdown(),
'xml': xml(),
};
view.dispatch({
effects: languageConfig.reconfigure(k_modes[mode])
});
}
export {
TildeFriendsEditorView,
EditorState,
EditorView,
extensions,
setEditorMode,
};

302
deps/codemirror_src/package-lock.json generated vendored
View File

@@ -9,6 +9,8 @@
"@codemirror/lang-html": "^6.4.8",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.0.1",
"@codemirror/lang-xml": "^6.0.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@rollup/plugin-node-resolve": "^15.2.3",
"codemirror": "^6.0.1",
@@ -19,9 +21,9 @@
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.19.1",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz",
"integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==",
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
"integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@@ -31,9 +33,9 @@
}
},
"node_modules/@codemirror/commands": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz",
"integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==",
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.1.tgz",
"integrity": "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@@ -97,24 +99,53 @@
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/lang-markdown": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz",
"integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.3.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/markdown": "^1.0.0"
}
},
"node_modules/@codemirror/lang-xml": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
"integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/xml": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.11.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
"version": "6.12.1",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.1.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.1.tgz",
"integrity": "sha512-te7To1EQHePBQQzasDKWmK2xKINIXpk+xAiSYr9ZN+VB4KaT+/Hi2PEkeErTk5BV3PTz1TLyQL4MtJfPkKZ9sw==",
"version": "6.9.2",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.2.tgz",
"integrity": "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
@@ -134,9 +165,9 @@
}
},
"node_modules/@codemirror/state": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.3.tgz",
"integrity": "sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
@@ -155,9 +186,9 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.38.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
"version": "6.39.7",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.7.tgz",
"integrity": "sha512-3Vif9hnNHJnl2YgOtkR/wzGzhYcQ8gy3LGdUhkLUU8xSBbgsTxrE8he/CMTpeINm5TgxLe2FmzvF6IYQL/BSAg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
@@ -217,9 +248,9 @@
}
},
"node_modules/@lezer/common": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz",
"integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.0.tgz",
"integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==",
"license": "MIT"
},
"node_modules/@lezer/css": {
@@ -234,18 +265,18 @@
}
},
"node_modules/@lezer/highlight": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.2.tgz",
"integrity": "sha512-z8TQwaBXXQIvG6i2g3e9cgMwUUXu9Ib7jo2qRRggdhwKpM56Dw3PM3wmexn+EGaaOZ7az0K7sjc3/gcGW7sz7A==",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/html": {
"version": "1.3.12",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.12.tgz",
"integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==",
"version": "1.3.13",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
@@ -276,14 +307,35 @@
}
},
"node_modules/@lezer/lr": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.5.tgz",
"integrity": "sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/markdown": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.2.tgz",
"integrity": "sha512-iNSdKrIK0FfOjVPVpV0fu7OykdncYpEzf4vkG9szFf60ql/ObZShoVbM9u1tgkogDOmubms1CyoNS2/unOXWNw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@lezer/xml": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
"integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^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",
@@ -360,9 +412,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
"cpu": [
"arm"
],
@@ -373,9 +425,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
"cpu": [
"arm64"
],
@@ -386,9 +438,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
"cpu": [
"arm64"
],
@@ -399,9 +451,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
"integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
"cpu": [
"x64"
],
@@ -412,9 +464,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
"cpu": [
"arm64"
],
@@ -425,9 +477,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
"cpu": [
"x64"
],
@@ -438,9 +490,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
"cpu": [
"arm"
],
@@ -451,9 +503,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
"cpu": [
"arm"
],
@@ -464,9 +516,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
"cpu": [
"arm64"
],
@@ -477,9 +529,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
"cpu": [
"arm64"
],
@@ -490,9 +542,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
"cpu": [
"loong64"
],
@@ -503,9 +555,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
"cpu": [
"ppc64"
],
@@ -516,9 +568,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
"cpu": [
"riscv64"
],
@@ -529,9 +581,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
"cpu": [
"riscv64"
],
@@ -542,9 +594,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
"cpu": [
"s390x"
],
@@ -555,9 +607,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
"cpu": [
"x64"
],
@@ -568,9 +620,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
"cpu": [
"x64"
],
@@ -581,9 +633,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
"cpu": [
"arm64"
],
@@ -594,9 +646,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
"cpu": [
"arm64"
],
@@ -607,9 +659,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
"cpu": [
"ia32"
],
@@ -620,9 +672,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
"cpu": [
"x64"
],
@@ -633,9 +685,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
"cpu": [
"x64"
],
@@ -825,9 +877,9 @@
}
},
"node_modules/rollup": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -840,28 +892,28 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.52.5",
"@rollup/rollup-android-arm64": "4.52.5",
"@rollup/rollup-darwin-arm64": "4.52.5",
"@rollup/rollup-darwin-x64": "4.52.5",
"@rollup/rollup-freebsd-arm64": "4.52.5",
"@rollup/rollup-freebsd-x64": "4.52.5",
"@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
"@rollup/rollup-linux-arm-musleabihf": "4.52.5",
"@rollup/rollup-linux-arm64-gnu": "4.52.5",
"@rollup/rollup-linux-arm64-musl": "4.52.5",
"@rollup/rollup-linux-loong64-gnu": "4.52.5",
"@rollup/rollup-linux-ppc64-gnu": "4.52.5",
"@rollup/rollup-linux-riscv64-gnu": "4.52.5",
"@rollup/rollup-linux-riscv64-musl": "4.52.5",
"@rollup/rollup-linux-s390x-gnu": "4.52.5",
"@rollup/rollup-linux-x64-gnu": "4.52.5",
"@rollup/rollup-linux-x64-musl": "4.52.5",
"@rollup/rollup-openharmony-arm64": "4.52.5",
"@rollup/rollup-win32-arm64-msvc": "4.52.5",
"@rollup/rollup-win32-ia32-msvc": "4.52.5",
"@rollup/rollup-win32-x64-gnu": "4.52.5",
"@rollup/rollup-win32-x64-msvc": "4.52.5",
"@rollup/rollup-android-arm-eabi": "4.54.0",
"@rollup/rollup-android-arm64": "4.54.0",
"@rollup/rollup-darwin-arm64": "4.54.0",
"@rollup/rollup-darwin-x64": "4.54.0",
"@rollup/rollup-freebsd-arm64": "4.54.0",
"@rollup/rollup-freebsd-x64": "4.54.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
"@rollup/rollup-linux-arm-musleabihf": "4.54.0",
"@rollup/rollup-linux-arm64-gnu": "4.54.0",
"@rollup/rollup-linux-arm64-musl": "4.54.0",
"@rollup/rollup-linux-loong64-gnu": "4.54.0",
"@rollup/rollup-linux-ppc64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-musl": "4.54.0",
"@rollup/rollup-linux-s390x-gnu": "4.54.0",
"@rollup/rollup-linux-x64-gnu": "4.54.0",
"@rollup/rollup-linux-x64-musl": "4.54.0",
"@rollup/rollup-openharmony-arm64": "4.54.0",
"@rollup/rollup-win32-arm64-msvc": "4.54.0",
"@rollup/rollup-win32-ia32-msvc": "4.54.0",
"@rollup/rollup-win32-x64-gnu": "4.54.0",
"@rollup/rollup-win32-x64-msvc": "4.54.0",
"fsevents": "~2.3.2"
}
},
@@ -943,9 +995,9 @@
}
},
"node_modules/terser": {
"version": "5.44.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
"version": "5.44.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {

View File

@@ -7,6 +7,8 @@
"@codemirror/lang-html": "^6.4.8",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.0.1",
"@codemirror/lang-xml": "^6.0.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@rollup/plugin-node-resolve": "^15.2.3",
"codemirror": "^6.0.1",

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2018 Jamie Wong
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,2 +0,0 @@
This is a self-contained release of https://github.com/jlfwong/speedscope.
To use it, open index.html in Chrome or Firefox.

View File

@@ -1,3 +0,0 @@
speedscope@1.24.0
Mon Oct 20 18:11:29 PDT 2025
fc76932551754a442cd5c4f0afdba28032d14d8a

File diff suppressed because one or more lines are too long

2531
deps/sqlite/shell.c vendored

File diff suppressed because it is too large Load Diff

5713
deps/sqlite/sqlite3.c vendored

File diff suppressed because it is too large Load Diff

415
deps/sqlite/sqlite3.h vendored
View File

@@ -146,9 +146,12 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.50.4"
#define SQLITE_VERSION_NUMBER 3050004
#define SQLITE_SOURCE_ID "2025-07-30 19:33:53 4d8adfb30e03f9cf27f800a2c1ba3c48fb4ca1b08b0f5ed59a4d5ecbf45e20a3"
#define SQLITE_VERSION "3.51.1"
#define SQLITE_VERSION_NUMBER 3051001
#define SQLITE_SOURCE_ID "2025-11-28 17:28:25 281fc0e9afc38674b9b0991943b9e9d1e64c6cbdb133d35f6f5c87ff6af38a88"
#define SQLITE_SCM_BRANCH "branch-3.51"
#define SQLITE_SCM_TAGS "release version-3.51.1"
#define SQLITE_SCM_DATETIME "2025-11-28T17:28:25.933Z"
/*
** CAPI3REF: Run-Time Library Version Numbers
@@ -168,9 +171,9 @@ extern "C" {
** assert( strcmp(sqlite3_libversion(),SQLITE_VERSION)==0 );
** </pre></blockquote>)^
**
** ^The sqlite3_version[] string constant contains the text of [SQLITE_VERSION]
** macro. ^The sqlite3_libversion() function returns a pointer to the
** to the sqlite3_version[] string constant. The sqlite3_libversion()
** ^The sqlite3_version[] string constant contains the text of the
** [SQLITE_VERSION] macro. ^The sqlite3_libversion() function returns a
** pointer to the sqlite3_version[] string constant. The sqlite3_libversion()
** function is provided for use in DLLs since DLL users usually do not have
** direct access to string constants within the DLL. ^The
** sqlite3_libversion_number() function returns an integer equal to
@@ -370,7 +373,7 @@ typedef int (*sqlite3_callback)(void*,int,char**, char**);
** without having to use a lot of C code.
**
** ^The sqlite3_exec() interface runs zero or more UTF-8 encoded,
** semicolon-separate SQL statements passed into its 2nd argument,
** semicolon-separated SQL statements passed into its 2nd argument,
** in the context of the [database connection] passed in as its 1st
** argument. ^If the callback function of the 3rd argument to
** sqlite3_exec() is not NULL, then it is invoked for each result row
@@ -403,7 +406,7 @@ typedef int (*sqlite3_callback)(void*,int,char**, char**);
** result row is NULL then the corresponding string pointer for the
** sqlite3_exec() callback is a NULL pointer. ^The 4th argument to the
** sqlite3_exec() callback is an array of pointers to strings where each
** entry represents the name of corresponding result column as obtained
** entry represents the name of a corresponding result column as obtained
** from [sqlite3_column_name()].
**
** ^If the 2nd parameter to sqlite3_exec() is a NULL pointer, a pointer
@@ -497,6 +500,9 @@ SQLITE_API int sqlite3_exec(
#define SQLITE_ERROR_MISSING_COLLSEQ (SQLITE_ERROR | (1<<8))
#define SQLITE_ERROR_RETRY (SQLITE_ERROR | (2<<8))
#define SQLITE_ERROR_SNAPSHOT (SQLITE_ERROR | (3<<8))
#define SQLITE_ERROR_RESERVESIZE (SQLITE_ERROR | (4<<8))
#define SQLITE_ERROR_KEY (SQLITE_ERROR | (5<<8))
#define SQLITE_ERROR_UNABLE (SQLITE_ERROR | (6<<8))
#define SQLITE_IOERR_READ (SQLITE_IOERR | (1<<8))
#define SQLITE_IOERR_SHORT_READ (SQLITE_IOERR | (2<<8))
#define SQLITE_IOERR_WRITE (SQLITE_IOERR | (3<<8))
@@ -531,6 +537,8 @@ SQLITE_API int sqlite3_exec(
#define SQLITE_IOERR_DATA (SQLITE_IOERR | (32<<8))
#define SQLITE_IOERR_CORRUPTFS (SQLITE_IOERR | (33<<8))
#define SQLITE_IOERR_IN_PAGE (SQLITE_IOERR | (34<<8))
#define SQLITE_IOERR_BADKEY (SQLITE_IOERR | (35<<8))
#define SQLITE_IOERR_CODEC (SQLITE_IOERR | (36<<8))
#define SQLITE_LOCKED_SHAREDCACHE (SQLITE_LOCKED | (1<<8))
#define SQLITE_LOCKED_VTAB (SQLITE_LOCKED | (2<<8))
#define SQLITE_BUSY_RECOVERY (SQLITE_BUSY | (1<<8))
@@ -589,7 +597,7 @@ SQLITE_API int sqlite3_exec(
** Note in particular that passing the SQLITE_OPEN_EXCLUSIVE flag into
** [sqlite3_open_v2()] does *not* cause the underlying database file
** to be opened using O_EXCL. Passing SQLITE_OPEN_EXCLUSIVE into
** [sqlite3_open_v2()] has historically be a no-op and might become an
** [sqlite3_open_v2()] has historically been a no-op and might become an
** error in future versions of SQLite.
*/
#define SQLITE_OPEN_READONLY 0x00000001 /* Ok for sqlite3_open_v2() */
@@ -683,7 +691,7 @@ SQLITE_API int sqlite3_exec(
** SQLite uses one of these integer values as the second
** argument to calls it makes to the xLock() and xUnlock() methods
** of an [sqlite3_io_methods] object. These values are ordered from
** lest restrictive to most restrictive.
** least restrictive to most restrictive.
**
** The argument to xLock() is always SHARED or higher. The argument to
** xUnlock is either SHARED or NONE.
@@ -924,7 +932,7 @@ struct sqlite3_io_methods {
** connection. See also [SQLITE_FCNTL_FILE_POINTER].
**
** <li>[[SQLITE_FCNTL_SYNC_OMITTED]]
** No longer in use.
** The SQLITE_FCNTL_SYNC_OMITTED file-control is no longer used.
**
** <li>[[SQLITE_FCNTL_SYNC]]
** The [SQLITE_FCNTL_SYNC] opcode is generated internally by SQLite and
@@ -999,7 +1007,7 @@ struct sqlite3_io_methods {
**
** <li>[[SQLITE_FCNTL_VFSNAME]]
** ^The [SQLITE_FCNTL_VFSNAME] opcode can be used to obtain the names of
** all [VFSes] in the VFS stack. The names are of all VFS shims and the
** all [VFSes] in the VFS stack. The names of all VFS shims and the
** final bottom-level VFS are written into memory obtained from
** [sqlite3_malloc()] and the result is stored in the char* variable
** that the fourth parameter of [sqlite3_file_control()] points to.
@@ -1013,7 +1021,7 @@ struct sqlite3_io_methods {
** ^The [SQLITE_FCNTL_VFS_POINTER] opcode finds a pointer to the top-level
** [VFSes] currently in use. ^(The argument X in
** sqlite3_file_control(db,SQLITE_FCNTL_VFS_POINTER,X) must be
** of type "[sqlite3_vfs] **". This opcodes will set *X
** of type "[sqlite3_vfs] **". This opcode will set *X
** to a pointer to the top-level VFS.)^
** ^When there are multiple VFS shims in the stack, this opcode finds the
** upper-most shim only.
@@ -1203,7 +1211,7 @@ struct sqlite3_io_methods {
** <li>[[SQLITE_FCNTL_EXTERNAL_READER]]
** The EXPERIMENTAL [SQLITE_FCNTL_EXTERNAL_READER] opcode is used to detect
** whether or not there is a database client in another process with a wal-mode
** transaction open on the database or not. It is only available on unix.The
** transaction open on the database or not. It is only available on unix. The
** (void*) argument passed with this file-control should be a pointer to a
** value of type (int). The integer value is set to 1 if the database is a wal
** mode database and there exists at least one client in another process that
@@ -1221,6 +1229,15 @@ struct sqlite3_io_methods {
** database is not a temp db, then the [SQLITE_FCNTL_RESET_CACHE] file-control
** purges the contents of the in-memory page cache. If there is an open
** transaction, or if the db is a temp-db, this opcode is a no-op, not an error.
**
** <li>[[SQLITE_FCNTL_FILESTAT]]
** The [SQLITE_FCNTL_FILESTAT] opcode returns low-level diagnostic information
** about the [sqlite3_file] objects used access the database and journal files
** for the given schema. The fourth parameter to [sqlite3_file_control()]
** should be an initialized [sqlite3_str] pointer. JSON text describing
** various aspects of the sqlite3_file object is appended to the sqlite3_str.
** The SQLITE_FCNTL_FILESTAT opcode is usually a no-op, unless compile-time
** options are used to enable it.
** </ul>
*/
#define SQLITE_FCNTL_LOCKSTATE 1
@@ -1266,6 +1283,7 @@ struct sqlite3_io_methods {
#define SQLITE_FCNTL_RESET_CACHE 42
#define SQLITE_FCNTL_NULL_IO 43
#define SQLITE_FCNTL_BLOCK_ON_CONNECT 44
#define SQLITE_FCNTL_FILESTAT 45
/* deprecated names */
#define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE
@@ -1628,7 +1646,7 @@ struct sqlite3_vfs {
** SQLite interfaces so that an application usually does not need to
** invoke sqlite3_initialize() directly. For example, [sqlite3_open()]
** calls sqlite3_initialize() so the SQLite library will be automatically
** initialized when [sqlite3_open()] is called if it has not be initialized
** initialized when [sqlite3_open()] is called if it has not been initialized
** already. ^However, if SQLite is compiled with the [SQLITE_OMIT_AUTOINIT]
** compile-time option, then the automatic calls to sqlite3_initialize()
** are omitted and the application must call sqlite3_initialize() directly
@@ -1885,21 +1903,21 @@ struct sqlite3_mem_methods {
** The [sqlite3_mem_methods]
** structure is filled with the currently defined memory allocation routines.)^
** This option can be used to overload the default memory allocation
** routines with a wrapper that simulations memory allocation failure or
** routines with a wrapper that simulates memory allocation failure or
** tracks memory usage, for example. </dd>
**
** [[SQLITE_CONFIG_SMALL_MALLOC]] <dt>SQLITE_CONFIG_SMALL_MALLOC</dt>
** <dd> ^The SQLITE_CONFIG_SMALL_MALLOC option takes single argument of
** <dd> ^The SQLITE_CONFIG_SMALL_MALLOC option takes a single argument of
** type int, interpreted as a boolean, which if true provides a hint to
** SQLite that it should avoid large memory allocations if possible.
** SQLite will run faster if it is free to make large memory allocations,
** but some application might prefer to run slower in exchange for
** but some applications might prefer to run slower in exchange for
** guarantees about memory fragmentation that are possible if large
** allocations are avoided. This hint is normally off.
** </dd>
**
** [[SQLITE_CONFIG_MEMSTATUS]] <dt>SQLITE_CONFIG_MEMSTATUS</dt>
** <dd> ^The SQLITE_CONFIG_MEMSTATUS option takes single argument of type int,
** <dd> ^The SQLITE_CONFIG_MEMSTATUS option takes a single argument of type int,
** interpreted as a boolean, which enables or disables the collection of
** memory allocation statistics. ^(When memory allocation statistics are
** disabled, the following SQLite interfaces become non-operational:
@@ -1944,7 +1962,7 @@ struct sqlite3_mem_methods {
** ^If pMem is NULL and N is non-zero, then each database connection
** does an initial bulk allocation for page cache memory
** from [sqlite3_malloc()] sufficient for N cache lines if N is positive or
** of -1024*N bytes if N is negative, . ^If additional
** of -1024*N bytes if N is negative. ^If additional
** page cache memory is needed beyond what is provided by the initial
** allocation, then SQLite goes to [sqlite3_malloc()] separately for each
** additional cache line. </dd>
@@ -1973,7 +1991,7 @@ struct sqlite3_mem_methods {
** <dd> ^(The SQLITE_CONFIG_MUTEX option takes a single argument which is a
** pointer to an instance of the [sqlite3_mutex_methods] structure.
** The argument specifies alternative low-level mutex routines to be used
** in place the mutex routines built into SQLite.)^ ^SQLite makes a copy of
** in place of the mutex routines built into SQLite.)^ ^SQLite makes a copy of
** the content of the [sqlite3_mutex_methods] structure before the call to
** [sqlite3_config()] returns. ^If SQLite is compiled with
** the [SQLITE_THREADSAFE | SQLITE_THREADSAFE=0] compile-time option then
@@ -2015,7 +2033,7 @@ struct sqlite3_mem_methods {
**
** [[SQLITE_CONFIG_GETPCACHE2]] <dt>SQLITE_CONFIG_GETPCACHE2</dt>
** <dd> ^(The SQLITE_CONFIG_GETPCACHE2 option takes a single argument which
** is a pointer to an [sqlite3_pcache_methods2] object. SQLite copies of
** is a pointer to an [sqlite3_pcache_methods2] object. SQLite copies off
** the current page cache implementation into that object.)^ </dd>
**
** [[SQLITE_CONFIG_LOG]] <dt>SQLITE_CONFIG_LOG</dt>
@@ -2032,7 +2050,7 @@ struct sqlite3_mem_methods {
** the logger function is a copy of the first parameter to the corresponding
** [sqlite3_log()] call and is intended to be a [result code] or an
** [extended result code]. ^The third parameter passed to the logger is
** log message after formatting via [sqlite3_snprintf()].
** a log message after formatting via [sqlite3_snprintf()].
** The SQLite logging interface is not reentrant; the logger function
** supplied by the application must not invoke any SQLite interface.
** In a multi-threaded application, the application-defined logger
@@ -2223,7 +2241,7 @@ struct sqlite3_mem_methods {
** These constants are the available integer configuration options that
** 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
** The [sqlite3_db_config()] interface is a var-args function. 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
@@ -2335,17 +2353,20 @@ struct sqlite3_mem_methods {
**
** [[SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER]]
** <dt>SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER</dt>
** <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 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.
** The second parameter is a pointer to an integer into which
** is written 0 or 1 to indicate whether fts3_tokenizer is disabled or enabled
** following this call. The second parameter may be a NULL pointer, in
** which case the new setting is not reported back. </dd>
** <dd> ^This option is used to enable or disable using the
** [fts3_tokenizer()] function - part of the [FTS3] full-text search engine
** extension - without using bound parameters as the parameters. Doing so
** is disabled by default. There must be two additional arguments. The first
** argument is an integer. If it is passed 0, then using fts3_tokenizer()
** without bound parameters is disabled. If it is passed a positive value,
** then calling fts3_tokenizer without bound parameters is enabled. If it
** is passed a negative value, this setting is not modified - this can be
** used to query for the current setting. The second parameter is a pointer
** to an integer into which is written 0 or 1 to indicate the current value
** of this setting (after it is modified, if applicable). The second
** parameter may be a NULL pointer, in which case the value of the setting
** is not reported back. Refer to [FTS3] documentation for further details.
** </dd>
**
** [[SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION]]
** <dt>SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION</dt>
@@ -2357,8 +2378,8 @@ struct sqlite3_mem_methods {
** 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.
** If the first argument is -1, then no changes are made to state of either the
** C-API or the SQL function.
** If the first argument is -1, then no changes are made to the state of either
** the C-API or the SQL function.
** The second parameter is a pointer to an integer into which
** is written 0 or 1 to indicate whether [sqlite3_load_extension()] interface
** is disabled or enabled following this call. The second parameter may
@@ -2476,7 +2497,7 @@ struct sqlite3_mem_methods {
** [[SQLITE_DBCONFIG_LEGACY_ALTER_TABLE]]
** <dt>SQLITE_DBCONFIG_LEGACY_ALTER_TABLE</dt>
** <dd>The SQLITE_DBCONFIG_LEGACY_ALTER_TABLE option activates or deactivates
** the legacy behavior of the [ALTER TABLE RENAME] command such it
** the legacy behavior of the [ALTER TABLE RENAME] command such that it
** behaves as it did prior to [version 3.24.0] (2018-06-04). See the
** "Compatibility Notice" on the [ALTER TABLE RENAME documentation] for
** additional information. This feature can also be turned on and off
@@ -2525,7 +2546,7 @@ struct sqlite3_mem_methods {
** <dt>SQLITE_DBCONFIG_LEGACY_FILE_FORMAT</dt>
** <dd>The SQLITE_DBCONFIG_LEGACY_FILE_FORMAT option activates or deactivates
** the legacy file format flag. When activated, this flag causes all newly
** created database file to have a schema format version number (the 4-byte
** created database files to have a schema format version number (the 4-byte
** integer found at offset 44 into the database header) of 1. This in turn
** means that the resulting database file will be readable and writable by
** any SQLite version back to 3.0.0 ([dateof:3.0.0]). Without this setting,
@@ -2552,7 +2573,7 @@ struct sqlite3_mem_methods {
** 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. <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
** 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
** processing the first argument is written into the integer that the second
@@ -2595,8 +2616,8 @@ struct sqlite3_mem_methods {
** <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,
** reenable this capability using the current DBCONFIG option. If
** 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]
@@ -2630,7 +2651,7 @@ struct sqlite3_mem_methods {
**
** <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 first argument (the third parameter to sqlite3_db_config()) is an 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
@@ -2920,7 +2941,7 @@ SQLITE_API int sqlite3_is_interrupted(sqlite3*);
** ^These routines return 0 if the statement is incomplete. ^If a
** memory allocation fails, then SQLITE_NOMEM is returned.
**
** ^These routines do not parse the SQL statements thus
** ^These routines do not parse the SQL statements and thus
** will not detect syntactically incorrect SQL.
**
** ^(If SQLite has not been initialized using [sqlite3_initialize()] prior
@@ -3037,7 +3058,7 @@ SQLITE_API int sqlite3_busy_timeout(sqlite3*, int ms);
** indefinitely if possible. The results of passing any other negative value
** are undefined.
**
** Internally, each SQLite database handle store two timeout values - the
** Internally, each SQLite database handle stores two timeout values - the
** busy-timeout (used for rollback mode databases, or if the VFS does not
** support blocking locks) and the setlk-timeout (used for blocking locks
** on wal-mode databases). The sqlite3_busy_timeout() method sets both
@@ -3067,7 +3088,7 @@ SQLITE_API int sqlite3_setlk_timeout(sqlite3*, int ms, int flags);
** This is a legacy interface that is preserved for backwards compatibility.
** Use of this interface is not recommended.
**
** Definition: A <b>result table</b> is memory data structure created by the
** Definition: A <b>result table</b> is a memory data structure created by the
** [sqlite3_get_table()] interface. A result table records the
** complete query results from one or more queries.
**
@@ -3210,7 +3231,7 @@ SQLITE_API char *sqlite3_vsnprintf(int,char*,const char*, va_list);
** ^Calling sqlite3_free() with a pointer previously returned
** by sqlite3_malloc() or sqlite3_realloc() releases that memory so
** that it might be reused. ^The sqlite3_free() routine is
** a no-op if is called with a NULL pointer. Passing a NULL pointer
** a no-op if it is called with a NULL pointer. Passing a NULL pointer
** to sqlite3_free() is harmless. After being freed, memory
** should neither be read nor written. Even reading previously freed
** memory might result in a segmentation fault or other severe error.
@@ -3228,13 +3249,13 @@ SQLITE_API char *sqlite3_vsnprintf(int,char*,const char*, va_list);
** sqlite3_free(X).
** ^sqlite3_realloc(X,N) returns a pointer to a memory allocation
** of at least N bytes in size or NULL if insufficient memory is available.
** ^If M is the size of the prior allocation, then min(N,M) bytes
** of the prior allocation are copied into the beginning of buffer returned
** ^If M is the size of the prior allocation, then min(N,M) bytes of the
** prior allocation are copied into the beginning of the buffer returned
** by sqlite3_realloc(X,N) and the prior allocation is freed.
** ^If sqlite3_realloc(X,N) returns NULL and N is positive, then the
** prior allocation is not freed.
**
** ^The sqlite3_realloc64(X,N) interfaces works the same as
** ^The sqlite3_realloc64(X,N) interface works the same as
** sqlite3_realloc(X,N) except that N is a 64-bit unsigned integer instead
** of a 32-bit signed integer.
**
@@ -3284,7 +3305,7 @@ SQLITE_API sqlite3_uint64 sqlite3_msize(void*);
** was last reset. ^The values returned by [sqlite3_memory_used()] and
** [sqlite3_memory_highwater()] include any overhead
** added by SQLite in its implementation of [sqlite3_malloc()],
** but not overhead added by the any underlying system library
** but not overhead added by any underlying system library
** routines that [sqlite3_malloc()] may call.
**
** ^The memory high-water mark is reset to the current value of
@@ -3736,7 +3757,7 @@ SQLITE_API void sqlite3_progress_handler(sqlite3*, int, int(*)(void*), void*);
** there is no harm in trying.)
**
** ^(<dt>[SQLITE_OPEN_SHAREDCACHE]</dt>
** <dd>The database is opened [shared cache] enabled, overriding
** <dd>The database is opened with [shared cache] enabled, overriding
** the default shared cache setting provided by
** [sqlite3_enable_shared_cache()].)^
** The [use of shared cache mode is discouraged] and hence shared cache
@@ -3744,7 +3765,7 @@ SQLITE_API void sqlite3_progress_handler(sqlite3*, int, int(*)(void*), void*);
** this option is a no-op.
**
** ^(<dt>[SQLITE_OPEN_PRIVATECACHE]</dt>
** <dd>The database is opened [shared cache] disabled, overriding
** <dd>The database is opened with [shared cache] disabled, overriding
** the default shared cache setting provided by
** [sqlite3_enable_shared_cache()].)^
**
@@ -4162,7 +4183,7 @@ SQLITE_API void sqlite3_free_filename(sqlite3_filename);
** subsequent calls to other SQLite interface functions.)^
**
** ^The sqlite3_errstr(E) interface returns the English-language text
** that describes the [result code] E, as UTF-8, or NULL if E is not an
** that describes the [result code] E, as UTF-8, or NULL if E is not a
** result code for which a text error message is available.
** ^(Memory to hold the error message string is managed internally
** and must not be freed by the application)^.
@@ -4170,7 +4191,7 @@ SQLITE_API void sqlite3_free_filename(sqlite3_filename);
** ^If the most recent error references a specific token in the input
** SQL, the sqlite3_error_offset() interface returns the byte offset
** of the start of that token. ^The byte offset returned by
** sqlite3_error_offset() assumes that the input SQL is UTF8.
** sqlite3_error_offset() assumes that the input SQL is UTF-8.
** ^If the most recent error does not reference a specific token in the input
** SQL, then the sqlite3_error_offset() function returns -1.
**
@@ -4195,6 +4216,34 @@ SQLITE_API const void *sqlite3_errmsg16(sqlite3*);
SQLITE_API const char *sqlite3_errstr(int);
SQLITE_API int sqlite3_error_offset(sqlite3 *db);
/*
** CAPI3REF: Set Error Codes And Message
** METHOD: sqlite3
**
** Set the error code of the database handle passed as the first argument
** to errcode, and the error message to a copy of nul-terminated string
** zErrMsg. If zErrMsg is passed NULL, then the error message is set to
** the default message associated with the supplied error code. Subsequent
** calls to [sqlite3_errcode()] and [sqlite3_errmsg()] and similar will
** return the values set by this routine in place of what was previously
** set by SQLite itself.
**
** This function returns SQLITE_OK if the error code and error message are
** successfully set, SQLITE_NOMEM if an OOM occurs, and SQLITE_MISUSE if
** the database handle is NULL or invalid.
**
** The error code and message set by this routine remains in effect until
** they are changed, either by another call to this routine or until they are
** changed to by SQLite itself to reflect the result of some subsquent
** API call.
**
** This function is intended for use by SQLite extensions or wrappers. The
** idea is that an extension or wrapper can use this routine to set error
** messages and error codes and thus behave more like a core SQLite
** feature from the point of view of an application.
*/
SQLITE_API int sqlite3_set_errmsg(sqlite3 *db, int errcode, const char *zErrMsg);
/*
** CAPI3REF: Prepared Statement Object
** KEYWORDS: {prepared statement} {prepared statements}
@@ -4269,8 +4318,8 @@ SQLITE_API int sqlite3_limit(sqlite3*, int id, int newVal);
**
** These constants define various performance limits
** that can be lowered at run-time using [sqlite3_limit()].
** The synopsis of the meanings of the various limits is shown below.
** Additional information is available at [limits | Limits in SQLite].
** A concise description of these limits follows, and additional information
** is available at [limits | Limits in SQLite].
**
** <dl>
** [[SQLITE_LIMIT_LENGTH]] ^(<dt>SQLITE_LIMIT_LENGTH</dt>
@@ -4335,7 +4384,7 @@ SQLITE_API int sqlite3_limit(sqlite3*, int id, int newVal);
/*
** CAPI3REF: Prepare Flags
**
** These constants define various flags that can be passed into
** These constants define various flags that can be passed into the
** "prepFlags" parameter of the [sqlite3_prepare_v3()] and
** [sqlite3_prepare16_v3()] interfaces.
**
@@ -4422,7 +4471,7 @@ SQLITE_API int sqlite3_limit(sqlite3*, int id, int newVal);
** there is a small performance advantage to passing an nByte parameter that
** is the number of bytes in the input string <i>including</i>
** the nul-terminator.
** Note that nByte measure the length of the input in bytes, not
** Note that nByte measures the length of the input in bytes, not
** characters, even for the UTF-16 interfaces.
**
** ^If pzTail is not NULL then *pzTail is made to point to the first byte
@@ -4556,7 +4605,7 @@ SQLITE_API int sqlite3_prepare16_v3(
**
** ^The sqlite3_expanded_sql() interface returns NULL if insufficient memory
** is available to hold the result, or if the result would exceed the
** the maximum string length determined by the [SQLITE_LIMIT_LENGTH].
** maximum string length determined by the [SQLITE_LIMIT_LENGTH].
**
** ^The [SQLITE_TRACE_SIZE_LIMIT] compile-time option limits the size of
** bound parameter expansions. ^The [SQLITE_OMIT_TRACE] compile-time
@@ -4744,7 +4793,7 @@ typedef struct sqlite3_value sqlite3_value;
**
** The context in which an SQL function executes is stored in an
** sqlite3_context object. ^A pointer to an sqlite3_context object
** is always first parameter to [application-defined SQL functions].
** is always the first parameter to [application-defined SQL functions].
** The application-defined SQL function implementation will pass this
** pointer through into calls to [sqlite3_result_int | sqlite3_result()],
** [sqlite3_aggregate_context()], [sqlite3_user_data()],
@@ -4868,9 +4917,11 @@ typedef struct sqlite3_context sqlite3_context;
** associated with the pointer P of type T. ^D is either a NULL pointer or
** a pointer to a destructor function for P. ^SQLite will invoke the
** destructor D with a single argument of P when it is finished using
** P. The T parameter should be a static string, preferably a string
** literal. The sqlite3_bind_pointer() routine is part of the
** [pointer passing interface] added for SQLite 3.20.0.
** P, even if the call to sqlite3_bind_pointer() fails. Due to a
** historical design quirk, results are undefined if D is
** SQLITE_TRANSIENT. The T parameter should be a static string,
** preferably a string literal. The sqlite3_bind_pointer() routine is
** part of the [pointer passing interface] added for SQLite 3.20.0.
**
** ^If any of the sqlite3_bind_*() routines are called with a NULL pointer
** for the [prepared statement] or with a prepared statement for which
@@ -5481,7 +5532,7 @@ SQLITE_API int sqlite3_column_type(sqlite3_stmt*, int iCol);
**
** ^The sqlite3_finalize() function is called to delete a [prepared statement].
** ^If the most recent evaluation of the statement encountered no errors
** or if the statement is never been evaluated, then sqlite3_finalize() returns
** or if the statement has never been evaluated, then sqlite3_finalize() returns
** SQLITE_OK. ^If the most recent evaluation of statement S failed, then
** sqlite3_finalize(S) returns the appropriate [error code] or
** [extended error code].
@@ -5713,7 +5764,7 @@ SQLITE_API int sqlite3_create_window_function(
/*
** CAPI3REF: Text Encodings
**
** These constant define integer codes that represent the various
** These constants define integer codes that represent the various
** text encodings supported by SQLite.
*/
#define SQLITE_UTF8 1 /* IMP: R-37514-35566 */
@@ -5805,7 +5856,7 @@ SQLITE_API int sqlite3_create_window_function(
** result.
** Every function that invokes [sqlite3_result_subtype()] should have this
** property. If it does not, then the call to [sqlite3_result_subtype()]
** might become a no-op if the function is used as term in an
** might become a no-op if the function is used as a term in an
** [expression index]. On the other hand, SQL functions that never invoke
** [sqlite3_result_subtype()] should avoid setting this property, as the
** purpose of this property is to disable certain optimizations that are
@@ -5932,7 +5983,7 @@ SQLITE_API SQLITE_DEPRECATED int sqlite3_memory_alarm(void(*)(void*,sqlite3_int6
** sqlite3_value_nochange(X) interface returns true if and only if
** the column corresponding to X is unchanged by the UPDATE operation
** that the xUpdate method call was invoked to implement and if
** and the prior [xColumn] method call that was invoked to extracted
** the prior [xColumn] method call that was invoked to extract
** the value for that column returned without setting a result (probably
** because it queried [sqlite3_vtab_nochange()] and found that the column
** was unchanging). ^Within an [xUpdate] method, any value for which
@@ -6205,6 +6256,7 @@ SQLITE_API void sqlite3_set_auxdata(sqlite3_context*, int N, void*, void (*)(voi
** or a NULL pointer if there were no prior calls to
** sqlite3_set_clientdata() with the same values of D and N.
** Names are compared using strcmp() and are thus case sensitive.
** It returns 0 on success and SQLITE_NOMEM on allocation failure.
**
** If P and X are both non-NULL, then the destructor X is invoked with
** argument P on the first of the following occurrences:
@@ -8881,9 +8933,18 @@ SQLITE_API int sqlite3_status64(
** ^The sqlite3_db_status() routine returns SQLITE_OK on success and a
** non-zero [error code] on failure.
**
** ^The sqlite3_db_status64(D,O,C,H,R) routine works exactly the same
** way as sqlite3_db_status(D,O,C,H,R) routine except that the C and H
** parameters are pointer to 64-bit integers (type: sqlite3_int64) instead
** of pointers to 32-bit integers, which allows larger status values
** to be returned. If a status value exceeds 2,147,483,647 then
** sqlite3_db_status() will truncate the value whereas sqlite3_db_status64()
** will return the full value.
**
** See also: [sqlite3_status()] and [sqlite3_stmt_status()].
*/
SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int resetFlg);
SQLITE_API int sqlite3_db_status64(sqlite3*,int,sqlite3_int64*,sqlite3_int64*,int);
/*
** CAPI3REF: Status Parameters for database connections
@@ -8980,6 +9041,10 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** If an IO or other error occurs while writing a page to disk, the effect
** on subsequent SQLITE_DBSTATUS_CACHE_WRITE requests is undefined.)^ ^The
** highwater mark associated with SQLITE_DBSTATUS_CACHE_WRITE is always 0.
** <p>
** ^(There is overlap between the quantities measured by this parameter
** (SQLITE_DBSTATUS_CACHE_WRITE) and SQLITE_DBSTATUS_TEMPBUF_SPILL.
** Resetting one will reduce the other.)^
** </dd>
**
** [[SQLITE_DBSTATUS_CACHE_SPILL]] ^(<dt>SQLITE_DBSTATUS_CACHE_SPILL</dt>
@@ -8995,6 +9060,18 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
** <dd>This parameter returns zero for the current value if and only if
** all foreign key constraints (deferred or immediate) have been
** resolved.)^ ^The highwater mark is always 0.
**
** [[SQLITE_DBSTATUS_TEMPBUF_SPILL] ^(<dt>SQLITE_DBSTATUS_TEMPBUF_SPILL</dt>
** <dd>^(This parameter returns the number of bytes written to temporary
** files on disk that could have been kept in memory had sufficient memory
** been available. This value includes writes to intermediate tables that
** are part of complex queries, external sorts that spill to disk, and
** writes to TEMP tables.)^
** ^The highwater mark is always 0.
** <p>
** ^(There is overlap between the quantities measured by this parameter
** (SQLITE_DBSTATUS_TEMPBUF_SPILL) and SQLITE_DBSTATUS_CACHE_WRITE.
** Resetting one will reduce the other.)^
** </dd>
** </dl>
*/
@@ -9011,7 +9088,8 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
#define SQLITE_DBSTATUS_DEFERRED_FKS 10
#define SQLITE_DBSTATUS_CACHE_USED_SHARED 11
#define SQLITE_DBSTATUS_CACHE_SPILL 12
#define SQLITE_DBSTATUS_MAX 12 /* Largest defined DBSTATUS */
#define SQLITE_DBSTATUS_TEMPBUF_SPILL 13
#define SQLITE_DBSTATUS_MAX 13 /* Largest defined DBSTATUS */
/*
@@ -9776,7 +9854,7 @@ SQLITE_API void sqlite3_log(int iErrCode, const char *zFormat, ...);
** is the number of pages currently in the write-ahead log file,
** including those that were just committed.
**
** The callback function should normally return [SQLITE_OK]. ^If an error
** ^The callback function should normally return [SQLITE_OK]. ^If an error
** code is returned, that error will propagate back up through the
** SQLite code base to cause the statement that provoked the callback
** to report an error, though the commit will have still occurred. If the
@@ -9784,13 +9862,26 @@ SQLITE_API void sqlite3_log(int iErrCode, const char *zFormat, ...);
** that does not correspond to any valid SQLite error code, the results
** are undefined.
**
** A single database handle may have at most a single write-ahead log callback
** registered at one time. ^Calling [sqlite3_wal_hook()] replaces any
** previously registered write-ahead log callback. ^The return value is
** a copy of the third parameter from the previous call, if any, or 0.
** ^Note that the [sqlite3_wal_autocheckpoint()] interface and the
** [wal_autocheckpoint pragma] both invoke [sqlite3_wal_hook()] and will
** overwrite any prior [sqlite3_wal_hook()] settings.
** ^A single database handle may have at most a single write-ahead log
** callback registered at one time. ^Calling [sqlite3_wal_hook()]
** replaces the default behavior or previously registered write-ahead
** log callback.
**
** ^The return value is a copy of the third parameter from the
** previous call, if any, or 0.
**
** ^The [sqlite3_wal_autocheckpoint()] interface and the
** [wal_autocheckpoint pragma] both invoke [sqlite3_wal_hook()] and
** will overwrite any prior [sqlite3_wal_hook()] settings.
**
** ^If a write-ahead log callback is set using this function then
** [sqlite3_wal_checkpoint_v2()] or [PRAGMA wal_checkpoint]
** should be invoked periodically to keep the write-ahead log file
** from growing without bound.
**
** ^Passing a NULL pointer for the callback disables automatic
** checkpointing entirely. To re-enable the default behavior, call
** sqlite3_wal_autocheckpoint(db,1000) or use [PRAGMA wal_checkpoint].
*/
SQLITE_API void *sqlite3_wal_hook(
sqlite3*,
@@ -9807,7 +9898,7 @@ SQLITE_API void *sqlite3_wal_hook(
** to automatically [checkpoint]
** after committing a transaction if there are N or
** more frames in the [write-ahead log] file. ^Passing zero or
** a negative value as the nFrame parameter disables automatic
** a negative value as the N parameter disables automatic
** checkpoints entirely.
**
** ^The callback registered by this function replaces any existing callback
@@ -9823,9 +9914,10 @@ SQLITE_API void *sqlite3_wal_hook(
**
** ^Every new [database connection] defaults to having the auto-checkpoint
** enabled with a threshold of 1000 or [SQLITE_DEFAULT_WAL_AUTOCHECKPOINT]
** pages. The use of this interface
** is only necessary if the default setting is found to be suboptimal
** for a particular application.
** pages.
**
** ^The use of this interface is only necessary if the default setting
** is found to be suboptimal for a particular application.
*/
SQLITE_API int sqlite3_wal_autocheckpoint(sqlite3 *db, int N);
@@ -9890,6 +9982,11 @@ SQLITE_API int sqlite3_wal_checkpoint(sqlite3 *db, const char *zDb);
** ^This mode works the same way as SQLITE_CHECKPOINT_RESTART with the
** addition that it also truncates the log file to zero bytes just prior
** to a successful return.
**
** <dt>SQLITE_CHECKPOINT_NOOP<dd>
** ^This mode always checkpoints zero frames. The only reason to invoke
** a NOOP checkpoint is to access the values returned by
** sqlite3_wal_checkpoint_v2() via output parameters *pnLog and *pnCkpt.
** </dl>
**
** ^If pnLog is not NULL, then *pnLog is set to the total number of frames in
@@ -9960,6 +10057,7 @@ SQLITE_API int sqlite3_wal_checkpoint_v2(
** See the [sqlite3_wal_checkpoint_v2()] documentation for details on the
** meaning of each of these checkpoint modes.
*/
#define SQLITE_CHECKPOINT_NOOP -1 /* Do no work at all */
#define SQLITE_CHECKPOINT_PASSIVE 0 /* Do as much as possible w/o blocking */
#define SQLITE_CHECKPOINT_FULL 1 /* Wait for writers, then checkpoint */
#define SQLITE_CHECKPOINT_RESTART 2 /* Like FULL but wait for readers */
@@ -10328,7 +10426,7 @@ SQLITE_API int sqlite3_vtab_in(sqlite3_index_info*, int iCons, int bHandle);
** &nbsp; ){
** &nbsp; // do something with pVal
** &nbsp; }
** &nbsp; if( rc!=SQLITE_OK ){
** &nbsp; if( rc!=SQLITE_DONE ){
** &nbsp; // an error has occurred
** &nbsp; }
** </pre></blockquote>)^
@@ -10787,7 +10885,7 @@ typedef struct sqlite3_snapshot {
** The [sqlite3_snapshot_get()] interface is only available when the
** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used.
*/
SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_get(
SQLITE_API int sqlite3_snapshot_get(
sqlite3 *db,
const char *zSchema,
sqlite3_snapshot **ppSnapshot
@@ -10836,7 +10934,7 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_get(
** The [sqlite3_snapshot_open()] interface is only available when the
** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used.
*/
SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_open(
SQLITE_API int sqlite3_snapshot_open(
sqlite3 *db,
const char *zSchema,
sqlite3_snapshot *pSnapshot
@@ -10853,7 +10951,7 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_open(
** The [sqlite3_snapshot_free()] interface is only available when the
** [SQLITE_ENABLE_SNAPSHOT] compile-time option is used.
*/
SQLITE_API SQLITE_EXPERIMENTAL void sqlite3_snapshot_free(sqlite3_snapshot*);
SQLITE_API void sqlite3_snapshot_free(sqlite3_snapshot*);
/*
** CAPI3REF: Compare the ages of two snapshot handles.
@@ -10880,7 +10978,7 @@ SQLITE_API SQLITE_EXPERIMENTAL void sqlite3_snapshot_free(sqlite3_snapshot*);
** This interface is only available if SQLite is compiled with the
** [SQLITE_ENABLE_SNAPSHOT] option.
*/
SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_cmp(
SQLITE_API int sqlite3_snapshot_cmp(
sqlite3_snapshot *p1,
sqlite3_snapshot *p2
);
@@ -10908,7 +11006,7 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_cmp(
** This interface is only available if SQLite is compiled with the
** [SQLITE_ENABLE_SNAPSHOT] option.
*/
SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb);
SQLITE_API int sqlite3_snapshot_recover(sqlite3 *db, const char *zDb);
/*
** CAPI3REF: Serialize a database
@@ -10982,12 +11080,13 @@ SQLITE_API unsigned char *sqlite3_serialize(
**
** The sqlite3_deserialize(D,S,P,N,M,F) interface causes the
** [database connection] D to disconnect from database S and then
** reopen S as an in-memory database based on the serialization contained
** in P. The serialized database P is N bytes in size. M is the size of
** the buffer P, which might be larger than N. If M is larger than N, and
** the SQLITE_DESERIALIZE_READONLY bit is not set in F, then SQLite is
** permitted to add content to the in-memory database as long as the total
** size does not exceed M bytes.
** reopen S as an in-memory database based on the serialization
** contained in P. If S is a NULL pointer, the main database is
** used. The serialized database P is N bytes in size. M is the size
** of the buffer P, which might be larger than N. If M is larger than
** N, and the SQLITE_DESERIALIZE_READONLY bit is not set in F, then
** SQLite is permitted to add content to the in-memory database as
** long as the total size does not exceed M bytes.
**
** If the SQLITE_DESERIALIZE_FREEONCLOSE bit is set in F, then SQLite will
** invoke sqlite3_free() on the serialization buffer when the database
@@ -11054,6 +11153,54 @@ SQLITE_API int sqlite3_deserialize(
#define SQLITE_DESERIALIZE_RESIZEABLE 2 /* Resize using sqlite3_realloc64() */
#define SQLITE_DESERIALIZE_READONLY 4 /* Database is read-only */
/*
** CAPI3REF: Bind array values to the CARRAY table-valued function
**
** The sqlite3_carray_bind(S,I,P,N,F,X) interface binds an array value to
** one of the first argument of the [carray() table-valued function]. The
** S parameter is a pointer to the [prepared statement] that uses the carray()
** functions. I is the parameter index to be bound. P is a pointer to the
** array to be bound, and N is the number of eements in the array. The
** F argument is one of constants [SQLITE_CARRAY_INT32], [SQLITE_CARRAY_INT64],
** [SQLITE_CARRAY_DOUBLE], [SQLITE_CARRAY_TEXT], or [SQLITE_CARRAY_BLOB] to
** indicate the datatype of the array being bound. The X argument is not a
** NULL pointer, then SQLite will invoke the function X on the P parameter
** after it has finished using P, even if the call to
** sqlite3_carray_bind() fails. The special-case finalizer
** SQLITE_TRANSIENT has no effect here.
*/
SQLITE_API int sqlite3_carray_bind(
sqlite3_stmt *pStmt, /* Statement to be bound */
int i, /* Parameter index */
void *aData, /* Pointer to array data */
int nData, /* Number of data elements */
int mFlags, /* CARRAY flags */
void (*xDel)(void*) /* Destructor for aData */
);
/*
** CAPI3REF: Datatypes for the CARRAY table-valued function
**
** The fifth argument to the [sqlite3_carray_bind()] interface musts be
** one of the following constants, to specify the datatype of the array
** that is being bound into the [carray table-valued function].
*/
#define SQLITE_CARRAY_INT32 0 /* Data is 32-bit signed integers */
#define SQLITE_CARRAY_INT64 1 /* Data is 64-bit signed integers */
#define SQLITE_CARRAY_DOUBLE 2 /* Data is doubles */
#define SQLITE_CARRAY_TEXT 3 /* Data is char* */
#define SQLITE_CARRAY_BLOB 4 /* Data is struct iovec */
/*
** Versions of the above #defines that omit the initial SQLITE_, for
** legacy compatibility.
*/
#define CARRAY_INT32 0 /* Data is 32-bit signed integers */
#define CARRAY_INT64 1 /* Data is 64-bit signed integers */
#define CARRAY_DOUBLE 2 /* Data is doubles */
#define CARRAY_TEXT 3 /* Data is char* */
#define CARRAY_BLOB 4 /* Data is struct iovec */
/*
** Undo the hack that converts floating point types to integer for
** builds on processors without floating point support.
@@ -12313,14 +12460,32 @@ SQLITE_API void sqlite3changegroup_delete(sqlite3_changegroup*);
** update the "main" database attached to handle db with the changes found in
** the changeset passed via the second and third arguments.
**
** All changes made by these functions are enclosed in a savepoint transaction.
** If any other error (aside from a constraint failure when attempting to
** write to the target database) occurs, then the savepoint transaction is
** rolled back, restoring the target database to its original state, and an
** SQLite error code returned. Additionally, starting with version 3.51.0,
** an error code and error message that may be accessed using the
** [sqlite3_errcode()] and [sqlite3_errmsg()] APIs are left in the database
** handle.
**
** The fourth argument (xFilter) passed to these functions is the "filter
** callback". If it is not NULL, then for each table affected by at least one
** change in the changeset, the filter callback is invoked with
** the table name as the second argument, and a copy of the context pointer
** passed as the sixth argument as the first. If the "filter callback"
** returns zero, then no attempt is made to apply any changes to the table.
** Otherwise, if the return value is non-zero or the xFilter argument to
** is NULL, all changes related to the table are attempted.
** callback". This may be passed NULL, in which case all changes in the
** changeset are applied to the database. For sqlite3changeset_apply() and
** sqlite3_changeset_apply_v2(), if it is not NULL, then it is invoked once
** for each table affected by at least one change in the changeset. In this
** case the table name is passed as the second argument, and a copy of
** the context pointer passed as the sixth argument to apply() or apply_v2()
** as the first. If the "filter callback" returns zero, then no attempt is
** made to apply any changes to the table. Otherwise, if the return value is
** non-zero, all changes related to the table are attempted.
**
** For sqlite3_changeset_apply_v3(), the xFilter callback is invoked once
** per change. The second argument in this case is an sqlite3_changeset_iter
** that may be queried using the usual APIs for the details of the current
** change. If the "filter callback" returns zero in this case, then no attempt
** is made to apply the current change. If it returns non-zero, the change
** is applied.
**
** For each table that is not excluded by the filter callback, this function
** tests that the target database contains a compatible table. A table is
@@ -12341,11 +12506,11 @@ SQLITE_API void sqlite3changegroup_delete(sqlite3_changegroup*);
** one such warning is issued for each table in the changeset.
**
** For each change for which there is a compatible table, an attempt is made
** to modify the table contents according to the UPDATE, INSERT or DELETE
** change. If a change cannot be applied cleanly, the conflict handler
** function passed as the fifth argument to sqlite3changeset_apply() may be
** invoked. A description of exactly when the conflict handler is invoked for
** each type of change is below.
** to modify the table contents according to each UPDATE, INSERT or DELETE
** change that is not excluded by a filter callback. If a change cannot be
** applied cleanly, the conflict handler function passed as the fifth argument
** to sqlite3changeset_apply() may be invoked. A description of exactly when
** the conflict handler is invoked for each type of change is below.
**
** Unlike the xFilter argument, xConflict may not be passed NULL. The results
** of passing anything other than a valid function pointer as the xConflict
@@ -12441,12 +12606,6 @@ SQLITE_API void sqlite3changegroup_delete(sqlite3_changegroup*);
** This can be used to further customize the application's conflict
** resolution strategy.
**
** All changes made by these functions are enclosed in a savepoint transaction.
** If any other error (aside from a constraint failure when attempting to
** write to the target database) occurs, then the savepoint transaction is
** rolled back, restoring the target database to its original state, and an
** SQLite error code returned.
**
** If the output parameters (ppRebase) and (pnRebase) are non-NULL and
** the input is a changeset (not a patchset), then sqlite3changeset_apply_v2()
** may set (*ppRebase) to point to a "rebase" that may be used with the
@@ -12496,6 +12655,23 @@ SQLITE_API int sqlite3changeset_apply_v2(
void **ppRebase, int *pnRebase, /* OUT: Rebase data */
int flags /* SESSION_CHANGESETAPPLY_* flags */
);
SQLITE_API int sqlite3changeset_apply_v3(
sqlite3 *db, /* Apply change to "main" db of this handle */
int nChangeset, /* Size of changeset in bytes */
void *pChangeset, /* Changeset blob */
int(*xFilter)(
void *pCtx, /* Copy of sixth arg to _apply() */
sqlite3_changeset_iter *p /* Handle describing change */
),
int(*xConflict)(
void *pCtx, /* Copy of sixth arg to _apply() */
int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */
sqlite3_changeset_iter *p /* Handle describing change and conflict */
),
void *pCtx, /* First argument passed to xConflict */
void **ppRebase, int *pnRebase, /* OUT: Rebase data */
int flags /* SESSION_CHANGESETAPPLY_* flags */
);
/*
** CAPI3REF: Flags for sqlite3changeset_apply_v2
@@ -12915,6 +13091,23 @@ SQLITE_API int sqlite3changeset_apply_v2_strm(
void **ppRebase, int *pnRebase,
int flags
);
SQLITE_API int sqlite3changeset_apply_v3_strm(
sqlite3 *db, /* Apply change to "main" db of this handle */
int (*xInput)(void *pIn, void *pData, int *pnData), /* Input function */
void *pIn, /* First arg for xInput */
int(*xFilter)(
void *pCtx, /* Copy of sixth arg to _apply() */
sqlite3_changeset_iter *p
),
int(*xConflict)(
void *pCtx, /* Copy of sixth arg to _apply() */
int eConflict, /* DATA, MISSING, CONFLICT, CONSTRAINT */
sqlite3_changeset_iter *p /* Handle describing change and conflict */
),
void *pCtx, /* First argument passed to xConflict */
void **ppRebase, int *pnRebase,
int flags
);
SQLITE_API int sqlite3changeset_concat_strm(
int (*xInputA)(void *pIn, void *pData, int *pnData),
void *pInA,

View File

@@ -368,6 +368,10 @@ struct sqlite3_api_routines {
int (*set_clientdata)(sqlite3*, const char*, void*, void(*)(void*));
/* Version 3.50.0 and later */
int (*setlk_timeout)(sqlite3*,int,int);
/* Version 3.51.0 and later */
int (*set_errmsg)(sqlite3*,int,const char*);
int (*db_status64)(sqlite3*,int,sqlite3_int64*,sqlite3_int64*,int);
};
/*
@@ -703,6 +707,9 @@ typedef int (*sqlite3_loadext_entry)(
#define sqlite3_set_clientdata sqlite3_api->set_clientdata
/* Version 3.50.0 and later */
#define sqlite3_setlk_timeout sqlite3_api->setlk_timeout
/* Version 3.51.0 and later */
#define sqlite3_set_errmsg sqlite3_api->set_errmsg
#define sqlite3_db_status64 sqlite3_api->db_status64
#endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)

4
docs/app_development.md Normal file
View File

@@ -0,0 +1,4 @@
@page app_development App Development
- @subpage app_development_cheat_sheet
- @subpage app_development_guide

View File

@@ -1,4 +1,4 @@
# App Development Cheat Sheet
@page app_development_cheat_sheet App Development Cheat Sheet
Making apps for the impatient tilde friend.

View File

@@ -1,4 +1,4 @@
# App Development Guide
@page app_development_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.

View File

@@ -1,3 +1,5 @@
@page connecting_manyverse How to Connect Manyverse
# Connecting with Manyverse
Communication with [Manyverse](https://www.manyver.se/) should Just Work (tm).

5
docs/howto.md Normal file
View File

@@ -0,0 +1,5 @@
@page howto How To
- @subpage upgrading
- @subpage transfer_account
- @subpage connecting_manyverse

View File

@@ -1,4 +1,4 @@
# Inspiration
@page inspiration 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.

32
docs/model.md Normal file
View File

@@ -0,0 +1,32 @@
@page model Model
A reasonable mental model of Tilde Friends is as a virtual computer. User
interace is through a web browser. Communication with the outside world is
through the Secure Scuttlebutt (SSB) network protocol. Persistence is through
an SSB store and an additional key-value store in an sqlite database.
The schema for the sqlite database is primarily a `messages` table and a
`blobs` table, which are what one would expect from the SSB specifications.
```dot
digraph {
web_browser -> tilde_friends_web_interface [dir=both];
web_browser [shape=rect,label="Web Browser"];
subgraph cluster_tilde_friends {
label = "Tilde Friends";
tilde_friends_web_interface -> example_app [dir=both];
subgraph cluster_sandbox {
label = "app sandbox";
example_app;
}
example_app -> tilde_friends_core;
tilde_friends_core -> example_app;
tilde_friends_web_interface -> tilde_friends_core;
tilde_friends_core -> "db.sqlite";
tilde_friends_core -> ssb;
"db.sqlite" [shape=cylinder];
}
ssb -> other_ssb_clients [label="Secure Handshake",dir=both];
other_ssb_clients [shape=rect,label="SSB Peers"];
}
```

5
docs/overview.md Normal file
View File

@@ -0,0 +1,5 @@
@page overview Overview
- @subpage inspiration
- @subpage model
- @subpage vision

View File

@@ -1,6 +1,6 @@
# Release Checklist
- make sure ci is passing
- make sure CI is passing
- run the tests
- format + prettier
- update metadata/en-US/changelogs
@@ -8,12 +8,13 @@
- git tag -f latest_release
- push
- make a release on gitea
- update ios screenshots if UI has substantially changed
- upload the artifacts
- upload the AppImage and zsyncmake
- upload to Google
- upload to Apple with dist-ios on macos
- upload to Apple with dist-iOS on macOS
- nix
- june and december: update release version
- June and December: update release version
- run `nix --extra-experimental-features nix-command --extra-experimental-features flakes flake update`
- comment out the hash in default.nix
- update the version

18
docs/transfer_account.md Normal file
View File

@@ -0,0 +1,18 @@
@page transfer_account How to Transfer an Account
Secure Scuttlebutt accounts can be easily transferred between apps and devices.
However, it is not recommended to use an account on multiple devices. If you
author a message on one device without having received all messages authored
from another, your account may become irrecoverably forked. Other clients may
stop receiving your messages if this happens.
1. In Tilde Friends, the _identity_ app will let you export and import your identity as a series of twelve English words for copying and pasting. Keep these words secret!
2. The _sneaker_ app will let you export and import your feed to a file, but it's likely easier and faster to use your initial account on the receiver as a throwaway account to connect, follow yourself, and do the initial replication.
Your identity and messages on the target device is all you need to resume posting. Deleting the identity from the source to avoid accidentally using it is probably a wise idea.
If you are moving accounts between applications on the same device, note that they may not be able to see each other if they attempt to use the same network port number.
You may also wish to make your account the server identity in the _identity_ app so that other devices on the network see and connect to you by your well-known identity.

View File

@@ -1,4 +1,4 @@
# Upgrading
@page upgrading Upgrading
Tilde Friends can be upgraded simply by running a new executable against an
existing database.

View File

@@ -60,6 +60,7 @@ options:
broadcast (default: true): Send network discovery broadcasts.
discovery (default: true): Receive network discovery broadcasts.
stay_connected (default: false): Whether to attempt to keep several peer connections open.
accepted_eula_version (default: 0): The version of the last accepted EULA.
-o, --one-proc Run everything in one process (unsafely!).
-z, --zip path Zip archive from which to load files.
-v, --verbose Log raw messages.

View File

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

8
flake.lock generated
View File

@@ -20,16 +20,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1758589230,
"narHash": "sha256-zMTCFGe8aVGTEr2RqUi/QzC1nOIQ0N1HRsbqB4f646k=",
"lastModified": 1766201043,
"narHash": "sha256-eplAP+rorKKd0gNjV3rA6+0WMzb1X1i16F5m5pASnjA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d1d883129b193f0b495d75c148c2c3a7d95789a0",
"rev": "b3aad468604d3e488d627c0b43984eb60e75e782",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"ref": "nixos-25.11",
"repo": "nixpkgs",
"type": "github"
}

View File

@@ -2,7 +2,7 @@
description = "Tilde Friends is a platform for making, running, and sharing web applications.";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
flake-utils.url = "github:numtide/flake-utils";
};

View File

@@ -0,0 +1,9 @@
* Fixed disagreement in identity information.
* Show more context when prompting for permissions.
* Reduce redundant queries to improve load times.
* Improved profile load times.
* Don't show an empty code of conduct.
* Resolve ambiguity when a user is both followed and blocked (they're blocked).
* Move perf tracing viewer into a trace app.
* Minor UI improvements.
* Updates: CodeMirror, emojis, libbacktrace, sqlite 3.51.0.

View File

@@ -0,0 +1,7 @@
* First launch after update may be noticeably slower due to database reindexing.
* Crash fixes.
* Faster loads.
* Fix channels with hyphens and various other characters not working correctly.
* Navigation bar, search UI, private message, and profile improvements.
* Fixed various broken links.
* Update CodeMirror, c-ares 1.34.6, speedscope 1.25.0, and sqlite 3.51.1.

6
package-lock.json generated
View File

@@ -11,9 +11,9 @@
}
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.unprompted.tildefriends"
android:versionCode="44"
android:versionName="0.2025.10">
android:versionCode="49"
android:versionName="0.2025.12">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application

File diff suppressed because it is too large Load Diff

View File

@@ -331,7 +331,7 @@ static void _http_add_body_bytes(tf_http_connection_t* connection, const void* d
{
if (size)
{
connection->body = tf_resize_vec(connection->body, connection->body_length + size);
connection->body = tf_resize_vec(connection->body, connection->body_length + size + 1);
memcpy((char*)connection->body + connection->body_length, data, size);
connection->body_length += size;
}
@@ -385,7 +385,7 @@ static void _http_add_body_bytes(tf_http_connection_t* connection, const void* d
if (!fin || connection->fragment_length)
{
connection->fragment = tf_resize_vec(connection->fragment, connection->fragment_length + length);
connection->fragment = tf_resize_vec(connection->fragment, connection->fragment_length + length + 1);
memcpy((uint8_t*)connection->fragment + connection->fragment_length, message, length);
connection->fragment_length += length;
}
@@ -394,10 +394,14 @@ static void _http_add_body_bytes(tf_http_connection_t* connection, const void* d
{
if (connection->request && connection->request->on_message)
{
uint8_t* payload = connection->fragment_length ? connection->fragment : message;
size_t payload_length = connection->fragment_length ? connection->fragment_length : length;
uint8_t backup = payload[payload_length];
payload[payload_length] = '\0';
tf_trace_begin(connection->http->trace, connection->trace_name ? connection->trace_name : "websocket");
connection->request->on_message(connection->request, connection->fragment_length ? connection->fragment_op_code : op_code,
connection->fragment_length ? connection->fragment : message, connection->fragment_length ? connection->fragment_length : length);
connection->request->on_message(connection->request, connection->fragment_length ? connection->fragment_op_code : op_code, payload, payload_length);
tf_trace_end(connection->http->trace);
payload[payload_length] = backup;
}
connection->fragment_length = 0;
}
@@ -946,11 +950,19 @@ void tf_http_request_websocket_send(tf_http_request_t* request, int op_code, con
copy[9] = (low >> 0) & 0xff;
header += 9;
}
memcpy(copy + header, data, size);
if (size)
{
memcpy(copy + header, data, size);
}
_http_write(request->connection, copy, header + size);
tf_free(copy);
}
void tf_http_request_websocket_close(tf_http_request_t* request)
{
_http_connection_destroy(request->connection, "websocket close");
}
void tf_http_respond(tf_http_request_t* request, int status, const char** headers, int headers_count, const void* body, size_t content_length)
{
if (request->connection->is_response_sent)

View File

@@ -210,6 +210,13 @@ const char* tf_http_get_cookie(const char* cookie_header, const char* name);
*/
void tf_http_request_websocket_send(tf_http_request_t* request, int op_code, const void* data, size_t size);
/**
** Close a websocket.
** @param request The HTTP request which was previously updated to a websocket
** session with tf_http_request_websocket_upgrade().
*/
void tf_http_request_websocket_close(tf_http_request_t* request);
/**
** Upgrade an HTTP request to a websocket session.
** @param request The HTTP request.

View File

@@ -1,13 +1,17 @@
#include "httpd.js.h"
#include "http.h"
#include "log.h"
#include "mem.h"
#include "sha1.h"
#include "ssb.db.h"
#include "ssb.h"
#include "task.h"
#include "taskstub.js.h"
#include "util.js.h"
#include "picohttpparser.h"
#include "uv.h"
#include <stdlib.h>
@@ -211,42 +215,572 @@ void tf_httpd_endpoint_app(tf_http_request_t* request)
tf_ssb_run_work(ssb, _httpd_endpoint_app_blob_work, _httpd_endpoint_app_blob_after_work, data);
}
void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
typedef struct _app_t
{
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
tf_http_request_t* request;
uv_timer_t timer;
const char* settings;
JSValue opaque;
JSValue credentials;
JSValue process;
uint64_t last_ping_ms;
uint64_t last_active_ms;
bool got_hello;
} app_t;
JSContext* context = tf_ssb_get_context(ssb);
JSValue global = JS_GetGlobalObject(context);
JSValue exports = JS_GetPropertyStr(context, global, "exports");
JSValue app_socket = JS_GetPropertyStr(context, exports, "app_socket");
static void _httpd_auth_query_work(tf_ssb_t* ssb, void* user_data)
{
app_t* work = user_data;
work->settings = tf_ssb_db_get_property(ssb, "core", "settings");
}
JSValue request_object = JS_NewObject(context);
JSValue headers = JS_NewObject(context);
for (int i = 0; i < request->headers_count; i++)
static void _httpd_app_kill_task(app_t* work)
{
JSContext* context = work->request->context;
if (JS_IsObject(work->process))
{
JS_SetPropertyStr(context, headers, request->headers[i].name, JS_NewString(context, request->headers[i].value));
JSValue task = JS_GetPropertyStr(context, work->process, "task");
if (JS_IsObject(task))
{
JSValue kill = JS_GetPropertyStr(context, task, "kill");
if (!JS_IsUndefined(kill))
{
JSValue result = JS_Call(context, kill, task, 0, NULL);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, kill);
}
}
JS_FreeValue(context, task);
}
JS_SetPropertyStr(context, request_object, "headers", headers);
}
JSValue response = tf_httpd_make_response_object(context, request);
tf_http_request_ref(request);
typedef struct _app_hello_t
{
app_t* app;
JSValue message;
const char* user;
const char* path;
char blob_id[k_id_base64_len];
tf_ssb_identity_info_t* identity_info;
tf_httpd_user_app_t* user_app;
} app_hello_t;
JSValue args[] = {
request_object,
response,
};
static void _httpd_app_hello_work(tf_ssb_t* ssb, void* user_data)
{
app_hello_t* work = user_data;
JSValue result = JS_Call(context, app_socket, JS_NULL, tf_countof(args), args);
work->user_app = tf_httpd_parse_user_app_from_path(work->path, NULL);
if (work->user_app)
{
size_t length = strlen("path:") + strlen(work->user_app->app) + 1;
char* key = alloca(length);
snprintf(key, length, "path:%s", work->user_app->app);
const char* value = tf_ssb_db_get_property(ssb, work->user_app->user, key);
tf_string_set(work->blob_id, sizeof(work->blob_id), value);
tf_free((void*)value);
}
else if (work->path[0] == '/' && (work->path[1] == '%' || work->path[1] == '&') && strlen(work->path) >= 1 + k_blob_id_len && strstr(work->path, ".sha256"))
{
memcpy(work->blob_id, work->path + 1, strstr(work->path, ".sha256") - work->path - 1 + strlen(".sha256"));
}
if (*work->blob_id)
{
work->identity_info = tf_ssb_db_get_identity_info(ssb, work->user, work->user_app ? work->user_app->user : NULL, work->user_app ? work->user_app->app : NULL);
}
}
static void _http_json_send(tf_http_request_t* request, JSContext* context, JSValue value)
{
JSValue json = JS_JSONStringify(context, value, JS_NULL, JS_NULL);
size_t json_length = 0;
const char* payload = JS_ToCStringLen(context, &json_length, json);
tf_http_request_websocket_send(request, 0x1, payload, json_length);
JS_FreeCString(context, payload);
JS_FreeValue(context, json);
}
static JSValue _httpd_app_on_tfrpc(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* func_data)
{
const char* id = tf_util_get_property_as_string(context, argv[0], "id");
if (id)
{
JSClassID class_id = 0;
app_t* app = JS_GetAnyOpaque(func_data[0], &class_id);
JSValue calls = JS_IsObject(app->process) ? JS_GetPropertyStr(context, app->process, "_calls") : JS_UNDEFINED;
JSValue call = JS_IsObject(calls) ? JS_GetPropertyStr(context, calls, id) : JS_UNDEFINED;
if (!JS_IsUndefined(call))
{
JSValue error = JS_GetPropertyStr(context, argv[0], "error");
if (!JS_IsUndefined(error))
{
JSValue reject = JS_GetPropertyStr(context, call, "reject");
JSValue result = JS_Call(context, reject, JS_UNDEFINED, 1, &error);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, reject);
}
else
{
JSValue resolve = JS_GetPropertyStr(context, call, "resolve");
JSValue message_result = JS_GetPropertyStr(context, argv[0], "result");
JSValue result = JS_Call(context, resolve, JS_UNDEFINED, 1, &message_result);
JS_FreeValue(context, message_result);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, resolve);
}
JS_FreeValue(context, error);
}
JS_FreeValue(context, call);
JS_FreeValue(context, calls);
}
JS_FreeCString(context, id);
return JS_UNDEFINED;
}
static JSValue _httpd_app_on_output(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* func_data)
{
JSClassID class_id = 0;
app_t* app = JS_GetAnyOpaque(func_data[0], &class_id);
if (app)
{
_http_json_send(app->request, context, argv[0]);
}
return JS_UNDEFINED;
}
static JSValue _httpd_app_on_process_start(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* func_data)
{
JSClassID class_id = 0;
app_t* app = JS_GetAnyOpaque(func_data[0], &class_id);
app->process = JS_DupValue(context, argv[0]);
JSValue client_api = JS_GetPropertyStr(context, app->process, "client_api");
JSValue tfrpc = JS_NewCFunctionData(context, _httpd_app_on_tfrpc, 1, 0, 1, func_data);
JS_SetPropertyStr(context, client_api, "tfrpc", tfrpc);
JS_FreeValue(context, client_api);
JSValue on_output = JS_NewCFunctionData(context, _httpd_app_on_output, 1, 0, 1, func_data);
JS_SetPropertyStr(context, app->process, "_on_output", on_output);
JSValue send = JS_GetPropertyStr(context, app->process, "send");
JSValue result = JS_Call(context, send, app->process, 0, NULL);
JS_FreeValue(context, send);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
for (int i = 0; i < tf_countof(args); i++)
return JS_UNDEFINED;
}
static void _httpd_app_hello_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
app_hello_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
if (!*work->blob_id)
{
JS_FreeValue(context, args[i]);
JSValue object = JS_NewObject(context);
JS_SetPropertyStr(context, object, "action", JS_NewString(context, "tfrpc"));
JS_SetPropertyStr(context, object, "method", JS_NewString(context, "error"));
JSValue params = JS_NewArray(context);
size_t length = strlen(work->path) + strlen(" not found") + 1;
char* message = alloca(length);
snprintf(message, length, "%s not found", work->path);
JS_SetPropertyUint32(context, params, 0, JS_NewString(context, message));
JS_SetPropertyStr(context, object, "params", params);
JS_SetPropertyStr(context, object, "id", JS_NewInt32(context, -1));
_http_json_send(work->app->request, context, object);
JS_FreeValue(context, object);
}
else
{
JSValue object = JS_NewObject(context);
JS_SetPropertyStr(context, object, "action", JS_NewString(context, "session"));
JS_SetPropertyStr(context, object, "credentials", JS_DupValue(context, work->app->credentials));
JS_SetPropertyStr(context, object, "id", JS_NewString(context, work->blob_id));
if (work->identity_info)
{
JSValue identities = JS_NewArray(context);
for (int i = 0; i < work->identity_info->count; i++)
{
JS_SetPropertyUint32(context, identities, i, JS_NewString(context, work->identity_info->identity[i]));
}
JS_SetPropertyStr(context, object, "identities", identities);
JSValue names = JS_NewObject(context);
for (int i = 0; i < work->identity_info->count; i++)
{
JS_SetPropertyStr(context, names, work->identity_info->identity[i],
JS_NewString(context, work->identity_info->name[i] ? work->identity_info->name[i] : work->identity_info->identity[i]));
}
JS_SetPropertyStr(context, object, "names", names);
JS_SetPropertyStr(context, object, "identity", JS_NewString(context, work->identity_info->active_identity));
}
_http_json_send(work->app->request, context, object);
JS_FreeValue(context, object);
JSValue edit_only = JS_GetPropertyStr(context, work->message, "edit_only");
bool is_edit_only = JS_ToBool(context, edit_only) > 0;
JS_FreeValue(context, edit_only);
if (is_edit_only)
{
JSValue global = JS_GetGlobalObject(context);
JSValue version = JS_GetPropertyStr(context, global, "version");
JS_FreeValue(context, global);
JSValue ready = JS_NewObject(context);
JS_SetPropertyStr(context, ready, "action", JS_NewString(context, "ready"));
JS_SetPropertyStr(context, ready, "version", JS_Call(context, version, JS_NULL, 0, NULL));
JS_SetPropertyStr(context, ready, "edit_only", JS_TRUE);
_http_json_send(work->app->request, context, ready);
JS_FreeValue(context, ready);
JS_FreeValue(context, version);
}
else
{
JSValue options = JS_NewObject(context);
JSValue api = JS_GetPropertyStr(context, work->message, "api");
JS_SetPropertyStr(context, options, "api", JS_IsUndefined(api) ? JS_NewArray(context) : api);
JS_SetPropertyStr(context, options, "credentials", JS_DupValue(context, work->app->credentials));
JS_SetPropertyStr(context, options, "packageOwner", work->user_app ? JS_NewString(context, work->user_app->user) : JS_UNDEFINED);
JS_SetPropertyStr(context, options, "packageName", work->user_app ? JS_NewString(context, work->user_app->app) : JS_UNDEFINED);
JS_SetPropertyStr(context, options, "url", JS_GetPropertyStr(context, work->message, "url"));
JSValue global = JS_GetGlobalObject(context);
JSValue exports = JS_GetPropertyStr(context, global, "exports");
JSValue get_process_blob = JS_GetPropertyStr(context, exports, "getProcessBlob");
static int64_t s_session_id;
char session_id[64];
snprintf(session_id, sizeof(session_id), "app_%" PRId64, ++s_session_id);
JSValue args[] = {
JS_NewString(context, work->blob_id),
JS_NewString(context, session_id),
options,
};
JSValue result = JS_Call(context, get_process_blob, JS_UNDEFINED, tf_countof(args), args);
tf_util_report_error(context, result);
JSValue promise_then = JS_GetPropertyStr(context, result, "then");
work->app->opaque = JS_NewObject(context);
JS_SetOpaque(work->app->opaque, work->app);
JSValue then = JS_NewCFunctionData(context, _httpd_app_on_process_start, 0, 0, 1, &work->app->opaque);
JSValue promise = JS_Call(context, promise_then, result, 1, &then);
tf_util_report_error(context, promise);
JS_FreeValue(context, promise);
/* except? */
JS_FreeValue(context, then);
JS_FreeValue(context, promise_then);
JS_FreeValue(context, result);
JS_FreeValue(context, get_process_blob);
JS_FreeValue(context, exports);
JS_FreeValue(context, global);
for (int i = 0; i < tf_countof(args); i++)
{
JS_FreeValue(context, args[i]);
}
}
}
JS_FreeValue(context, app_socket);
JS_FreeValue(context, exports);
JS_FreeValue(context, global);
tf_http_request_unref(work->app->request);
JS_FreeCString(context, work->user);
JS_FreeCString(context, work->path);
JS_FreeValue(context, work->message);
tf_free(work->identity_info);
tf_free(work->user_app);
tf_free(work);
}
static void _httpd_app_message_hello(app_t* work, JSValue message)
{
JSContext* context = work->request->context;
tf_task_t* task = tf_task_get(context);
tf_ssb_t* ssb = tf_task_get_ssb(task);
tf_http_request_ref(work->request);
work->got_hello = true;
JSValue session = JS_IsObject(work->credentials) ? JS_GetPropertyStr(context, work->credentials, "session") : JS_UNDEFINED;
const char* user = tf_util_get_property_as_string(context, session, "name");
JS_FreeValue(context, session);
app_hello_t* hello = tf_malloc(sizeof(app_hello_t));
*hello = (app_hello_t) {
.app = work,
.user = user,
.message = JS_DupValue(context, message),
.path = tf_util_get_property_as_string(context, message, "path"),
};
tf_ssb_run_work(ssb, _httpd_app_hello_work, _httpd_app_hello_after_work, hello);
}
static bool _httpd_app_message_call_client_api(app_t* work, JSValue message, const char* action_string)
{
bool called = false;
JSContext* context = work->request->context;
JSValue client_api = JS_IsObject(work->process) ? JS_GetPropertyStr(context, work->process, "client_api") : JS_UNDEFINED;
JSValue callback = JS_IsObject(client_api) ? JS_GetPropertyStr(context, client_api, action_string) : JS_UNDEFINED;
if (!JS_IsUndefined(callback))
{
JSValue result = JS_Call(context, callback, JS_NULL, 1, &message);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
called = true;
}
JS_FreeValue(context, callback);
JS_FreeValue(context, client_api);
return called;
}
static bool _httpd_app_message_call_message_handler(app_t* work, JSValue message)
{
bool called = false;
JSContext* context = work->request->context;
JSValue event_handlers = JS_GetPropertyStr(context, work->process, "eventHandlers");
JSValue handler_array = JS_GetPropertyStr(context, event_handlers, "message");
if (!JS_IsUndefined(handler_array))
{
for (int i = 0; i < tf_util_get_length(context, handler_array); i++)
{
JSValue handler = JS_GetPropertyUint32(context, handler_array, i);
JSValue result = JS_Call(context, handler, JS_NULL, 1, &message);
tf_util_report_error(context, result);
JS_FreeValue(context, result);
JS_FreeValue(context, handler);
called = true;
}
}
JS_FreeValue(context, handler_array);
JS_FreeValue(context, event_handlers);
return called;
}
static void _httpd_app_on_message(tf_http_request_t* request, int op_code, const void* data, size_t size)
{
app_t* work = request->user_data;
JSContext* context = request->context;
tf_task_t* task = tf_task_get(context);
work->last_active_ms = uv_now(tf_task_get_loop(task));
switch (op_code)
{
/* TEXT */
case 0x1:
/* BINARY */
case 0x2:
{
JSValue message = JS_ParseJSON(context, data, size, NULL);
if (JS_IsException(message) || !JS_IsObject(message))
{
tf_util_report_error(context, message);
tf_http_request_websocket_close(request);
}
else
{
JSValue action = JS_GetPropertyStr(context, message, "action");
const char* action_string = JS_ToCString(context, action);
if (action_string && !work->got_hello && strcmp(action_string, "hello") == 0)
{
_httpd_app_message_hello(work, message);
}
else if (!_httpd_app_message_call_client_api(work, message, action_string))
{
_httpd_app_message_call_message_handler(work, message);
}
JS_FreeCString(context, action_string);
JS_FreeValue(context, action);
}
JS_FreeValue(context, message);
}
break;
/* CLOSE */
case 0x8:
_httpd_app_kill_task(work);
tf_http_request_websocket_send(request, 0x8, data, tf_min(size, sizeof(uint16_t)));
break;
/* PONG */
case 0xa:
break;
}
}
static void _httpd_app_on_timer_close(uv_handle_t* handle)
{
app_t* work = handle->data;
handle->data = NULL;
tf_free(work);
}
static void _httpd_app_on_close(tf_http_request_t* request)
{
JSContext* context = request->context;
app_t* work = request->user_data;
JS_SetOpaque(work->opaque, NULL);
JS_FreeValue(context, work->credentials);
_httpd_app_kill_task(work);
JS_FreeValue(context, work->process);
JS_FreeValue(context, work->opaque);
work->process = JS_UNDEFINED;
uv_close((uv_handle_t*)&work->timer, _httpd_app_on_timer_close);
tf_http_request_unref(request);
}
static void _httpd_app_on_timer(uv_timer_t* timer)
{
app_t* app = timer->data;
uint64_t now_ms = uv_now(timer->loop);
uint64_t repeat_ms = uv_timer_get_repeat(timer);
if (now_ms - app->last_active_ms < repeat_ms)
{
/* Active. */
}
else if (app->last_ping_ms > app->last_active_ms)
{
/* Timed out. */
tf_http_request_websocket_close(app->request);
}
else
{
tf_http_request_websocket_send(app->request, 0x9, NULL, 0);
app->last_ping_ms = now_ms;
}
}
static void _httpd_auth_query_after_work(tf_ssb_t* ssb, int status, void* user_data)
{
app_t* work = user_data;
JSContext* context = tf_ssb_get_context(ssb);
JSValue session = JS_GetPropertyStr(context, work->credentials, "session");
JSValue name = JS_GetPropertyStr(context, session, "name");
JS_FreeValue(context, session);
const char* name_string = JS_ToCString(context, name);
JSValue settings_value = work->settings ? JS_ParseJSON(context, work->settings, strlen(work->settings), NULL) : JS_UNDEFINED;
tf_free((void*)work->settings);
work->settings = NULL;
JSValue out_permissions = JS_NewObject(context);
JS_SetPropertyStr(context, work->credentials, "permissions", out_permissions);
JSValue permissions = !JS_IsUndefined(settings_value) ? JS_GetPropertyStr(context, settings_value, "permissions") : JS_UNDEFINED;
JSValue user_permissions = name_string && !JS_IsUndefined(permissions) ? JS_GetPropertyStr(context, permissions, name_string) : JS_UNDEFINED;
int length = !JS_IsUndefined(user_permissions) ? tf_util_get_length(context, user_permissions) : 0;
for (int i = 0; i < length; i++)
{
JSValue permission = JS_GetPropertyUint32(context, user_permissions, i);
const char* permission_string = JS_ToCString(context, permission);
JS_SetPropertyStr(context, out_permissions, permission_string, JS_TRUE);
JS_FreeCString(context, permission_string);
JS_FreeValue(context, permission);
}
JS_FreeValue(context, user_permissions);
JS_FreeValue(context, permissions);
JS_FreeValue(context, settings_value);
JS_FreeValue(context, name);
tf_http_request_t* request = work->request;
const char* header_sec_websocket_key = tf_http_request_get_header(request, "sec-websocket-key");
static const char* k_magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
size_t key_length = strlen(header_sec_websocket_key);
size_t size = key_length + 36;
uint8_t* key_magic = alloca(size);
memcpy(key_magic, header_sec_websocket_key, key_length);
memcpy(key_magic + key_length, k_magic, 36);
uint8_t digest[20];
SHA1_CTX sha1 = { 0 };
SHA1Init(&sha1);
SHA1Update(&sha1, key_magic, size);
SHA1Final(digest, &sha1);
char key[41] = { 0 };
tf_base64_encode(digest, sizeof(digest), key, sizeof(key));
const char* headers[64] = { 0 };
int headers_count = 0;
headers[headers_count * 2 + 0] = "Upgrade";
headers[headers_count * 2 + 1] = "websocket";
headers_count++;
headers[headers_count * 2 + 0] = "Connection";
headers[headers_count * 2 + 1] = "Upgrade";
headers_count++;
headers[headers_count * 2 + 0] = "Sec-WebSocket-Accept";
headers[headers_count * 2 + 1] = key;
headers_count++;
const char* session_token = tf_httpd_make_session_jwt(tf_ssb_get_context(ssb), ssb, name_string);
const char* cookie = tf_httpd_make_set_session_cookie_header(request, session_token);
tf_free((void*)session_token);
headers[headers_count * 2 + 0] = "Set-Cookie";
headers[headers_count * 2 + 1] = cookie ? cookie : "";
headers_count++;
bool send_version = !tf_http_request_get_header(request, "sec-websocket-version") || strcmp(tf_http_request_get_header(request, "sec-websocket-version"), "13") != 0;
if (send_version)
{
headers[headers_count * 2 + 0] = "Sec-WebSocket-Accept";
headers[headers_count * 2 + 1] = key;
headers_count++;
}
tf_http_request_websocket_upgrade(request);
tf_http_respond(request, 101, headers, headers_count, NULL, 0);
uv_timer_start(&work->timer, _httpd_app_on_timer, 6 * 1000, 6 * 1000);
tf_free((void*)cookie);
JS_FreeCString(context, name_string);
request->on_message = _httpd_app_on_message;
request->on_close = _httpd_app_on_close;
request->context = context;
request->user_data = work;
}
void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
{
const char* header_connection = tf_http_request_get_header(request, "connection");
const char* header_upgrade = tf_http_request_get_header(request, "upgrade");
const char* header_sec_websocket_key = tf_http_request_get_header(request, "sec-websocket-key");
if (!header_connection || !header_upgrade || !header_sec_websocket_key || !strstr(header_connection, "Upgrade") || strcasecmp(header_upgrade, "websocket"))
{
tf_http_respond(request, 500, NULL, 0, NULL, 0);
return;
}
tf_task_t* task = request->user_data;
tf_ssb_t* ssb = tf_task_get_ssb(task);
JSContext* context = tf_task_get_context(task);
const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
JSValue jwt = tf_httpd_authenticate_jwt(ssb, context, session);
tf_free((void*)session);
JSValue credentials = JS_NewObject(context);
if (!JS_IsUndefined(jwt))
{
JS_SetPropertyStr(context, credentials, "session", jwt);
}
tf_http_request_ref(request);
app_t* work = tf_malloc(sizeof(app_t));
*work = (app_t) {
.request = request,
.credentials = credentials,
.timer = { .data = work },
};
uv_timer_init(tf_ssb_get_loop(ssb), &work->timer);
tf_ssb_run_work(ssb, _httpd_auth_query_work, _httpd_auth_query_after_work, work);
}

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