Compare commits

..

No commits in common. "main" and "v0.0.9" have entirely different histories.
main ... v0.0.9

19155 changed files with 1321596 additions and 66202 deletions

View File

@ -1,20 +0,0 @@
# Format Style Options - Created with Clang Power Tools
---
BasedOnStyle: WebKit
AlignEscapedNewlines: DontAlign
AlignOperands: DontAlign
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: false
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Allman
ColumnLimit: 180
ContinuationIndentWidth: 4
IndentCaseBlocks: true
IndentWidth: 4
MaxEmptyLinesToKeep: 1
ObjCBlockIndentWidth: 4
ObjCBreakBeforeNestedBlockParam: false
SortIncludes: true
TabWidth: 4
UseTab: Always
...

View File

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

View File

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

18
.gitignore vendored
View File

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

28
.gitmodules vendored
View File

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

View File

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

View File

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

View File

@ -1,37 +0,0 @@
# Contributing to Tilde Friends
Thank you for your interest in Tilde Friends.
Above all, Tilde Friends aims to be a fun, safe place to play. When that is at
odds with the course of development, we will work through it with respectful
communication.
## How can I contribute?
The nature of Tilde Friends makes for a wide range of ways to contribute
- Just use it. Really, just kicking the tires will probably shake out issues
in useful ways at this point.
- Report and comment on bugs: https://dev.tildefriends.net/issues.
- Make apps. You don't need my permission to make and share apps with Tilde
Friends. I hope that an ecosystem of good apps grows outside of this
repository. If you want to recreate better versions of the stock apps, just
do it. If you make a better ssb app or whatever and drop me a line however
is most convenient for you, I will probably take a look and consider
replacing the stock one with it.
- Write about it. Docs in the git repository, blog posts, private messages to
me with ideas...really there is no wrong answer. Just make some noise, and
I'll do my best to incorporate or otherwise link your feedback and make the
most of it.
- Write C code in the git repository. I'm really striving for it to be the
case that other people don't really need to meddle in there, but if you can
help out, I will gladly review your pull requests via
https://dev.tildefriends.net/pulls.
## Best practices
- The C code is formatted with clang-format. Run `make format`.
- The rest is formatted with prettier. Run `npm run prettier`.
- We strive to have code compile on all platforms with no warnings and run with
no sanitizer issues.
- There are tests. Run `out/debug/tildefriends test`.

2815
Doxyfile

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

526
Makefile Normal file
View File

@ -0,0 +1,526 @@
.ONESHELL:
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
VERSION_CODE := 9
VERSION_NUMBER := 0.0.9
VERSION_NAME := Failure is the only opportunity to begin again.
PROJECT = tildefriends
BUILD_DIR ?= out
BUILD_TYPES := debug release windebug winrelease androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64
UNAME_M := $(shell uname -m)
CFLAGS += \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-cast-function-type \
-MMD \
-ffunction-sections \
-fdata-sections \
-fno-exceptions \
-g
LDFLAGS += -Wl,--gc-sections
ANDROID_SDK ?= ~/Android/Sdk
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/33.0.1
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-33
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/23.1.7779620
ANDROID_NDK_API_VERSION := 31
ANDROID_MIN_SDK_VERSION := 26
ANDROID_ARM64_TARGETS := \
out/androiddebug/tildefriends \
out/androidrelease/tildefriends
ANDROID_X86_64_TARGETS := \
out/androiddebug-x86_64/tildefriends \
out/androidrelease-x86_64/tildefriends
ANDROID_TARGETS := \
$(ANDROID_X86_64_TARGETS) \
$(ANDROID_ARM64_TARGETS)
DEBUG_TARGETS := \
out/debug/tildefriends \
out/windebug/tildefriends \
out/androiddebug/tildefriends \
out/androiddebug-x86_64/tildefriends
RELEASE_TARGETS := \
out/release/tildefriends \
out/winrelease/tildefriends \
out/androidrelease/tildefriends \
out/androidrelease-x86_64/tildefriends
ANDROID_RELEASE_TARGETS := $(filter-out $(DEBUG_TARGETS),$(ANDROID_TARGETS))
NONANDROID_RELEASE_TARGETS := $(filter-out $(ANDROID_ARM64_TARGETS),$(RELEASE_TARGETS))
NONANDROID_TARGETS := $(filter-out $(ANDROID_TARGETS),$(DEBUG_TARGETS) $(RELEASE_TARGETS))
$(NONANDROID_TARGETS): CFLAGS += -fno-omit-frame-pointer
$(NONANDROID_TARGETS): LDFLAGS += -rdynamic
$(ANDROID_TARGETS): CFLAGS += \
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
-fPIC \
-fdebug-compilation-dir . \
-fomit-frame-pointer \
-fno-asynchronous-unwind-tables
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
$(RELEASE_TARGETS): CFLAGS += -DNDEBUG
$(NONANDROID_RELEASE_TARGETS): CFLAGS += -O3
$(ANDROID_RELEASE_TARGETS): CFLAGS += -Os
windebug winrelease: CC = x86_64-w64-mingw32-gcc-win32
windebug winrelease: AS = $(CC)
windebug winrelease: CFLAGS += \
-D_WIN32_WINNT=0x0A00 \
-DWINVER=0x0A00 \
-DNTDDI_VERSION=NTDDI_WIN10 \
-Ideps/openssl/mingw64/include
windebug winrelease: LDFLAGS += \
-static \
-lm \
-Ldeps/openssl/mingw64/lib
$(ANDROID_X86_64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := x86_64-linux-android
$(ANDROID_ARM64_TARGETS): ANDROID_NDK_TARGET_TRIPLE := aarch64-linux-android
$(ANDROID_TARGETS): CC = $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
$(ANDROID_TARGETS): AS = $(CC)
$(ANDROID_TARGETS): CFLAGS += \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
-Wno-unknown-warning-option
$(ANDROID_ARM64_TARGETS): CFLAGS += -Ideps/openssl/android/arm64-v8a/usr/local/include
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Ldeps/openssl/android/arm64-v8a/usr/local/lib
$(ANDROID_X86_64_TARGETS): CFLAGS += -Ideps/openssl/android/x86_64/usr/local/include
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Ldeps/openssl/android/x86_64/usr/local/lib
ifeq ($(UNAME_M),x86_64)
debug: CFLAGS += -fsanitize=address -fsanitize=undefined -fno-common
debug: LDFLAGS += -fsanitize=address -fsanitize=undefined
endif
get_objs = \
$(foreach build_type,$(BUILD_TYPES),$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)))))) \
$(foreach build_type,debug release,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix))))) \
$(foreach build_type,windebug winrelease,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_win))))) \
$(foreach build_type,androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_android))))) \
$(foreach build_type,androiddebug androidrelease androiddebug-x86_64 androidrelease-x86_64,$(addprefix $(BUILD_DIR)/$(build_type)/,$(addsuffix .o,$(basename $(value $(1)_unix)))))
APP_SOURCES := $(wildcard src/*.c)
APP_OBJS := $(call get_objs,APP_SOURCES)
$(APP_OBJS): CFLAGS += \
-Ideps/base64c/include \
-Ideps/crypt_blowfish \
-Ideps/libbacktrace \
-Ideps/libsodium \
-Ideps/libsodium/src/libsodium/include \
-Ideps/libuv/include \
-Ideps/zlib \
-Ideps/zlib/contrib/minizip \
-Ideps/picohttpparser \
-Ideps/quickjs \
-Ideps/sqlite \
-Ideps/valgrind \
-Ideps/xopt \
-Wdouble-promotion \
-Werror
BLOWFISH_SOURCES := \
deps/crypt_blowfish/crypt_blowfish.c \
deps/crypt_blowfish/crypt_gensalt.c \
deps/crypt_blowfish/wrapper.c
BLOWFISH_SOURCES_win = \
deps/crypt_blowfish/x86.S
BLOWFISH_OBJS := $(call get_objs,BLOWFISH_SOURCES)
UV_SOURCES := \
deps/libuv/src/fs-poll.c \
deps/libuv/src/idna.c \
deps/libuv/src/inet.c \
deps/libuv/src/random.c \
deps/libuv/src/strscpy.c \
deps/libuv/src/strtok.c \
deps/libuv/src/threadpool.c \
deps/libuv/src/timer.c \
deps/libuv/src/uv-common.c \
deps/libuv/src/uv-data-getter-setters.c \
deps/libuv/src/version.c
UV_SOURCES_unix := \
deps/libuv/src/unix/async.c \
deps/libuv/src/unix/core.c \
deps/libuv/src/unix/dl.c \
deps/libuv/src/unix/fs.c \
deps/libuv/src/unix/getaddrinfo.c \
deps/libuv/src/unix/getnameinfo.c \
deps/libuv/src/unix/linux.c \
deps/libuv/src/unix/loop-watcher.c \
deps/libuv/src/unix/loop.c \
deps/libuv/src/unix/pipe.c \
deps/libuv/src/unix/poll.c \
deps/libuv/src/unix/process.c \
deps/libuv/src/unix/procfs-exepath.c \
deps/libuv/src/unix/proctitle.c \
deps/libuv/src/unix/random-devurandom.c \
deps/libuv/src/unix/random-getrandom.c \
deps/libuv/src/unix/random-sysctl-linux.c \
deps/libuv/src/unix/signal.c \
deps/libuv/src/unix/stream.c \
deps/libuv/src/unix/tcp.c \
deps/libuv/src/unix/thread.c \
deps/libuv/src/unix/tty.c \
deps/libuv/src/unix/udp.c
UV_SOURCES_android := \
deps/libuv/src/unix/random-getentropy.c
UV_SOURCES_win := \
deps/libuv/src/win/async.c \
deps/libuv/src/win/core.c \
deps/libuv/src/win/detect-wakeup.c \
deps/libuv/src/win/dl.c \
deps/libuv/src/win/error.c \
deps/libuv/src/win/fs-event.c \
deps/libuv/src/win/fs.c \
deps/libuv/src/win/getaddrinfo.c \
deps/libuv/src/win/getnameinfo.c \
deps/libuv/src/win/handle.c \
deps/libuv/src/win/loop-watcher.c \
deps/libuv/src/win/pipe.c \
deps/libuv/src/win/poll.c \
deps/libuv/src/win/process-stdio.c \
deps/libuv/src/win/process.c \
deps/libuv/src/win/signal.c \
deps/libuv/src/win/snprintf.c \
deps/libuv/src/win/stream.c \
deps/libuv/src/win/tcp.c \
deps/libuv/src/win/thread.c \
deps/libuv/src/win/tty.c \
deps/libuv/src/win/udp.c \
deps/libuv/src/win/util.c \
deps/libuv/src/win/winapi.c \
deps/libuv/src/win/winsock.c
UV_OBJS := $(call get_objs,UV_SOURCES)
$(UV_OBJS): CFLAGS += \
-Ideps/libuv/include \
-Ideps/libuv/src \
-Wno-dangling-pointer \
-Wno-incompatible-pointer-types \
-Wno-maybe-uninitialized \
-Wno-sign-compare \
-Wno-unused-but-set-variable \
-Wno-unused-result \
-Wno-unused-variable \
-D_GNU_SOURCE
SODIUM_SOURCES := \
deps/libsodium/src/libsodium/crypto_auth/hmacsha512/auth_hmacsha512.c \
deps/libsodium/src/libsodium/crypto_auth/hmacsha512256/auth_hmacsha512256.c \
deps/libsodium/src/libsodium/crypto_box/crypto_box.c \
deps/libsodium/src/libsodium/crypto_box/curve25519xsalsa20poly1305/box_curve25519xsalsa20poly1305.c \
deps/libsodium/src/libsodium/crypto_core/ed25519/ref10/ed25519_ref10.c \
deps/libsodium/src/libsodium/crypto_core/hsalsa20/ref2/core_hsalsa20_ref2.c \
deps/libsodium/src/libsodium/crypto_core/salsa/ref/core_salsa_ref.c \
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-compress-ref.c \
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-ref.c \
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/generichash_blake2b.c \
deps/libsodium/src/libsodium/crypto_hash/sha256/cp/hash_sha256_cp.c \
deps/libsodium/src/libsodium/crypto_hash/sha256/hash_sha256.c \
deps/libsodium/src/libsodium/crypto_hash/sha512/cp/hash_sha512_cp.c \
deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/donna/poly1305_donna.c \
deps/libsodium/src/libsodium/crypto_onetimeauth/poly1305/onetimeauth_poly1305.c \
deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-core.c \
deps/libsodium/src/libsodium/crypto_pwhash/argon2/argon2-fill-block-ref.c \
deps/libsodium/src/libsodium/crypto_pwhash/argon2/blake2b-long.c \
deps/libsodium/src/libsodium/crypto_scalarmult/crypto_scalarmult.c \
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c \
deps/libsodium/src/libsodium/crypto_scalarmult/curve25519/scalarmult_curve25519.c \
deps/libsodium/src/libsodium/crypto_secretbox/crypto_secretbox_easy.c \
deps/libsodium/src/libsodium/crypto_secretbox/xsalsa20poly1305/secretbox_xsalsa20poly1305.c \
deps/libsodium/src/libsodium/crypto_sign/crypto_sign.c \
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/keypair.c \
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/open.c \
deps/libsodium/src/libsodium/crypto_sign/ed25519/ref10/sign.c \
deps/libsodium/src/libsodium/crypto_sign/ed25519/sign_ed25519.c \
deps/libsodium/src/libsodium/crypto_stream/chacha20/ref/chacha20_ref.c \
deps/libsodium/src/libsodium/crypto_stream/chacha20/stream_chacha20.c \
deps/libsodium/src/libsodium/crypto_stream/salsa20/ref/salsa20_ref.c \
deps/libsodium/src/libsodium/crypto_stream/salsa20/stream_salsa20.c \
deps/libsodium/src/libsodium/crypto_stream/xsalsa20/stream_xsalsa20.c \
deps/libsodium/src/libsodium/crypto_verify/sodium/verify.c \
deps/libsodium/src/libsodium/randombytes/randombytes.c \
deps/libsodium/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c \
deps/libsodium/src/libsodium/sodium/core.c \
deps/libsodium/src/libsodium/sodium/codecs.c \
deps/libsodium/src/libsodium/sodium/runtime.c \
deps/libsodium/src/libsodium/sodium/utils.c \
deps/libsodium/src/libsodium/sodium/version.c
SODIUM_OBJS := $(call get_objs,SODIUM_SOURCES)
$(SODIUM_OBJS): CFLAGS += \
-DCONFIGURED=1 \
-DMINIMAL=1 \
-Wno-unused-function \
-Wno-unused-variable \
-Wno-type-limits \
-Wno-unknown-pragmas \
-Ideps/libsodium/builds/msvc \
-Ideps/libsodium/src/libsodium/include/sodium
SQLITE_SOURCES := deps/sqlite/sqlite3.c
SQLITE_OBJS := $(call get_objs,SQLITE_SOURCES)
$(SQLITE_OBJS): CFLAGS += \
-DSQLITE_DBCONFIG_DEFAULT_DEFENSIVE \
-DSQLITE_DEFAULT_MEMSTATUS=0 \
-DSQLITE_DQS=0 \
-DSQLITE_ENABLE_MEMSYS5 \
-DSQLITE_ENABLE_FTS5 \
-DSQLITE_ENABLE_JSON1 \
-DSQLITE_LIKE_DOESNT_MATCH_BLOBS \
-DSQLITE_MAX_ATTACHED=0 \
-DSQLITE_MAX_COLUMN=100 \
-DSQLITE_MAX_COMPOUND_SELECT=300 \
-DSQLITE_MAX_EXPR_DEPTH=40 \
-DSQLITE_MAX_FUNCTION_ARG=8 \
-DSQLITE_MAX_LENGTH=5242880 \
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
-DSQLITE_MAX_SQL_LENGTH=100000 \
-DSQLITE_MAX_TRIGGER_DEPTH=10 \
-DSQLITE_MAX_VARIABLE_NUMBER=100 \
-DSQLITE_MAX_VDBE_OP=25000 \
-DSQLITE_OMIT_DEPRECATED \
-DSQLITE_OMIT_DESERIALIZE \
-DSQLITE_OMIT_LOAD_EXTENSION \
-DSQLITE_OMIT_TCL_VARIABLE \
-DSQLITE_PRAGMA_DEFAULT_WAL_SYNCHRONOUS=1 \
-DSQLITE_SECURE_DELETE \
-DSQLITE_THREADSAFE=0 \
-DSQLITE_UNTESTABLE \
-DSQLITE_USE_ALLOCA \
-DHAVE_ISNAN \
-Wno-implicit-fallthrough \
-Wno-unused-but-set-variable \
-Wno-unused-function \
-Wno-unused-variable
XOPT_SOURCES := deps/xopt/xopt.c
XOPT_OBJS := $(call get_objs,XOPT_SOURCES)
$(filter $(BUILD_DIR)/win%,$(XOPT_OBJS)): CFLAGS += \
-DHAVE_SNPRINTF \
-DHAVE_VSNPRINTF \
-DHAVE_VASNPRINTF \
-DHAVE_VASPRINTF \
-Dvsnprintf=rpl_vsnprintf
$(XOPT_OBJS): CFLAGS += \
-Wno-implicit-const-int-float-conversion
QUICKJS_SOURCES := \
deps/quickjs/cutils.c \
deps/quickjs/libbf.c \
deps/quickjs/libregexp.c \
deps/quickjs/libunicode.c \
deps/quickjs/quickjs.c
QUICKJS_OBJS := $(call get_objs,QUICKJS_SOURCES)
$(QUICKJS_OBJS): CFLAGS += \
-DCONFIG_VERSION=\"$(shell cat deps/quickjs/VERSION)\" \
-DCONFIG_BIGNUM \
-D_GNU_SOURCE \
-Wno-enum-conversion \
-Wno-implicit-const-int-float-conversion \
-Wno-implicit-fallthrough \
-Wno-sign-compare \
-Wno-unused-but-set-variable \
-Wno-unused-variable
$(NONANDROID_TARGETS): CFLAGS += -DDUMP_LEAKS
LIBBACKTRACE_SOURCES := \
deps/libbacktrace/atomic.c \
deps/libbacktrace/backtrace.c \
deps/libbacktrace/dwarf.c \
deps/libbacktrace/fileline.c \
deps/libbacktrace/print.c \
deps/libbacktrace/simple.c \
deps/libbacktrace/sort.c \
deps/libbacktrace/state.c
LIBBACKTRACE_SOURCES_unix := \
deps/libbacktrace/elf.c \
deps/libbacktrace/mmap.c \
deps/libbacktrace/mmapio.c \
deps/libbacktrace/posix.c
LIBBACKTRACE_SOURCES_win := \
deps/libbacktrace/alloc.c \
deps/libbacktrace/pecoff.c \
deps/libbacktrace/posix.c \
deps/libbacktrace/read.c
LIBBACKTRACE_OBJS := $(call get_objs,LIBBACKTRACE_SOURCES)
$(LIBBACKTRACE_OBJS): CFLAGS += \
-Ideps/libbacktrace_config \
-Wno-unused-but-set-variable \
-Wno-maybe-initialized \
-Wno-unused-function \
-DBACKTRACE_ELF_SIZE=64
PICOHTTPPARSER_SOURCES := \
deps/picohttpparser/picohttpparser.c
PICOHTTPPARSER_OBJS := $(call get_objs,PICOHTTPPARSER_SOURCES)
MINIUNZIP_SOURCES := \
deps/zlib/contrib/minizip/unzip.c \
deps/zlib/contrib/minizip/ioapi.c \
deps/zlib/adler32.c \
deps/zlib/crc32.c \
deps/zlib/inffast.c \
deps/zlib/inflate.c \
deps/zlib/inftrees.c \
deps/zlib/zutil.c
MINIUNZIP_OBJS := $(call get_objs,MINIUNZIP_SOURCES)
$(MINIUNZIP_OBJS): CFLAGS += \
-Ideps/zlib \
-Wno-maybe-uninitialized
LDFLAGS += \
-pthread \
-lm
debug release: LDFLAGS += \
-ldl \
-lssl \
-lcrypto
windebug winrelease: LDFLAGS += \
-lssl \
-lcrypto \
-lcrypt32 \
-ldbghelp \
-liphlpapi \
-lkernel32 \
-lole32 \
-luserenv \
-luuid \
-lws2_32 \
-lwsock32
$(ANDROID_TARGETS): LDFLAGS += \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_NDK_API_VERSION) \
-ldl \
-llog \
-lssl \
-lcrypto
unix: debug release
win: windebug winrelease
all: $(BUILD_TYPES) out/TildeFriends-debug.apk out/TildeFriends-release.apk
.PHONY: all win unix
ALL_APP_OBJS := \
$(APP_OBJS) \
$(BLOWFISH_OBJS) \
$(LIBBACKTRACE_OBJS) \
$(MINIUNZIP_OBJS) \
$(PICOHTTPPARSER_OBJS) \
$(QUICKJS_OBJS) \
$(SODIUM_OBJS) \
$(SQLITE_OBJS) \
$(UV_OBJS) \
$(XOPT_OBJS)
DEPS = $(ALL_APP_OBJS:.o=.d)
-include $(DEPS)
define build_rules
$(1): $(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe)
.PHONY: $(1)
$(BUILD_DIR)/$(1)/$(PROJECT)$(if $(filter win%,$(1)),.exe): $(filter $(BUILD_DIR)/$(1)/%,$(ALL_APP_OBJS))
@echo [link] $$@
@$$(CC) -o $$@ -Wl,-Map,$$@.map $$^ $$(LDFLAGS)
$(BUILD_DIR)/$(1)/%.o: %.c
@mkdir -p $$(dir $$@)
@echo [c] $$@
@$$(CC) $$(CFLAGS) -c $$< -o $$@
$(BUILD_DIR)/$(1)/%.o: %.S
@mkdir -p $$(dir $$@)
@echo [as] $$@
@$$(AS) -c $$< -o $$@
endef
$(foreach build_type,$(BUILD_TYPES),$(eval $(call build_rules,$(build_type))))
src/version.h : $(firstword $(MAKEFILE_LIST))
@echo [version] $@
@echo "#define VERSION_NUMBER \"$(VERSION_NUMBER)\"\n#define VERSION_NAME \"$(VERSION_NAME)\"\n" > $@
src/android/AndroidManifest.xml : $(firstword $(MAKEFILE_LIST))
@echo [android_version] $@
@sed -i \
-e 's/versionCode=".*"/versionCode="$(VERSION_CODE)"/' \
-e 's/versionName=".*"/versionName="$(VERSION_NUMBER)"/' \
-e 's/android:minSdkVersion=".*"/android:minSdkVersion="$(ANDROID_MIN_SDK_VERSION)"/' \
$@
# Android support.
out/res/layout_activity_main.xml.flat: src/android/res/layout/activity_main.xml
@mkdir -p $(dir $@)
@echo [aapt2] $@
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/layout/activity_main.xml
out/res/drawable_icon.xml.flat: src/android/res/drawable/icon.xml
@mkdir -p $(dir $@)
@echo [aapt2] $@
@$(ANDROID_BUILD_TOOLS)/aapt2 compile -o out/res/ src/android/res/drawable/icon.xml
out/apk/res.apk out/gen/com/unprompted/tildefriends/R.java: out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat src/android/AndroidManifest.xml
@mkdir -p $(dir $@)
@$(ANDROID_BUILD_TOOLS)/aapt2 link -I $(ANDROID_PLATFORM)/android.jar out/res/layout_activity_main.xml.flat out/res/drawable_icon.xml.flat --manifest src/android/AndroidManifest.xml -o out/apk/res.apk --java out/gen/
JAVA_FILES := out/gen/com/unprompted/tildefriends/R.java $(wildcard src/android/com/unprompted/tildefriends/*.java)
CLASS_FILES := $(foreach src,$(JAVA_FILES),out/classes/com/unprompted/tildefriends/$(notdir $(src:.java=.class)))
$(CLASS_FILES) &: $(JAVA_FILES)
@echo [javac] $(CLASS_FILES)
@javac --release 8 -Xlint:deprecation -classpath $(ANDROID_PLATFORM)/android.jar -d out/classes $(JAVA_FILES)
out/apk/classes.dex: $(CLASS_FILES)
@mkdir -p $(dir $@)
@echo [d8] $@
@$(ANDROID_BUILD_TOOLS)/d8 --$(BUILD_TYPE) --lib $(ANDROID_PLATFORM)/android.jar --output $(dir $@) out/classes/com/unprompted/tildefriends/*.class
PACKAGE_DIRS := \
apps/ \
core/ \
deps/codemirror/ \
deps/lit/ \
deps/split/ \
deps/smoothie/
RAW_FILES := $(shell find $(PACKAGE_DIRS) -type f)
out/apk/TildeFriends-debug.unsigned.apk: BUILD_TYPE := debug
out/apk/TildeFriends-release.unsigned.apk: BUILD_TYPE := release
out/apk/TildeFriends-debug.unsigned.apk: out/apk/classes.dex out/androiddebug/tildefriends out/androiddebug-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
out/apk/TildeFriends-release.unsigned.apk: out/apk/classes.dex out/androidrelease/tildefriends out/androidrelease-x86_64/tildefriends $(RAW_FILES) out/apk/res.apk
out/%.unsigned.apk:
@mkdir -p $(dir $@) out/apk$(BUILD_TYPE)/bin/aarch64/ out/apk$(BUILD_TYPE)/bin/x86_64/
@echo [aapt] $@
@cp out/android$(BUILD_TYPE)/tildefriends out/apk$(BUILD_TYPE)/bin/aarch64/
@cp out/android$(BUILD_TYPE)-x86_64/tildefriends out/apk$(BUILD_TYPE)/bin/x86_64/
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/aarch64/tildefriends
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/apk$(BUILD_TYPE)/bin/x86_64/tildefriends
@cp out/apk/res.apk $@
@cp out/apk/classes.dex out/apk$(BUILD_TYPE)/
@cd out/apk$(BUILD_TYPE) && zip -u ../../$@ -q -9 -r . && cd ../../
@zip -u $@ -q -9 -x '*.map' -r $(PACKAGE_DIRS) $(RAW_FILES)
out/%.apk: out/apk/%.unsigned.apk
@echo [apksigner] $(notdir $@)
@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks keystore.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --out $@ $<
apk: out/TildeFriends-release.apk
.PHONY: apk
apkgo: out/TildeFriends-release.apk
@adb install $<
@adb shell am start com.unprompted.tildefriends/.MainActivity
.PHONY: apkgo
apklog:
@adb logcat *:S tildefriends
.PHONY: apklog
clean:
rm -rf $(BUILD_DIR)
.PHONY: clean

View File

@ -1,65 +1,36 @@
# Tilde Friends
Tilde Friends is a tool for making and sharing.
A public instance lives at https://www.tildefriends.net/.
It is both a peer-to-peer social network client, participating in Secure
Scuttlebutt, as well as a platform for writing and running web applications.
## Goals
1. Make it easy and fun to run all sorts of web applications.
2. Provide security that is easy to understand and protects your data.
3. Make creating and sharing web applications accessible to anyone with a
browser.
## Building
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
all of those host platforms plus mingw64, iOS, and android.
Tilde Friends uses git submodules, so either:
```
git clone --recurse-submodules https://dev.tildefriends.net/cory/tildefriends.git
```
or:
```
git clone https://dev.tildefriends.net/cory/tildefriends.git
cd tildefriends
git submodule update --init --recursive
```
The `.tar.xz` source releases are all-inclusive.
1. On Linux only, system OpenSSL libraries (`libssl-dev`, in debian-speak) are
assumed to be available.
2. To build, run `make debug` or `make release`. An executable will be
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
are kept up to date in the tree.
2. To build, run `make debug` or `make release`. An executable will be
generated in a subdirectory of `out/`.
3. It's possible to build for Android, iOS, and Windows on Linux, if you have
the right dependencies in the right places. `make windebug winrelease
iosdebug-ipa iosrelease-ipa release-apk`.
3. `make windebug` or `make winrelease` will generate a windows executable
which might work.
4. To build in docker, `docker build .`.
5. `make format` will normalize formatting to the coding standard.
## Running
By default, running the built `tildefriends` executable will start a web server
at <http://localhost:12345/>. `tildefriends -h` lists further options.
at <http://localhost:12345/>. `tildefriends -h` lists further options.
The first user to create an account and log in will be granted administrative
privileges. Further administration can be done at
<http://localhost:12345/~core/admin/>.
privileges. Further administration can be done at
<http://localhost:12345/~core/admin/`>.
## Documentation
Docs are a work in progress:
<https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
There are the very beginnings of developer documentation in `apps/docs/`
that can be read in-place or at <http://localhost:12345/~core/docs/>.
## License
All code unless otherwise noted in is provided under the
[MIT](https://opensource.org/licenses/MIT) license.

View File

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

View File

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

View File

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

View File

@ -3,113 +3,76 @@ import * as tfrpc from '/static/tfrpc.js';
function delete_user(user) {
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
tfrpc.rpc
.delete_user(user)
.then(function () {
alert(`User "${user}" deleted successfully.`);
})
.catch(function (error) {
alert(
`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`
);
});
tfrpc.rpc.delete_user(user).then(function() {
alert(`User "${user}" deleted successfully.`);
}).catch(function(error) {
alert(`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`);
});
}
}
function global_settings_set(key, value) {
tfrpc.rpc
.global_settings_set(key, value)
.then(function () {
alert(`Set "${key}" to "${value}".`);
})
.catch(function (error) {
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
});
tfrpc.rpc.global_settings_set(key, value).then(function() {
alert(`Set "${key}" to "${value}".`);
}).catch(function(error) {
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
});
}
function title_case(name) {
return name
.split('_')
.map((x) => x.charAt(0).toUpperCase() + x.substring(1))
.join(' ');
}
window.addEventListener('load', function () {
const permission_template = (permission) => html` <code>${permission}</code>`;
window.addEventListener('load', function() {
const permission_template = (permission) =>
html` <code>${permission}</code>`;
function input_template(key, description) {
if (description.type === 'boolean') {
return html`
<li class="w3-row">
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
<div class="w3-quarter w3-padding">${description.description}</div>
<div class="w3-quarter w3-padding w3-center"><input class="w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input></div>
<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstChild.checked)}>Set</button>
</li>
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
<input type="checkbox" ?checked=${description.value} ?id=${'gs_' + key} style="grid-column: 2"></input>
<div style="grid-column: 3">
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.checked)}>Set</button>
<span>${description.description}</span>
</div>
`;
} else if (description.type === 'textarea') {
return html`
<li class="w3-row">
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold"
>${title_case(key)}</label
>
<div class="w3-rest w3-padding">${description.description}</div>
<textarea
class="w3-input"
style="vertical-align: top; resize: vertical"
id=${'gs_' + key}
>
${description.value}</textarea
>
<button
class="w3-button w3-right w3-quarter w3-theme-action"
@click=${(e) =>
global_settings_set(
key,
e.srcElement.previousElementSibling.value
)}
>
Set
</button>
</li>
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
<textarea style="vertical-align: top" rows=20 cols=80 ?id=${'gs_' + key}>${description.value}</textarea>
<div style="grid-column: 3">
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
<span>${description.description}</span>
</div>
`;
} else {
return html`
<li class="w3-row">
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
<div class="w3-quarter w3-padding">${description.description}</div>
<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input>
<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
</li>
<label ?for=${'gs_' + key} style="grid-column: 1">${key}: </label>
<input type="text" value="${description.value}" ?id=${'gs_' + key}></input>
<div style="grid-column: 3">
<button @click=${(e) => global_settings_set(key, e.srcElement.parentElement.previousElementSibling.value)}>Set</button>
<span>${description.description}</span>
</div>
`;
}
}
const user_template = (user, permissions) => html`
<li class="w3-card w3-margin">
<button
class="w3-button w3-theme-action"
@click=${(e) => delete_user(user)}
>
<li>
<button @click=${(e) => delete_user(user)}>
Delete
</button>
${user}: ${permissions.map((x) => permission_template(x))}
${user}:
${permissions.map(x => permission_template(x))}
</li>
`;
const users_template = (users) =>
html` <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>`;
html`<h2>Users</h2>
<ul>
${Object.entries(users).map(u => user_template(u[0], u[1]))}
</ul>`;
const page_template = (data) =>
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header>
<div class="w3-container">
<ul class="w3-ul">
${Object.keys(data.settings)
.sort()
.map((x) => html`${input_template(x, data.settings[x])}`)}
</ul>
</div>
${users_template(data.users)}
</div> `;
html`<div>
<h2>Global Settings</h2>
<div style="display: grid">
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
</div>
${users_template(data.users)}
</div>`;
render(page_template(g_data), document.body);
});
});

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -1,356 +0,0 @@
export const docs = {};
docs.global = `# Tilde Friends API Documentation
Welcome to the Tilde Friends API documentation.
* [App Globals](#App_Globals)
* [Database Interface](#Database)
* [Remote Procedure Calls](#tfrpc)
<a id="App_Globals"></a>
## <span style="color: #aaf">App Globals</span>
The following are functions and values exposed to all apps in their \`app.js\` or \`handler.js\`. Most
of these are asynchronous, returning a \`Promise\` that will be resolved when the call completes, unless
noted otherwise.
This is all a work in progess. These are liable to change without warning. Feedback is welcome.
The exposed functions in this API balance multiple competing needs:
* The surface area of the exposed API ought to be fairly minimal. If something can be implemented entirely app-side, that is
generally preferred over building it into the core.
* Everything is built on this API. Ideally the admin app, the SSB app, and the editor all use standard API exposed to all
apps, with appropriate permission guards in place making it so that only trusted apps do potentially destructive operations.
There will be some things here that aren't necessarily general use to support what's required.
If you are looking at the [Tilde Friends source code](https://www.tildefriends.net/~cory/releases/),
the vast majority of these are implemented in \`src/*.js.c\` files, and exposed to apps via \`core/core.js\`.
`;
docs['core.user.credentials.session.name'] = `
*String* The name of the authenticated user.
`;
docs['app.setDocument()'] = `
Set the contents of the client &lt;iframe/&gt;.
### Parameters
* *String* **html** The HTML contents.
`;
docs['core.apps()'] = `
Gets a list of apps owned by the current user.
### Returns
*Array* An array of string names of the apps owned by the current user.
`;
docs['core.url'] = `
The url by which the running app is being invoked.
`;
docs['app.localStorageSet()'] = `
Set a value in browser local storage.
### Parameters
*String* **key** The localStorage key to set.
*String* **value** The localStorage value to set.
`;
docs['app.localStorageGet()'] = `
Gets a value from browser local storage.
### Parameters
*String* **key** The key with which the value was set.
### Returns
*String* The value, or undefined.
`;
docs['app.print()'] = `
Log information for debugging purposes to the server and to the connected browser console.
### Parameters
* ... Any args to print.
`;
docs['ssb.createIdentity()'] = `
Create a new SSB identity.
### Returns
*String* The created identity public key.
`;
docs['ssb.getIdentities()'] = `
Get all SSB identities owned by the current user.
### Returns
*Array* An array of public key strings.
`;
docs['ssb.sqlAsync()'] = `
Run an SQL query against the sqlite database.
### Parameters
* *String* **query** The sqlite query.
* *Array* **args** The query arguments to bind.
* *Function* **callback** Callback called for each row result.
`;
docs['ssb.appendMessageWithIdentity()'] = `
Signs and stores a message in the SSB database.
### Parameters
* *String* **id** The public key of an SSB identity owned by the authenticated user.
* *Object* **message** The unsigned message.
`;
docs['ssb.storeMessage()'] = `
Verifies and stores a signed message in the SSB database.
### Parameters
* *Object* **message** The valid, signed message to store.
`;
docs['ssb.blobStore()'] = `
Store a blob in the SSB database.
### Parameters
* *String*/*Uint8Array* **blob** The blob contents to store
### Returns
*String* The stored blob ID.
`;
docs['ssb.blobGet()'] = `
Fetches a blob from the database.
### Parameters
* *String* **blob_id** The blob identifier to fetch (\`&....sha256\`).
### Returns
*ArrayBuffer* The blob data.
`;
docs['print()'] = `
Log debug information both to the server's console and to the visiting user's browser console when possible.
### Parameters
* **...** Whatever you want to log. Will be joined with spaces.
`;
docs['database()'] = `
Returns a database instance that is specific to the authenticated user and the given key.
### Parameters
* *String* **key** The database key.
### Returns
*Database* A database.
`;
docs['my_shared_database()'] = `
Returns a database instance that is specific to the authenticated user and the given key.
### Parameters
* *String* **package_name** The database package name.
* *String* **key** The database key.
### Returns
*Database* A database.
`;
docs['shared_database()'] = `
Returns a database instance that is shared between all users of the app, determined by its owner and app name.
### Parameters
* *String* **key** The database key.
### Returns
*Database* A database.
`;
docs['base64Decode()'] = `
Decode a base64 string to bytes.
Completes synchronously.
### Parameters
* *String* value The base64-encoded string.
### Returns
*Uint8Array* The decoded bytes.
`;
docs['base64Encode()'] = `
Encode bytes to a base64 string.
Completes synchronously.
### Parameters
* *Uint8Array* The bytes to encode.
### Returns
*String* The base64-encoded string.
`;
docs['utf8Decode()'] = `
Decode UTF-8 bytes to a string.
Completes synchronously.
### Parameters
* *Uint8Array* **value** The value to decode.
### Returns
*String* The value as a string.
`;
docs['utf8Encode()'] = `
Encodes a string to UTF-8 bytes.
Completes synchronously.
### Parameters
* *String* **value** The value to encode.
### Returns
*Uint8Array* The encoded \`value\`.
`;
docs['setTimeout()'] = `
Call a function after some delay.
### Parameters
* *Function* **callback** The function to call.
* *Number* **timeout** Number of milliseconds to wait before calling the callback function.
`;
docs['parseHttpRequest()'] = `
Parses an HTTP request.
### Parameters
* *Uint8Array* **request** The request data. Maybe be partial or contain extra data. The return value will
indicate when and where it is complete.
* *Number* **last_length** The length of the data passed on a previous attempt for the same request, or 0 initially.
### Returns
* *Integer* **-2** if the request is incomplete.
* *Integer* **-1** if the request could not be parsed.
* *Object* An object with **bytes_parsed**, **minor_version**, **path**, and **headers** fields on successful parse.
`;
docs['parseHttpResponse()'] = `
Parses an HTTP response.
### Parameters
* *Uint8Array* **response** The response data. Maybe be partial or contain extra data. The return value will
indicate when and where it is complete.
* *Number* **last_length** The length of the data passed on a previous attempt for the same response, or 0 initially.
### Returns
* *Integer* **-2** if the response is incomplete.
* *Integer* **-1** if the response could not be parsed.
* *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse.
`;
docs['sha1Digest()'] = `
Calculates a SHA1 digest.
Completes synchronously.
### Parameters
* *String* **value** The value for which to calculate the digest.
### Returns
*String* The SHA1 digest of UTF-8 encoded \`value\`.
`;
docs['maskBytes()'] = `
Masks bytes for WebSocket communication.
Completes synchronously.
### Parameters
* *Uint8Array* **bytes** The byte array of data to mask.
* *Uint32* **mask** The mask to apply.
### Returns
*Uint32Array* The masked bytes.
`;
docs['exit()'] = `
Exits the app. But why would you want to do that?
Completes synchronously.
### Parameters
* *Integer* **exit_code** System exit code.
`;
docs['version()'] = `
Gets version information for the running server.
### Returns
*Object* Keys are things like \`name\` and \`number\` for the server itself and \`libuv\` and \`openssl\` for
dependencies. Values are *String* version numbers.
`;
docs['platform()'] = `
Gets the host operating system platform of the running server.
### Returns
*String* The platform, one of \`windows\`, \`android\`, \`linux\`, or \`other\`.
`;
docs['getFile()'] = `
Gets a file from the running app.
### Parameters
* *String* **name** Name of the file to retrieve.
### Returns
*Uint8Array* The contents of a file from the app with the given name, or *undefined*.
`;
docs.database = `
# <span style="color: #aaf">Database</span>
Local-only storage is provided by a \`Database\` type representing a key-value store.
`;
docs['database.get()'] = `
Gets a value from the database.
### Parameters
* *String* **key** The key.
### Returns
*String* The value from the database or undefined if not found.
`;
docs['database.getAll()'] = `
Gets all keys from the database.
### Returns
*Array* An array of *String* key names for all keys in the given database.
`;
docs['database.getLike()'] = `
Gets all keys and values from the database matching a pattern.
### Parameters
* *String* **pattern** An sqlite \`LIKE\` pattern to match keys against.
### Returns
*Object* An object whose keys are the database keys and values are the database values that match the given pattern.
`;
docs['database.set()'] = `
Sets a value in the database, creating a new entry or replacing an existing entry.
### Parameters
* *String* **key** The key.
* *String* **value** The value.
`;
docs['database.exchange()'] = `
Performs an atomic compare and exchange operation, setting a value in the database only if its current value matches what is expected.
### Parameters
* *String* **key** The key.
* *String* **expected** The expected value.
* *String* **value** The new value.
### Returns
*Boolean* true if the value is now the given value.
`;
docs['database.remove()'] = `
Removes an entry from the database if it exists.
### Parameters
* *String* **key** The key.
`;
docs.tfrpc = `
# <span style="color: #aaf" id="tfrpc">tfrpc</span>
\`tfrpc.js\` is a small helper script that is available to be used to facilitate communication between parts of an application.
\`tfrpc.js\` can be used to asynchronously make calls between the app code running in a sandboxed iframe in the browser
and the app process on the server.
From \`app.js\`:
\`\`\`
import * as tfrpc from '/tfrpc.js';
\`\`\`
From script running in the browser:
\`\`\`
import * as tfrpc from '/static/tfrpc.js';
\`\`\`
Either side can register or call functions, though they must be registered before they can be called. Arguments and return
values are ultimately serialized by means that attempt to preserve most JSON-serializable values as well as functions themselves.
`;
docs['tfrpc.register()'] = `
Register a function, allowing it to be called remotely.
### Parameters
* *Function* **function** The function to register. Its name will be how it will be called.
`;
docs['tfrpc.rpc.*()'] = `
Call a remote function.
### Parameters
* **...** Parameters to pass to the function.
### Returns
The return value of the called function.
`;

View File

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

View File

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

4
apps/appstore.json Normal file
View File

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

55
apps/appstore/app.js Normal file
View File

@ -0,0 +1,55 @@
async function get_apps() {
let results = {};
await ssb.sqlAsync(`
SELECT messages.*
FROM messages_fts('"application/tildefriends"')
JOIN messages ON messages.rowid = messages_fts.rowid
ORDER BY timestamp
`,
[],
function(row) {
let content = JSON.parse(row.content);
for (let mention of content.mentions) {
if (mention?.type === 'application/tildefriends') {
results[JSON.stringify([row.author, mention.name])] = {
message: row,
blob: mention.link,
name: mention.name,
};
}
}
});
return Object.values(results).sort((x, y) => y.message.timestamp - x.message.timestamp);
}
function render_app(app) {
return `
<div style="border: 2px solid white; display: inline-block; margin: 8px; padding: 8px">
<a href="/~cory/ssb/#${app.message.author}">@</a>
<a href="/~cory/ssb/#${app.message.id}">%</a>
<a href="/${app.blob}/">${app.name}</a>
</div>
`;
}
async function main() {
let apps = await get_apps();
app.setDocument(`
<html>
<head>
<base target="_top">
<style>
a:link { color: #bbf; }
a:visited { color: #ddd; }
a:hover { color: #ddf; }
</style>
</head>
<body style="color: #fff">
<h1>${apps.length} apps</h1>
${apps.map(render_app).join('\n')}
</body>
</html>
`);
}
main();

View File

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

View File

@ -1,8 +0,0 @@
import * as blog from './blog.js';
async function main() {
let blogs = await blog.get_posts();
await app.setDocument(blog.render_html(blogs));
}
main();

View File

@ -1,207 +0,0 @@
import * as commonmark from './commonmark.min.js';
function escape(text) {
return (text ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
}
function escapeAttribute(text) {
return (text ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
export async function get_blog_message(id) {
let message;
await ssb.sqlAsync(
'SELECT author, timestamp, content FROM messages WHERE id = ?',
[id],
function (row) {
let content = JSON.parse(row.content);
message = {
author: row.author,
timestamp: row.timestamp,
blog: content?.blog,
title: content?.title,
};
}
);
if (message) {
await ssb.sqlAsync(
`
SELECT json_extract(content, '$.name') AS name
FROM messages
WHERE author = ?
AND json_extract(content, '$.type') = 'about'
AND json_extract(content, '$.about') = author
AND name IS NOT NULL
ORDER BY sequence DESC LIMIT 1
`,
[message.author],
function (row) {
message.name = row.name;
}
);
}
return message;
}
export function markdown(md) {
let reader = new commonmark.Parser({safe: true});
let writer = new commonmark.HtmlRenderer();
let parsed = reader.parse(md || '');
let walker = parsed.walker();
let event, node;
while ((event = walker.next())) {
node = event.node;
if (event.entering) {
if (node.destination?.startsWith('&')) {
node.destination =
'/' + node.destination + '/view?filename=' + node.firstChild?.literal;
} else if (
node.destination?.startsWith('@') ||
node.destination?.startsWith('%')
) {
node.destination = '/~core/ssb/#' + escape(node.destination);
}
}
}
return writer.render(parsed);
}
export async function render_blog_post_html(blog_post) {
let blob = utf8Decode(await ssb.blobGet(blog_post.blog));
return `<!DOCTYPE html>
<html>
<head>
<title>🪵Tilde Friends Blog - ${markdown(blog_post.title)}</title>
<base target="_top">
</head>
<body>
<h1><a href="./">🪵Tilde Friends Blog</a></h1>
<div>
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
<div>${markdown(blob)}</div>
</div>
</body>
</html>
`;
}
function render_blog_post(blog_post) {
return `
<div>
<h2><a href="/~${core.app.owner}/${core.app.name}/${escapeAttribute(blog_post.id)}">${escape(blog_post.title)}</a></h2>
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
<div>${markdown(blog_post.summary)}</div>
</div>
`;
}
export function render_html(blogs) {
return `<!DOCTYPE html>
<html>
<head>
<title>🪵Tilde Friends Blog</title>
<link href="./atom" type="application/atom+xml" rel="alternate" title="🪵Tilde Blog"/>
<style>
html {
background-color: #ccc;
}
</style>
<base target="_top">
</head>
<body>
<div style="display: flex; flex-direction: row; align-items: center; gap: 1em">
<h1>🪵Tilde Friends Blog</h1>
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
</div>
${blogs.map((blog_post) => render_blog_post(blog_post)).join('\n')}
</body>
</html>`;
}
function render_blog_post_atom(blog_post) {
return `<entry>
<title>${escape(blog_post.title)}</title>
<link href="/~cory/ssb/#${blog_post.id}" />
<id>${blog_post.id}</id>
<published>${escape(new Date(blog_post.timestamp).toString())}</published>
<summary>${escape(blog_post.summary)}</summary>
<author>
<name>${escape(blog_post.name)}</name>
<feed>${escape(blog_post.author)}</feed>
</author>
</entry>`;
}
export function render_atom(blogs) {
return `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>🪵Tilde Blog</title>
<subtitle>A subtitle.</subtitle>
<link href="${core.url}/atom" rel="self"/>
<link href="${core.url}"/>
<id>${core.url}</id>
<updated>${new Date().toString()}</updated>
${blogs.map((blog_post) => render_blog_post_atom(blog_post)).join('\n')}
</feed>`;
}
export async function get_posts() {
let blogs = [];
let ids = await ssb.getIdentities();
await ssb.sqlAsync(
`
WITH
blogs AS (
SELECT
messages.author,
messages.id,
json_extract(messages.content, '$.title') AS title,
json_extract(messages.content, '$.summary') AS summary,
json_extract(messages.content, '$.blog') AS blog,
messages.timestamp
FROM messages_fts('blog')
JOIN messages ON messages.rowid = messages_fts.rowid
WHERE json_extract(messages.content, '$.type') = 'blog'),
public AS (
SELECT author FROM (
SELECT
messages.author,
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
json_extract(messages.content, '$.publicWebHosting') AS is_public
FROM messages_fts('about')
JOIN messages ON messages.rowid = messages_fts.rowid
WHERE json_extract(messages.content, '$.type') = 'about' AND is_public IS NOT NULL)
WHERE author_rank = 1 AND is_public),
names AS (
SELECT author, name FROM (
SELECT
messages.author,
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
json_extract(messages.content, '$.name') AS name
FROM messages_fts('about')
JOIN messages ON messages.rowid = messages_fts.rowid
WHERE json_extract(messages.content, '$.type') = 'about' AND
json_extract(messages.content, '$.about') = messages.author AND
name IS NOT NULL)
WHERE author_rank = 1)
SELECT blogs.*, names.name FROM blogs
JOIN json_each(?) AS self ON self.value = blogs.author
JOIN public ON public.author = blogs.author
LEFT OUTER JOIN names ON names.author = blogs.author
ORDER BY blogs.timestamp DESC LIMIT 20
`,
[JSON.stringify(ids)],
function (row) {
blogs.push(row);
}
);
return blogs;
}

File diff suppressed because one or more lines are too long

View File

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

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

View File

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

4
apps/docs.json Normal file
View File

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

29
apps/docs/app.js Normal file

File diff suppressed because one or more lines are too long

16
apps/docs/guide.md Normal file
View File

@ -0,0 +1,16 @@
# Tilde Friends Developer's Guide
[Back to index](#index)
A Tilde Friends application runs on the server. To make an interesting
application that interacts with the client, it's necessary to understand
how the parts work together.
## Hello, world!
A simple starting point. Presents `Hello, world!` in the browser when
visited.
**app.js**:
```
app.setDocument('<h1>Hello, world!</h1>');
```

12
apps/docs/index.md Normal file
View File

@ -0,0 +1,12 @@
# Tilde Friends Documentation
Tilde Friends is a participating member of a greater social
network, [Secure Scuttlebutt](https://scuttlebutt.nz/),
adding a way to safely and securely write, share,
and run code in the form of server-side web applications.
- [Tilde Friends Vision](#vision)
- [Secure Scuttlebutt from Scratch](#ssb)
- [Structure](#structure)
- [Guide](#guide)
- [TODO](#todo)

41
apps/docs/ssb.md Normal file
View File

@ -0,0 +1,41 @@
# Secure Scuttlebutt from Scratch
[Back to index](#index)
This aims to be the missing reference for those who wish to create a Secure
Scuttlebutt client from scratch.
## Discovery
A good way to get started is to participate in local network discovery with a known working
client on the same network. The
[Scuttlebutt Programming Guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#local-network)
is a good start, here, with a few things to note:
1. Some clients advertise multiple addresses separated by semicolons (`;`).
2. Some clients advertise alternative protocols than `shs` and use hostnames instead of
IPv4 addresses.
So be prepared to accept variations.
There also an undocumented "new" style of discovery message.
## Secret Handshake, Box Stream, and RPC Protocol
Now that two clients are aware of eachother, they need to complete a secret handshake.
The [programming guide](https://ssbc.github.io/scuttlebutt-protocol-guide/#handshake)
is once again a good reference.
The box stream and RPC protocol can both be implemented from the
[same documentation](https://ssbc.github.io/scuttlebutt-protocol-guide/#box-stream)
without surprises.
## Synchronizing Data
... `ebt.replicate` or `createHistoryStream` ...
## Rooms
TODO
## References
* [https://ssbc.github.io/scuttlebutt-protocol-guide/](https://ssbc.github.io/scuttlebutt-protocol-guide/)
* [https://dev.planetary.social/](https://dev.planetary.social/)
* [https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints](https://dev.scuttlebutt.nz/#/golang/?id=muxrpc-endpoints)

65
apps/docs/structure.md Normal file
View File

@ -0,0 +1,65 @@
# Tilde Friends Structure
[Back to index](#index)
Tilde Friends is a mostly-self-contained executable written in C.
In combines the following key components:
- A Secure Scuttlebutt (SSB) client/server. This talks with other SSB
instances, storing messages and blobs for anyone visible to local
users as they are encountered and sharing anything published locally
as appropriate.
- An sqlite database. This is where the SSB instance stores its data.
The general schema involves a `messages` table, storing mostly JSON,
a `blobs` table storing arbitrary blob data, and a `properties` table,
storing arbitrary state gleaned from `messages` and `blobs`, generally
updated on demand and incrementally.
- A QuickJS runtime. The core process runs stock scripts and has access
and permission to use all resources. All other processes, which
includes everything which runs untrusted code created by Tilde Friends
users, are strictly sandboxed in ways similar to how web browsers run
untrusted code. All attempts to access potentially sensitive resources
are mediated through the core process.
When run with no arguments, it starts a web server on
[http://localhost:12345/](http://localhost:12345/) and an SSB node.
## Web Interface
The Tilde Friends web server provides access to Tilde Friends applications,
which are arbitrary user-defined web applications.
At the top left, in addition to some basic navigation links, is an `edit`
link. Anyone can view, modify, and run in-place the code to any Tilde
Friends application by using the in-browser editor.
At the top right, one can `login` (to save work in their own space)
or `logout` (proceeding as a guest).
The rest of the page is an iframe belonging to the application.
## Special Paths
- `/~user/app/` - Tilde Friends application paths take the form `/~user/app/`, where `user`
is a username of a Tilde Friends account, and `app` is an arbitrary name
of an application saved by the given user.
- `/~user/app/file` - A raw file in an app.
- `/&blobid.ed25519` - A raw blob. Content-Type is inferred for at least
a few common image types.
## Communication Channels
Web Browser <-> Core <-> Sandbox
Visiting an application path delivers stock HTML and JavaScript which
establishes a WebSocket connection back to the server.
At this point, a new sandbox process is started in Tilde Friends, much
as a new sandboxed process might be started for a new tab in a web
browser. This process has a custom RPC connection to the core process
which holds the WebSocket connection to the browser.
The custom RPC communication between the sandbox process and the core
process facilitates passing and calling functions remotely. Calling a
function in another process returns a `Promise`.
An application will typically call `app.setDocument()` at startup to
populate the app's iframe in the web browser with its own client web
application resources.

63
apps/docs/todo.md Normal file
View File

@ -0,0 +1,63 @@
# Tilde Friends TODO
[Back to index](#index)
## MVP3
- Sync status (problem feeds, messages/seconds stats, ...)
- app: wiki
- app: public blog
- Content-Disposition: download
- remove SSB credentials
- export SSB credentials
- initial: better empty news screen
- initial: remembered wrong user across login/logout
- initial: bad experience when following nobody
- make a cool independent app
- indicate when workspace differs from installed
- / => Something good.
- update docs
- audit + document API exposed to apps
- fix weird HTTP warnings
- channels
- placeholder/missing images
- no denial of service
- package standalone executable
- editor without app iframe
- sequence_before_author -> flags
- linkify ssb: links
- perfect rooms support
- connections 2.0
- make a better connections API
## Maybe Done
- blob_wants 2.0
- image downsample
- app: todo
- app: build archive
- update README
- administrators config
- apps name characters
- initial: can't switch to account when there is only one
- get tarball under 5MB
- rooms
- initial: doesn't refresh when create identity
- tf account timeout why
- ssb don't overflow boxes
- jwt for session tokens
- linkify https://...
- emoji reaction picker
- expose loads of stats
- confirm posting all new messages
- multiple identities per user, in database
- auto-populate data on initial launch
- make the docker image good / test it / use it
- leaking imports / exports
- file upload widget
- keep working on good error feedback
- build for windows
- installable apps (bring back an app message?)
- sqlStream => sqlExec or something
- !ssb from child process?
## Done
- update LICENSE
- logging to browser

62
apps/docs/vision.md Normal file
View File

@ -0,0 +1,62 @@
# Tilde Friends Vision
[Back to index](#index)
Tilde Friends is a tool for making and sharing.
It is both a peer-to-peer social network client, participating in Secure
Scuttlebutt, and an environment for creating and running web applications.
## Why
This is a thing that I wanted to exist and wanted to work on. No other reason.
There is not a business model. I believe it is interesting and unique.
## Goals
1. Make it **easy and fun** to run all sorts of web applications.
2. Provide **security** that is easy to understand and protects your data.
3. Make **creating and sharing** web applications accessible to anyone with a
browser.
## Ways to Use Tilde Friends
1. **Social Network User**: This is a social network first. You are just here,
because your friends are. Or you like how we limit your message length or
short videos or whatever the trend is. If you are ambitious, you click links
and see interactive experiences (apps) that you wouldn't see elsewhere.
2. **Web Visitor**: You get links from a friend to meeting invites, polls, games,
lists, wiki pages, ..., and you interact with them as though they were
cloud-hosted by a megacorporation. They just work, and you don't think twice.
3. **Group leader**: You host or use a small public instance, installing apps for
a group of friends to use as web visitors.
4. **Developer**: You like to write code and make or improve apps for fun or to
solve problems. When you encounter a Tilde Friends app on a strange server,
you know you can trivially modify it or download it to your own instance.
## Future Goals / Endgame
1. Mobile apps. This can run on your old phone. Maybe you won't be hosting
the web interface publicly, but you can sync, install and edit apps, and
otherwise get the full experience from a tiny touch screen.
2. The universal application runtime. The web browser is the universal
platform, but even for the simplest application that you might want to host
for your friends, cloud hosting, containers, and complicated dependencies might
all enter the mix. Tilde Friends, though it is yet another thing to host,
includes everything you need out of the box to run a vast variety of interesting
apps.
Tilde Friends will be built out, gradually providing safe access to host
resources and client resources the same way web browsers extended access to
resources like GPU, persistent storage, cameras, ... over the years.
Not much effort has been put forward yet to having a robust, long-lasting API,
but since the client side longevity is already handled by web browsers, it
seems possible that the server-side API can be managed in a similar way.
3. An awesome development environment. Right now it runs JavaScript from the
first embeddable text editor I could poorly configure enough to edit code,
but it could incorporate a debugger, source control integration a la ssb-git,
merge tools, and transpiling from all sorts of different languages.

View File

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

View File

@ -1,233 +1,105 @@
let g_about_cache = {};
var g_following_cache = {};
var g_following_deep_cache = {};
var g_about_cache = {};
async function query(sql, args) {
let result = [];
await ssb.sqlAsync(sql, args, function (row) {
result.push(row);
});
return result;
}
async function contacts_internal(id, last_row_id, following, max_row_id) {
let result = Object.assign({}, following[id] || {});
result.following = result.following || {};
result.blocking = result.blocking || {};
let contacts = await query(
`
SELECT content FROM messages
WHERE author = ? AND
rowid > ? AND
rowid <= ? AND
json_extract(content, '$.type') = 'contact'
ORDER BY sequence
`,
[id, last_row_id, max_row_id]
);
for (let row of contacts) {
let contact = JSON.parse(row.content);
if (contact.following === true) {
result.following[contact.contact] = true;
} else if (contact.following === false) {
delete result.following[contact.contact];
} else if (contact.blocking === true) {
result.blocking[contact.contact] = true;
} else if (contact.blocking === false) {
delete result.blocking[contact.contact];
}
async function following(db, id) {
if (g_following_cache[id]) {
return g_following_cache[id];
}
following[id] = result;
return result;
}
async function contact(id, last_row_id, following, max_row_id) {
return await contacts_internal(id, last_row_id, following, max_row_id);
}
async function following_deep_internal(
ids,
depth,
blocking,
last_row_id,
following,
max_row_id
) {
let contacts = await Promise.all(
[...new Set(ids)].map((x) => contact(x, last_row_id, following, max_row_id))
);
let result = {};
for (let i = 0; i < ids.length; i++) {
let id = ids[i];
let contact = contacts[i];
let all_blocking = Object.assign({}, contact.blocking, blocking);
let found = Object.keys(contact.following).filter((y) => !all_blocking[y]);
let deeper =
depth > 1
? await following_deep_internal(
found,
depth - 1,
all_blocking,
last_row_id,
following,
max_row_id
)
: [];
result[id] = [id, ...found, ...deeper];
var o = await db.get(id + ":following");
const k_version = 5;
var f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {users: [], sequence: 0, version: k_version};
}
return [...new Set(Object.values(result).flat())];
}
async function following_deep(ids, depth, blocking) {
let db = await database('cache');
const k_cache_version = 5;
let cache = await db.get('following');
cache = cache ? JSON.parse(cache) : {};
if (cache.version !== k_cache_version) {
cache = {
version: k_cache_version,
following: {},
last_row_id: 0,
};
}
let max_row_id = (
await query(
`
SELECT MAX(rowid) AS max_row_id FROM messages
`,
[]
)
)[0].max_row_id;
let result = await following_deep_internal(
ids,
depth,
blocking,
cache.last_row_id,
cache.following,
max_row_id
);
cache.last_row_id = max_row_id;
let store = JSON.stringify(cache);
await db.set('following', store);
return result;
}
async function fetch_about(db, ids, users) {
const k_cache_version = 1;
let cache = await db.get('about');
cache = cache ? JSON.parse(cache) : {};
if (cache.version !== k_cache_version) {
cache = {
version: k_cache_version,
about: {},
last_row_id: 0,
};
}
let max_row_id = 0;
f.users = new Set(f.users);
await ssb.sqlAsync(
`
SELECT MAX(rowid) AS max_row_id FROM messages
`,
[],
function (row) {
max_row_id = row.max_row_id;
}
);
for (let id of Object.keys(cache.about)) {
if (ids.indexOf(id) == -1) {
delete cache.about[id];
}
"SELECT "+
" sequence, "+
" json_extract(content, '$.contact') AS contact, "+
" json_extract(content, '$.following') AS following "+
"FROM messages "+
"WHERE "+
" author = ?1 AND "+
" sequence > ?2 AND "+
" json_extract(content, '$.type') = 'contact' "+
"UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
"ORDER BY sequence",
[id, f.sequence],
function(row) {
if (row.following) {
f.users.add(row.contact);
} else {
f.users.delete(row.contact);
}
f.sequence = row.sequence;
});
var as_set = f.users;
f.users = Array.from(f.users).sort();
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":following", j);
}
f.users = as_set;
g_following_cache[id] = f.users;
return f.users;
}
let abouts = [];
await ssb.sqlAsync(
`
SELECT
messages.*
FROM
messages,
json_each(?1) AS following
WHERE
messages.author = following.value AND
messages.rowid > ?3 AND
messages.rowid <= ?4 AND
json_extract(messages.content, '$.type') = 'about'
UNION
SELECT
messages.*
FROM
messages,
json_each(?2) AS following
WHERE
messages.author = following.value AND
messages.rowid <= ?4 AND
json_extract(messages.content, '$.type') = 'about'
ORDER BY messages.author, messages.sequence
`,
[
JSON.stringify(ids.filter((id) => cache.about[id])),
JSON.stringify(ids.filter((id) => !cache.about[id])),
cache.last_row_id,
max_row_id,
]
);
for (let about of abouts) {
let content = JSON.parse(about.content);
if (content.about === about.author) {
delete content.type;
delete content.about;
cache.about[about.author] = Object.assign(
cache.about[about.author] || {},
content
);
}
async function followingDeep(db, seed_ids, depth) {
if (depth <= 0) {
return seed_ids;
}
cache.last_row_id = max_row_id;
await db.set('about', JSON.stringify(cache));
users = users || {};
for (let id of Object.keys(cache.about)) {
users[id] = Object.assign(users[id] || {}, cache.about[id]);
var key = JSON.stringify([seed_ids, depth]);
if (g_following_deep_cache[key]) {
return g_following_deep_cache[key];
}
return Object.assign({}, users);
var f = await Promise.all(seed_ids.map(x => following(db, x).then(x => [...x])));
var ids = [].concat(...f);
var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1);
x = [...new Set([].concat(...x, ...seed_ids))].sort();
g_following_deep_cache[key] = x;
return x;
}
async function getAbout(db, id) {
if (g_about_cache[id]) {
return g_about_cache[id];
}
let o = await db.get(id + ':about');
var o = await db.get(id + ":about");
const k_version = 4;
let f = o ? JSON.parse(o) : o;
var f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {about: {}, sequence: 0, version: k_version};
}
await ssb.sqlAsync(
'SELECT ' +
' sequence, ' +
' content ' +
'FROM messages ' +
'WHERE ' +
' author = ?1 AND ' +
' sequence > ?2 AND ' +
" json_extract(content, '$.type') = 'about' AND " +
" json_extract(content, '$.about') = ?1 " +
'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
'ORDER BY sequence',
"SELECT "+
" sequence, "+
" content "+
"FROM messages "+
"WHERE "+
" author = ?1 AND "+
" sequence > ?2 AND "+
" json_extract(content, '$.type') = 'about' AND "+
" json_extract(content, '$.about') = ?1 "+
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
"ORDER BY sequence",
[id, f.sequence],
function (row) {
function(row) {
f.sequence = row.sequence;
if (row.content) {
let about = {};
var about = {};
try {
about = JSON.parse(row.content);
} catch {}
} catch {
}
delete about.about;
delete about.type;
f.about = Object.assign(f.about, about);
}
}
);
let j = JSON.stringify(f);
});
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ':about', j);
await db.set(id + ":about", j);
}
g_about_cache[id] = f.about;
return f.about;
@ -236,34 +108,14 @@ async function getAbout(db, id) {
async function getSize(db, id) {
let size = 0;
await ssb.sqlAsync(
'SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1',
"SELECT (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
[id],
function (row) {
size += row.size;
}
);
});
return size;
}
async function getSizes(ids) {
let sizes = {};
await ssb.sqlAsync(
`
SELECT
author,
(SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(messages.id))) AS size
FROM messages
JOIN json_each(?) AS ids ON author = ids.value
GROUP BY author
`,
[JSON.stringify(ids)],
function (row) {
sizes[row.author] = row.size;
}
);
return sizes;
}
function niceSize(bytes) {
let value = bytes;
let unit = 'B';
@ -279,39 +131,27 @@ function niceSize(bytes) {
return Math.round(value * 10) / 10 + ' ' + unit;
}
function escape(value) {
return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
async function buildTree(db, root, indent, depth) {
var f = await following(db, root);
var result = indent + '[' + f.size + '] ' + '<a target="_top" href="../index/#' + root + '">' + ((await getAbout(db, root)).name || root) + '</a> ' + niceSize(await getSize(db, root)) + '\n';
if (depth > 0) {
for (let next of f) {
result += await buildTree(db, next, indent + ' ', depth - 1);
}
}
return result;
}
async function main() {
await app.setDocument('<pre style="color: #fff">building...</pre>');
let db = await database('ssb');
let whoami = await ssb.getIdentities();
let tree = '';
await app.setDocument(
`<pre style="color: #fff">Enumerating followed users...</pre>`
);
let following = await following_deep(whoami, 2, {});
await app.setDocument(
`<pre style="color: #fff">Getting names and sizes...</pre>`
);
let [about, sizes] = await Promise.all([
fetch_about(db, following, {}),
getSizes(following),
]);
await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
following.sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0));
for (let id of following) {
tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
var db = await database('ssb');
var whoami = await ssb.getIdentities();
var tree = '';
for (let id of whoami) {
await app.setDocument(`<pre style="color: #fff">building... ${id}</pre>`);
tree += await buildTree(db, id, '', 2);
}
await app.setDocument(
'<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' +
tree +
'</ul>\n</body>\n</html>'
);
await app.setDocument('<pre style="color: #fff">FOLLOWING:\n' + tree + '</pre>');
}
main();
main();

View File

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

View File

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

View File

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

View File

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

View File

@ -1,107 +0,0 @@
import * as tfrpc from '/tfrpc.js';
let g_database;
let g_hash;
tfrpc.register(async function localStorageGet(key) {
return app.localStorageGet(key);
});
tfrpc.register(async function localStorageSet(key, value) {
return app.localStorageSet(key, value);
});
tfrpc.register(async function databaseGet(key) {
return g_database ? g_database.get(key) : undefined;
});
tfrpc.register(async function databaseSet(key, value) {
return g_database ? g_database.set(key, value) : undefined;
});
tfrpc.register(async function createIdentity() {
return ssb.createIdentity();
});
tfrpc.register(async function getIdentities() {
return ssb.getIdentities();
});
tfrpc.register(async function getAllIdentities() {
return ssb.getAllIdentities();
});
tfrpc.register(async function getBroadcasts() {
return ssb.getBroadcasts();
});
tfrpc.register(async function getConnections() {
return ssb.connections();
});
tfrpc.register(async function getStoredConnections() {
return ssb.storedConnections();
});
tfrpc.register(async function forgetStoredConnection(connection) {
return ssb.forgetStoredConnection(connection);
});
tfrpc.register(async function createTunnel(portal, target) {
return ssb.createTunnel(portal, target);
});
tfrpc.register(async function connect(token) {
await ssb.connect(token);
});
tfrpc.register(async function closeConnection(id) {
await ssb.closeConnection(id);
});
tfrpc.register(async function query(sql, args) {
let result = [];
await ssb.sqlAsync(sql, args, function callback(row) {
result.push(row);
});
return result;
});
tfrpc.register(async function appendMessage(id, message) {
return ssb.appendMessageWithIdentity(id, message);
});
core.register('message', async function message_handler(message) {
if (message.event == 'hashChange') {
g_hash = message.hash;
await tfrpc.rpc.hashChanged(message.hash);
}
});
tfrpc.register(function getHash(id, message) {
return g_hash;
});
tfrpc.register(function setHash(hash) {
return app.setHash(hash);
});
tfrpc.register(async function store_blob(blob) {
if (Array.isArray(blob)) {
blob = Uint8Array.from(blob);
}
return await ssb.blobStore(blob);
});
tfrpc.register(async function get_blob(id) {
return utf8Decode(await ssb.blobGet(id));
});
tfrpc.register(async function store_message(message) {
return await ssb.storeMessage(message);
});
tfrpc.register(function apps() {
return core.apps();
});
tfrpc.register(function getActiveIdentity() {
return ssb.getActiveIdentity();
});
tfrpc.register(async function try_decrypt(id, content) {
return await ssb.privateMessageDecrypt(id, content);
});
core.register('onMessage', async function (id) {
await tfrpc.rpc.notifyNewMessage(id);
});
core.register('onBroadcastsChanged', async function () {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
});
core.register('onConnectionsChanged', async function () {
await tfrpc.rpc.set('connections', await ssb.connections());
});
async function main() {
if (typeof database !== 'undefined') {
g_database = await database('ssb');
}
await app.setDocument(utf8Decode(await getFile('index.html')));
}
main();

File diff suppressed because one or more lines are too long

View File

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

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,248 +0,0 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import * as tfutils from './tf-utils.js';
const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256';
class TfComposeElement extends LitElement {
static get properties() {
return {
value: {type: String},
};
}
input() {
let input = this.renderRoot.getElementById('input');
let preview = this.renderRoot.getElementById('preview');
if (input && preview) {
preview.innerHTML = tfutils.markdown(input.value);
}
}
submit() {
this.dispatchEvent(
new CustomEvent('tf-submit', {
bubbles: true,
composed: true,
detail: {
value: this.renderRoot.getElementById('input').value,
},
})
);
this.renderRoot.getElementById('input').value = '';
this.input();
}
render() {
return html`
<div style="display: flex; flex-direction: row">
<textarea id="input" @input=${this.input} style="flex: 1 1">${this.value}</textarea>
<div id="preview" style="flex: 1 1"></div>
</div>
<input type="submit" value="Submit" @click=${this.submit}></input>
`;
}
}
customElements.define('tf-compose', TfComposeElement);
class TfIssuesAppElement extends LitElement {
static get properties() {
return {
issues: {type: Array},
selected: {type: Object},
};
}
constructor() {
super();
this.issues = [];
this.load();
}
async load() {
let issues = {};
let messages = await tfrpc.rpc.query(
`
WITH issues AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM messages_refs JOIN messages ON
messages.id = messages_refs.message
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'),
edits AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM issues JOIN messages_refs ON
issues.id = messages_refs.ref JOIN messages ON
messages.id = messages_refs.message
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
SELECT * FROM issues
UNION
SELECT * FROM edits ORDER BY timestamp
`,
[k_project]
);
for (let message of messages) {
let content = JSON.parse(message.content);
switch (content.type) {
case 'issue':
issues[message.id] = {
id: message.id,
author: message.author,
text: content.text,
updates: [],
created: message.timestamp,
open: true,
};
break;
case 'issue-edit':
case 'post':
for (let issue of content.issues || []) {
if (issues[issue.link]) {
if (issue.open !== undefined) {
issues[issue.link].open = issue.open;
message.open = issue.open;
}
issues[issue.link].updates.push(message);
issues[issue.link].updated = message.timestamp;
}
}
break;
}
}
this.issues = Object.values(issues).sort(
(x, y) => y.open - x.open || y.created - x.created
);
if (this.selected) {
for (let issue of this.issues) {
if (issue.id == this.selected.id) {
this.selected = issue;
}
}
}
}
render_issue_table_row(issue) {
return html`
<tr>
<td>${issue.open ? '☐ open' : '☑ closed'}</td>
<td
style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
>
${issue.author}
</td>
<td
style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer"
@click=${() => (this.selected = issue)}
>
${issue.text.split('\n')?.[0]}
</td>
<td>
${new Date(issue.updated ?? issue.created).toLocaleDateString()}
</td>
</tr>
`;
}
render_update(update) {
let content = JSON.parse(update.content);
let message;
if (content.text) {
message = unsafeHTML(tfutils.markdown(content.text));
}
return html`
<div style="border-left: 2px solid #fff; padding-left: 8px">
<div>${new Date(update.timestamp).toLocaleString()}</div>
<div>${update.author}</div>
<div>${message}</div>
<div>
${update.open !== undefined
? update.open
? 'issue opened'
: 'issue closed'
: undefined}
</div>
</div>
`;
}
async set_open(id, open) {
if (
confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)
) {
let whoami = await tfrpc.rpc.getActiveIdentity();
await tfrpc.rpc.appendMessage(whoami, {
type: 'issue-edit',
issues: [
{
link: id,
open: open,
},
],
});
await this.load();
}
}
async create_issue(event) {
let whoami = await tfrpc.rpc.getActiveIdentity();
await tfrpc.rpc.appendMessage(whoami, {
type: 'issue',
project: k_project,
text: event.detail.value,
});
await this.load();
}
async reply_to_issue(event) {
let whoami = await tfrpc.rpc.getActiveIdentity();
await tfrpc.rpc.appendMessage(whoami, {
type: 'post',
text: event.detail.value,
root: this.selected.id,
branch: this.selected.updates.length
? this.selected.updates[this.selected.updates.length - 1].id
: this.selected.id,
issues: [
{
link: this.selected.id,
},
],
});
await this.load();
}
render() {
let header = html` <h1>Tilde Friends Issues</h1> `;
if (this.selected) {
return html`
${header}
<div>
<input type="button" value="Back" @click=${() => (this.selected = undefined)}></input>
${
this.selected.open
? html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>`
: html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`
}
</div>
<div>${new Date(this.selected.created).toLocaleString()}</div>
<div>${this.selected.author}</div>
<div>${this.selected.id}</div>
<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
${this.selected.updates.map((x) => this.render_update(x))}
<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
`;
} else {
return html`
${header}
<h2>New Issue</h2>
<tf-compose @tf-submit=${this.create_issue}></tf-compose>
<table>
<tr>
<th>Status</th>
<th>Author</th>
<th>Title</th>
<th>Date</th>
</tr>
${this.issues.map((x) => this.render_issue_table_row(x))}
</table>
`;
}
}
}
customElements.define('tf-issues-app', TfIssuesAppElement);

View File

@ -1,113 +0,0 @@
import * as linkify from './commonmark-linkify.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="" alt="');
} else {
this.lit('<img src="' + this.esc(node.destination) + '" alt="');
}
}
this.disableTags += 1;
} else {
this.disableTags -= 1;
if (this.disableTags === 0) {
if (node.title) {
this.lit('" title="' + this.esc(node.title));
}
this.lit('" />');
}
}
}
}
export function markdown(md) {
var reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer();
writer.image = image;
var parsed = reader.parse(md || '');
parsed = linkify.transform(parsed);
var walker = parsed.walker();
var 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 = '#' + node.destination;
} else if (
node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')
) {
node.destination = '#' + 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 writer.render(parsed);
}
export function human_readable_size(bytes) {
let v = bytes;
let u = 'B';
for (let unit of ['kB', 'MB', 'GB']) {
if (v > 1024) {
v /= 1024;
u = unit;
} else {
break;
}
}
return `${Math.round(v * 10) / 10} ${u}`;
}

View File

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

View File

@ -1,185 +0,0 @@
import * as tfrpc from '/tfrpc.js';
let g_hash;
let g_collection_notifies = {};
tfrpc.register(async function getOwnerIdentities() {
return ssb.getOwnerIdentities();
});
tfrpc.register(async function getIdentities() {
return ssb.getIdentities();
});
tfrpc.register(async function query(sql, args) {
let result = [];
await ssb.sqlAsync(sql, args, function callback(row) {
result.push(row);
});
return result;
});
tfrpc.register(async function localStorageGet(key) {
return app.localStorageGet(key);
});
tfrpc.register(async function localStorageSet(key, value) {
return app.localStorageSet(key, value);
});
tfrpc.register(async function following(ids, depth) {
return ssb.following(ids, depth);
});
tfrpc.register(async function appendMessage(id, message) {
return ssb.appendMessageWithIdentity(id, message);
});
tfrpc.register(async function store_blob(blob) {
if (Array.isArray(blob)) {
blob = Uint8Array.from(blob);
}
return await ssb.blobStore(blob);
});
tfrpc.register(async function get_blob(id) {
return utf8Decode(await ssb.blobGet(id));
});
let g_new_message_resolve;
let g_new_message_promise = new Promise(function (resolve, reject) {
g_new_message_resolve = resolve;
});
function new_message() {
return g_new_message_promise;
}
core.register('onMessage', function (id) {
let resolve = g_new_message_resolve;
g_new_message_promise = new Promise(function (resolve, reject) {
g_new_message_resolve = resolve;
});
if (resolve) {
resolve();
}
});
core.register('message', async function message_handler(message) {
if (message.event == 'hashChange') {
print('hash change', message.hash);
g_hash = message.hash;
await tfrpc.rpc.hash_changed(message.hash);
}
});
tfrpc.register(function set_hash(hash) {
if (g_hash != hash) {
return app.setHash(hash);
}
});
tfrpc.register(function get_hash(id, message) {
return g_hash;
});
tfrpc.register(async function try_decrypt(id, content) {
return await ssb.privateMessageDecrypt(id, content);
});
tfrpc.register(async function encrypt(id, recipients, content) {
return await ssb.privateMessageEncrypt(id, recipients, content);
});
async function process_message(whoami, collection, message, kind, parent) {
let content = JSON.parse(message.content);
if (typeof content == 'string') {
let x;
for (let id of whoami) {
x = await ssb.privateMessageDecrypt(id, content);
if (x) {
content = JSON.parse(x);
break;
}
}
if (!x) {
return;
}
if (content.type !== kind || (parent && content.parent !== parent)) {
return;
}
}
if (content?.key) {
if (content?.tombstone) {
delete collection[content.key];
} else {
collection[content.key] = Object.assign(
collection[content.key] || {},
content
);
}
} else {
collection[message.id] = Object.assign(content, {id: message.id});
}
return true;
}
tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
let whoami = await ssb.getIdentities();
data = data ?? {};
let rowid = 0;
await ssb.sqlAsync(
'SELECT MAX(rowid) AS rowid FROM messages',
[],
function (row) {
rowid = row.rowid;
}
);
while (true) {
if (rowid == max_rowid) {
await new_message();
await ssb.sqlAsync(
'SELECT MAX(rowid) AS rowid FROM messages',
[],
function (row) {
rowid = row.rowid;
}
);
}
let modified = false;
let rows = [];
await ssb.sqlAsync(
`
SELECT messages.id, author, content, timestamp
FROM messages
JOIN json_each(?1) AS id ON messages.author = id.value
WHERE
messages.rowid > ?2 AND
messages.rowid <= ?3 AND
((json_extract(messages.content, '$.type') = ?4 AND
(?5 IS NULL OR json_extract(messages.content, '$.parent') = ?5)) OR
content LIKE '"%')
`,
[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent],
function (row) {
rows.push(row);
}
);
max_rowid = rowid;
for (let row of rows) {
if (await process_message(whoami, data, row, kind, parent)) {
modified = true;
}
}
if (modified) {
break;
}
}
return [rowid, data];
});
async function main() {
await app.setDocument(utf8Decode(await getFile('index.html')));
}
main();

File diff suppressed because one or more lines are too long

View File

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

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

View File

@ -1,89 +0,0 @@
import {LitElement, html, keyed, live} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
class TfJournalAppElement extends LitElement {
static get properties() {
return {
ids: {type: Array},
owner_ids: {type: Array},
whoami: {type: String},
journals: {type: Object},
};
}
constructor() {
super();
this.ids = [];
this.owner_ids = [];
this.journals = {};
this.load();
}
async load() {
this.ids = await tfrpc.rpc.getIdentities();
this.whoami = await tfrpc.rpc.localStorageGet('journal_whoami');
await this.read_journals();
}
async read_journals() {
let max_rowid;
let journals;
while (true) {
[max_rowid, journals] = await tfrpc.rpc.collection(
[this.whoami],
'journal-entry',
undefined,
max_rowid,
journals
);
this.journals = Object.assign({}, journals);
console.log('JOURNALS', this.journals);
}
}
async on_whoami_changed(event) {
let new_id = event.srcElement.selected;
await tfrpc.rpc.localStorageSet('journal_whoami', new_id);
this.whoami = new_id;
}
async on_journal_publish(event) {
let key = event.detail.key;
let text = event.detail.text;
let message = {
type: 'journal-entry',
key: key,
text: text,
};
message.recps = [this.whoami];
print(message);
message = await tfrpc.rpc.encrypt(
this.whoami,
message.recps,
JSON.stringify(message)
);
print(message);
await tfrpc.rpc.appendMessage(this.whoami, message);
}
render() {
console.log('RENDER APP', this.journals);
let self = this;
return html`
<div>
<tf-id-picker
.ids=${this.ids}
selected=${this.whoami}
@change=${this.on_whoami_changed}
></tf-id-picker>
</div>
<tf-journal-entry
whoami=${this.whoami}
.journals=${this.journals}
@publish=${this.on_journal_publish}
></tf-journal-entry>
`;
}
}
customElements.define('tf-journal-app', TfJournalAppElement);

View File

@ -1,97 +0,0 @@
import {LitElement, html, unsafeHTML, range} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
class TfJournalEntryElement extends LitElement {
static get properties() {
return {
whoami: {type: String},
key: {type: String},
journals: {type: Object},
text: {type: String},
};
}
constructor() {
super();
this.journals = {};
this.key = new Date().toISOString().split('T')[0];
}
markdown(md) {
var reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer();
var parsed = reader.parse(md || '');
return writer.render(parsed);
}
on_discard(event) {
this.text = undefined;
}
async on_publish() {
console.log('publish', this.text);
this.dispatchEvent(
new CustomEvent('publish', {
bubbles: true,
detail: {
key: this.shadowRoot.getElementById('date_picker').value,
text: this.text,
},
})
);
}
back_dates(count) {
let now = new Date();
let result = [];
for (let i = 0; i < count; i++) {
let next = new Date(now);
next.setDate(now.getDate() - i);
result.push(next.toISOString().split('T')[0]);
}
return result;
}
on_edit(event) {
this.text = event.srcElement.value;
}
on_date_change(event) {
this.key = event.srcElement.value;
this.text = this.journals[this.key]?.text;
}
render() {
console.log('RENDER ENTRY', this.key, this.journals?.[this.key]);
return html`
<select id="date_picker" @change=${this.on_date_change}>
${this.back_dates(10).map(
(x) => html` <option value=${x}>${x}</option> `
)}
</select>
<div style="display: inline-flex; flex-direction: row">
<button
?disabled=${this.text == this.journals?.[this.key]?.text}
@click=${this.on_publish}
>
Publish
</button>
<button @click=${this.on_discard}>Discard</button>
</div>
<div style="display: flex; flex-direction: row">
<textarea
style="flex: 1 1; min-height: 10em"
@input=${this.on_edit}
.value=${this.text ?? this.journals?.[this.key]?.text ?? ''}
></textarea>
<div style="flex: 1 1">
${unsafeHTML(
this.markdown(this.text ?? this.journals?.[this.key]?.text)
)}
</div>
</div>
`;
}
}
customElements.define('tf-journal-entry', TfJournalEntryElement);

View File

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

View File

@ -1,13 +0,0 @@
async function main() {
let host = core.url.match(/.*\/\/(.*?)\//)[1];
let id = (await ssb.getServerIdentity()).substring(1);
let room = `net:${host}:${ssb.port}~shs:${id}:SSB+Room+SK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
await app.setDocument(`
<body style="color: #fff">
<h1>Server</h1>
<div>The local server address is:</div>
<div><input type="text" readonly value="${room}" style="width: 100%"></input></div>
</body>
`);
}
main();

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -18,21 +18,12 @@ tfrpc.register(async function databaseSet(key, value) {
tfrpc.register(async function createIdentity() {
return ssb.createIdentity();
});
tfrpc.register(async function getServerIdentity() {
return ssb.getServerIdentity();
});
tfrpc.register(async function setServerFollowingMe(id, following) {
return ssb.setServerFollowingMe(id, following);
});
tfrpc.register(async function getIdentities() {
return ssb.getIdentities();
});
tfrpc.register(async function getAllIdentities() {
return ssb.getAllIdentities();
});
tfrpc.register(async function following(ids, depth) {
return ssb.following(ids, depth);
});
tfrpc.register(async function getBroadcasts() {
return ssb.getBroadcasts();
});
@ -76,7 +67,7 @@ tfrpc.register(function getHash(id, message) {
tfrpc.register(function setHash(hash) {
return app.setHash(hash);
});
core.register('onMessage', async function (id) {
ssb.addEventListener('message', async function(id) {
await tfrpc.rpc.notifyNewMessage(id);
});
tfrpc.register(async function store_blob(blob) {
@ -97,30 +88,18 @@ tfrpc.register(function apps() {
tfrpc.register(async function try_decrypt(id, content) {
return await ssb.privateMessageDecrypt(id, content);
});
tfrpc.register(async function encrypt(id, recipients, content) {
return await ssb.privateMessageEncrypt(id, recipients, content);
});
tfrpc.register(async function getActiveIdentity() {
return await ssb.getActiveIdentity();
});
tfrpc.register(async function sync() {
return await ssb.sync();
});
core.register('onBroadcastsChanged', async function () {
ssb.addEventListener('broadcasts', async function() {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
});
core.register('onConnectionsChanged', async function () {
core.register('onConnectionsChanged', async function() {
await tfrpc.rpc.set('connections', await ssb.connections());
});
core.register('setActiveIdentity', async function (id) {
await tfrpc.rpc.set('identity', id);
});
async function main() {
if (typeof database !== 'undefined') {
if (typeof(database) !== 'undefined') {
g_database = await database('ssb');
}
await app.setDocument(utf8Decode(await getFile('index.html')));
}
main();
main();

View File

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

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,8 @@
<!doctype html>
<html>
<!DOCTYPE html>
<html style="color: #fff">
<head>
<title>Tilde Friends</title>
<base target="_top" />
<base target="_top">
<link rel="stylesheet" href="tribute.css" />
<style>
.tribute-container {
@ -10,15 +10,13 @@
}
</style>
</head>
<body style="margin: 0; padding: 0">
<tf-app></tf-app>
<tf-reactions-modal id="reactions_modal"></tf-reactions-modal>
<script>
window.litDisableBundleWarning = true;
</script>
<body>
<tf-app/>
<script>window.litDisableBundleWarning = true;</script>
<script src="filesaver.min.js"></script>
<script src="commonmark.min.js"></script>
<script src="commonmark-linkify.js" type="module"></script>
<script src="commonmark-hashtag.js" type="module"></script>
<script src="script.js" type="module"></script>
</body>
</html>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,17 +1,16 @@
import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import * as tf_id_picker from './tf-id-picker.js';
import * as tf_app from './tf-app.js';
import * as tf_message from './tf-message.js';
import * as tf_user from './tf-user.js';
import * as tf_compose from './tf-compose.js';
import * as tf_news from './tf-news.js';
import * as tf_profile from './tf-profile.js';
import * as tf_reactions_modal from './tf-reactions-modal.js';
import * as tf_tab_mentions from './tf-tab-mentions.js';
import * as tf_tab_news from './tf-tab-news.js';
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
import * as tf_tab_search from './tf-tab-search.js';
import * as tf_tab_connections from './tf-tab-connections.js';
import * as tf_tab_query from './tf-tab-query.js';
import * as tf_tag from './tf-tag.js';
import * as tf_tag from './tf-tag.js';

View File

@ -34,13 +34,9 @@ class TfElement extends LitElement {
this.users = {};
this.loaded = false;
this.tags = [];
tfrpc.rpc.getBroadcasts().then((b) => {
self.broadcasts = b || [];
});
tfrpc.rpc.getConnections().then((c) => {
self.connections = c || [];
});
tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || []; });
tfrpc.rpc.getConnections().then(c => { self.connections = c || []; });
tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
tfrpc.register(function hashChanged(hash) {
self.set_hash(hash);
});
@ -52,35 +48,103 @@ class TfElement extends LitElement {
self.broadcasts = value;
} else if (name === 'connections') {
self.connections = value;
} else if (name === 'identity') {
self.whoami = value;
}
});
this.initial_load();
}
async initial_load() {
let whoami = await tfrpc.rpc.getActiveIdentity();
let whoami = await tfrpc.rpc.localStorageGet('whoami');
let ids = (await tfrpc.rpc.getIdentities()) || [];
this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
this.ids = ids;
}
set_hash(hash) {
this.hash = decodeURIComponent(hash || '#');
this.hash = hash || '#';
if (this.hash.startsWith('#q=')) {
this.tab = 'search';
} else if (this.hash === '#connections') {
this.tab = 'connections';
} else if (this.hash === '#mentions') {
this.tab = 'mentions';
} else if (this.hash.startsWith('#sql=')) {
this.tab = 'query';
} else {
this.tab = 'news';
}
}
async contacts_internal(id, last_row_id, following, max_row_id) {
let result = Object.assign({}, following[id] || {});
result.following = result.following || {};
result.blocking = result.blocking || {};
let contacts = await tfrpc.rpc.query(
`
SELECT content FROM messages
WHERE author = ? AND
rowid > ? AND
rowid <= ? AND
json_extract(content, '$.type') = 'contact'
ORDER BY sequence
`,
[id, last_row_id, max_row_id]);
for (let row of contacts) {
let contact = JSON.parse(row.content);
if (contact.following === true) {
result.following[contact.contact] = true;
} else if (contact.following === false) {
delete result.following[contact.contact];
} else if (contact.blocking === true) {
result.blocking[contact.contact] = true;
} else if (contact.blocking === false) {
delete result.blocking[contact.contact];
}
}
following[id] = result;
return result;
}
async contact(id, last_row_id, following, max_row_id) {
return await this.contacts_internal(id, last_row_id, following, max_row_id);
}
async following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) {
let contacts = await Promise.all([...new Set(ids)].map(x => this.contact(x, last_row_id, following, max_row_id)));
let result = {};
for (let i = 0; i < ids.length; i++) {
let id = ids[i];
let contact = contacts[i];
let all_blocking = Object.assign({}, contact.blocking, blocking);
let found = Object.keys(contact.following).filter(y => !all_blocking[y]);
let deeper = depth > 1 ? await this.following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : [];
result[id] = [id, ...found, ...deeper];
}
return [...new Set(Object.values(result).flat())];
}
async following_deep(ids, depth, blocking) {
const k_cache_version = 5;
let cache = await tfrpc.rpc.databaseGet('following');
cache = cache ? JSON.parse(cache) : {};
if (cache.version !== k_cache_version) {
cache = {
version: k_cache_version,
following: {},
last_row_id: 0,
};
}
let max_row_id = (await tfrpc.rpc.query(`
SELECT MAX(rowid) AS max_row_id FROM messages
`, []))[0].max_row_id;
let result = await this.following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
cache.last_row_id = max_row_id;
let store = JSON.stringify(cache);
/* 2023-02-20: Exceeding message size. */
//if (store.length < 512 * 1024) {
await tfrpc.rpc.databaseSet('following', store);
//}
return [result, cache.following];
}
async fetch_about(ids, users) {
const k_cache_version = 1;
let cache = await tfrpc.rpc.databaseGet('about');
@ -92,14 +156,9 @@ class TfElement extends LitElement {
last_row_id: 0,
};
}
let max_row_id = (
await tfrpc.rpc.query(
`
let max_row_id = (await tfrpc.rpc.query(`
SELECT MAX(rowid) AS max_row_id FROM messages
`,
[]
)
)[0].max_row_id;
`, []))[0].max_row_id;
for (let id of Object.keys(cache.about)) {
if (ids.indexOf(id) == -1) {
delete cache.about[id];
@ -109,7 +168,7 @@ class TfElement extends LitElement {
let abouts = await tfrpc.rpc.query(
`
SELECT
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
messages.*
FROM
messages,
json_each(?1) AS following
@ -120,7 +179,7 @@ class TfElement extends LitElement {
json_extract(messages.content, '$.type') = 'about'
UNION
SELECT
messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
messages.*
FROM
messages,
json_each(?2) AS following
@ -131,21 +190,17 @@ class TfElement extends LitElement {
ORDER BY messages.author, messages.sequence
`,
[
JSON.stringify(ids.filter((id) => cache.about[id])),
JSON.stringify(ids.filter((id) => !cache.about[id])),
JSON.stringify(ids.filter(id => cache.about[id])),
JSON.stringify(ids.filter(id => !cache.about[id])),
cache.last_row_id,
max_row_id,
]
);
]);
for (let about of abouts) {
let content = JSON.parse(about.content);
if (content.about === about.author) {
delete content.type;
delete content.about;
cache.about[about.author] = Object.assign(
cache.about[about.author] || {},
content
);
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
}
}
cache.last_row_id = max_row_id;
@ -160,16 +215,17 @@ class TfElement extends LitElement {
async fetch_new_message(id) {
let messages = await tfrpc.rpc.query(
`
SELECT messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
SELECT messages.*
FROM messages
JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.id = ?
`,
[JSON.stringify(this.following), id]
);
[
JSON.stringify(this.following),
id,
]);
if (messages && messages.length) {
this.unread = [...this.unread, ...messages];
this.unread = this.unread.slice(this.unread.length - 1024);
}
}
@ -186,21 +242,24 @@ class TfElement extends LitElement {
}
async create_identity() {
if (confirm('Are you sure you want to create a new identity?')) {
if (confirm("Are you sure you want to create a new identity?")) {
await tfrpc.rpc.createIdentity();
this.ids = (await tfrpc.rpc.getIdentities()) || [];
if (this.ids && !this.whoami) {
this.whoami = this.ids[0];
}
}
}
render_id_picker() {
return html`
<tf-id-picker id="picker" selected=${this.whoami} .ids=${this.ids} @change=${this._handle_whoami_changed}></tf-id-picker>
<button @click=${this.create_identity}>Create Identity</button>
`;
}
async load_recent_tags() {
let start = new Date();
this.tags = await tfrpc.rpc.query(
`
this.tags = await tfrpc.rpc.query(`
WITH
recent AS (SELECT id, json(content) AS content FROM messages
recent AS (SELECT id, content FROM messages
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
ORDER BY timestamp DESC LIMIT 1024),
recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
@ -212,38 +271,16 @@ class TfElement extends LitElement {
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
by_message AS (SELECT DISTINCT id, tag FROM combined)
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
`,
[new Date() - 7 * 24 * 60 * 60 * 1000]
);
`, [new Date() - 7 * 24 * 60 * 60 * 1000]);
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
}
async load() {
let whoami = this.whoami;
let tags = this.load_recent_tags();
let following = await tfrpc.rpc.following([whoami], 2);
let users = {};
let by_count = [];
for (let [id, v] of Object.entries(following)) {
users[id] = {
following: v.of,
blocking: v.ob,
followed: v.if,
blocked: v.ib,
};
by_count.push({count: v.of, id: id});
}
console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
let start_time = new Date();
users = await this.fetch_about(Object.keys(following).sort(), users);
console.log(
'about took',
(new Date() - start_time) / 1000.0,
'seconds for',
Object.keys(users).length,
'users'
);
this.following = Object.keys(following);
let [following, users] = await this.following_deep([whoami], 2, {});
users = await this.fetch_about(following.sort(), users);
this.following = following;
this.users = users;
await tags;
console.log(`load finished ${whoami} => ${this.whoami}`);
@ -256,54 +293,19 @@ class TfElement extends LitElement {
let users = this.users;
if (this.tab === 'news') {
return html`
<tf-tab-news
id="tf-tab-news"
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
hash=${this.hash}
.unread=${this.unread}
@refresh=${() => (this.unread = [])}
?loading=${this.loading}
></tf-tab-news>
<tf-tab-news .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
`;
} else if (this.tab === 'connections') {
return html`
<tf-tab-connections
.users=${this.users}
.connections=${this.connections}
.broadcasts=${this.broadcasts}
></tf-tab-connections>
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
`;
} else if (this.tab === 'mentions') {
return html`
<tf-tab-mentions
.following=${this.following}
whoami=${this.whoami}
.users="${this.users}}"
></tf-tab-mentions>
<tf-tab-mentions .following=${this.following} whoami=${this.whoami} .users=${this.users}}></tf-tab-mentions>
`;
} else if (this.tab === 'search') {
return html`
<tf-tab-search
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
query=${this.hash?.startsWith('#q=')
? decodeURIComponent(this.hash.substring(3))
: null}
></tf-tab-search>
`;
} else if (this.tab === 'query') {
return html`
<tf-tab-query
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
query=${this.hash?.startsWith('#sql=')
? decodeURIComponent(this.hash.substring(5))
: null}
></tf-tab-query>
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
`;
}
}
@ -316,8 +318,6 @@ class TfElement extends LitElement {
await tfrpc.rpc.setHash('#connections');
} else if (tab === 'mentions') {
await tfrpc.rpc.setHash('#mentions');
} else if (tab === 'query') {
await tfrpc.rpc.setHash('#sql=');
}
}
@ -325,65 +325,34 @@ class TfElement extends LitElement {
let self = this;
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
console.log(`starting loading ${this.whoami} ${this.loaded}`);
this.loading = true;
this.load().finally(function () {
this.load().finally(function() {
self.loading = false;
});
}
const k_tabs = {
'📰': 'news',
'📡': 'connections',
'@': 'mentions',
'🔍': 'search',
'👩‍💻': 'query',
};
let tabs = html`
<div class="w3-bar w3-theme-l1">
${Object.entries(k_tabs).map(
([k, v]) => html`
<button
title=${v}
class="w3-bar-item w3-padding w3-hover-theme tab ${self.tab == v
? 'w3-theme-l2'
: 'w3-theme-l1'}"
@click=${() => self.set_tab(v)}
>
${k}
<span class=${self.tab == v ? '' : 'w3-hide-small'}
>${v.charAt(0).toUpperCase() + v.substring(1)}</span
>
</button>
`
)}
<div>
<input type="button" class="tab" value="News" ?disabled=${self.tab == 'news'} @click=${() => self.set_tab('news')}></input>
<input type="button" class="tab" value="Connections" ?disabled=${self.tab == 'connections'} @click=${() => self.set_tab('connections')}></input>
<input type="button" class="tab" value="Mentions" ?disabled=${self.tab == 'mentions'} @click=${() => self.set_tab('mentions')}></input>
<input type="button" class="tab" value="Search" ?disabled=${self.tab == 'search'} @click=${() => self.set_tab('search')}></input>
</div>
`;
let contents = !this.loaded
? this.loading
? html`<div
class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge"
>
Loading...
</div>
${this.render_tab()}`
: html`<div>Select or create an identity.</div>`
: this.render_tab();
let contents =
!this.loaded ?
this.loading ?
html`<div>Loading...</div>` :
html`<div>Select or create an identity.</div>` :
this.render_tab();
return html`
<div
style="width: 100vw; min-height: 100vh; height: 100%"
class="w3-theme-dark"
>
${tabs}
<div style="padding: 8px">
${this.tags.map(
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
)}
${contents}
</div>
</div>
${this.render_id_picker()}
${tabs}
${this.tags.map(x => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`)}
${contents}
`;
}
}
customElements.define('tf-app', TfElement);
customElements.define('tf-app', TfElement);

View File

@ -1,4 +1,4 @@
import {LitElement, html, unsafeHTML, live} from './lit-all.min.js';
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfutils from './tf-utils.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
@ -13,7 +13,6 @@ class TfComposeElement extends LitElement {
branch: {type: String},
apps: {type: Object},
drafts: {type: Object},
author: {type: String},
};
}
@ -26,7 +25,6 @@ class TfComposeElement extends LitElement {
this.branch = undefined;
this.apps = undefined;
this.drafts = {};
this.author = undefined;
}
process_text(text) {
@ -60,13 +58,11 @@ class TfComposeElement extends LitElement {
link: link,
};
}
draft.mentions[link].name = name.startsWith('@')
? name.substring(1)
: name;
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
updated = true;
}
if (updated) {
setTimeout(() => this.notify(draft), 0);
this.requestUpdate();
}
return tfutils.markdown(text);
}
@ -74,31 +70,36 @@ class TfComposeElement extends LitElement {
input(event) {
let edit = this.renderRoot.getElementById('edit');
let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = this.process_text(edit.innerText);
preview.innerHTML = this.process_text(edit.value);
let content_warning = this.renderRoot.getElementById('content_warning');
let draft = this.get_draft();
draft.text = edit.innerText;
draft.content_warning = content_warning?.value;
setTimeout(() => this.notify(draft), 0);
let content_warning_preview = this.renderRoot.getElementById('content_warning_preview');
if (content_warning && content_warning_preview) {
content_warning_preview.innerText = content_warning.value;
}
}
notify(draft) {
this.dispatchEvent(
new CustomEvent('tf-draft', {
bubbles: true,
composed: true,
detail: {
id: this.branch,
draft: draft,
},
})
);
this.dispatchEvent(new CustomEvent('tf-draft', {
bubbles: true,
composed: true,
detail: {
id: this.branch,
draft: draft
},
}));
}
change() {
let draft = this.get_draft();
draft.text = this.renderRoot.getElementById('edit')?.value;
draft.content_warning = this.renderRoot.getElementById('content_warning')?.value;
this.notify(draft);
}
convert_to_format(buffer, type, mime_type) {
return new Promise(function (resolve, reject) {
return new Promise(function(resolve, reject) {
let img = new Image();
img.onload = function () {
img.onload = function() {
let canvas = document.createElement('canvas');
let width_scale = Math.min(img.width, 1024) / img.width;
let height_scale = Math.min(img.height, 1024) / img.height;
@ -108,17 +109,13 @@ class TfComposeElement extends LitElement {
let context = canvas.getContext('2d');
context.drawImage(img, 0, 0, canvas.width, canvas.height);
let data_url = canvas.toDataURL(mime_type);
let result = atob(data_url.split(',')[1])
.split('')
.map((x) => x.charCodeAt(0));
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
resolve(result);
};
img.onerror = function (event) {
img.onerror = function(event) {
reject(new Error('Failed to load image.'));
};
let raw = Array.from(new Uint8Array(buffer))
.map((b) => String.fromCharCode(b))
.join('');
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
let original = `data:${type};base64,${btoa(raw)}`;
img.src = original;
});
@ -134,11 +131,7 @@ class TfComposeElement extends LitElement {
let best_buffer;
let best_type;
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
let test_buffer = await self.convert_to_format(
buffer,
file.type,
format
);
let test_buffer = await self.convert_to_format(buffer, file.type, format);
if (!best_buffer || test_buffer.length < best_buffer.length) {
best_buffer = test_buffer;
best_type = format;
@ -161,9 +154,10 @@ class TfComposeElement extends LitElement {
size: buffer.length ?? buffer.byteLength,
};
let edit = self.renderRoot.getElementById('edit');
edit.innerText += `\n![${name}](${id})`;
edit.value += `\n![${name}](${id})`;
self.change();
self.input();
} catch (e) {
} catch(e) {
alert(e?.message);
}
}
@ -180,22 +174,15 @@ class TfComposeElement extends LitElement {
break;
}
}
event.preventDefault();
document.execCommand(
'insertText',
false,
event.clipboardData.getData('text/plain')
);
}
async submit() {
submit() {
let self = this;
let draft = this.get_draft();
let edit = this.renderRoot.getElementById('edit');
let message = {
type: 'post',
text: edit.innerText,
text: edit.value,
};
if (this.root || this.branch) {
message.root = this.root;
@ -208,106 +195,43 @@ class TfComposeElement extends LitElement {
message.contentWarning = draft.content_warning;
}
console.log('Would post:', message);
if (draft.encrypt_to) {
let to = new Set(draft.encrypt_to);
to.add(this.whoami);
to = [...to];
message.recps = to;
console.log('message is now', message);
message = await tfrpc.rpc.encrypt(
this.whoami,
to,
JSON.stringify(message)
);
console.log('encrypted as', message);
}
try {
await tfrpc.rpc.appendMessage(this.whoami, message);
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
edit.value = '';
self.change();
self.notify(undefined);
} catch (error) {
self.requestUpdate();
}).catch(function(error) {
alert(error.message);
}
});
}
discard() {
let edit = this.renderRoot.getElementById('edit');
edit.value = '';
this.change();
let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = '';
this.notify(undefined);
}
attach() {
let self = this;
let edit = this.renderRoot.getElementById('edit');
let input = document.createElement('input');
input.type = 'file';
input.onchange = function (event) {
input.onchange = function(event) {
let file = event.target.files[0];
self.add_file(file);
};
input.click();
}
async autocomplete(text, callback) {
this.last_autocomplete = text;
let results = [];
try {
let rows = await tfrpc.rpc.query(
`
SELECT json(messages.content) AS content FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
WHERE json(messages.content) LIKE ?
ORDER BY timestamp DESC LIMIT 10
`,
['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]
);
for (let row of rows) {
for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
results.push({key: match[1], value: match[2]});
}
}
}
} finally {
if (this.last_autocomplete === text) {
callback(results);
}
}
}
firstUpdated() {
let values = Object.entries(this.users).map((x) => ({
key: x[1].name ?? x[0],
value: x[0],
}));
if (this.author) {
values = [].concat(
[
{
key: this.users[this.author]?.name,
value: this.author,
},
],
values
);
}
let tribute = new Tribute({
iframe: this.shadowRoot,
collection: [
{
values: values,
selectTemplate: function (item) {
return item
? `[@${item.original.key}](${item.original.value})`
: undefined;
},
},
{
trigger: '&',
values: this.autocomplete,
selectTemplate: function (item) {
return item
? `![${item.original.key}](${item.original.value})`
: undefined;
},
},
],
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
selectTemplate: function(item) {
return `[@${item.original.key}](${item.original.value})`;
},
});
tribute.attach(this.renderRoot.getElementById('edit'));
}
@ -315,59 +239,36 @@ class TfComposeElement extends LitElement {
updated() {
super.updated();
let edit = this.renderRoot.getElementById('edit');
if (this.last_updated_text !== edit.innerText) {
if (this.last_updated_text !== edit.value) {
let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = this.process_text(edit.innerText);
this.last_updated_text = edit.innerText;
}
let encrypt = this.renderRoot.getElementById('encrypt_to');
if (encrypt) {
let tribute = new Tribute({
iframe: this.shadowRoot,
values: Object.entries(this.users).map((x) => ({
key: x[1].name,
value: x[0],
})),
selectTemplate: function (item) {
return item.original.value;
},
});
tribute.attach(encrypt);
preview.innerHTML = this.process_text(edit.value);
this.last_updated_text = edit.value;
}
}
remove_mention(id) {
let draft = this.get_draft();
delete draft.mentions[id];
setTimeout(() => this.notify(), 0);
this.notify(draft);
this.requestUpdate();
}
render_mention(mention) {
let self = this;
return html` <div style="display: flex; flex-direction: row">
<div style="align-self: center; margin: 0.5em">
<button
class="w3-button w3-theme-d1"
title="Remove ${mention.name} mention"
@click=${() => self.remove_mention(mention.link)}
>
🚮
</button>
</div>
<div style="display: flex; flex-direction: column">
<h3>${mention.name}</h3>
<div style="padding-left: 1em">
${Object.entries(mention)
.filter((x) => x[0] != 'name')
.map(
(x) =>
html`<div>
<span style="font-weight: bold">${x[0]}</span>: ${x[1]}
</div>`
)}
return html`
<div style="display: flex; flex-direction: row">
<div style="align-self: center; margin: 0.5em">
<input type="button" value="🚮" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}></input>
</div>
</div>
</div>`;
<div style="display: flex; flex-direction: column">
<h3>${mention.name}</h3>
<div style="padding-left: 1em">
${Object.entries(mention)
.filter(x => x[0] != 'name')
.map(x => html`<div><span style="font-weight: bold">${x[0]}</span>: ${x[1]}</div>`)}
</div>
</div>
</div>`;
}
render_attach_app() {
@ -400,23 +301,14 @@ class TfComposeElement extends LitElement {
if (this.apps) {
return html`
<div class="w3-card-4 w3-margin w3-padding">
<select id="select" class="w3-select w3-theme-d1">
${Object.keys(self.apps).map(
(app) => html`<option value=${app}>${app}</option>`
)}
<div>
<select id="select">
${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
</select>
<button class="w3-button w3-theme-d1" @click=${attach_selected_app}>
Attach
</button>
<button
class="w3-button w3-theme-d1"
@click=${() => (this.apps = null)}
>
Cancel
</button>
<input type="button" value="Attach" @click=${attach_selected_app}></input>
<input type="button" value="Cancel" @click=${() => this.apps = null}></input>
</div>
`;
`;
}
}
@ -426,16 +318,9 @@ class TfComposeElement extends LitElement {
self.apps = await tfrpc.rpc.apps();
}
if (!this.apps) {
return html`<button class="w3-button w3-theme-d1" @click=${attach_app}>
Attach App
</button>`;
return html`<input type="button" value="Attach App" @click=${attach_app}></input>`;
} else {
return html`<button
class="w3-button w3-theme-d1"
@click=${() => (this.apps = null)}
>
Discard App
</button>`;
return html`<input type="button" value="Discard App" @click=${() => this.apps = null}></input>`;
}
}
@ -451,17 +336,15 @@ class TfComposeElement extends LitElement {
let draft = this.get_draft();
if (draft.content_warning !== undefined) {
return html`
<div class="w3-container w3-padding">
<p>
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input>
<label for="cw">CW</label>
</p>
<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${self.input} value=${draft.content_warning}></input>
<div>
<input type="checkbox" id="cw" @change=${() => self.set_content_warning(undefined)} checked></input>
<label for="cw">CW</label>
<input type="text" id="content_warning" @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
</div>
`;
} else {
return html`
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input>
<input type="checkbox" id="cw" @change=${() => self.set_content_warning('')}></input>
<label for="cw">CW</label>
`;
}
@ -471,107 +354,31 @@ class TfComposeElement extends LitElement {
return this.drafts[this.branch || ''] || {};
}
update_encrypt(event) {
let input = event.srcElement;
let matches = input.value.match(/@.*?\.ed25519/g);
if (matches) {
let draft = this.get_draft();
let to = [...new Set(matches.concat(draft.encrypt_to))];
this.set_encrypt(to);
input.value = '';
}
}
render_encrypt() {
let draft = this.get_draft();
if (draft.encrypt_to === undefined) {
return;
}
return html`
<div style="display: flex; flex-direction: row; width: 100%">
<label for="encrypt_to">🔐 To:</label>
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
<button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button>
</div>
<ul>
${draft.encrypt_to.map(
(x) => html`
<li>
<tf-user id=${x} .users=${this.users}></tf-user>
<input type="button" class="w3-button w3-theme-d1" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input>
</li>`
)}
</ul>
`;
}
set_encrypt(encrypt) {
let draft = this.get_draft();
draft.encrypt_to = encrypt;
this.notify(draft);
this.requestUpdate();
}
render() {
let self = this;
let draft = self.get_draft();
let content_warning =
draft.content_warning !== undefined
? html`<div class="w3-panel w3-round-xlarge w3-theme-d2">
<p id="content_warning_preview">${draft.content_warning}</p>
</div>`
: undefined;
let encrypt =
draft.encrypt_to !== undefined
? undefined
: html`<button
class="w3-button w3-theme-d1"
@click=${() => this.set_encrypt([])}
>
🔐
</button>`;
draft.content_warning !== undefined ?
html`<div id="content_warning_preview" class="content_warning">${draft.content_warning}</div>` :
undefined;
let result = html`
<div
class="w3-card-4 w3-theme-d4 w3-padding-small"
style="box-sizing: border-box"
>
${this.render_encrypt()}
<div class="w3-container w3-padding-small">
<div class="w3-half">
<span
class="w3-input w3-theme-d1 w3-border"
style="resize: vertical; width: 100%; overflow: hidden; white-space: pre-wrap"
placeholder="Write a post here."
id="edit"
@input=${this.input}
@paste=${this.paste}
contenteditable="plaintext-only"
.innerText=${live(draft.text ?? '')}
></span>
</div>
<div class="w3-half w3-padding">
${content_warning}
<div id="preview"></div>
</div>
<div style="display: flex; flex-direction: row; width: 100%">
<textarea id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste} style="flex: 1 0 50%">${draft.text}</textarea>
<div style="flex: 1 0 50%">
${content_warning}
<div id="preview"></div>
</div>
${Object.values(draft.mentions || {}).map((x) =>
self.render_mention(x)
)}
${this.render_attach_app()} ${this.render_content_warning()}
<button class="w3-button w3-theme-d1" id="submit" @click=${this.submit}>
Submit
</button>
<button class="w3-button w3-theme-d1" @click=${this.attach}>
Attach
</button>
${this.render_attach_app_button()} ${encrypt}
<button class="w3-button w3-theme-d1" @click=${this.discard}>
Discard
</button>
</div>
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
${this.render_content_warning()}
${this.render_attach_app()}
<input type="button" value="Submit" @click=${this.submit}></input>
<input type="button" value="Attach" @click=${this.attach}></input>
${this.render_attach_app_button()}
<input type="button" value="Discard" @click=${this.discard}></input>
`;
return result;
}
}
customElements.define('tf-compose', TfComposeElement);
customElements.define('tf-compose', TfComposeElement);

37
apps/ssb/tf-id-picker.js Normal file
View File

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

View File

@ -1,4 +1,4 @@
import {LitElement, html, render, unsafeHTML} from './lit-all.min.js';
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import * as tfutils from './tf-utils.js';
import * as emojis from './emojis.js';
@ -11,9 +11,10 @@ class TfMessageElement extends LitElement {
message: {type: Object},
users: {type: Object},
drafts: {type: Object},
format: {type: String},
raw: {type: Boolean},
blog_data: {type: String},
expanded: {type: Object},
decrypted: {type: Object},
};
}
@ -26,38 +27,18 @@ class TfMessageElement extends LitElement {
this.message = {};
this.users = {};
this.drafts = {};
this.format = 'message';
this.raw = false;
this.expanded = {};
this.decrypted = undefined;
}
show_reply() {
let event = new CustomEvent('tf-draft', {
bubbles: true,
composed: true,
detail: {
id: this.message?.id,
draft: {
encrypt_to: this.message?.decrypted?.recps,
},
},
});
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: ''}});
this.dispatchEvent(event);
}
discard_reply() {
this.dispatchEvent(
new CustomEvent('tf-draft', {
bubbles: true,
composed: true,
detail: {id: this.id, draft: undefined},
})
);
}
show_reactions() {
let modal = document.getElementById('reactions_modal');
modal.users = this.users;
modal.votes = this.message?.votes || [];
this.dispatchEvent(new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.id, draft: undefined}}));
}
render_votes() {
@ -72,26 +53,12 @@ class TfMessageElement extends LitElement {
return expression;
}
}
if (this.message?.votes?.length) {
return html` <div class="w3-container">
<div
class="w3-button w3-bar w3-padding-small"
@click=${this.show_reactions}
>
${(this.message.votes || []).map(
(vote) => html`
<span
class="w3-bar-item w3-padding-small"
title="${this.users[vote.author]?.name ??
vote.author} ${new Date(vote.timestamp)}"
>
${normalize_expression(vote.content.vote.expression)}
</span>
`
)}
</div>
</div>`;
}
return html`<div>${(this.message.votes || []).map(
vote => html`
<span title="${this.users[vote.author]?.name ?? vote.author} ${new Date(vote.timestamp)}">
${normalize_expression(vote.content.vote.expression)}
</span>
`)}</div>`;
}
render_raw() {
@ -105,40 +72,30 @@ class TfMessageElement extends LitElement {
content: this.message?.content,
signature: this.message?.signature,
};
return html`<div style="white-space: pre-wrap">
${JSON.stringify(raw, null, 2)}
</div>`;
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`;
}
vote(emoji) {
let reaction = emoji;
let message = this.message.id;
if (
confirm(
'Are you sure you want to react with ' +
reaction +
' to ' +
message +
'?'
)
) {
tfrpc.rpc
.appendMessage(this.whoami, {
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
tfrpc.rpc.appendMessage(
this.whoami,
{
type: 'vote',
vote: {
link: message,
value: 1,
expression: reaction,
},
})
.catch(function (error) {
}).catch(function(error) {
alert(error?.message);
});
}
}
react(event) {
emojis.picker((x) => this.vote(x), null, this.whoami);
emojis.picker(x => this.vote(x));
}
show_image(link) {
@ -172,12 +129,9 @@ class TfMessageElement extends LitElement {
body_click(event) {
if (event.srcElement.tagName == 'IMG') {
this.show_image(event.srcElement.src);
} else if (
event.srcElement.tagName == 'DIV' &&
event.srcElement.classList.contains('img_caption')
) {
} else if (event.srcElement.tagName == 'DIV' && event.srcElement.classList.contains('img_caption')) {
let next = event.srcElement.nextSibling;
if (next.style.display != 'none') {
if (next.style.display == 'block') {
next.style.display = 'none';
} else {
next.style.display = 'block';
@ -186,75 +140,50 @@ class TfMessageElement extends LitElement {
}
render_mention(mention) {
if (!mention?.link || typeof mention.link != 'string') {
if (!mention?.link || typeof(mention.link) != 'string') {
return html` <pre>${JSON.stringify(mention)}</pre>`;
} else if (
mention?.link?.startsWith('&') &&
mention?.type?.startsWith('image/')
) {
} else if (mention?.link?.startsWith('&') &&
mention?.type?.startsWith('image/')) {
return html`
<img
src=${'/' + mention.link + '/view'}
style="max-width: 128px; max-height: 128px"
title=${mention.name}
@click=${() => this.show_image('/' + mention.link + '/view')}
/>
<img src=${'/' + mention.link + '/view'} style="max-width: 128px; max-height: 128px" title=${mention.name} @click=${() => this.show_image('/' + mention.link + '/view')}>
`;
} else if (
mention.link?.startsWith('&') &&
mention.name?.startsWith('audio:')
) {
} else if (mention.link?.startsWith('&') &&
mention.name?.startsWith('audio:')) {
return html`
<audio controls style="height: 32px">
<source src=${'/' + mention.link + '/view'}></source>
</audio>
`;
} else if (
mention.link?.startsWith('&') &&
mention.name?.startsWith('video:')
) {
} else if (mention.link?.startsWith('&') &&
mention.name?.startsWith('video:')) {
return html`
<video controls style="max-height: 240px; max-width: 128px">
<source src=${'/' + mention.link + '/view'}></source>
</video>
`;
} else if (
mention.link?.startsWith('&') &&
mention?.type === 'application/tildefriends'
) {
} else if (mention.link?.startsWith('&') &&
mention?.type === 'application/tildefriends') {
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
return html` <a href=${'#' + encodeURIComponent(mention.link)}
>${mention.name}</a
>`;
return html` <a href=${'#' + encodeURIComponent(mention.link)}>${mention.name}</a>`;
} else if (mention.link?.startsWith('#')) {
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}
>${mention.link}</a
>`;
} else if (
Object.keys(mention).length == 2 &&
mention.link &&
mention.name
) {
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}>${mention.link}</a>`;
} else if (Object.keys(mention).length == 2 && mention.link && mention.name) {
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
} else {
return html` <pre style="white-space: pre-wrap">
${JSON.stringify(mention, null, 2)}</pre
>`;
return html` <pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>`;
}
}
render_mentions() {
let mentions = this.message?.content?.mentions || [];
mentions = mentions.filter(
(x) => this.message?.content?.text?.indexOf(x.link) === -1
);
mentions = mentions.filter(x => this.message?.content?.text?.indexOf(x.link) === -1);
if (mentions.length) {
let self = this;
return html`
<fieldset style="padding: 0.5em; border: 1px solid black">
<fieldset style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black">
<legend>Mentions</legend>
${mentions.map((x) => self.render_mention(x))}
${mentions.map(x => self.render_mention(x))}
</fieldset>
`;
}
@ -265,63 +194,36 @@ ${JSON.stringify(mention, null, 2)}</pre
return 0;
}
let total = message.child_messages.length;
for (let m of message.child_messages) {
for (let m of message.child_messages)
{
total += this.total_child_messages(m);
}
return total;
}
set_expanded(expanded, tag) {
this.dispatchEvent(
new CustomEvent('tf-expand', {
bubbles: true,
composed: true,
detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded},
})
);
this.dispatchEvent(new CustomEvent('tf-expand', {bubbles: true, composed: true, detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded}}));
}
toggle_expanded(tag) {
this.set_expanded(
!this.expanded[(this.message.id || '') + (tag || '')],
tag
);
this.set_expanded(!this.expanded[(this.message.id || '') + (tag || '')], tag);
}
render_children() {
let self = this;
if (this.message.child_messages?.length) {
if (!this.expanded[this.message.id]) {
return html`<button
class="w3-button w3-theme-d1"
@click=${() => self.set_expanded(true)}
>
+ ${this.total_child_messages(this.message) + ' More'}
</button>`;
return html`<input type="button" value=${this.total_child_messages(this.message) + ' More'} @click=${() => self.set_expanded(true)}></input>`;
} else {
return html`<button
class="w3-button w3-theme-d1"
@click=${() => self.set_expanded(false)}
>
Collapse</button
>${(this.message.child_messages || []).map(
(x) =>
html`<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
></tf-message>`
)}`;
return html`<input type="button" value="Collapse" @click=${() => self.set_expanded(false)}></input>${(this.message.child_messages || []).map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`)}`;
}
}
}
render_channels() {
let content = this.message?.content;
if (this?.messsage?.decrypted?.type == 'post') {
content = this.message.decrypted;
if (this.decrypted?.type == 'post') {
content = this.decrypted;
}
let channels = [];
if (typeof content.channel === 'string') {
@ -329,142 +231,61 @@ ${JSON.stringify(mention, null, 2)}</pre
}
if (Array.isArray(content.mentions)) {
for (let mention of content.mentions) {
if (typeof mention?.link === 'string' && mention.link.startsWith('#')) {
if (typeof mention?.link === 'string' &&
mention.link.startsWith('#')) {
channels.push(mention.link);
}
}
}
return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
return channels.map(x => html`<tf-tag tag=${x}></tf-tag>`);
}
async try_decrypt(content) {
let result = await tfrpc.rpc.try_decrypt(this.whoami, content);
if (result) {
this.decrypted = JSON.parse(result);
} else {
this.decrypted = false;
}
}
render() {
let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
if (this.decrypted?.type == 'post') {
content = this.decrypted;
}
let class_background = this.message?.decrypted
? 'w3-pale-red'
: 'w3-theme-d4';
let self = this;
let raw_button;
switch (this.format) {
case 'raw':
if (content?.type == 'post' || content?.type == 'blog') {
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'md')}
>
Markdown
</button>`;
} else {
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'message')}
>
Message
</button>`;
}
break;
case 'md':
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'message')}
>
Message
</button>`;
break;
case 'decrypted':
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'raw')}
>
Raw
</button>`;
break;
default:
if (this.message.decrypted) {
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'decrypted')}
>
Decrypted
</button>`;
} else {
raw_button = html`<button
class="w3-button w3-theme-d1"
@click=${() => (self.format = 'raw')}
>
Raw
</button>`;
}
break;
}
let raw_button = this.raw ?
html`<input type="button" value="Message" @click=${() => self.raw = false}></input>` :
html`<input type="button" value="Raw" @click=${() => self.raw = true}></input>`;
function small_frame(inner) {
let body;
return html`
<div
class="w3-card-4 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
>
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere">
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
<span style="padding-right: 8px"
><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(
self.message.timestamp
).toLocaleString()}</span
>
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
${raw_button}
${self.raw ? self.render_raw() : inner}
${self.render_votes()}
${(self.message.child_messages || []).map(
(x) => html`
<tf-message
.message=${x}
whoami=${self.whoami}
.users=${self.users}
.drafts=${self.drafts}
.expanded=${self.expanded}
></tf-message>
`
)}
</div>
`;
}
if (this.message?.type === 'contact_group') {
return html` <div
class="w3-card-4 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
>
${this.message.messages.map(
(x) =>
html`<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
></tf-message>`
)}
</div>`;
return html`
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
${this.message.messages.map(x =>
html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`
)}
</div>`;
} else if (this.message.placeholder) {
return html` <div
class="w3-card-4 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
>
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
(placeholder)
<div>${this.render_votes()}</div>
${(this.message.child_messages || []).map(
(x) => html`
<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
></tf-message>
`
)}
</div>`;
} else if (typeof (content?.type === 'string')) {
return html`
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere">
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
<div>${this.render_votes()}</div>
${(this.message.child_messages || []).map(x => html`
<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>
`)}
</div>`;
} else if (typeof(content?.type === 'string')) {
if (content.type == 'about') {
let name;
let image;
@ -474,7 +295,7 @@ ${JSON.stringify(mention, null, 2)}</pre
}
if (content.image !== undefined) {
image = html`
<div><img src=${'/' + (typeof content.image?.link == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
<div><img src=${'/' + (typeof(content.image?.link) == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
`;
}
if (content.description !== undefined) {
@ -484,98 +305,66 @@ ${JSON.stringify(mention, null, 2)}</pre
</div>
`;
}
let update =
content.about == this.message.author
? html`<div style="font-weight: bold">Updated profile.</div>`
: html`<div style="font-weight: bold">
Updated profile for
<tf-user id=${content.about} .users=${this.users}></tf-user>.
</div>`;
return small_frame(html` ${update} ${name} ${image} ${description} `);
let update = content.about == this.message.author ?
html`<div style="font-weight: bold">Updated profile.</div>` :
html`<div style="font-weight: bold">Updated profile for <tf-user id=${content.about} .users=${this.users}></tf-user>.</div>`;
return small_frame(html`
${update}
${name}
${image}
${description}
`);
} else if (content.type == 'contact') {
return html`
<div>
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
is
${content.blocking === true
? 'blocking'
: content.blocking === false
? 'no longer blocking'
: content.following === true
? 'following'
: content.following === false
? 'no longer following'
: '?'}
<tf-user
id=${this.message.content.contact}
.users=${this.users}
></tf-user>
${
content.blocking === true ? 'blocking' :
content.blocking === false ? 'no longer blocking' :
content.following === true ? 'following' :
content.following === false ? 'no longer following' :
'?'
}
<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user>
</div>
`;
} else if (content.type == 'post') {
let reply =
this.drafts[this.message?.id] !== undefined
? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
author=${this.message.author}
></tf-compose>
`
: html`
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
Reply
</button>
`;
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${this.message.content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}></tf-compose>
` : html`
<input type="button" value="Reply" @click=${this.show_reply}></input>
`;
let self = this;
let body;
switch (this.format) {
case 'raw':
body = this.render_raw();
break;
case 'md':
body = html`<code
style="white-space: pre-wrap; overflow-wrap: anywhere"
>${content.text}</code
>`;
break;
case 'message':
body = unsafeHTML(tfutils.markdown(content.text));
break;
case 'decrypted':
body = html`<pre
style="white-space: pre-wrap; overflow-wrap: anywhere"
>
${JSON.stringify(content, null, 2)}</pre
>`;
break;
}
let body = this.raw ?
this.render_raw() :
unsafeHTML(tfutils.markdown(content.text));
let content_warning = html`
<div
class="w3-panel w3-round-xlarge w3-theme-l4"
style="cursor: pointer"
@click=${(x) => this.toggle_expanded(':cw')}
>
<p>${content.contentWarning}</p>
</div>
`;
let content_html = html`
${this.render_channels()}
<div @click=${this.body_click}>${body}</div>
${this.render_mentions()}
`;
let payload = content.contentWarning
? self.expanded[(this.message.id || '') + ':cw']
? html` ${content_warning} ${content_html} `
: content_warning
: content_html;
let is_encrypted = this.message?.decrypted
? html`<span style="align-self: center">🔓</span>`
: undefined;
<div class="content_warning" @click=${x => this.toggle_expanded(':cw')}>${content.contentWarning}</div>
`;
let content_html =
html`
${this.render_channels()}
<div @click=${this.body_click}>${body}</div>
${this.render_mentions()}
`;
let payload =
content.contentWarning ?
self.expanded[(this.message.id || '') + ':cw'] ?
html`
${content_warning}
${content_html}
` :
content_warning :
content_html;
let is_encrypted = this.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
let style_background = this.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
return html`
<style>
code {
@ -591,97 +380,38 @@ ${JSON.stringify(content, null, 2)}</pre
display: block;
}
</style>
<div
class="w3-card-4 ${class_background} w3-border-theme"
style="margin-top: 8px; padding: 16px"
>
<div style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
<span style="flex: 1"></span>
<span style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
<span>${raw_button}</span>
</div>
${payload} ${this.render_votes()}
<p>
${payload}
${this.render_votes()}
<div>
${reply}
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
</p>
${this.render_children()}
</div>
`;
} else if (content.type === 'issue') {
let is_encrypted = this.message?.decrypted
? html`<span style="align-self: center">🔓</span>`
: undefined;
return html`
<style>
code {
white-space: pre-wrap;
overflow-wrap: break-word;
}
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4 ${class_background} w3-border-theme"
style="margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
<span style="flex: 1"></span>
<span style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
<input type="button" value="React" @click=${this.react}></input>
</div>
${content.text} ${this.render_votes()}
<p>
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
</p>
${this.render_children()}
</div>
`;
} else if (content.type === 'blog') {
let self = this;
tfrpc.rpc.get_blob(content.blog).then(function (data) {
tfrpc.rpc.get_blob(content.blog).then(function(data) {
self.blog_data = data;
});
let payload = this.expanded[(this.message.id || '') + ':blog']
? html`<div>
${this.blog_data
? unsafeHTML(tfutils.markdown(this.blog_data))
: 'Loading...'}
</div>`
: undefined;
let body;
switch (this.format) {
case 'raw':
body = this.render_raw();
break;
case 'md':
body = content.summary;
break;
case 'message':
body = html`
let payload =
this.expanded[(this.message.id || '') + ':blog'] ?
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
undefined;
let body = this.raw ?
this.render_raw() :
html`
<div
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
@click=${(x) => self.toggle_expanded(':blog')}>
@click=${x => self.toggle_expanded(':blog')}>
<h2>${content.title}</h2>
<div style="display: flex; flex-direction: row">
<img src=/${content.thumbnail}/view></img>
@ -690,26 +420,6 @@ ${JSON.stringify(content, null, 2)}</pre
</div>
${payload}
`;
break;
}
let reply =
this.drafts[this.message?.id] !== undefined
? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
author=${this.message.author}
></tf-compose>
`
: html`
<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
Reply
</button>
`;
return html`
<style>
code {
@ -725,73 +435,46 @@ ${JSON.stringify(content, null, 2)}</pre
display: block;
}
</style>
<div
class="w3-card-4 w3-theme-d4 w3-border-theme"
style="margin-top: 8px; padding: 16px"
>
<div style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
<span style="flex: 1"></span>
<span style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
<span>${raw_button}</span>
</div>
<div>${body}</div>
${this.render_mentions()}
<div>
${reply}
<button class="w3-button w3-theme-d1" @click=${this.react}>
React
</button>
</div>
${this.render_votes()} ${this.render_children()}
${this.render_votes()}
</div>
`;
} else if (content.type === 'pub') {
return small_frame(
html` <style>
span {
overflow-wrap: anywhere;
}
</style>
<span>
<div>
🍻
<tf-user
.users=${this.users}
id=${content.address.key}
></tf-user>
</div>
<pre>${content.address.host}:${content.address.port}</pre>
</span>`
);
return small_frame(html`
<style>
span {
overflow-wrap: anywhere;
}
</style>
<span>
<div>
🍻 <tf-user .users=${this.users} id=${content.address.key}></tf-user>
</div>
<pre>${content.address.host}:${content.address.port}</pre>
</span>`);
} else if (content.type === 'channel') {
return small_frame(html`
<div>
${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
<a href=${'#q=' + encodeURIComponent('#' + content.channel)}
>#${content.channel}</a
>
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} <a href=${'#q=' + encodeURIComponent('#' + content.channel)}>#${content.channel}</a>
</div>
`);
} else if (typeof this.message.content == 'string') {
if (this.message?.decrypted) {
if (this.format == 'decrypted') {
return small_frame(
html`<span>🔓</span>
<pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`
);
} else {
return small_frame(
html`<span>🔓</span>
<div>${this.message.decrypted.type}</div>`
);
}
} else if (typeof(this.message.content) == 'string') {
if (this.decrypted) {
return small_frame(html`<span>🔓</span><pre>${JSON.stringify(this.decrypted, null, 2)}</pre>`);
} else if (this.decrypted === undefined) {
this.try_decrypt(content);
return small_frame(html`<span>🔐</span>`);
} else {
return small_frame(html`<span>🔒</span>`);
return small_frame(html`<span>🔒</span>`);
}
} else {
return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
@ -802,4 +485,4 @@ ${JSON.stringify(content, null, 2)}</pre
}
}
customElements.define('tf-message', TfMessageElement);
customElements.define('tf-message', TfMessageElement);

View File

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

View File

@ -11,9 +11,6 @@ class TfProfileElement extends LitElement {
id: {type: String},
users: {type: Object},
size: {type: Number},
server_follows_me: {type: Boolean},
following: {type: Boolean},
blocking: {type: Boolean},
};
}
@ -27,75 +24,14 @@ class TfProfileElement extends LitElement {
this.id = null;
this.users = {};
this.size = 0;
this.server_follows_me = undefined;
}
async load() {
if (this.whoami !== this._follow_whoami) {
this._follow_whoami = this.whoami;
this.following = undefined;
this.blocking = undefined;
let result = await tfrpc.rpc.query(
`
SELECT json_extract(content, '$.following') AS following
FROM messages WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? AND
following IS NOT NULL
ORDER BY sequence DESC LIMIT 1
`,
[this.whoami, this.id]
);
this.following = result?.[0]?.following ?? false;
result = await tfrpc.rpc.query(
`
SELECT json_extract(content, '$.blocking') AS blocking
FROM messages WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? AND
blocking IS NOT NULL
ORDER BY sequence DESC LIMIT 1
`,
[this.whoami, this.id]
);
this.blocking = result?.[0]?.blocking ?? false;
}
}
async initial_load() {
this.server_follows_me = undefined;
let server_id = await tfrpc.rpc.getServerIdentity();
let followed = await tfrpc.rpc.query(
`
SELECT json_extract(content, '$.following') AS following
FROM messages
WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
`,
[server_id, this.whoami]
);
let is_followed = false;
for (let row of followed) {
is_followed = row.following != 0;
}
this.server_follows_me = is_followed;
}
modify(change) {
tfrpc.rpc
.appendMessage(
this.whoami,
Object.assign(
{
type: 'contact',
contact: this.id,
},
change
)
)
.catch(function (error) {
tfrpc.rpc.appendMessage(this.whoami,
Object.assign({
type: 'contact',
contact: this.id,
}, change)).catch(function(error) {
alert(error?.message);
});
}
@ -122,7 +58,6 @@ class TfProfileElement extends LitElement {
name: original.name,
description: original.description,
image: original.image,
publicWebHosting: original.publicWebHosting,
};
console.log(this.editing);
}
@ -138,14 +73,11 @@ class TfProfileElement extends LitElement {
message[key] = this.editing[key];
}
}
tfrpc.rpc
.appendMessage(this.whoami, message)
.then(function () {
self.editing = null;
})
.catch(function (error) {
alert(error?.message);
});
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
self.editing = null;
}).catch(function(error) {
alert(error?.message);
});
}
discard_edits() {
@ -156,59 +88,27 @@ class TfProfileElement extends LitElement {
let self = this;
let input = document.createElement('input');
input.type = 'file';
input.onchange = function (event) {
input.onchange = function(event) {
let file = event.target.files[0];
file
.arrayBuffer()
.then(function (buffer) {
let bin = Array.from(new Uint8Array(buffer));
return tfrpc.rpc.store_blob(bin);
})
.then(function (id) {
self.editing = Object.assign({}, self.editing, {image: id});
console.log(self.editing);
})
.catch(function (e) {
alert(e.message);
});
file.arrayBuffer().then(function(buffer) {
let bin = Array.from(new Uint8Array(buffer));
return tfrpc.rpc.store_blob(bin);
}).then(function(id) {
self.editing = Object.assign({}, self.editing, {image: id});
console.log(self.editing);
}).catch(function(e) {
alert(e.message);
});
};
input.click();
}
async server_follow_me(follow) {
try {
await tfrpc.rpc.setServerFollowingMe(this.whoami, follow);
} catch (e) {
console.log(e);
}
try {
await this.initial_load();
} catch (e) {
console.log(e);
}
}
copy_id() {
navigator.clipboard.writeText(this.id);
}
render() {
if (
this.id == this.whoami &&
this.editing &&
this.server_follows_me === undefined
) {
this.initial_load();
}
this.load();
let self = this;
let profile = this.users[this.id] || {};
tfrpc.rpc
.query(
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
[this.id]
)
.then(function (result) {
tfrpc.rpc.query(
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
[this.id]).then(function(result) {
self.size = result[0].size;
});
let edit;
@ -216,84 +116,50 @@ class TfProfileElement extends LitElement {
let block;
if (this.id === this.whoami) {
if (this.editing) {
let server_follow;
if (this.server_follows_me === true) {
server_follow = html`<button
class="w3-button w3-theme-d1"
@click=${() => this.server_follow_me(false)}
>
Server, Stop Following Me
</button>`;
} else if (this.server_follows_me === false) {
server_follow = html`<button
class="w3-button w3-theme-d1"
@click=${() => this.server_follow_me(true)}
>
Server, Follow Me
</button>`;
}
edit = html`
<button class="w3-button w3-theme-d1" @click=${this.save_edits}>
Save Profile
</button>
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
Discard
</button>
${server_follow}
<input type="button" value="Save Profile" @click=${this.save_edits}></input>
<input type="button" value="Discard" @click=${this.discard_edits}></input>
`;
} else {
edit = html`<button class="w3-button w3-theme-d1" @click=${this.edit}>
Edit Profile
</button>`;
edit = html`<input type="button" value="Edit Profile" @click=${this.edit}></input>`;
}
}
if (this.id !== this.whoami && this.following !== undefined) {
follow = this.following
? html`<button class="w3-button w3-theme-d1" @click=${this.unfollow}>
Unfollow
</button>`
: html`<button class="w3-button w3-theme-d1" @click=${this.follow}>
Follow
</button>`;
if (this.id !== this.whoami &&
this.users[this.whoami]?.following) {
follow =
this.users[this.whoami].following[this.id] ?
html`<input type="button" value="Unfollow" @click=${this.unfollow}></input>` :
html`<input type="button" value="Follow" @click=${this.follow}></input>`;
}
if (this.id !== this.whoami && this.blocking !== undefined) {
block = this.blocking
? html`<button class="w3-button w3-theme-d1" @click=${this.unblock}>
Unblock
</button>`
: html`<button class="w3-button w3-theme-d1" @click=${this.block}>
Block
</button>`;
if (this.id !== this.whoami &&
this.users[this.whoami]?.blocking) {
block =
this.users[this.whoami].blocking[this.id] ?
html`<input type="button" value="Unblock" @click=${this.unblock}></input>` :
html`<input type="button" value="Block" @click=${this.block}></input>`;
}
let edit_profile = this.editing
? html`
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
<div class="w3-container">
<div>
<label for="name">Name:</label>
<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
</div>
<div><label for="description">Description:</label></div>
<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
<div>
<label for="public_web_hosting">Public Web Hosting:</label>
<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
</div>
<div>
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
</div>
let edit_profile = this.editing ? html`
<div style="flex: 1 0 50%">
<div>
<label for="name">Name:</label>
<input type="text" id="name" value=${this.editing.name} @input=${event => this.editing = Object.assign({}, this.editing, {name: event.srcElement.value})}></input>
</div>
</div>`
: null;
let image =
typeof profile.image == 'string' ? profile.image : profile.image?.link;
<div>
<div><label for="description">Description:</label></div>
<textarea id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
</div>
<div>
<label for="public_web_hosting">Public Web Hosting:</label>
<input type="checkbox" id="public_web_hosting" value=${this.editing.public_web_hosting} @input=${event => this.editing = Object.assign({}, this.editing, {publicWebHosting: event.srcElement.checked})}></input>
</div>
<input type="button" value="Attach Image" @click=${this.attach_image}></input>
</div>` : null;
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
image = this.editing?.image ?? image;
let description = this.editing?.description ?? profile.description;
return html`<div class="w3-container" style="box-sizing: border-box; border: 2px solid black; background-color: rgba(255, 255, 255, 0.2)">
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
<input type="text" class="w3-input w3-border w3-theme-d1" readonly value=${this.id}></input>
<button class="w3-button w3-theme-d1 w3-ripple" @click=${this.copy_id}>Copy</button>
<div style="display: flex; flex-direction: row; gap: 1em">
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
<div style="display: flex; flex-direction: row">
${edit_profile}
<div style="flex: 1 0 50%">
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
@ -301,18 +167,18 @@ class TfProfileElement extends LitElement {
</div>
</div>
<div>
Following ${profile.following} identities.
Followed by ${profile.followed} identities.
Blocking ${profile.blocking} identities.
Blocked by ${profile.blocked} identities.
Following ${Object.keys(profile.following || {}).length} identities.
Followed by ${Object.values(self.users).filter(x => (x.following || {})[self.id]).length} identities.
Blocking ${Object.keys(profile.blocking || {}).length} identities.
Blocked by ${Object.values(self.users).filter(x => (x.blocking || {})[self.id]).length} identities.
</div>
<p>
<div>
${edit}
${follow}
${block}
</p>
</div>
</div>`;
}
}
customElements.define('tf-profile', TfProfileElement);
customElements.define('tf-profile', TfProfileElement);

View File

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

View File

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

View File

@ -1,58 +1,38 @@
import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
class TfTabConnectionsElement extends LitElement {
static get properties() {
return {
broadcasts: {type: Array},
identities: {type: Array},
my_identities: {type: Array},
connections: {type: Array},
stored_connections: {type: Array},
users: {type: Object},
server_identity: {type: String},
};
}
static styles = styles;
static k_broadcast_emojis = {
discovery: '🏓',
room: '🚪',
peer_exchange: '🕸',
};
constructor() {
super();
let self = this;
this.broadcasts = [];
this.identities = [];
this.my_identities = [];
this.connections = [];
this.stored_connections = [];
this.users = {};
tfrpc.rpc.getIdentities().then(function (identities) {
self.my_identities = identities || [];
});
tfrpc.rpc.getAllIdentities().then(function (identities) {
tfrpc.rpc.getAllIdentities().then(function(identities) {
self.identities = identities || [];
});
tfrpc.rpc.getStoredConnections().then(function (connections) {
tfrpc.rpc.getStoredConnections().then(function(connections) {
self.stored_connections = connections || [];
});
tfrpc.rpc.getServerIdentity().then(function (identity) {
self.server_identity = identity;
});
}
render_connection_summary(connection) {
if (connection.address && connection.port) {
return html`<div>
<small>${connection.address}:${connection.port}</small>
</div>`;
return html`(<small>${connection.address}:${connection.port}</small>)`;
} else if (connection.tunnel) {
return html`<div>room peer</div>`;
return html`(room peer)`;
} else {
return JSON.stringify(connection);
}
@ -60,12 +40,13 @@ class TfTabConnectionsElement extends LitElement {
render_room_peers(connection) {
let self = this;
let peers = this.broadcasts.filter((x) => x.tunnel?.id == connection);
let peers = this.broadcasts.filter(x => x.tunnel?.id == connection);
if (peers.length) {
let connections = this.connections.map((x) => x.id);
return html`${peers
.filter((x) => connections.indexOf(x.pubkey) == -1)
.map((x) => html`${self.render_room_peer(x)}`)}`;
return html`
<ul>
${peers.map(x => html`${self.render_room_peer(x)}`)}
</ul>
`;
}
}
@ -77,31 +58,18 @@ class TfTabConnectionsElement extends LitElement {
let self = this;
return html`
<li>
<button
class="w3-button w3-theme-d1"
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
>
Connect
</button>
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
<input type="button" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} value="Connect"></input>
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
</li>
`;
}
render_broadcast(connection) {
return html`
<li class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap">
<button
class="w3-bar-item w3-button w3-theme-d1"
@click=${() => tfrpc.rpc.connect(connection)}
>
Connect
</button>
<div class="w3-bar-item">
${TfTabConnectionsElement.k_broadcast_emojis[connection.origin]}
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
${this.render_connection_summary(connection)}
</div>
<li>
<input type="button" @click=${() => tfrpc.rpc.connect(connection)} value="Connect"></input>
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
${this.render_connection_summary(connection)}
</li>
`;
}
@ -111,140 +79,44 @@ class TfTabConnectionsElement extends LitElement {
this.stored_connections = (await tfrpc.rpc.getStoredConnections()) || [];
}
render_connection(connection) {
let requests = Object.values(
connection.requests.reduce(function (accumulator, value) {
let key = `${value.name}:${Math.sign(value.request_number)}`;
if (!accumulator[key]) {
accumulator[key] = Object.assign({count: 0}, value);
}
accumulator[key].count++;
return accumulator;
}, {})
);
return html`
<button
class="w3-button w3-theme-d1"
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
>
Close
</button>
${connection.flags.one_shot ? '🔃' : undefined}
<tf-user id=${connection.id} .users=${this.users}></tf-user>
${connection.tunnel !== undefined
? '🚇'
: html`(${connection.host}:${connection.port})`}
<div>
${requests.map(
(x) => html`
<span
class=${'w3-tag w3-small ' +
(x.active ? 'w3-theme-l3' : 'w3-theme-d3')}
>${x.request_number > 0 ? '🟩' : '🟥'} ${x.name}
<span
class="w3-badge w3-white"
style=${x.count > 1 ? undefined : 'display: none'}
>${x.count}</span
></span
>
`
)}
</div>
<ul>
${this.connections
.filter((x) => x.tunnel === this.connections.indexOf(connection))
.map((x) => html`<li>${this.render_connection(x)}</li>`)}
${this.render_room_peers(connection.id)}
</ul>
`;
}
refresh() {
tfrpc.rpc.sync();
}
render() {
let self = this;
return html`
<div class="w3-container" style="box-sizing: border-box">
<button
class="w3-button w3-theme-l3 w3-circle w3-ripple w3-large"
@click=${this.refresh}
>
🔃
</button>
<h2>New Connection</h2>
<textarea class="w3-input w3-theme-d1" id="code"></textarea>
<button
class="w3-button w3-theme-d1"
@click=${() =>
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
>
Connect
</button>
<h2>Broadcasts</h2>
<ul class="w3-ul w3-border">
${this.broadcasts
.filter((x) => x.address)
.map((x) => self.render_broadcast(x))}
</ul>
<h2>Connections</h2>
<ul class="w3-ul w3-border">
${this.connections
.filter((x) => x.tunnel === undefined)
.map(
(x) => html`
<li class="w3-bar">${this.render_connection(x)}</li>
`
)}
</ul>
<h2>Stored Connections</h2>
<ul class="w3-ul w3-border">
${this.stored_connections.map(
(x) => html`
<li class="w3-bar">
<button
class="w3-bar-item w3-button w3-theme-d1"
@click=${() => self.forget_stored_connection(x)}
>
Forget
</button>
<button
class="w3-bar-item w3-button w3-theme-d1"
@click=${() => tfrpc.rpc.connect(x)}
>
Connect
</button>
<div class="w3-bar-item">
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
<div><small>${x.address}:${x.port}</small></div>
</div>
</li>
`
)}
</ul>
<h2>Local Accounts</h2>
<ul class="w3-ul w3-border">
${this.identities.map(
(x) =>
html`<li class="w3-bar">
${x == this.server_identity
? html`<span class="w3-tag w3-medium w3-round w3-theme-l1"
>🖥 local server</span
>`
: undefined}
${this.my_identities.indexOf(x) != -1
? html`<span class="w3-tag w3-medium w3-round w3-theme-d1"
>😎 you</span
>`
: undefined}
<tf-user id=${x} .users=${this.users}></tf-user>
</li>`
)}
</ul>
<h2>New Connection</h2>
<div style="display: flex; flex-direction: column">
<textarea id="code"></textarea>
</div>
<input type="button" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)} value="Connect"></input>
<h2>Broadcasts</h2>
<ul>
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
</ul>
<h2>Connections</h2>
<ul>
${this.connections.map(x => html`
<li>
<input type="button" @click=${() => tfrpc.rpc.closeConnection(x)} value="Close"></input>
<tf-user id=${x} .users=${this.users}></tf-user>
${self.render_room_peers(x)}
</li>
`)}
</ul>
<h2>Stored Connections (WIP)</h2>
<ul>
${this.stored_connections.map(x => html`
<li>
<input type="button" @click=${() => self.forget_stored_connection(x)} value="Forget"></input>
<input type="button" @click=${() => tfrpc.rpc.connect(x)} value="Connect"></input>
${x.address}:${x.port} <tf-user id=${x.pubkey} .users=${self.users}></tf-user>
</li>
`)}
</ul>
<h2>Local Accounts</h2>
<ul>
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
</ul>
`;
}
}
customElements.define('tf-tab-connections', TfTabConnectionsElement);
customElements.define('tf-tab-connections', TfTabConnectionsElement);

View File

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

View File

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

View File

@ -12,7 +12,6 @@ class TfTabNewsElement extends LitElement {
following: {type: Array},
drafts: {type: Object},
expanded: {type: Object},
loading: {type: Boolean},
};
}
@ -29,7 +28,7 @@ class TfTabNewsElement extends LitElement {
this.cache = {};
this.drafts = {};
this.expanded = {};
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
tfrpc.rpc.localStorageGet('drafts').then(function(d) {
self.drafts = JSON.parse(d || '{}');
});
}
@ -49,9 +48,7 @@ class TfTabNewsElement extends LitElement {
let news = this.shadowRoot?.getElementById('news');
if (news) {
console.log('injecting messages', news.messages);
news.add_messages(
Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x])))
);
news.messages = Object.values(Object.fromEntries([...this.unread, ...news.messages].map(x => [x.id, x])));
this.dispatchEvent(new CustomEvent('refresh'));
}
}
@ -65,16 +62,11 @@ class TfTabNewsElement extends LitElement {
let type = 'private';
try {
type = JSON.parse(message.content).type || type;
} catch {}
} catch {
}
counts[type] = (counts[type] || 0) + 1;
}
return (
'↻ Show New: ' +
Object.keys(counts)
.sort()
.map((x) => counts[x].toString() + ' ' + x + 's')
.join(', ')
);
return 'Show New: ' + Object.keys(counts).sort().map(x => (counts[x].toString() + ' ' + x + 's')).join(', ');
}
draft(event) {
@ -85,7 +77,10 @@ class TfTabNewsElement extends LitElement {
} else {
delete this.drafts[id];
}
this.drafts = Object.assign({}, this.drafts);
/* Only trigger a re-render if we're creating a new draft or discarding an old one. */
if ((previous !== undefined) != (event.detail.draft !== undefined)) {
this.drafts = Object.assign({}, this.drafts);
}
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
}
@ -101,68 +96,24 @@ class TfTabNewsElement extends LitElement {
}
on_keypress(event) {
if (event.target === document.body && event.key == '.') {
if (event.target === document.body &&
event.key == '.') {
this.show_more();
}
}
render() {
let profile = this.hash.startsWith('#@')
? html`<tf-profile
id=${this.hash.substring(1)}
whoami=${this.whoami}
.users=${this.users}
></tf-profile>`
: undefined;
let edit_profile;
if (
!this.loading &&
this.users[this.whoami]?.name === undefined &&
this.hash.substring(1) != this.whoami
) {
edit_profile = html` <div
class="w3-panel w3-padding w3-round w3-card-4 w3-theme-l3"
>
Follow your identity link above to edit your profile and set your
name.
</div>`;
}
let profile = this.hash.startsWith('#@') ?
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
return html`
<p class="w3-bar">
<button
class="w3-bar-item w3-button w3-theme-d1"
@click=${this.show_more}
>
${this.new_messages_text()}
</button>
</p>
<div class="w3-bar">
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
${edit_profile}
</div>
<div>
<tf-compose
id="tf-compose"
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
@tf-draft=${this.draft}
></tf-compose>
</div>
<div><input type="button" value=${this.new_messages_text()} @click=${this.show_more}></input></div>
<a target="_top" href="#" ?hidden=${this.hash.length <= 1}>🏠Home</a>
<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div>
<div><tf-compose whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
${profile}
<tf-tab-news-feed
id="news"
whoami=${this.whoami}
.users=${this.users}
.following=${this.following}
hash=${this.hash}
.drafts=${this.drafts}
.expanded=${this.expanded}
@tf-draft=${this.draft}
@tf-expand=${this.on_expand}
></tf-tab-news-feed>
<tf-tab-news-feed id="news" whoami=${this.whoami} .users=${this.users} .following=${this.following} hash=${this.hash} .drafts=${this.drafts} .expanded=${this.expanded} @tf-draft=${this.draft} @tf-expand=${this.on_expand}></tf-tab-news-feed>
`;
}
}
customElements.define('tf-tab-news', TfTabNewsElement);
customElements.define('tf-tab-news', TfTabNewsElement);

View File

@ -1,136 +0,0 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js';
class TfTabQueryElement extends LitElement {
static get properties() {
return {
whoami: {type: String},
users: {type: Object},
following: {type: Array},
query: {type: String},
expanded: {type: Object},
results: {type: Array},
error: {type: Object},
duration: {type: Number},
};
}
static styles = styles;
constructor() {
super();
let self = this;
this.whoami = null;
this.users = {};
this.following = [];
this.expanded = {};
this.duration = undefined;
}
async search(query) {
console.log('Searching...', this.whoami, query);
this.results = [];
this.error = undefined;
this.duration = undefined;
let search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
}
await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query));
let start_time = new Date();
try {
this.results = await tfrpc.rpc.query(query, []);
} catch (error) {
this.error = error;
}
let end_time = new Date();
this.duration = (end_time - start_time).valueOf();
console.log('Done.');
search = this.renderRoot.getElementById('search');
if (search) {
search.value = query;
search.focus();
}
}
search_keydown(event) {
if (event.keyCode == 13 && event.ctrlKey) {
this.query = this.renderRoot.getElementById('search').value;
event.preventDefault();
}
}
on_expand(event) {
if (event.detail.expanded) {
let expand = {};
expand[event.detail.id] = true;
this.expanded = Object.assign({}, this.expanded, expand);
} else {
delete this.expanded[event.detail.id];
this.expanded = Object.assign({}, this.expanded);
}
}
render_results() {
if (!this.results?.length) {
return html`<div>No results.</div>`;
} else {
let keys = Object.keys(this.results[0]).sort();
return html`<table style="width: 100%; max-width: 100%">
<tr>
${keys.map((key) => html`<th>${key}</th>`)}
</tr>
${this.results.map(
(row) =>
html`<tr>
${keys.map((key) => html`<td>${row[key]}</td>`)}
</tr>`
)}
</table>`;
}
}
render_error() {
if (this.error) {
return html`<h2 style="color: red">${this.error.message}</h2>
<pre style="color: red">${this.error.stack}</pre>`;
}
}
render() {
if (this.query !== this.last_query) {
this.last_query = this.query;
this.search(this.query);
}
let self = this;
return html`
<div style="display: flex; flex-direction: row; gap: 4px">
<textarea
id="search"
rows="8"
class="w3-input w3-theme-d1"
style="flex: 1; resize: vertical"
@keydown=${this.search_keydown}
>
${this.query}</textarea
>
<button
class="w3-button w3-theme-d1"
@click=${(event) =>
self.search(self.renderRoot.getElementById('search').value)}
>
Execute
</button>
</div>
<div ?hidden=${this.duration === undefined}>
Took ${this.duration / 1000.0} seconds.
</div>
<div ?hidden=${this.duration !== undefined}>Executing...</div>
${this.render_error()} ${this.render_results()}
`;
}
}
customElements.define('tf-tab-query', TfTabQueryElement);

View File

@ -5,7 +5,6 @@ import {styles} from './tf-styles.js';
class TfTabSearchElement extends LitElement {
static get properties() {
return {
drafts: {type: Object},
whoami: {type: String},
users: {type: Object},
following: {type: Array},
@ -23,34 +22,28 @@ class TfTabSearchElement extends LitElement {
this.users = {};
this.following = [];
this.expanded = {};
this.drafts = {};
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}');
});
}
async search(query) {
console.log('Searching...', this.whoami, query);
let search = this.renderRoot.getElementById('search');
if (search) {
if (search ) {
search.value = query;
search.focus();
search.select();
}
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
let results = await tfrpc.rpc.query(
`
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
let results = await tfrpc.rpc.query(`
SELECT messages.*
FROM messages_fts(?)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?) AS following ON messages.author = following.value
ORDER BY timestamp DESC limit 100
`,
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
);
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]);
console.log('Done.');
search = this.renderRoot.getElementById('search');
if (search) {
if (search ) {
search.value = query;
search.focus();
search.select();
@ -75,18 +68,6 @@ class TfTabSearchElement extends LitElement {
}
}
draft(event) {
let id = event.detail.id || '';
let previous = this.drafts[id];
if (event.detail.draft !== undefined) {
this.drafts[id] = event.detail.draft;
} else {
delete this.drafts[id];
}
this.drafts = Object.assign({}, this.drafts);
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
}
render() {
if (this.query !== this.last_query) {
this.last_query = this.query;
@ -94,13 +75,13 @@ class TfTabSearchElement extends LitElement {
}
let self = this;
return html`
<div style="display: flex; flex-direction: row; gap: 4px">
<input type="text" class="w3-input w3-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 style="display: flex; flex-direction: row">
<input type="text" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
<input type="button" value="Search" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}></input>
</div>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} .drafts=${this.drafts} @tf-expand=${this.on_expand} @tf-draft=${this.draft}></tf-news>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
`;
}
}
customElements.define('tf-tab-search', TfTabSearchElement);
customElements.define('tf-tab-search', TfTabSearchElement);

View File

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

View File

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

View File

@ -1,34 +1,21 @@
import * as linkify from './commonmark-linkify.js';
import * as hashtagify from './commonmark-hashtag.js';
const k_code_classes = 'w3-theme-l4 w3-theme-border w3-round';
function image(node, entering) {
if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')
) {
if (node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')) {
if (entering) {
this.lit(
'<video style="max-width: 100%; max-height: 480px" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</video>');
}
} else if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')
) {
} else if (node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')) {
if (entering) {
this.lit(
'<audio style="height: 32px; max-width: 100%" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
@ -38,11 +25,7 @@ function image(node, entering) {
} else {
if (entering) {
if (this.disableTags === 0) {
this.lit(
'<div class="img_caption">' +
this.esc(node.firstChild?.literal || node.destination) +
'</div>'
);
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>');
if (this.options.safe && potentiallyUnsafe(node.destination)) {
this.lit('<img src="" alt="');
} else {
@ -62,52 +45,27 @@ function image(node, entering) {
}
}
function code(node) {
let attrs = this.attrs(node);
attrs.push(['class', k_code_classes]);
this.tag('code', attrs);
this.out(node.literal);
this.tag('/code');
}
function attrs(node) {
let result = commonmark.HtmlRenderer.prototype.attrs.bind(this)(node);
if (node.type == 'block_quote') {
result.push(['class', 'w3-theme-d1']);
} else if (node.type == 'code_block') {
result.push(['class', k_code_classes]);
}
return result;
}
export function markdown(md) {
let reader = new commonmark.Parser({safe: true});
let writer = new commonmark.HtmlRenderer();
var reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer();
writer.image = image;
writer.code = code;
writer.attrs = attrs;
let parsed = reader.parse(md || '');
var parsed = reader.parse(md || '');
parsed = linkify.transform(parsed);
parsed = hashtagify.transform(parsed);
let walker = parsed.walker();
let event, node;
var walker = parsed.walker();
var event, node;
while ((event = walker.next())) {
node = event.node;
if (event.entering) {
if (node.type == 'link') {
if (
node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')
) {
if (node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')) {
node.destination = '#' + node.destination;
} else if (
node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')
) {
} else if (node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')) {
node.destination = '#' + node.destination;
} else if (
node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')
) {
} else if (node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')) {
node.destination = '/' + node.destination + '/view';
}
} else if (node.type == 'image') {
@ -132,4 +90,4 @@ export function human_readable_size(bytes) {
}
}
return `${Math.round(v * 10) / 10} ${u}`;
}
}

View File

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

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