Compare commits
	
		
			2 Commits
		
	
	
		
			v0.2025.8
			...
			tasiaiso-n
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						d67e47ae4b
	
				 | 
					
					
						|||
| 
						
						
							
						
						b43b8da9ab
	
				 | 
					
					
						
@@ -48,7 +48,7 @@ jobs:
 | 
			
		||||
      - name: Build documentation
 | 
			
		||||
        run: |
 | 
			
		||||
          mkdir -p out/html/ ~/.ssh/
 | 
			
		||||
          make -j`nproc` docs
 | 
			
		||||
          make docs
 | 
			
		||||
          echo 'pildefriends ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3Kde5vDO0TrMBDK0IGGeNGe/XinWAZkSQ/rXxwUjt' >> ~/.ssh/known_hosts
 | 
			
		||||
          rsync -avP --delete -e "ssh -i /opt/keys/ssh.ed25519" out/html/ tfdocs@pildefriends:docs/html/
 | 
			
		||||
      - name: Setup JDK
 | 
			
		||||
@@ -59,11 +59,11 @@ jobs:
 | 
			
		||||
      - name: Setup Android SDK
 | 
			
		||||
        uses: android-actions/setup-android@v3
 | 
			
		||||
        with:
 | 
			
		||||
          packages: 'tools platform-tools build-tools;35.0.0 platforms;android-35 ndk;27.2.12479018'
 | 
			
		||||
          packages: 'tools platform-tools build-tools;34.0.0 platforms;android-34 ndk;26.3.11579264'
 | 
			
		||||
      - name: Docker build
 | 
			
		||||
        run: DOCKER_BUILDKIT=1 docker build .
 | 
			
		||||
      - name: Build
 | 
			
		||||
        run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist
 | 
			
		||||
        run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist docs
 | 
			
		||||
      - name: Upload artifacts
 | 
			
		||||
        uses: actions/upload-artifact@v3
 | 
			
		||||
        with:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								Doxyfile
									
									
									
									
									
								
							
							
						
						@@ -342,7 +342,7 @@ OPTIMIZE_OUTPUT_SLICE  = NO
 | 
			
		||||
#
 | 
			
		||||
# Note see also the list of default file extension mappings.
 | 
			
		||||
 | 
			
		||||
EXTENSION_MAPPING      = js=javascript
 | 
			
		||||
EXTENSION_MAPPING      =
 | 
			
		||||
 | 
			
		||||
# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
 | 
			
		||||
# according to the Markdown format, which allows for more readable
 | 
			
		||||
@@ -943,14 +943,7 @@ WARN_LOGFILE           =
 | 
			
		||||
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
 | 
			
		||||
# Note: If this tag is empty the current directory is searched.
 | 
			
		||||
 | 
			
		||||
INPUT                  = README.md \
 | 
			
		||||
                         core/app.js \
 | 
			
		||||
                         core/client.js \
 | 
			
		||||
                         core/core.js \
 | 
			
		||||
                         core/http.js \
 | 
			
		||||
                         core/tfrpc.js \
 | 
			
		||||
                         docs/ \
 | 
			
		||||
                         src/
 | 
			
		||||
INPUT                  = README.md docs/ src/
 | 
			
		||||
 | 
			
		||||
# This tag can be used to specify the character encoding of the source files
 | 
			
		||||
# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
 | 
			
		||||
@@ -991,7 +984,6 @@ INPUT_FILE_ENCODING    =
 | 
			
		||||
# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice.
 | 
			
		||||
 | 
			
		||||
FILE_PATTERNS          = *.h \
 | 
			
		||||
                         *.js \
 | 
			
		||||
                         *.md
 | 
			
		||||
 | 
			
		||||
# The RECURSIVE tag can be used to specify whether or not subdirectories should
 | 
			
		||||
@@ -1057,7 +1049,7 @@ EXAMPLE_RECURSIVE      = NO
 | 
			
		||||
# that contain images that are to be included in the documentation (see the
 | 
			
		||||
# \image command).
 | 
			
		||||
 | 
			
		||||
IMAGE_PATH             = docs/images/
 | 
			
		||||
IMAGE_PATH             =
 | 
			
		||||
 | 
			
		||||
# The INPUT_FILTER tag can be used to specify a program that doxygen should
 | 
			
		||||
# invoke to filter for each input file. Doxygen will invoke the filter program
 | 
			
		||||
@@ -2276,7 +2268,7 @@ GENERATE_AUTOGEN_DEF   = NO
 | 
			
		||||
# database with symbols found by doxygen stored in tables.
 | 
			
		||||
# The default value is: NO.
 | 
			
		||||
 | 
			
		||||
#GENERATE_SQLITE3       = NO
 | 
			
		||||
GENERATE_SQLITE3       = NO
 | 
			
		||||
 | 
			
		||||
# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be
 | 
			
		||||
# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put
 | 
			
		||||
@@ -2284,7 +2276,7 @@ GENERATE_AUTOGEN_DEF   = NO
 | 
			
		||||
# The default directory is: sqlite3.
 | 
			
		||||
# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
 | 
			
		||||
 | 
			
		||||
#SQLITE3_OUTPUT         = sqlite3
 | 
			
		||||
SQLITE3_OUTPUT         = sqlite3
 | 
			
		||||
 | 
			
		||||
# The SQLITE3_OVERWRITE_DB tag is set to YES, the existing doxygen_sqlite3.db
 | 
			
		||||
# database file will be recreated with each doxygen run. If set to NO, doxygen
 | 
			
		||||
@@ -2292,7 +2284,7 @@ GENERATE_AUTOGEN_DEF   = NO
 | 
			
		||||
# The default value is: YES.
 | 
			
		||||
# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
 | 
			
		||||
 | 
			
		||||
#SQLITE3_RECREATE_DB    = YES
 | 
			
		||||
SQLITE3_RECREATE_DB    = YES
 | 
			
		||||
 | 
			
		||||
#---------------------------------------------------------------------------
 | 
			
		||||
# Configuration options related to the Perl module output
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										95
									
								
								GNUmakefile
									
									
									
									
									
								
							
							
						
						@@ -16,14 +16,14 @@ MAKEFLAGS += --no-builtin-rules
 | 
			
		||||
## LD := Linker.
 | 
			
		||||
## ANDROID_SDK := Path to the Android SDK.
 | 
			
		||||
 | 
			
		||||
VERSION_CODE := 42
 | 
			
		||||
VERSION_CODE_IOS := 16
 | 
			
		||||
VERSION_NUMBER := 0.2025.8
 | 
			
		||||
VERSION_CODE := 33
 | 
			
		||||
VERSION_CODE_IOS := 8
 | 
			
		||||
VERSION_NUMBER := 0.0.28-wip
 | 
			
		||||
VERSION_NAME := This program kills fascists.
 | 
			
		||||
 | 
			
		||||
IPHONEOS_VERSION_MIN=14.0
 | 
			
		||||
 | 
			
		||||
SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3500400.zip
 | 
			
		||||
SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3490100.zip
 | 
			
		||||
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
 | 
			
		||||
APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
 | 
			
		||||
APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d  out/appimagetool
 | 
			
		||||
@@ -45,10 +45,6 @@ export TZ=UTC
 | 
			
		||||
 | 
			
		||||
ifeq ($(UNAME_S),Darwin)
 | 
			
		||||
BUILD_TYPES := debug release iosdebug iosrelease iossimdebug iossimrelease
 | 
			
		||||
HAVE_ANDROID = 0
 | 
			
		||||
HAVE_LINUX_IOS = 0
 | 
			
		||||
HAVE_LINUX_MACOS = 0
 | 
			
		||||
HAVE_WIN = 0
 | 
			
		||||
else ifeq ($(UNAME_S),Linux)
 | 
			
		||||
BUILD_TYPES := debug release
 | 
			
		||||
HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1)
 | 
			
		||||
@@ -66,10 +62,6 @@ LDFLAGS += \
 | 
			
		||||
	-lnetwork \
 | 
			
		||||
	-Wno-stringop-overflow
 | 
			
		||||
USE_SYSTEM_SSL := 1
 | 
			
		||||
HAVE_ANDROID = 0
 | 
			
		||||
HAVE_LINUX_IOS = 0
 | 
			
		||||
HAVE_LINUX_MACOS = 0
 | 
			
		||||
HAVE_WIN = 0
 | 
			
		||||
else ifeq ($(UNAME_S),OpenBSD)
 | 
			
		||||
BUILD_TYPES := debug release
 | 
			
		||||
CFLAGS += \
 | 
			
		||||
@@ -106,10 +98,10 @@ LDFLAGS += \
 | 
			
		||||
	-Wno-aggressive-loop-optimizations
 | 
			
		||||
 | 
			
		||||
ANDROID_MIN_SDK_VERSION := 24
 | 
			
		||||
ANDROID_TARGET_SDK_VERSION := 35
 | 
			
		||||
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/35.0.0
 | 
			
		||||
ANDROID_TARGET_SDK_VERSION := 34
 | 
			
		||||
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
 | 
			
		||||
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-$(ANDROID_TARGET_SDK_VERSION)
 | 
			
		||||
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/27.2.12479018
 | 
			
		||||
ANDROID_NDK ?= $(ANDROID_SDK)/ndk/26.3.11579264
 | 
			
		||||
 | 
			
		||||
ANDROID_ARMV7A_TARGETS := \
 | 
			
		||||
	out/androiddebug-armv7a/tildefriends \
 | 
			
		||||
@@ -253,10 +245,7 @@ $(ANDROID_TARGETS): CFLAGS += \
 | 
			
		||||
	-fno-asynchronous-unwind-tables \
 | 
			
		||||
	-funwind-tables \
 | 
			
		||||
	-Wno-unknown-warning-option
 | 
			
		||||
$(ANDROID_TARGETS): LDFLAGS += \
 | 
			
		||||
	--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
 | 
			
		||||
	-Wl,-z,max-page-size=16384 \
 | 
			
		||||
	-fPIC
 | 
			
		||||
$(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
 | 
			
		||||
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
 | 
			
		||||
$(DEBUG_TARGETS): LDFLAGS += -Og
 | 
			
		||||
$(RELEASE_TARGETS): CFLAGS += \
 | 
			
		||||
@@ -617,16 +606,15 @@ $(UV_OBJS): CFLAGS += \
 | 
			
		||||
	-Ideps/libuv/include \
 | 
			
		||||
	-Ideps/libuv/src \
 | 
			
		||||
	-Wno-dangling-pointer \
 | 
			
		||||
	-Wno-format-truncation \
 | 
			
		||||
	-Wno-incompatible-pointer-types \
 | 
			
		||||
	-Wno-maybe-uninitialized \
 | 
			
		||||
	-Wno-nonnull \
 | 
			
		||||
	-Wno-sign-compare \
 | 
			
		||||
	-Wno-unknown-attributes \
 | 
			
		||||
	-Wno-unused-but-set-parameter \
 | 
			
		||||
	-Wno-unused-but-set-variable \
 | 
			
		||||
	-Wno-unused-result \
 | 
			
		||||
	-Wno-unused-variable
 | 
			
		||||
	-Wno-unused-variable \
 | 
			
		||||
	-Wno-nonnull
 | 
			
		||||
$(filter out/win%,$(UV_OBJS)): \
 | 
			
		||||
	CFLAGS += \
 | 
			
		||||
		-Wno-cast-function-type \
 | 
			
		||||
@@ -653,7 +641,6 @@ SODIUM_SOURCES := \
 | 
			
		||||
	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_core/softaes/softaes.c \
 | 
			
		||||
	deps/libsodium/src/libsodium/crypto_generichash/crypto_generichash.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 \
 | 
			
		||||
@@ -723,12 +710,12 @@ $(SQLITE_OBJS): CFLAGS += \
 | 
			
		||||
	-DSQLITE_MAX_COMPOUND_SELECT=300 \
 | 
			
		||||
	-DSQLITE_MAX_EXPR_DEPTH=40 \
 | 
			
		||||
	-DSQLITE_MAX_FUNCTION_ARG=8 \
 | 
			
		||||
	-DSQLITE_MAX_LENGTH=10485760 \
 | 
			
		||||
	-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=50000 \
 | 
			
		||||
	-DSQLITE_MAX_VDBE_OP=25000 \
 | 
			
		||||
	-DSQLITE_OMIT_DEPRECATED \
 | 
			
		||||
	-DSQLITE_OMIT_DESERIALIZE \
 | 
			
		||||
	-DSQLITE_OMIT_LOAD_EXTENSION \
 | 
			
		||||
@@ -747,7 +734,7 @@ $(SQLITE_OBJS): CFLAGS += \
 | 
			
		||||
 | 
			
		||||
QUICKJS_SOURCES := \
 | 
			
		||||
	deps/quickjs/cutils.c \
 | 
			
		||||
	deps/quickjs/dtoa.c \
 | 
			
		||||
	deps/quickjs/libbf.c \
 | 
			
		||||
	deps/quickjs/libregexp.c \
 | 
			
		||||
	deps/quickjs/libunicode.c \
 | 
			
		||||
	deps/quickjs/quickjs.c
 | 
			
		||||
@@ -998,8 +985,7 @@ PACKAGE_DIRS := \
 | 
			
		||||
	core \
 | 
			
		||||
	deps/codemirror \
 | 
			
		||||
	deps/prettier \
 | 
			
		||||
	deps/lit \
 | 
			
		||||
	deps/speedscope
 | 
			
		||||
	deps/lit
 | 
			
		||||
 | 
			
		||||
RAW_FILES := $(sort $(filter-out apps/blog% apps/issues% apps/welcome% apps/journal% %.map, $(shell find $(PACKAGE_DIRS) -type f -not -name '.*')))
 | 
			
		||||
 | 
			
		||||
@@ -1020,7 +1006,7 @@ $(BUNDLETOOL):
 | 
			
		||||
	@curl -q -L --create-dirs -o $@ $(BUNDLETOOL_URL)
 | 
			
		||||
 | 
			
		||||
out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGETS)) $(RAW_FILES) out/apk/res.apk src/android/AndroidManifest.xml $(BUNDLETOOL)
 | 
			
		||||
	@rm -rf out/aab/staging/ out/aab/base.zip
 | 
			
		||||
	@rm -rf out/aab/staging/
 | 
			
		||||
	@mkdir -p out/aab/staging
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/aapt2 link --proto-format -o out/aab/temporary.apk \
 | 
			
		||||
		-I $(ANDROID_PLATFORM)/android.jar \
 | 
			
		||||
@@ -1040,11 +1026,14 @@ out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGET
 | 
			
		||||
	@cp out/apk/classes.dex out/aab/staging/dex/
 | 
			
		||||
	@rm -fv out/base.zip
 | 
			
		||||
	@mkdir -p out/aab/staging/lib/arm64-v8a out/aab/staging/lib/armeabi-v7a out/aab/staging/lib/x86_64 out/aab/staging/lib/x86
 | 
			
		||||
	@mkdir -p out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/arm64-v8a out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/armeabi-v7a out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86_64 out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/androidrelease/tildefriends -o out/aab/staging/lib/arm64-v8a/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/androidrelease-armv7a/tildefriends -o out/aab/staging/lib/armeabi-v7a/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/androidrelease-x86_64/tildefriends -o out/aab/staging/lib/x86_64/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/androidrelease-x86/tildefriends -o out/aab/staging/lib/x86/libtildefriends.so
 | 
			
		||||
	@cp out/androidrelease/tildefriends out/aab/staging/lib/arm64-v8a/libtildefriends.so
 | 
			
		||||
	@cp out/androidrelease-armv7a/tildefriends out/aab/staging/lib/armeabi-v7a/libtildefriends.so
 | 
			
		||||
	@cp out/androidrelease-x86_64/tildefriends out/aab/staging/lib/x86_64/libtildefriends.so
 | 
			
		||||
	@cp out/androidrelease-x86/tildefriends out/aab/staging/lib/x86/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/arm64-v8a/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/armeabi-v7a/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86_64/libtildefriends.so
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/aab/staging/lib/x86/libtildefriends.so
 | 
			
		||||
	@cp -r apps/ out/aab/staging/root/
 | 
			
		||||
	@rm -rf out/aab/staging/root/apps/welcome*
 | 
			
		||||
	@cp -r core/ out/aab/staging/root/
 | 
			
		||||
@@ -1053,12 +1042,7 @@ out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGET
 | 
			
		||||
	@cp -r deps/codemirror/ out/aab/staging/root/deps/
 | 
			
		||||
	@cd out/aab/staging/; zip -r ../base.zip *; cd ../../../
 | 
			
		||||
	@java -jar $(BUNDLETOOL) build-bundle --overwrite --config=src/android/BundleConfig.json --modules=out/aab/base.zip --output=$@
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --only-keep-debug out/androidrelease/tildefriends -o out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/arm64-v8a/libtildefriends.so.sym
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --only-keep-debug out/androidrelease-armv7a/tildefriends -o out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/armeabi-v7a/libtildefriends.so.sym
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --only-keep-debug out/androidrelease-x86_64/tildefriends -o out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86_64/libtildefriends.so.sym
 | 
			
		||||
	@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --only-keep-debug out/androidrelease-x86/tildefriends -o out/aab/staging/BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86/libtildefriends.so.sym
 | 
			
		||||
	@cd out/aab/staging; zip -u ../../../$@ BUNDLE-METADATA/com.android.tools.build.debugsymbols/arm64-v8a/libtildefriends.so.sym BUNDLE-METADATA/com.android.tools.build.debugsymbols/armeabi-v7a/libtildefriends.so.sym BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86_64/libtildefriends.so.sym BUNDLE-METADATA/com.android.tools.build.debugsymbols/x86/libtildefriends.so.sym; cd ../../../
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/apksigner sign -ks .keys/android.jks --ks-key-alias androidKey -ks-pass pass:android --min-sdk-version=$(ANDROID_MIN_SDK_VERSION) --alignment-preserved $@
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/apksigner sign -ks .keys/android.jks --ks-key-alias androidKey -ks-pass pass:android --min-sdk-version=$(ANDROID_MIN_SDK_VERSION) $@
 | 
			
		||||
 | 
			
		||||
aab: out/TildeFriends.aab ## Build an Android App Bundle.
 | 
			
		||||
.PHONY: aab
 | 
			
		||||
@@ -1120,12 +1104,12 @@ out/apk/TildeFriends-%.fdroid.unsigned.apk:
 | 
			
		||||
 | 
			
		||||
out/%.apk: out/apk/%.unsigned.apk
 | 
			
		||||
	@echo "[apksigner] $(notdir $@)"
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ --alignment-preserved $<
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $<
 | 
			
		||||
 | 
			
		||||
out/%.zopfli.apk: out/%.apk
 | 
			
		||||
	@echo "[zopfli] $(notdir $@)"
 | 
			
		||||
	$(ANDROID_BUILD_TOOLS)/zipalign -f -z 4 $< $@.zopfli
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ --alignment-preserved $@.zopfli
 | 
			
		||||
	@$(ANDROID_BUILD_TOOLS)/apksigner sign --ks .keys/android.jks --ks-key-alias androidKey --ks-pass pass:android --key-pass pass:android --min-sdk-version $(ANDROID_MIN_SDK_VERSION) --out $@ $@.zopfli
 | 
			
		||||
 | 
			
		||||
release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk ## Build an Android release APK.
 | 
			
		||||
.PHONY: release-apk
 | 
			
		||||
@@ -1143,11 +1127,6 @@ releaseapkgo: out/TildeFriends-arm-release.apk ## Build, install, and run a rele
 | 
			
		||||
	@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
 | 
			
		||||
.PHONY: releaseapkgo
 | 
			
		||||
 | 
			
		||||
x86releaseapkgo: out/TildeFriends-x86-release.apk ## Build, install, and run an x86 release Android APK.
 | 
			
		||||
	@adb install -r $<
 | 
			
		||||
	@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
 | 
			
		||||
.PHONY: x86releaseapkgo
 | 
			
		||||
 | 
			
		||||
apklog: ## Display Android log output.
 | 
			
		||||
	@adb logcat *:S tildefriends
 | 
			
		||||
.PHONY: apklog
 | 
			
		||||
@@ -1174,13 +1153,7 @@ out/zsign_build/zsign: $(wildcard deps/zsign/*.cpp deps/zsign/*.h deps/zsign/*.t
 | 
			
		||||
	@cmake -B out/zsign_build deps/zsign
 | 
			
		||||
	@cmake --build out/zsign_build -- COLOR=0 VERBOSE=0 MAKESILENT=-s
 | 
			
		||||
 | 
			
		||||
ifeq ($(HAVE_LINUX_IOS),1)
 | 
			
		||||
ZSIGN_DEP = out/zsign_build/zsign
 | 
			
		||||
else
 | 
			
		||||
ZSIGN_DEP =
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip $(ZSIGN_DEP)
 | 
			
		||||
out/tildefriends-%.app/tildefriends: out/%/tildefriends out/tildefriends-%.app/Info.plist out/tildefriends-%.app/tildefriends.png out/data.zip $(if $(HAVE_LINUX_IOS),out/zsign_build/zsign)
 | 
			
		||||
	@mkdir -p $(dir $@)
 | 
			
		||||
	@cp -v $(filter-out out/zsign%,$<) $@
 | 
			
		||||
	@cp -v out/data.zip $(@D)/
 | 
			
		||||
@@ -1446,7 +1419,7 @@ dist-ios: iosrelease-app
 | 
			
		||||
	mkdir -p out/Payload/tildefriends.app
 | 
			
		||||
	cp -avR out/tildefriends-iosrelease.app/* out/Payload/tildefriends.app/
 | 
			
		||||
	cp src/ios/tildefriends.png out/Payload/tildefriends.app/
 | 
			
		||||
	xcrun -sdk iphoneos actool --compile out/Payload/tildefriends.app/ --platform iphoneos --minimum-deployment-target $(IPHONEOS_VERSION_MIN) --app-icon AppIcon src/ios/icons/Assets.xcassets src/ios/icons/*.png --output-partial-info-plist out/actool.plist
 | 
			
		||||
	cp src/ios/icons/Assets.car out/Payload/tildefriends.app/
 | 
			
		||||
	cp src/ios/distribution.mobileprovision out/Payload/tildefriends.app/embedded.mobileprovision
 | 
			
		||||
	xcrun -sdk iphoneos codesign -f -s 'Apple Distribution' --entitlements src/ios/Entitlements.plist --generate-entitlement-der out/Payload/tildefriends.app
 | 
			
		||||
	cd out; zip -r tildefriends.ipa Payload; cd ..
 | 
			
		||||
@@ -1492,18 +1465,6 @@ help: ## Display this help message.
 | 
			
		||||
.PHONY: help
 | 
			
		||||
.DEFAULT_GOAL := help
 | 
			
		||||
 | 
			
		||||
docs: debug
 | 
			
		||||
docs: ## Build HTML docs.
 | 
			
		||||
	@echo '# CLI Usage\n' > docs/usage.md
 | 
			
		||||
	@echo "## tildefriends -h" >> docs/usage.md
 | 
			
		||||
	@echo '\n```' >> docs/usage.md
 | 
			
		||||
	@out/debug/tildefriends -h >> docs/usage.md
 | 
			
		||||
	@echo '```' >> docs/usage.md
 | 
			
		||||
	@for command in $$(out/debug/tildefriends -h | grep -Po '[A-Za-z_]*(?= - )'); do
 | 
			
		||||
	@  echo "\n## tildefriends $$command -h" >> docs/usage.md
 | 
			
		||||
	@  echo '\n```' >> docs/usage.md
 | 
			
		||||
	@  out/debug/tildefriends $$command -h >> docs/usage.md
 | 
			
		||||
	@  echo '```' >> docs/usage.md
 | 
			
		||||
	@done
 | 
			
		||||
	@doxygen
 | 
			
		||||
.PHONY: docs
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ dependencies in the right places.
 | 
			
		||||
 | 
			
		||||
### Requirements
 | 
			
		||||
 | 
			
		||||
System OpenSSL libraries are assumed to be available on Haiku and OpenBSD.
 | 
			
		||||
System OpenSSL libraries are assumed to be available on Haiku and OpenSSL.
 | 
			
		||||
 | 
			
		||||
On MacOS, Xcode's command-line tools are expected to be available.
 | 
			
		||||
 | 
			
		||||
@@ -55,8 +55,9 @@ standard.
 | 
			
		||||
## Running
 | 
			
		||||
 | 
			
		||||
By default, running the built `out/debug/tildefriends` executable will start a
 | 
			
		||||
web server at <http://localhost:12345/>. `tildefriends -h` lists further
 | 
			
		||||
options.
 | 
			
		||||
web server at <http://localhost:12345/>. It expects to be run with the
 | 
			
		||||
repository root as the current working directory. `tildefriends -h` lists
 | 
			
		||||
further options.
 | 
			
		||||
 | 
			
		||||
The first user to create an account and log in will be granted administrative
 | 
			
		||||
privileges. Further administration can be done in the `admin` app at
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
 | 
			
		||||
/* 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}
 | 
			
		||||
@@ -108,8 +108,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
 | 
			
		||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
 | 
			
		||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
 | 
			
		||||
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
 | 
			
		||||
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
 | 
			
		||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
 | 
			
		||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
 | 
			
		||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
 | 
			
		||||
@@ -150,7 +148,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-button:hover{color:#000!important;background-color:#ccc!important}
 | 
			
		||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
 | 
			
		||||
.w3-hover-none:hover{box-shadow:none!important}
 | 
			
		||||
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
 | 
			
		||||
/* Colors */
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
 | 
			
		||||
@@ -178,19 +175,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
 | 
			
		||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
 | 
			
		||||
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
 | 
			
		||||
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
 | 
			
		||||
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
 | 
			
		||||
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
 | 
			
		||||
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
 | 
			
		||||
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
 | 
			
		||||
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
 | 
			
		||||
.w3-danger{color:#fff!important;background-color:#dd0000!important}
 | 
			
		||||
.w3-note{color:#000!important;background-color:#fff599!important}
 | 
			
		||||
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
 | 
			
		||||
.w3-warning{color:#000!important;background-color:#ffb305!important}
 | 
			
		||||
.w3-success{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
 | 
			
		||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
 | 
			
		||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
 | 
			
		||||
@@ -248,4 +232,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.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}
 | 
			
		||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
 | 
			
		||||
/* 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}
 | 
			
		||||
@@ -108,8 +108,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
 | 
			
		||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
 | 
			
		||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
 | 
			
		||||
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
 | 
			
		||||
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
 | 
			
		||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
 | 
			
		||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
 | 
			
		||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
 | 
			
		||||
@@ -150,7 +148,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-button:hover{color:#000!important;background-color:#ccc!important}
 | 
			
		||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
 | 
			
		||||
.w3-hover-none:hover{box-shadow:none!important}
 | 
			
		||||
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
 | 
			
		||||
/* Colors */
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
 | 
			
		||||
@@ -178,19 +175,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
 | 
			
		||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
 | 
			
		||||
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
 | 
			
		||||
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
 | 
			
		||||
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
 | 
			
		||||
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
 | 
			
		||||
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
 | 
			
		||||
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
 | 
			
		||||
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
 | 
			
		||||
.w3-danger{color:#fff!important;background-color:#dd0000!important}
 | 
			
		||||
.w3-note{color:#000!important;background-color:#fff599!important}
 | 
			
		||||
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
 | 
			
		||||
.w3-warning{color:#000!important;background-color:#ffb305!important}
 | 
			
		||||
.w3-success{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
 | 
			
		||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
 | 
			
		||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
 | 
			
		||||
@@ -248,4 +232,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.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}
 | 
			
		||||
.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}
 | 
			
		||||
							
								
								
									
										42
									
								
								apps/blog/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,4 +1,4 @@
 | 
			
		||||
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
 | 
			
		||||
/* 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}
 | 
			
		||||
@@ -108,8 +108,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
 | 
			
		||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
 | 
			
		||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
 | 
			
		||||
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
 | 
			
		||||
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
 | 
			
		||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
 | 
			
		||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
 | 
			
		||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
 | 
			
		||||
@@ -150,7 +148,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-button:hover{color:#000!important;background-color:#ccc!important}
 | 
			
		||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
 | 
			
		||||
.w3-hover-none:hover{box-shadow:none!important}
 | 
			
		||||
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
 | 
			
		||||
/* Colors */
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
 | 
			
		||||
@@ -178,19 +175,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
 | 
			
		||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
 | 
			
		||||
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
 | 
			
		||||
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
 | 
			
		||||
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
 | 
			
		||||
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
 | 
			
		||||
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
 | 
			
		||||
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
 | 
			
		||||
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
 | 
			
		||||
.w3-danger{color:#fff!important;background-color:#dd0000!important}
 | 
			
		||||
.w3-note{color:#000!important;background-color:#fff599!important}
 | 
			
		||||
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
 | 
			
		||||
.w3-warning{color:#000!important;background-color:#ffb305!important}
 | 
			
		||||
.w3-success{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
 | 
			
		||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
 | 
			
		||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
 | 
			
		||||
@@ -248,4 +232,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.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}
 | 
			
		||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "💡",
 | 
			
		||||
	"previous": "&eN6DNPpQUNhGvxneLuLPgsOXR6qyFZ7u+MAz0b4fa7k=.sha256"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
import * as tfrpc from '/tfrpc.js';
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	await app.setDocument(utf8Decode(getFile('index.html')));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tfrpc.register(async function complete() {
 | 
			
		||||
	if (
 | 
			
		||||
		core.user?.credentials?.permissions?.administration &&
 | 
			
		||||
		(await core.globalSettingsGet('index')) == '/~core/intro/'
 | 
			
		||||
	) {
 | 
			
		||||
		return await core.globalSettingsSet('index', '/~core/ssb/');
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
@@ -1,286 +0,0 @@
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html style="height: 100%; margin: 0; padding: 0; box-sizing: border-box">
 | 
			
		||||
	<head>
 | 
			
		||||
		<meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
			
		||||
		<link rel="stylesheet" type="text/css" href="w3.css" />
 | 
			
		||||
		<style>
 | 
			
		||||
			.slide {
 | 
			
		||||
				display: none;
 | 
			
		||||
				margin-left: auto;
 | 
			
		||||
				margin-right: auto;
 | 
			
		||||
			}
 | 
			
		||||
			.dot {
 | 
			
		||||
				width: 1em;
 | 
			
		||||
				height: 1em;
 | 
			
		||||
				cursor: pointer;
 | 
			
		||||
			}
 | 
			
		||||
			.w3-left,
 | 
			
		||||
			.w3-right {
 | 
			
		||||
				cursor: pointer;
 | 
			
		||||
			}
 | 
			
		||||
		</style>
 | 
			
		||||
	</head>
 | 
			
		||||
	<body
 | 
			
		||||
		style="
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			height: 100%;
 | 
			
		||||
			max-width: 100%;
 | 
			
		||||
			max-height: 100%;
 | 
			
		||||
			margin: 0;
 | 
			
		||||
			padding: 0;
 | 
			
		||||
			flex-direction: column;
 | 
			
		||||
			align-items: center;
 | 
			
		||||
		"
 | 
			
		||||
		class="w3-flex w3-dark-gray w3-center"
 | 
			
		||||
	>
 | 
			
		||||
		<div
 | 
			
		||||
			style="
 | 
			
		||||
				flex: 1 1 auto;
 | 
			
		||||
				overflow: auto;
 | 
			
		||||
				contain: content;
 | 
			
		||||
				padding-top: 16px;
 | 
			
		||||
				padding-bottom: 16px;
 | 
			
		||||
			"
 | 
			
		||||
		>
 | 
			
		||||
			<div class="slide">
 | 
			
		||||
				<div
 | 
			
		||||
					class="w3-content w3-xlarge w3-card-4 w3-blue w3-panel w3-padding-32 w3-round-xlarge"
 | 
			
		||||
					style="margin: 32px"
 | 
			
		||||
				>
 | 
			
		||||
					<div>
 | 
			
		||||
						<div>Welcome to</div>
 | 
			
		||||
						<div>~😎 Tilde Friends.</div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<footer>
 | 
			
		||||
						<button class="w3-button w3-yellow proceed">Next</button>
 | 
			
		||||
					</footer>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="slide w3-card-4 w3-gray" style="width: 90%">
 | 
			
		||||
				<header class="w3-container w3-blue w3-xlarge">
 | 
			
		||||
					<h1>This brief tutorial will introduce:</h1>
 | 
			
		||||
				</header>
 | 
			
		||||
				<ul class="w3-large w3-left-align">
 | 
			
		||||
					<li><b>Secure Scuttlebutt</b>, a decentralized social network.</li>
 | 
			
		||||
					<li>
 | 
			
		||||
						<b>Tilde Friends</b>, the application platform that you are using
 | 
			
		||||
						right now.
 | 
			
		||||
					</li>
 | 
			
		||||
					<li>
 | 
			
		||||
						<b>How to get started</b> if you want to get gossiping right away.
 | 
			
		||||
					</li>
 | 
			
		||||
				</ul>
 | 
			
		||||
				<footer class="w3-center w3-xlarge w3-padding">
 | 
			
		||||
					<button class="w3-button w3-yellow proceed">Onward</button>
 | 
			
		||||
				</footer>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="slide w3-gray" style="width: 90%">
 | 
			
		||||
				<div class="w3-card-4 w3-xlarge">
 | 
			
		||||
					<header class="w3-container w3-blue">
 | 
			
		||||
						<h1>💻Secure Scuttlebutt in a Nutshell🦀</h1>
 | 
			
		||||
					</header>
 | 
			
		||||
					<div class="w3-container w3-large w3-left-align">
 | 
			
		||||
						<p>
 | 
			
		||||
							Secure Scuttlebutt is a social network whose technical operation
 | 
			
		||||
							attempts to mirror human social interaction.
 | 
			
		||||
						</p>
 | 
			
		||||
						<ul>
 | 
			
		||||
							<li>
 | 
			
		||||
								You can create your own account and post to your own feed on
 | 
			
		||||
								your own device. This is all <b>local</b> with no external
 | 
			
		||||
								communication. This puts you fully in control of your own words
 | 
			
		||||
								and actions.
 | 
			
		||||
							</li>
 | 
			
		||||
							<li>
 | 
			
		||||
								Before you can interact with others, you need to
 | 
			
		||||
								<b>connect over the network</b>, either directly to your friends
 | 
			
		||||
								(i.e., peer-to-peer between your phones on coffee shop Wi-Fi) or
 | 
			
		||||
								to 🚪<i>rooms</i> and 🍻<i>pubs</i> (hint: search the web for
 | 
			
		||||
								<i>#ssbroom</i>).
 | 
			
		||||
							</li>
 | 
			
		||||
							<li>
 | 
			
		||||
								Who you choose to <b>follow</b> determines what you see, with
 | 
			
		||||
								most people choosing to see messages from friends and friends of
 | 
			
		||||
								those friends. If you encounter content you'd rather not see,
 | 
			
		||||
								<b>block</b> the offending account to improve the experience for
 | 
			
		||||
								you and your followers.
 | 
			
		||||
							</li>
 | 
			
		||||
							<li>
 | 
			
		||||
								Your feed is an <b>immutable</b> log of your activity. Post with
 | 
			
		||||
								care, because like your words in real life, posts can't be taken
 | 
			
		||||
								back.
 | 
			
		||||
							</li>
 | 
			
		||||
						</ul>
 | 
			
		||||
					</div>
 | 
			
		||||
					<footer class="w3-center w3-xlarge w3-padding">
 | 
			
		||||
						<a
 | 
			
		||||
							class="w3-button w3-light-gray"
 | 
			
		||||
							href="https://scuttlebutt.nz/"
 | 
			
		||||
							target="_blank"
 | 
			
		||||
							>See scuttlebutt.nz</a
 | 
			
		||||
						>
 | 
			
		||||
						<button class="w3-button w3-yellow proceed">Got It</button>
 | 
			
		||||
					</footer>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="slide w3-gray" style="width: 90%">
 | 
			
		||||
				<div class="w3-card-4 w3-xlarge">
 | 
			
		||||
					<header class="w3-container w3-blue w3-center">
 | 
			
		||||
						<h1>~😎 Let's Talk Tilde Friends ~😎</h1>
 | 
			
		||||
					</header>
 | 
			
		||||
					<div class="w3-container w3-large w3-left-align">
 | 
			
		||||
						<p>
 | 
			
		||||
							Tilde Friends is an application platform that is an application of
 | 
			
		||||
							its own.
 | 
			
		||||
						</p>
 | 
			
		||||
						<ul>
 | 
			
		||||
							<li>
 | 
			
		||||
								This intro is a Tilde Friends app. You can click <b>edit</b> at
 | 
			
		||||
								the top to look under the hood and make changes.
 | 
			
		||||
							</li>
 | 
			
		||||
							<li>
 | 
			
		||||
								It is already possible to make and share new applications using
 | 
			
		||||
								only Tilde Friends and Secure Scuttlebutt without having to set
 | 
			
		||||
								up development environments, configure web servers, register
 | 
			
		||||
								domain names, or pay for hosting services.
 | 
			
		||||
							</li>
 | 
			
		||||
							<li>
 | 
			
		||||
								But it's also set up so that you can't just break an app that
 | 
			
		||||
								everybody is using or do malicious things with personal content.
 | 
			
		||||
								There are <b>protections</b> in place like an operating system.
 | 
			
		||||
								The intent is also for it to be <b>safe</b> to run strange apps
 | 
			
		||||
								without worrying about adverse effects.
 | 
			
		||||
							</li>
 | 
			
		||||
							<li>
 | 
			
		||||
								But this is all a big 🚧work in progress🚧 and
 | 
			
		||||
								<b>experiment</b>. Let's see where it takes us.
 | 
			
		||||
							</li>
 | 
			
		||||
						</ul>
 | 
			
		||||
					</div>
 | 
			
		||||
					<footer class="w3-center w3-xlarge w3-padding">
 | 
			
		||||
						<button class="w3-button w3-yellow proceed">Okay</button>
 | 
			
		||||
					</footer>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="slide w3-gray" style="width: 90%">
 | 
			
		||||
				<div class="w3-card-4 w3-xlarge">
 | 
			
		||||
					<header class="w3-container w3-blue w3-center">
 | 
			
		||||
						<h1>🦀Let's Get this Tilde Friends Party Started🎉</h1>
 | 
			
		||||
					</header>
 | 
			
		||||
					<div class="w3-container w3-large w3-left-align">
 | 
			
		||||
						<p>The button below will take you to the Secure Scuttlebutt app.</p>
 | 
			
		||||
						<ul>
 | 
			
		||||
							<li>
 | 
			
		||||
								Remember:
 | 
			
		||||
								<ol>
 | 
			
		||||
									<li>You are in charge. This is all on your device.</li>
 | 
			
		||||
									<li>
 | 
			
		||||
										Make network connections to exchange messages with others.
 | 
			
		||||
									</li>
 | 
			
		||||
									<li>
 | 
			
		||||
										Follow more accounts to see more content, and block those
 | 
			
		||||
										posting content you'd rather not see.
 | 
			
		||||
									</li>
 | 
			
		||||
									<li>
 | 
			
		||||
										Be respectful, and consider the consequences of what you
 | 
			
		||||
										post.
 | 
			
		||||
									</li>
 | 
			
		||||
									<li>
 | 
			
		||||
										This is all under active development. Exercise patience, and
 | 
			
		||||
										report issues encountered.
 | 
			
		||||
									</li>
 | 
			
		||||
								</ol>
 | 
			
		||||
							</li>
 | 
			
		||||
							<li>
 | 
			
		||||
								To see this tutorial again later, select <b>apps</b> ->
 | 
			
		||||
								<b>Core Apps</b> -> <b>intro</b>.
 | 
			
		||||
							</li>
 | 
			
		||||
						</ul>
 | 
			
		||||
					</div>
 | 
			
		||||
					<footer class="w3-center w3-xlarge w3-padding">
 | 
			
		||||
						<button class="w3-button w3-yellow" id="complete">Let's Go!</button>
 | 
			
		||||
					</footer>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div
 | 
			
		||||
			class="w3-text-white w3-xlarge w3-center w3-flex"
 | 
			
		||||
			style="
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				flex: 0 1;
 | 
			
		||||
				flex-direction: row;
 | 
			
		||||
				align-items: center;
 | 
			
		||||
				gap: 8px;
 | 
			
		||||
			"
 | 
			
		||||
		>
 | 
			
		||||
			<div class="w3-jumbo" id="left" style="flex: 1 0; cursor: pointer">
 | 
			
		||||
				❮
 | 
			
		||||
			</div>
 | 
			
		||||
			<span class="w3-badge dot w3-border w3-hover-yellow"></span>
 | 
			
		||||
			<span class="w3-badge dot w3-border w3-hover-yellow"></span>
 | 
			
		||||
			<span class="w3-badge dot w3-border w3-hover-yellow"></span>
 | 
			
		||||
			<span class="w3-badge dot w3-border w3-hover-yellow"></span>
 | 
			
		||||
			<span class="w3-badge dot w3-border w3-hover-yellow"></span>
 | 
			
		||||
			<div class="w3-jumbo" style="flex: 1 0; cursor: pointer" id="right">
 | 
			
		||||
				❯
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<script type="module">
 | 
			
		||||
			import * as tfrpc from '/static/tfrpc.js';
 | 
			
		||||
 | 
			
		||||
			let index = 0;
 | 
			
		||||
			function set(i) {
 | 
			
		||||
				show(i - index);
 | 
			
		||||
			}
 | 
			
		||||
			function show(delta) {
 | 
			
		||||
				let slides = [...document.getElementsByClassName('slide')];
 | 
			
		||||
				let dots = [...document.getElementsByClassName('dot')];
 | 
			
		||||
				index = (index + delta + slides.length) % slides.length;
 | 
			
		||||
				for (let slide of slides) {
 | 
			
		||||
					slide.style.display =
 | 
			
		||||
						slides.indexOf(slide) == index ? 'block' : 'none';
 | 
			
		||||
				}
 | 
			
		||||
				for (let dot of dots) {
 | 
			
		||||
					if (dots.indexOf(dot) == index) {
 | 
			
		||||
						dot.classList.add('w3-white');
 | 
			
		||||
					} else {
 | 
			
		||||
						dot.classList.remove('w3-white');
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				document.getElementById('left').style.visibility =
 | 
			
		||||
					index == 0 ? 'hidden' : 'visible';
 | 
			
		||||
				document.getElementById('right').style.visibility =
 | 
			
		||||
					index == slides.length - 1 ? 'hidden' : 'visible';
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			let dots = [...document.getElementsByClassName('dot')];
 | 
			
		||||
			for (let dot of dots) {
 | 
			
		||||
				dot.onclick = () => set(dots.indexOf(dot));
 | 
			
		||||
			}
 | 
			
		||||
			for (let button of document.getElementsByClassName('proceed')) {
 | 
			
		||||
				button.onclick = () => show(1);
 | 
			
		||||
			}
 | 
			
		||||
			document.getElementById('left').onclick = () => show(-1);
 | 
			
		||||
			document.getElementById('right').onclick = () => show(1);
 | 
			
		||||
			document.getElementById('complete').onclick = function () {
 | 
			
		||||
				console.log('completing');
 | 
			
		||||
				tfrpc.rpc.complete().finally(function () {
 | 
			
		||||
					console.log('completed');
 | 
			
		||||
					let a = document.createElement('a');
 | 
			
		||||
					a.href = '/~core/ssb/';
 | 
			
		||||
					a.target = '_top';
 | 
			
		||||
					document.body.appendChild(a);
 | 
			
		||||
					a.click();
 | 
			
		||||
				});
 | 
			
		||||
			};
 | 
			
		||||
			window.addEventListener('keyup', function (event) {
 | 
			
		||||
				if (event.key == 'ArrowLeft') {
 | 
			
		||||
					show(-1);
 | 
			
		||||
				} else if (event.key == 'ArrowRight') {
 | 
			
		||||
					show(1);
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			show(0);
 | 
			
		||||
		</script>
 | 
			
		||||
	</body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -1,251 +0,0 @@
 | 
			
		||||
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
 | 
			
		||||
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
 | 
			
		||||
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
 | 
			
		||||
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
 | 
			
		||||
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
 | 
			
		||||
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
 | 
			
		||||
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
 | 
			
		||||
a{background-color:transparent}a:active,a:hover{outline-width:0}
 | 
			
		||||
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
 | 
			
		||||
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
 | 
			
		||||
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
 | 
			
		||||
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
 | 
			
		||||
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
 | 
			
		||||
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
 | 
			
		||||
button,input{overflow:visible}button,select{text-transform:none}
 | 
			
		||||
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
 | 
			
		||||
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
 | 
			
		||||
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
 | 
			
		||||
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
 | 
			
		||||
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
 | 
			
		||||
[type=checkbox],[type=radio]{padding:0}
 | 
			
		||||
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
 | 
			
		||||
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
 | 
			
		||||
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
 | 
			
		||||
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
 | 
			
		||||
/* End extract */
 | 
			
		||||
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
 | 
			
		||||
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
 | 
			
		||||
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
 | 
			
		||||
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
 | 
			
		||||
hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
 | 
			
		||||
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
 | 
			
		||||
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
 | 
			
		||||
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
 | 
			
		||||
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
 | 
			
		||||
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
 | 
			
		||||
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
 | 
			
		||||
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
 | 
			
		||||
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
 | 
			
		||||
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}   
 | 
			
		||||
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
 | 
			
		||||
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
 | 
			
		||||
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
 | 
			
		||||
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
 | 
			
		||||
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
 | 
			
		||||
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
 | 
			
		||||
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
 | 
			
		||||
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
 | 
			
		||||
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
 | 
			
		||||
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
 | 
			
		||||
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
 | 
			
		||||
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
 | 
			
		||||
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
 | 
			
		||||
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
 | 
			
		||||
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
 | 
			
		||||
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
 | 
			
		||||
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
 | 
			
		||||
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
 | 
			
		||||
.w3-main,#main{transition:margin-left .4s}
 | 
			
		||||
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
 | 
			
		||||
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
 | 
			
		||||
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
 | 
			
		||||
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
 | 
			
		||||
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
 | 
			
		||||
.w3-bar .w3-button{white-space:normal}
 | 
			
		||||
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
 | 
			
		||||
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
 | 
			
		||||
.w3-responsive{display:block;overflow-x:auto}
 | 
			
		||||
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
 | 
			
		||||
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
 | 
			
		||||
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
 | 
			
		||||
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
 | 
			
		||||
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
 | 
			
		||||
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
 | 
			
		||||
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
 | 
			
		||||
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
 | 
			
		||||
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
 | 
			
		||||
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
 | 
			
		||||
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
 | 
			
		||||
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
 | 
			
		||||
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
 | 
			
		||||
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
 | 
			
		||||
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
 | 
			
		||||
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
 | 
			
		||||
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
 | 
			
		||||
@media (max-width:1205px){.w3-auto{max-width:95%}}
 | 
			
		||||
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
 | 
			
		||||
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}	
 | 
			
		||||
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
 | 
			
		||||
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
 | 
			
		||||
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
 | 
			
		||||
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
 | 
			
		||||
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
 | 
			
		||||
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
 | 
			
		||||
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
 | 
			
		||||
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
 | 
			
		||||
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
 | 
			
		||||
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
 | 
			
		||||
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
 | 
			
		||||
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
 | 
			
		||||
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
 | 
			
		||||
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
 | 
			
		||||
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
 | 
			
		||||
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
 | 
			
		||||
.w3-display-position{position:absolute}
 | 
			
		||||
.w3-circle{border-radius:50%}
 | 
			
		||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
 | 
			
		||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
 | 
			
		||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
 | 
			
		||||
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
 | 
			
		||||
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
 | 
			
		||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
 | 
			
		||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
 | 
			
		||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
 | 
			
		||||
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
 | 
			
		||||
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
 | 
			
		||||
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
 | 
			
		||||
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
 | 
			
		||||
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
 | 
			
		||||
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
 | 
			
		||||
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
 | 
			
		||||
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
 | 
			
		||||
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
 | 
			
		||||
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
 | 
			
		||||
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
 | 
			
		||||
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
 | 
			
		||||
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
 | 
			
		||||
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
 | 
			
		||||
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
 | 
			
		||||
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
 | 
			
		||||
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
 | 
			
		||||
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
 | 
			
		||||
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
 | 
			
		||||
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
 | 
			
		||||
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
 | 
			
		||||
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
 | 
			
		||||
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
 | 
			
		||||
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
 | 
			
		||||
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
 | 
			
		||||
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
 | 
			
		||||
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
 | 
			
		||||
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
 | 
			
		||||
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
 | 
			
		||||
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
 | 
			
		||||
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
 | 
			
		||||
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
 | 
			
		||||
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
 | 
			
		||||
.w3-left{float:left!important}.w3-right{float:right!important}
 | 
			
		||||
.w3-button:hover{color:#000!important;background-color:#ccc!important}
 | 
			
		||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
 | 
			
		||||
.w3-hover-none:hover{box-shadow:none!important}
 | 
			
		||||
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
 | 
			
		||||
/* Colors */
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
 | 
			
		||||
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
 | 
			
		||||
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
 | 
			
		||||
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
 | 
			
		||||
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
 | 
			
		||||
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
 | 
			
		||||
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
 | 
			
		||||
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
 | 
			
		||||
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
 | 
			
		||||
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
 | 
			
		||||
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
 | 
			
		||||
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
 | 
			
		||||
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
 | 
			
		||||
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
 | 
			
		||||
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
 | 
			
		||||
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
 | 
			
		||||
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
 | 
			
		||||
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
 | 
			
		||||
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
 | 
			
		||||
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
 | 
			
		||||
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
 | 
			
		||||
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
 | 
			
		||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
 | 
			
		||||
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
 | 
			
		||||
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
 | 
			
		||||
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
 | 
			
		||||
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
 | 
			
		||||
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
 | 
			
		||||
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
 | 
			
		||||
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
 | 
			
		||||
.w3-danger{color:#fff!important;background-color:#dd0000!important}
 | 
			
		||||
.w3-note{color:#000!important;background-color:#fff599!important}
 | 
			
		||||
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
 | 
			
		||||
.w3-warning{color:#000!important;background-color:#ffb305!important}
 | 
			
		||||
.w3-success{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
 | 
			
		||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
 | 
			
		||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
 | 
			
		||||
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
 | 
			
		||||
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
 | 
			
		||||
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
 | 
			
		||||
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
 | 
			
		||||
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
 | 
			
		||||
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
 | 
			
		||||
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
 | 
			
		||||
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
 | 
			
		||||
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
 | 
			
		||||
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
 | 
			
		||||
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
 | 
			
		||||
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
 | 
			
		||||
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
 | 
			
		||||
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
 | 
			
		||||
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
 | 
			
		||||
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
 | 
			
		||||
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
 | 
			
		||||
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
 | 
			
		||||
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
 | 
			
		||||
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
 | 
			
		||||
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
 | 
			
		||||
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
 | 
			
		||||
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
 | 
			
		||||
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
 | 
			
		||||
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
 | 
			
		||||
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
 | 
			
		||||
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
 | 
			
		||||
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
 | 
			
		||||
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
 | 
			
		||||
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
 | 
			
		||||
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
 | 
			
		||||
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
 | 
			
		||||
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
 | 
			
		||||
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
 | 
			
		||||
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
 | 
			
		||||
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
 | 
			
		||||
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
 | 
			
		||||
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
 | 
			
		||||
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
 | 
			
		||||
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
 | 
			
		||||
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
 | 
			
		||||
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
 | 
			
		||||
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
 | 
			
		||||
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
 | 
			
		||||
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
 | 
			
		||||
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
 | 
			
		||||
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
 | 
			
		||||
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
 | 
			
		||||
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
 | 
			
		||||
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
 | 
			
		||||
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
 | 
			
		||||
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
 | 
			
		||||
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
 | 
			
		||||
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
 | 
			
		||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
 | 
			
		||||
							
								
								
									
										42
									
								
								apps/issues/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										42
									
								
								apps/journal/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🚪",
 | 
			
		||||
	"previous": "&DJwkqNfYWtW9yBtJQMseEXm7l04Enpi+yAxZulLq9Vk=.sha256"
 | 
			
		||||
	"previous": "&HXCdDG8gGYXElTyEFbg85jqa6lDXNL2ENPIA9UoJNbI=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
async function main() {
 | 
			
		||||
	print(core.url);
 | 
			
		||||
	let host = core.url.match(/.*?\/\/([^:/]*)/)[1];
 | 
			
		||||
	let port = await ssb.port();
 | 
			
		||||
	let id = (await ssb.getServerIdentity()).substring(1).split('.')[0];
 | 
			
		||||
	let room = `net:${host}:${port}~shs:${id}:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
 | 
			
		||||
	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>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								apps/sneaker/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🦀",
 | 
			
		||||
	"previous": "&Hd6CuhhnZIf13PdFJYZBUYLYZO84WdaKvWXLC29M7Ac=.sha256"
 | 
			
		||||
	"previous": "&jAAzd36Nmpw0sRA1Dx9wLiIwGX+q//+S/Han+RLlEOw=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -106,15 +106,6 @@ tfrpc.register(async function sync() {
 | 
			
		||||
tfrpc.register(async function url() {
 | 
			
		||||
	return core.url;
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function globalSettingsGet(key) {
 | 
			
		||||
	return core.globalSettingsGet(key);
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function globalSettingsSet(key, value) {
 | 
			
		||||
	return core.globalSettingsSet(key, value);
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(function isAdministrator() {
 | 
			
		||||
	return core.user?.credentials?.permissions?.administration;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
core.register('onBroadcastsChanged', async function () {
 | 
			
		||||
	await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,23 @@ function get_emojis() {
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function picker(callback, anchor, author, recent) {
 | 
			
		||||
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 async function picker(callback, anchor, author) {
 | 
			
		||||
	let json = await get_emojis();
 | 
			
		||||
	let recent = await get_recent(author);
 | 
			
		||||
 | 
			
		||||
	let div = document.createElement('div');
 | 
			
		||||
	div.id = 'emoji_picker';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								apps/ssb/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -11,7 +11,6 @@ class TfElement extends LitElement {
 | 
			
		||||
			broadcasts: {type: Array},
 | 
			
		||||
			connections: {type: Array},
 | 
			
		||||
			loading: {type: Boolean},
 | 
			
		||||
			loading_about: {type: Number},
 | 
			
		||||
			loaded: {type: Boolean},
 | 
			
		||||
			following: {type: Array},
 | 
			
		||||
			users: {type: Object},
 | 
			
		||||
@@ -21,13 +20,7 @@ class TfElement extends LitElement {
 | 
			
		||||
			channels_latest: {type: Object},
 | 
			
		||||
			guest: {type: Boolean},
 | 
			
		||||
			url: {type: String},
 | 
			
		||||
			private_closed: {type: Object},
 | 
			
		||||
			private_messages: {type: Array},
 | 
			
		||||
			grouped_private_messages: {type: Object},
 | 
			
		||||
			recent_reactions: {type: Array},
 | 
			
		||||
			is_administrator: {type: Boolean},
 | 
			
		||||
			stay_connected: {type: Boolean},
 | 
			
		||||
			progress: {type: Number},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -43,14 +36,11 @@ class TfElement extends LitElement {
 | 
			
		||||
		this.following = [];
 | 
			
		||||
		this.users = {};
 | 
			
		||||
		this.loaded = false;
 | 
			
		||||
		this.loading_about = 0;
 | 
			
		||||
		this.channels = [];
 | 
			
		||||
		this.channels_unread = {};
 | 
			
		||||
		this.channels_latest = {};
 | 
			
		||||
		this.loading_latest = 0;
 | 
			
		||||
		this.loading_latest_scheduled = 0;
 | 
			
		||||
		this.recent_reactions = [];
 | 
			
		||||
		this.private_closed = {};
 | 
			
		||||
		tfrpc.rpc.getBroadcasts().then((b) => {
 | 
			
		||||
			self.broadcasts = b || [];
 | 
			
		||||
		});
 | 
			
		||||
@@ -60,7 +50,6 @@ class TfElement extends LitElement {
 | 
			
		||||
		tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
 | 
			
		||||
		tfrpc.register(function hashChanged(hash) {
 | 
			
		||||
			self.set_hash(hash);
 | 
			
		||||
			self.reset_progress();
 | 
			
		||||
		});
 | 
			
		||||
		tfrpc.register(async function notifyNewMessage(id) {
 | 
			
		||||
			await self.fetch_new_message(id);
 | 
			
		||||
@@ -80,30 +69,13 @@ class TfElement extends LitElement {
 | 
			
		||||
	async initial_load() {
 | 
			
		||||
		let whoami = await tfrpc.rpc.getActiveIdentity();
 | 
			
		||||
		let ids = (await tfrpc.rpc.getIdentities()) || [];
 | 
			
		||||
		this.is_administrator = await tfrpc.rpc.isAdministrator();
 | 
			
		||||
		this.stay_connected =
 | 
			
		||||
			this.is_administrator &&
 | 
			
		||||
			(await tfrpc.rpc.globalSettingsGet('stay_connected'));
 | 
			
		||||
		this.url = await tfrpc.rpc.url();
 | 
			
		||||
		this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
 | 
			
		||||
		this.guest = !this.whoami?.length;
 | 
			
		||||
		this.ids = ids;
 | 
			
		||||
		let private_closed =
 | 
			
		||||
			(await tfrpc.rpc.databaseGet('private_closed')) ?? '{}';
 | 
			
		||||
		this.private_closed = JSON.parse(private_closed);
 | 
			
		||||
		await this.load_channels();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async close_private_chat(event) {
 | 
			
		||||
		let update = {};
 | 
			
		||||
		update[event.detail.key] = true;
 | 
			
		||||
		this.private_closed = Object.assign(update, this.private_closed);
 | 
			
		||||
		await tfrpc.rpc.databaseSet(
 | 
			
		||||
			'private_closed',
 | 
			
		||||
			JSON.stringify(this.private_closed)
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load_channels() {
 | 
			
		||||
		let channels = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
@@ -151,34 +123,8 @@ class TfElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	visible_private() {
 | 
			
		||||
		if (!this.grouped_private_messages || !this.private_closed) {
 | 
			
		||||
			return [];
 | 
			
		||||
		}
 | 
			
		||||
		let self = this;
 | 
			
		||||
		return Object.fromEntries(
 | 
			
		||||
			Object.entries(this.grouped_private_messages).filter(([key, value]) => {
 | 
			
		||||
				let channel = '🔐' + [...new Set(JSON.parse(key))].sort().join(',');
 | 
			
		||||
				let grouped_latest = Math.max(...value.map((x) => x.rowid));
 | 
			
		||||
				return (
 | 
			
		||||
					!self.private_closed[key] ||
 | 
			
		||||
					self.channels_unread[channel] === undefined ||
 | 
			
		||||
					grouped_latest > self.channels_unread[channel]
 | 
			
		||||
				);
 | 
			
		||||
			})
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	next_channel(delta) {
 | 
			
		||||
		let channel_names = [
 | 
			
		||||
			'',
 | 
			
		||||
			'@',
 | 
			
		||||
			'👍',
 | 
			
		||||
			...Object.keys(this.visible_private())
 | 
			
		||||
				.sort()
 | 
			
		||||
				.map((x) => '🔐' + JSON.parse(x).join(',')),
 | 
			
		||||
			...this.channels.map((x) => '#' + x),
 | 
			
		||||
		];
 | 
			
		||||
		let channel_names = ['', '@', '🔐', ...this.channels.map((x) => '#' + x)];
 | 
			
		||||
		let index = channel_names.indexOf(this.hash.substring(1));
 | 
			
		||||
		index = index != -1 ? index + delta : 0;
 | 
			
		||||
		tfrpc.rpc.setHash(
 | 
			
		||||
@@ -203,9 +149,8 @@ class TfElement extends LitElement {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async fetch_about(following, users) {
 | 
			
		||||
		this.loading_about++;
 | 
			
		||||
		let ids = Object.keys(following).sort();
 | 
			
		||||
		const k_cache_version = 3;
 | 
			
		||||
		const k_cache_version = 1;
 | 
			
		||||
		let cache = await tfrpc.rpc.databaseGet('about');
 | 
			
		||||
		let original_cache = cache;
 | 
			
		||||
		cache = cache ? JSON.parse(cache) : {};
 | 
			
		||||
@@ -213,86 +158,81 @@ class TfElement extends LitElement {
 | 
			
		||||
			cache = {
 | 
			
		||||
				version: k_cache_version,
 | 
			
		||||
				about: {},
 | 
			
		||||
				last_row_id: 0,
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let ids_out_of_date = ids.filter(
 | 
			
		||||
			(x) =>
 | 
			
		||||
				(users[x]?.seq && !cache.about[x]?.seq) ||
 | 
			
		||||
				(users[x]?.seq && users[x]?.seq > cache.about[x].seq)
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		let max_row_id = (
 | 
			
		||||
			await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					SELECT MAX(rowid) AS max_row_id FROM messages
 | 
			
		||||
				`,
 | 
			
		||||
				[]
 | 
			
		||||
			)
 | 
			
		||||
		)[0].max_row_id;
 | 
			
		||||
		for (let id of Object.keys(cache.about)) {
 | 
			
		||||
			if (ids.indexOf(id) == -1) {
 | 
			
		||||
				delete cache.about[id];
 | 
			
		||||
			} else {
 | 
			
		||||
				users[id] = Object.assign(cache.about[id], users[id] || {});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		console.log(
 | 
			
		||||
			'loading about for',
 | 
			
		||||
			ids.length,
 | 
			
		||||
			'accounts',
 | 
			
		||||
			ids_out_of_date.length,
 | 
			
		||||
			'out of date'
 | 
			
		||||
		let abouts = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
				SELECT
 | 
			
		||||
					messages.author, json(messages.content) AS content, messages.sequence
 | 
			
		||||
				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.author, json(messages.content) AS content, messages.sequence
 | 
			
		||||
				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,
 | 
			
		||||
			]
 | 
			
		||||
		);
 | 
			
		||||
		if (ids_out_of_date.length) {
 | 
			
		||||
			try {
 | 
			
		||||
				let rows = await tfrpc.rpc.query(
 | 
			
		||||
					`
 | 
			
		||||
						SELECT all_abouts.author, json(json_group_object(all_abouts.key, all_abouts.value)) AS about
 | 
			
		||||
						FROM (
 | 
			
		||||
							SELECT
 | 
			
		||||
								messages.author,
 | 
			
		||||
								fields.key,
 | 
			
		||||
								RANK() OVER (PARTITION BY messages.author, fields.key ORDER BY messages.sequence DESC) AS rank,
 | 
			
		||||
								fields.value
 | 
			
		||||
							FROM messages JOIN json_each(messages.content) AS fields
 | 
			
		||||
							WHERE
 | 
			
		||||
								messages.content ->> '$.type' = 'about' AND
 | 
			
		||||
								messages.content ->> '$.about' = messages.author AND
 | 
			
		||||
								NOT fields.key IN ('about', 'type')) all_abouts
 | 
			
		||||
						JOIN json_each(?) AS following ON all_abouts.author = following.value
 | 
			
		||||
						WHERE rank = 1
 | 
			
		||||
						GROUP BY all_abouts.author
 | 
			
		||||
					`,
 | 
			
		||||
					[JSON.stringify(ids_out_of_date)]
 | 
			
		||||
		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
 | 
			
		||||
				);
 | 
			
		||||
				users = users || {};
 | 
			
		||||
				for (let row of rows) {
 | 
			
		||||
					users[row.author] = Object.assign(
 | 
			
		||||
						users[row.author] || {},
 | 
			
		||||
						JSON.parse(row.about)
 | 
			
		||||
					);
 | 
			
		||||
					cache.about[row.author] = Object.assign(
 | 
			
		||||
						{seq: users[row.author].seq},
 | 
			
		||||
						JSON.parse(row.about)
 | 
			
		||||
					);
 | 
			
		||||
				}
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				console.log(e);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for (let id of ids_out_of_date) {
 | 
			
		||||
			if (!cache.about[id]?.seq) {
 | 
			
		||||
				cache.about[id] = Object.assign(cache.about[id] ?? {}, {
 | 
			
		||||
					seq: users[id]?.seq ?? 0,
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.loading_about--;
 | 
			
		||||
 | 
			
		||||
		cache.last_row_id = max_row_id;
 | 
			
		||||
		let new_cache = JSON.stringify(cache);
 | 
			
		||||
		if (new_cache != original_cache) {
 | 
			
		||||
		if (new_cache !== original_cache) {
 | 
			
		||||
			let start_time = new Date();
 | 
			
		||||
			tfrpc.rpc.databaseSet('about', new_cache).then(function () {
 | 
			
		||||
				console.log('saving about took', (new Date() - start_time) / 1000);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		users = users || {};
 | 
			
		||||
		for (let id of Object.keys(cache.about)) {
 | 
			
		||||
			users[id] = Object.assign(
 | 
			
		||||
				{follow_depth: following[id]?.d},
 | 
			
		||||
				users[id] || {},
 | 
			
		||||
				cache.about[id]
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
		return Object.assign({}, users);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -352,7 +292,11 @@ class TfElement extends LitElement {
 | 
			
		||||
				ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
 | 
			
		||||
			}
 | 
			
		||||
			for (let i = cache.range[0]; i >= 0; i -= k_chunk_size) {
 | 
			
		||||
				ranges.push([Math.max(i - k_chunk_size, 0), i, false]);
 | 
			
		||||
				ranges.push([
 | 
			
		||||
					Math.max(i - k_chunk_size, 0),
 | 
			
		||||
					Math.min(cache.range[0], i + k_chunk_size),
 | 
			
		||||
					false,
 | 
			
		||||
				]);
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			for (let i = 0; i < latest; i += k_chunk_size) {
 | 
			
		||||
@@ -368,7 +312,7 @@ class TfElement extends LitElement {
 | 
			
		||||
							messages.rowid > ?1 AND
 | 
			
		||||
							messages.rowid <= ?2 AND
 | 
			
		||||
							json(messages.content) LIKE '"%'
 | 
			
		||||
						ORDER BY messages.rowid DESC
 | 
			
		||||
						ORDER BY sequence DESC
 | 
			
		||||
					`,
 | 
			
		||||
				[range[0], range[1]]
 | 
			
		||||
			);
 | 
			
		||||
@@ -394,125 +338,52 @@ class TfElement extends LitElement {
 | 
			
		||||
		return [cache.latest, cache.messages];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async query_timed(sql, args) {
 | 
			
		||||
		let start = new Date();
 | 
			
		||||
		let result = await tfrpc.rpc.query(sql, args);
 | 
			
		||||
		let end = new Date();
 | 
			
		||||
		console.log((end - start) / 1000, sql.replaceAll(/\s+/g, ' ').trim());
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async group_private_messages(messages) {
 | 
			
		||||
		let groups = {};
 | 
			
		||||
		let result = await this.decrypt(
 | 
			
		||||
			await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
				SELECT messages.rowid, messages.id, author, timestamp, json(content) AS content
 | 
			
		||||
				FROM messages
 | 
			
		||||
				JOIN json_each(?) AS ids
 | 
			
		||||
				WHERE messages.id = ids.value
 | 
			
		||||
				ORDER BY timestamp DESC
 | 
			
		||||
			`,
 | 
			
		||||
				[JSON.stringify(messages)]
 | 
			
		||||
			)
 | 
			
		||||
		);
 | 
			
		||||
		for (let message of result) {
 | 
			
		||||
			let key = JSON.stringify(
 | 
			
		||||
				[
 | 
			
		||||
					...new Set(
 | 
			
		||||
						message?.decrypted?.recps?.filter((x) => x != this.whoami)
 | 
			
		||||
					),
 | 
			
		||||
				].sort() ?? []
 | 
			
		||||
			);
 | 
			
		||||
			if (!groups[key]) {
 | 
			
		||||
				groups[key] = [];
 | 
			
		||||
			}
 | 
			
		||||
			groups[key].push(message);
 | 
			
		||||
		}
 | 
			
		||||
		return groups;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load_channels_latest(following) {
 | 
			
		||||
		let start_time = new Date();
 | 
			
		||||
		let latest_private = this.get_latest_private(following);
 | 
			
		||||
		const k_args = [
 | 
			
		||||
			JSON.stringify(this.channels),
 | 
			
		||||
			JSON.stringify(following),
 | 
			
		||||
			'"' + this.whoami.replace('"', '""') + '"',
 | 
			
		||||
			this.whoami,
 | 
			
		||||
		];
 | 
			
		||||
		let channels = (
 | 
			
		||||
			await Promise.all([
 | 
			
		||||
				this.query_timed(
 | 
			
		||||
					`
 | 
			
		||||
					SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
 | 
			
		||||
					JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value
 | 
			
		||||
					JOIN json_each(?2) AS following ON messages.author = following.value
 | 
			
		||||
					WHERE
 | 
			
		||||
						messages.content ->> 'type' = 'post' AND
 | 
			
		||||
						messages.content ->> 'root' IS NULL AND
 | 
			
		||||
						messages.author != ?4
 | 
			
		||||
					GROUP by channel
 | 
			
		||||
				`,
 | 
			
		||||
					k_args
 | 
			
		||||
				),
 | 
			
		||||
				this.query_timed(
 | 
			
		||||
					`
 | 
			
		||||
					SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
 | 
			
		||||
					JOIN messages_refs ON messages.id = messages_refs.message
 | 
			
		||||
					JOIN json_each(?1) AS channels ON messages_refs.ref = '#' || channels.value
 | 
			
		||||
					JOIN json_each(?2) AS following ON messages.author = following.value
 | 
			
		||||
					WHERE
 | 
			
		||||
						messages.content ->> 'type' = 'post' AND
 | 
			
		||||
						messages.content ->> 'root' IS NULL AND
 | 
			
		||||
						messages.author != ?4
 | 
			
		||||
					GROUP by channel
 | 
			
		||||
				`,
 | 
			
		||||
					k_args
 | 
			
		||||
				),
 | 
			
		||||
				this.query_timed(
 | 
			
		||||
					`
 | 
			
		||||
					SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
 | 
			
		||||
					JOIN json_each(?2) AS following ON messages.author = following.value
 | 
			
		||||
					WHERE
 | 
			
		||||
						messages.content ->> 'type' = 'post' AND
 | 
			
		||||
						messages.content ->> 'root' IS NULL AND
 | 
			
		||||
						messages.author != ?4
 | 
			
		||||
				`,
 | 
			
		||||
					k_args
 | 
			
		||||
				),
 | 
			
		||||
				this.query_timed(
 | 
			
		||||
					`
 | 
			
		||||
					SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
 | 
			
		||||
					JOIN messages ON messages.rowid = messages_fts.rowid
 | 
			
		||||
					JOIN json_each(?2) AS following ON messages.author = following.value
 | 
			
		||||
					WHERE messages.author != ?4
 | 
			
		||||
				`,
 | 
			
		||||
					k_args
 | 
			
		||||
				),
 | 
			
		||||
			])
 | 
			
		||||
		).flat();
 | 
			
		||||
		let latest = {};
 | 
			
		||||
		for (let row of channels) {
 | 
			
		||||
			if (!latest[row.channel]) {
 | 
			
		||||
				latest[row.channel] = row.rowid;
 | 
			
		||||
			} else {
 | 
			
		||||
				latest[row.channel] = Math.max(row.rowid, latest[row.channel]);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		this.channels_latest = latest;
 | 
			
		||||
		let channels = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
			SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
 | 
			
		||||
			JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value
 | 
			
		||||
			JOIN json_each(?2) AS following ON messages.author = following.value
 | 
			
		||||
			WHERE
 | 
			
		||||
				messages.content ->> 'type' = 'post' AND
 | 
			
		||||
				messages.content ->> 'root' IS NULL AND
 | 
			
		||||
				messages.author != ?4
 | 
			
		||||
			GROUP by channel
 | 
			
		||||
			UNION
 | 
			
		||||
			SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
 | 
			
		||||
			JOIN json_each(?2) AS following ON messages.author = following.value
 | 
			
		||||
			WHERE
 | 
			
		||||
				messages.content ->> 'type' = 'post' AND
 | 
			
		||||
				messages.content ->> 'root' IS NULL AND
 | 
			
		||||
				messages.author != ?4
 | 
			
		||||
			UNION
 | 
			
		||||
			SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
 | 
			
		||||
			JOIN messages ON messages.rowid = messages_fts.rowid
 | 
			
		||||
			JOIN json_each(?2) AS following ON messages.author = following.value
 | 
			
		||||
			WHERE messages.author != ?4
 | 
			
		||||
		`,
 | 
			
		||||
			[
 | 
			
		||||
				JSON.stringify(this.channels),
 | 
			
		||||
				JSON.stringify(following),
 | 
			
		||||
				'"' + this.whoami.replace('"', '""') + '"',
 | 
			
		||||
				this.whoami,
 | 
			
		||||
			]
 | 
			
		||||
		);
 | 
			
		||||
		this.channels_latest = Object.fromEntries(
 | 
			
		||||
			channels.map((x) => [x.channel, x.rowid])
 | 
			
		||||
		);
 | 
			
		||||
		console.log('channels took', (new Date() - start_time) / 1000.0);
 | 
			
		||||
		let self = this;
 | 
			
		||||
		start_time = new Date();
 | 
			
		||||
		latest_private.then(async function (latest) {
 | 
			
		||||
		latest_private.then(function (latest) {
 | 
			
		||||
			self.channels_latest = Object.assign({}, self.channels_latest, {
 | 
			
		||||
				'🔐': latest[0],
 | 
			
		||||
			});
 | 
			
		||||
			console.log('private took', (new Date() - start_time) / 1000.0);
 | 
			
		||||
			console.log(latest);
 | 
			
		||||
			self.private_messages = latest[1];
 | 
			
		||||
			self.grouped_private_messages = await self.group_private_messages(
 | 
			
		||||
				latest[1]
 | 
			
		||||
			);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -521,28 +392,7 @@ class TfElement extends LitElement {
 | 
			
		||||
		this.schedule_load_latest();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	reset_progress() {
 | 
			
		||||
		if (this.progress === undefined) {
 | 
			
		||||
			this._progress_start = new Date();
 | 
			
		||||
			requestAnimationFrame(this.update_progress.bind(this));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	update_progress() {
 | 
			
		||||
		if (
 | 
			
		||||
			!this.loading_latest &&
 | 
			
		||||
			!this.loading_latest_scheduled &&
 | 
			
		||||
			!this.shadowRoot.getElementById('tf-tab-news')?.is_loading()
 | 
			
		||||
		) {
 | 
			
		||||
			this.progress = undefined;
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		this.progress = (new Date() - this._progress_start).valueOf();
 | 
			
		||||
		requestAnimationFrame(this.update_progress.bind(this));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	schedule_load_latest() {
 | 
			
		||||
		this.reset_progress();
 | 
			
		||||
		if (!this.loading_latest) {
 | 
			
		||||
			this.shadowRoot.getElementById('tf-tab-news')?.load_latest();
 | 
			
		||||
			this.load();
 | 
			
		||||
@@ -562,59 +412,43 @@ class TfElement extends LitElement {
 | 
			
		||||
			[JSON.stringify(Object.keys(users))]
 | 
			
		||||
		);
 | 
			
		||||
		for (let row of info) {
 | 
			
		||||
			users[row.author] = Object.assign(users[row.author], {
 | 
			
		||||
				seq: row.max_sequence,
 | 
			
		||||
				ts: row.max_ts,
 | 
			
		||||
			});
 | 
			
		||||
			users[row.author].seq = row.max_seq;
 | 
			
		||||
			users[row.author].ts = row.max_ts;
 | 
			
		||||
		}
 | 
			
		||||
		return users;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load_recent_reactions() {
 | 
			
		||||
		this.recent_reactions = (
 | 
			
		||||
			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
 | 
			
		||||
		`,
 | 
			
		||||
				[this.whoami]
 | 
			
		||||
			)
 | 
			
		||||
		).map((x) => x.value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load() {
 | 
			
		||||
		this.loading_latest = true;
 | 
			
		||||
		this.reset_progress();
 | 
			
		||||
		try {
 | 
			
		||||
			let start_time = new Date();
 | 
			
		||||
			let whoami = this.whoami;
 | 
			
		||||
			let following = await tfrpc.rpc.following([whoami], 2);
 | 
			
		||||
			let old_users = this.users ?? {};
 | 
			
		||||
			let users = {};
 | 
			
		||||
			let by_count = [];
 | 
			
		||||
			for (let [id, v] of Object.entries(following)) {
 | 
			
		||||
				users[id] = Object.assign(
 | 
			
		||||
					{
 | 
			
		||||
						following: v.of,
 | 
			
		||||
						blocking: v.ob,
 | 
			
		||||
						followed: v.if,
 | 
			
		||||
						blocked: v.ib,
 | 
			
		||||
						follow_depth: following[id]?.d,
 | 
			
		||||
					},
 | 
			
		||||
					old_users[id]
 | 
			
		||||
				);
 | 
			
		||||
				users[id] = {
 | 
			
		||||
					following: v.of,
 | 
			
		||||
					blocking: v.ob,
 | 
			
		||||
					followed: v.if,
 | 
			
		||||
					blocked: v.ib,
 | 
			
		||||
				};
 | 
			
		||||
				by_count.push({count: v.of, id: id});
 | 
			
		||||
			}
 | 
			
		||||
			let reactions = this.load_recent_reactions();
 | 
			
		||||
			this.load_channels_latest(Object.keys(following));
 | 
			
		||||
			this.channels_unread = JSON.parse(
 | 
			
		||||
				(await tfrpc.rpc.databaseGet('unread')) ?? '{}'
 | 
			
		||||
			);
 | 
			
		||||
			this.following = Object.keys(following);
 | 
			
		||||
			let about_start_time = new Date();
 | 
			
		||||
			users = await this.fetch_about(following, users);
 | 
			
		||||
			console.log(
 | 
			
		||||
				'about took',
 | 
			
		||||
				(new Date() - about_start_time) / 1000.0,
 | 
			
		||||
				'seconds for',
 | 
			
		||||
				Object.keys(users).length,
 | 
			
		||||
				'users'
 | 
			
		||||
			);
 | 
			
		||||
			start_time = new Date();
 | 
			
		||||
			users = await this.fetch_user_info(users);
 | 
			
		||||
			console.log(
 | 
			
		||||
@@ -623,22 +457,9 @@ class TfElement extends LitElement {
 | 
			
		||||
				'seconds'
 | 
			
		||||
			);
 | 
			
		||||
			this.users = users;
 | 
			
		||||
 | 
			
		||||
			let self = this;
 | 
			
		||||
			this.fetch_about(following, users).then(function (result) {
 | 
			
		||||
				self.users = result;
 | 
			
		||||
				console.log(
 | 
			
		||||
					'about took',
 | 
			
		||||
					(new Date() - about_start_time) / 1000.0,
 | 
			
		||||
					'seconds for',
 | 
			
		||||
					Object.keys(users).length,
 | 
			
		||||
					'users'
 | 
			
		||||
				);
 | 
			
		||||
			});
 | 
			
		||||
			console.log(
 | 
			
		||||
				`load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}`
 | 
			
		||||
			);
 | 
			
		||||
			await reactions;
 | 
			
		||||
			this.whoami = whoami;
 | 
			
		||||
			this.loaded = whoami;
 | 
			
		||||
		} finally {
 | 
			
		||||
@@ -689,21 +510,13 @@ class TfElement extends LitElement {
 | 
			
		||||
					whoami=${this.whoami}
 | 
			
		||||
					.users=${this.users}
 | 
			
		||||
					hash=${this.hash}
 | 
			
		||||
					?loading=${this.loading || this.loading_about != 0}
 | 
			
		||||
					?loading=${this.loading}
 | 
			
		||||
					.channels=${this.channels}
 | 
			
		||||
					.channels_latest=${this.channels_latest}
 | 
			
		||||
					.channels_unread=${this.channels_unread}
 | 
			
		||||
					@channelsetunread=${this.channel_set_unread}
 | 
			
		||||
					@refresh=${this.refresh}
 | 
			
		||||
					@toggle_stay_connected=${this.toggle_stay_connected}
 | 
			
		||||
					@loadmessages=${this.reset_progress}
 | 
			
		||||
					@closeprivatechat=${this.close_private_chat}
 | 
			
		||||
					.connections=${this.connections}
 | 
			
		||||
					.private_messages=${this.private_messages}
 | 
			
		||||
					.grouped_private_messages=${this.visible_private()}
 | 
			
		||||
					.recent_reactions=${this.recent_reactions}
 | 
			
		||||
					?is_administrator=${this.is_administrator}
 | 
			
		||||
					?stay_connected=${this.stay_connected}
 | 
			
		||||
				></tf-tab-news>
 | 
			
		||||
			`;
 | 
			
		||||
		} else if (this.tab === 'connections') {
 | 
			
		||||
@@ -742,7 +555,6 @@ class TfElement extends LitElement {
 | 
			
		||||
	async set_tab(tab) {
 | 
			
		||||
		this.tab = tab;
 | 
			
		||||
		if (tab === 'news') {
 | 
			
		||||
			this.schedule_load_latest();
 | 
			
		||||
			await tfrpc.rpc.setHash('#');
 | 
			
		||||
		} else if (tab === 'connections') {
 | 
			
		||||
			await tfrpc.rpc.setHash('#connections');
 | 
			
		||||
@@ -755,18 +567,6 @@ class TfElement extends LitElement {
 | 
			
		||||
		tfrpc.rpc.sync();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async toggle_stay_connected() {
 | 
			
		||||
		let stay_connected = await tfrpc.rpc.globalSettingsGet('stay_connected');
 | 
			
		||||
		let new_stay_connected = !this.stay_connected;
 | 
			
		||||
		try {
 | 
			
		||||
			if (new_stay_connected != stay_connected) {
 | 
			
		||||
				await tfrpc.rpc.globalSettingsSet('stay_connected', new_stay_connected);
 | 
			
		||||
			}
 | 
			
		||||
		} finally {
 | 
			
		||||
			this.stay_connected = await tfrpc.rpc.globalSettingsGet('stay_connected');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let self = this;
 | 
			
		||||
 | 
			
		||||
@@ -789,26 +589,14 @@ class TfElement extends LitElement {
 | 
			
		||||
				class="w3-bar w3-theme-l1"
 | 
			
		||||
				style="position: static; top: 0; z-index: 10"
 | 
			
		||||
			>
 | 
			
		||||
				${this.is_administrator && self.tab != 'news'
 | 
			
		||||
					? html`
 | 
			
		||||
							<button
 | 
			
		||||
								class=${'w3-bar-item w3-button w3-circle w3-ripple' +
 | 
			
		||||
								(this.connections?.some((x) => x.flags.one_shot)
 | 
			
		||||
									? ' w3-spin'
 | 
			
		||||
									: '')}
 | 
			
		||||
								style="width: 1.5em; height: 1.5em; padding: 8px"
 | 
			
		||||
								@click=${this.refresh}
 | 
			
		||||
							>
 | 
			
		||||
								↻
 | 
			
		||||
							</button>
 | 
			
		||||
							<button
 | 
			
		||||
								class="w3-bar-item w3-button w3-ripple"
 | 
			
		||||
								@click=${this.toggle_stay_connected}
 | 
			
		||||
							>
 | 
			
		||||
								${this.stay_connected ? '🔗' : '⛓️💥'}
 | 
			
		||||
							</button>
 | 
			
		||||
						`
 | 
			
		||||
					: undefined}
 | 
			
		||||
				<button
 | 
			
		||||
					class=${'w3-bar-item w3-button w3-circle w3-ripple' +
 | 
			
		||||
					(this.connections?.some((x) => x.flags.one_shot) ? ' w3-spin' : '')}
 | 
			
		||||
					style="width: 1.5em; height: 1.5em; padding: 8px"
 | 
			
		||||
					@click=${this.refresh}
 | 
			
		||||
				>
 | 
			
		||||
					↻
 | 
			
		||||
				</button>
 | 
			
		||||
				${Object.entries(k_tabs).map(
 | 
			
		||||
					([k, v]) => html`
 | 
			
		||||
						<button
 | 
			
		||||
@@ -848,23 +636,11 @@ class TfElement extends LitElement {
 | 
			
		||||
						Loading...
 | 
			
		||||
					</div>`
 | 
			
		||||
				: this.render_tab();
 | 
			
		||||
		let progress =
 | 
			
		||||
			this.progress !== undefined
 | 
			
		||||
				? html`
 | 
			
		||||
						<div style="position: absolute; width: 100%" id="progress">
 | 
			
		||||
							<div
 | 
			
		||||
								class="w3-theme-l3"
 | 
			
		||||
								style=${`height: 4px; position: absolute; right: ${Math.cos(this.progress / 250) > 0 ? 'auto' : '0'}; width: ${50 * Math.sin(this.progress / 250) + 50}%`}
 | 
			
		||||
							></div>
 | 
			
		||||
						</div>
 | 
			
		||||
					`
 | 
			
		||||
				: undefined;
 | 
			
		||||
		return html`
 | 
			
		||||
			<div
 | 
			
		||||
				style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
 | 
			
		||||
				class="w3-theme-dark"
 | 
			
		||||
			>
 | 
			
		||||
				${progress}
 | 
			
		||||
				<div style="flex: 0 0">${tabs}</div>
 | 
			
		||||
				<div style="flex: 1 1; overflow: auto; contain: layout">
 | 
			
		||||
					${contents}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,6 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
			author: {type: String},
 | 
			
		||||
			channel: {type: String},
 | 
			
		||||
			new_thread: {type: Boolean},
 | 
			
		||||
			recipients: {type: Array},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -92,9 +91,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
				bubbles: true,
 | 
			
		||||
				composed: true,
 | 
			
		||||
				detail: {
 | 
			
		||||
					id:
 | 
			
		||||
						this.branch ??
 | 
			
		||||
						(this.recipients ? this.recipients.join(',') : undefined),
 | 
			
		||||
					id: this.branch,
 | 
			
		||||
					draft: draft,
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
@@ -258,12 +255,10 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let input = document.createElement('input');
 | 
			
		||||
		input.type = 'file';
 | 
			
		||||
		input.addEventListener('change', function (event) {
 | 
			
		||||
			input.parentNode.removeChild(input);
 | 
			
		||||
		input.onchange = function (event) {
 | 
			
		||||
			let file = event.target.files[0];
 | 
			
		||||
			self.add_file(file);
 | 
			
		||||
		});
 | 
			
		||||
		document.body.appendChild(input);
 | 
			
		||||
		};
 | 
			
		||||
		input.click();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -294,7 +289,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	get_values() {
 | 
			
		||||
	firstUpdated() {
 | 
			
		||||
		let values = Object.entries(this.users).map((x) => ({
 | 
			
		||||
			key: x[1].name ?? x[0],
 | 
			
		||||
			value: x[0],
 | 
			
		||||
@@ -310,15 +305,11 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
				values
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
		return values;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	firstUpdated() {
 | 
			
		||||
		let tribute = new Tribute({
 | 
			
		||||
			iframe: this.shadowRoot,
 | 
			
		||||
			collection: [
 | 
			
		||||
				{
 | 
			
		||||
					values: this.get_values(),
 | 
			
		||||
					values: values,
 | 
			
		||||
					selectTemplate: function (item) {
 | 
			
		||||
						return item
 | 
			
		||||
							? `[@${item.original.key}](${item.original.value})`
 | 
			
		||||
@@ -337,7 +328,6 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
			],
 | 
			
		||||
		});
 | 
			
		||||
		tribute.attach(this.renderRoot.getElementById('edit'));
 | 
			
		||||
		this._tribute = tribute;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updated() {
 | 
			
		||||
@@ -348,7 +338,6 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
			preview.innerHTML = this.process_text(edit.innerText);
 | 
			
		||||
			this.last_updated_text = edit.innerText;
 | 
			
		||||
		}
 | 
			
		||||
		this._tribute.collection[0].values = this.get_values();
 | 
			
		||||
		let encrypt = this.renderRoot.getElementById('encrypt_to');
 | 
			
		||||
		if (encrypt) {
 | 
			
		||||
			let tribute = new Tribute({
 | 
			
		||||
@@ -368,7 +357,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
	remove_mention(id) {
 | 
			
		||||
		let draft = this.get_draft();
 | 
			
		||||
		delete draft.mentions[id];
 | 
			
		||||
		setTimeout(() => this.notify(draft), 0);
 | 
			
		||||
		setTimeout(() => this.notify(), 0);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_mention(mention) {
 | 
			
		||||
@@ -455,15 +444,12 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
			self.apps = await tfrpc.rpc.apps();
 | 
			
		||||
		}
 | 
			
		||||
		if (!this.apps) {
 | 
			
		||||
			return html`<button
 | 
			
		||||
				class="w3-button w3-bar-item w3-theme-d1"
 | 
			
		||||
				@click=${attach_app}
 | 
			
		||||
			>
 | 
			
		||||
			return html`<button class="w3-button w3-theme-d1" @click=${attach_app}>
 | 
			
		||||
				Attach App
 | 
			
		||||
			</button>`;
 | 
			
		||||
		} else {
 | 
			
		||||
			return html`<button
 | 
			
		||||
				class="w3-button w3-bar-item w3-theme-d1"
 | 
			
		||||
				class="w3-button w3-theme-d1"
 | 
			
		||||
				@click=${() => (this.apps = null)}
 | 
			
		||||
			>
 | 
			
		||||
				Discard App
 | 
			
		||||
@@ -484,9 +470,18 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		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>
 | 
			
		||||
			`;
 | 
			
		||||
		} else {
 | 
			
		||||
			return html`
 | 
			
		||||
				<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input>
 | 
			
		||||
				<label for="cw">CW</label>
 | 
			
		||||
			`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -505,17 +500,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	get_draft() {
 | 
			
		||||
		let key =
 | 
			
		||||
			this.branch ||
 | 
			
		||||
			(this.recipients ? this.recipients.join(',') : undefined) ||
 | 
			
		||||
			'';
 | 
			
		||||
		let draft = this.drafts[key] || {};
 | 
			
		||||
		if (this.recipients && !draft.encrypt_to?.length) {
 | 
			
		||||
			draft.encrypt_to = [
 | 
			
		||||
				...new Set(this.recipients).union(new Set(draft.encrypt_to ?? [])),
 | 
			
		||||
			];
 | 
			
		||||
		}
 | 
			
		||||
		return draft;
 | 
			
		||||
		return this.drafts[this.branch || ''] || {};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	update_encrypt(event) {
 | 
			
		||||
@@ -537,7 +522,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		return html`
 | 
			
		||||
			<div style="display: flex; flex-direction: row; width: 100%">
 | 
			
		||||
				<label for="encrypt_to">🔐 To:</label>
 | 
			
		||||
				<input type="text" id="encrypt_to" class="w3-input w3-theme-d1 w3-border" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
 | 
			
		||||
				<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>
 | 
			
		||||
@@ -559,31 +544,6 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		this.requestUpdate();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	toggle_menu(event) {
 | 
			
		||||
		event.srcElement.parentNode
 | 
			
		||||
			.querySelector('.w3-dropdown-content')
 | 
			
		||||
			.classList.toggle('w3-show');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	connectedCallback() {
 | 
			
		||||
		super.connectedCallback();
 | 
			
		||||
		this._click_callback = this.document_click.bind(this);
 | 
			
		||||
		document.body.addEventListener('mouseup', this._click_callback);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	disconnectedCallback() {
 | 
			
		||||
		super.disconnectedCallback();
 | 
			
		||||
		document.body.removeEventListener('mouseup', this._click_callback);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	document_click(event) {
 | 
			
		||||
		let content = this.renderRoot.querySelector('.w3-dropdown-content');
 | 
			
		||||
		let target = event.target;
 | 
			
		||||
		if (content && !content.contains(target)) {
 | 
			
		||||
			content.classList.remove('w3-show');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let draft = self.get_draft();
 | 
			
		||||
@@ -597,10 +557,10 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
			draft.encrypt_to !== undefined
 | 
			
		||||
				? undefined
 | 
			
		||||
				: html`<button
 | 
			
		||||
						class="w3-button w3-bar-item w3-theme-d1"
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						@click=${() => this.set_encrypt([])}
 | 
			
		||||
					>
 | 
			
		||||
						🔐 Encrypt
 | 
			
		||||
						🔐
 | 
			
		||||
					</button>`;
 | 
			
		||||
		let result = html`
 | 
			
		||||
			<style>
 | 
			
		||||
@@ -621,7 +581,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
						: undefined}
 | 
			
		||||
					${this.render_encrypt()}
 | 
			
		||||
				</header>
 | 
			
		||||
				<div class="w3-container" style="padding: 0 0 16px 0">
 | 
			
		||||
				<div class="w3-container w3-padding-small">
 | 
			
		||||
					<div class="w3-half">
 | 
			
		||||
						<span
 | 
			
		||||
							class="w3-input w3-theme-d1 w3-border"
 | 
			
		||||
@@ -642,7 +602,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
				${Object.values(draft.mentions || {}).map((x) =>
 | 
			
		||||
					self.render_mention(x)
 | 
			
		||||
				)}
 | 
			
		||||
				<footer>
 | 
			
		||||
				<footer class="w3-container">
 | 
			
		||||
					${this.render_attach_app()} ${this.render_content_warning()}
 | 
			
		||||
					${this.render_new_thread()}
 | 
			
		||||
					<button
 | 
			
		||||
@@ -652,43 +612,13 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
					>
 | 
			
		||||
						Submit
 | 
			
		||||
					</button>
 | 
			
		||||
					<div class="w3-dropdown-click">
 | 
			
		||||
						<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}>
 | 
			
		||||
							⚙️
 | 
			
		||||
						</button>
 | 
			
		||||
						<div class="w3-dropdown-content w3-bar-block">
 | 
			
		||||
							${this.get_draft().content_warning === undefined
 | 
			
		||||
								? html`
 | 
			
		||||
										<button
 | 
			
		||||
											class="w3-button w3-bar-item w3-theme-d1"
 | 
			
		||||
											@click=${() => self.set_content_warning('')}
 | 
			
		||||
										>
 | 
			
		||||
											Add Content Warning
 | 
			
		||||
										</button>
 | 
			
		||||
									`
 | 
			
		||||
								: html`
 | 
			
		||||
										<button
 | 
			
		||||
											class="w3-button w3-bar-item w3-theme-d1"
 | 
			
		||||
											@click=${() => self.set_content_warning(undefined)}
 | 
			
		||||
										>
 | 
			
		||||
											Remove Content Warning
 | 
			
		||||
										</button>
 | 
			
		||||
									`}
 | 
			
		||||
							<button
 | 
			
		||||
								class="w3-button w3-bar-item w3-theme-d1"
 | 
			
		||||
								@click=${this.attach}
 | 
			
		||||
							>
 | 
			
		||||
								Attach
 | 
			
		||||
							</button>
 | 
			
		||||
							${this.render_attach_app_button()} ${encrypt}
 | 
			
		||||
							<button
 | 
			
		||||
								class="w3-button w3-bar-item w3-theme-d1"
 | 
			
		||||
								@click=${this.discard}
 | 
			
		||||
							>
 | 
			
		||||
								Discard
 | 
			
		||||
							</button>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.attach}>
 | 
			
		||||
						Attach
 | 
			
		||||
					</button>
 | 
			
		||||
					${this.render_attach_app_button()} ${encrypt}
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.discard}>
 | 
			
		||||
						Discard
 | 
			
		||||
					</button>
 | 
			
		||||
				</footer>
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,4 @@
 | 
			
		||||
import {
 | 
			
		||||
	LitElement,
 | 
			
		||||
	css,
 | 
			
		||||
	html,
 | 
			
		||||
	repeat,
 | 
			
		||||
	render,
 | 
			
		||||
	unsafeHTML,
 | 
			
		||||
} from './lit-all.min.js';
 | 
			
		||||
import {LitElement, html, repeat, render, unsafeHTML} from './lit-all.min.js';
 | 
			
		||||
import * as tfrpc from '/static/tfrpc.js';
 | 
			
		||||
import * as tfutils from './tf-utils.js';
 | 
			
		||||
import * as emojis from './emojis.js';
 | 
			
		||||
@@ -23,7 +16,6 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
			expanded: {type: Object},
 | 
			
		||||
			channel: {type: String},
 | 
			
		||||
			channel_unread: {type: Number},
 | 
			
		||||
			recent_reactions: {type: Array},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -39,26 +31,6 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
		this.format = 'message';
 | 
			
		||||
		this.expanded = {};
 | 
			
		||||
		this.channel_unread = -1;
 | 
			
		||||
		this.recent_reactions = [];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	connectedCallback() {
 | 
			
		||||
		super.connectedCallback();
 | 
			
		||||
		this._click_callback = this.document_click.bind(this);
 | 
			
		||||
		document.body.addEventListener('mouseup', this._click_callback);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	disconnectedCallback() {
 | 
			
		||||
		super.disconnectedCallback();
 | 
			
		||||
		document.body.removeEventListener('mouseup', this._click_callback);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	document_click(event) {
 | 
			
		||||
		let content = this.renderRoot.querySelector('.w3-dropdown-content');
 | 
			
		||||
		let target = event.target;
 | 
			
		||||
		if (content && !content.contains(target)) {
 | 
			
		||||
			content.classList.remove('w3-show');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	show_reply() {
 | 
			
		||||
@@ -93,27 +65,20 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
 | 
			
		||||
	render_votes() {
 | 
			
		||||
		function normalize_expression(expression) {
 | 
			
		||||
			if (
 | 
			
		||||
				expression === 'Unlike' ||
 | 
			
		||||
				expression === 'unlike' ||
 | 
			
		||||
				expression == 'undig'
 | 
			
		||||
			) {
 | 
			
		||||
			if (expression === 'Like' || !expression) {
 | 
			
		||||
				return '👍';
 | 
			
		||||
			} else if (expression === 'Unlike') {
 | 
			
		||||
				return '👎';
 | 
			
		||||
			} else if (expression === 'heart') {
 | 
			
		||||
				return '❤️';
 | 
			
		||||
			} else if (
 | 
			
		||||
				(expression ?? '').split('').every((x) => x.charCodeAt(0) < 256)
 | 
			
		||||
			) {
 | 
			
		||||
				return '👍';
 | 
			
		||||
			} else {
 | 
			
		||||
				return expression;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if (this.message?.votes?.length) {
 | 
			
		||||
			return html` <footer class="w3-container">
 | 
			
		||||
			return html` <div class="w3-container">
 | 
			
		||||
				<div
 | 
			
		||||
					class="w3-button w3-bar"
 | 
			
		||||
					style="padding: 0"
 | 
			
		||||
					class="w3-button w3-bar w3-padding-small"
 | 
			
		||||
					@click=${this.show_reactions}
 | 
			
		||||
				>
 | 
			
		||||
					${(this.message.votes || []).map(
 | 
			
		||||
@@ -128,7 +93,7 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
						`
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
			</footer>`;
 | 
			
		||||
			</div>`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -171,12 +136,7 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	react(event) {
 | 
			
		||||
		emojis.picker(
 | 
			
		||||
			(x) => this.vote(x),
 | 
			
		||||
			null,
 | 
			
		||||
			this.whoami,
 | 
			
		||||
			this.recent_reactions
 | 
			
		||||
		);
 | 
			
		||||
		emojis.picker((x) => this.vote(x), null, this.whoami);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	show_image(link) {
 | 
			
		||||
@@ -191,12 +151,12 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
		div.style.display = 'grid';
 | 
			
		||||
		let img = document.createElement('img');
 | 
			
		||||
		img.src = link;
 | 
			
		||||
		img.style.maxWidth = '100vw';
 | 
			
		||||
		img.style.maxHeight = '100vh';
 | 
			
		||||
		img.style.maxWidth = '100%';
 | 
			
		||||
		img.style.maxHeight = '100%';
 | 
			
		||||
		img.style.display = 'block';
 | 
			
		||||
		img.style.margin = 'auto';
 | 
			
		||||
		img.style.objectFit = 'contain';
 | 
			
		||||
		img.style.width = '100vw';
 | 
			
		||||
		img.style.width = '100%';
 | 
			
		||||
		div.appendChild(img);
 | 
			
		||||
		function image_close(event) {
 | 
			
		||||
			document.body.removeChild(div);
 | 
			
		||||
@@ -310,69 +270,53 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
		return total;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expanded_key() {
 | 
			
		||||
		return this.message?.id || this.messages?.map((x) => x.id).join(':');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	set_expanded(expanded, tag) {
 | 
			
		||||
		let key = this.expanded_key();
 | 
			
		||||
		this.dispatchEvent(
 | 
			
		||||
			new CustomEvent('tf-expand', {
 | 
			
		||||
				bubbles: true,
 | 
			
		||||
				composed: true,
 | 
			
		||||
				detail: {id: key + (tag || ''), expanded: expanded},
 | 
			
		||||
				detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded},
 | 
			
		||||
			})
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	toggle_expanded(tag) {
 | 
			
		||||
		let key = this.expanded_key();
 | 
			
		||||
		this.set_expanded(!this.expanded[key + (tag || '')], tag);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	is_expanded(tag) {
 | 
			
		||||
		let key = this.expanded_key();
 | 
			
		||||
		return this.expanded[key + (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.expanded_key()]) {
 | 
			
		||||
				return html`
 | 
			
		||||
					<button
 | 
			
		||||
						class="w3-button w3-theme-d1 w3-block w3-bar"
 | 
			
		||||
						style="box-sizing: border-box"
 | 
			
		||||
						@click=${() => self.set_expanded(true)}
 | 
			
		||||
					>
 | 
			
		||||
						+ ${this.total_child_messages(this.message) + ' More'}
 | 
			
		||||
					</button>
 | 
			
		||||
				`;
 | 
			
		||||
			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>`;
 | 
			
		||||
			} else {
 | 
			
		||||
				return html` <div class="w3-container w3-margin-bottom">
 | 
			
		||||
						${repeat(
 | 
			
		||||
							this.message.child_messages || [],
 | 
			
		||||
							(x) => x.id,
 | 
			
		||||
							(x) =>
 | 
			
		||||
								html`<tf-message
 | 
			
		||||
									.message=${x}
 | 
			
		||||
									whoami=${this.whoami}
 | 
			
		||||
									.users=${this.users}
 | 
			
		||||
									.drafts=${this.drafts}
 | 
			
		||||
									.expanded=${this.expanded}
 | 
			
		||||
									channel=${this.channel}
 | 
			
		||||
									channel_unread=${this.channel_unread}
 | 
			
		||||
									.recent_reactions=${this.recent_reactions}
 | 
			
		||||
								></tf-message>`
 | 
			
		||||
						)}
 | 
			
		||||
					</div>
 | 
			
		||||
					<button
 | 
			
		||||
						class="w3-button w3-theme-d1 w3-block w3-bar"
 | 
			
		||||
						style="box-sizing: border-box"
 | 
			
		||||
				return html`<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						@click=${() => self.set_expanded(false)}
 | 
			
		||||
					>
 | 
			
		||||
						Collapse
 | 
			
		||||
					</button>`;
 | 
			
		||||
						Collapse</button
 | 
			
		||||
					>${repeat(
 | 
			
		||||
						this.message.child_messages || [],
 | 
			
		||||
						(x) => x.id,
 | 
			
		||||
						(x) =>
 | 
			
		||||
							html`<tf-message
 | 
			
		||||
								.message=${x}
 | 
			
		||||
								whoami=${this.whoami}
 | 
			
		||||
								.users=${this.users}
 | 
			
		||||
								.drafts=${this.drafts}
 | 
			
		||||
								.expanded=${this.expanded}
 | 
			
		||||
								channel=${this.channel}
 | 
			
		||||
								channel_unread=${this.channel_unread}
 | 
			
		||||
							></tf-message>`
 | 
			
		||||
					)}`;
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			return undefined;
 | 
			
		||||
@@ -414,7 +358,7 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
	class_background() {
 | 
			
		||||
		return this.message?.decrypted
 | 
			
		||||
			? 'w3-pale-red'
 | 
			
		||||
			: this.allow_unread() && this.message?.rowid >= this.channel_unread
 | 
			
		||||
			: this.message?.rowid >= this.channel_unread
 | 
			
		||||
				? 'w3-theme-d2'
 | 
			
		||||
				: 'w3-theme-d4';
 | 
			
		||||
	}
 | 
			
		||||
@@ -427,74 +371,62 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
		return content;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	copy_id(event) {
 | 
			
		||||
		navigator.clipboard.writeText(this.message?.id);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	toggle_menu(event) {
 | 
			
		||||
		event.srcElement.parentNode
 | 
			
		||||
			.querySelector('.w3-dropdown-content')
 | 
			
		||||
			.classList.toggle('w3-show');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_menu() {
 | 
			
		||||
	render_raw_button() {
 | 
			
		||||
		let content = this.get_content();
 | 
			
		||||
		let formats = [['message', 'Message']];
 | 
			
		||||
		if (content?.type == 'post' || content?.type == 'blog') {
 | 
			
		||||
			formats.push(['md', 'Markdown']);
 | 
			
		||||
		}
 | 
			
		||||
		if (this.message?.decrypted) {
 | 
			
		||||
			formats.push(['decrypted', 'Decrypted']);
 | 
			
		||||
		}
 | 
			
		||||
		formats.push(['raw', 'Raw']);
 | 
			
		||||
		return html`
 | 
			
		||||
			<div class="w3-bar-item w3-right">
 | 
			
		||||
				<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}>
 | 
			
		||||
					%
 | 
			
		||||
				</button>
 | 
			
		||||
				<div
 | 
			
		||||
					class="w3-dropdown-content w3-bar-block w3-card-4 w3-theme-l1"
 | 
			
		||||
					style="right: 48px"
 | 
			
		||||
		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=${() => (this.format = 'md')}
 | 
			
		||||
					>
 | 
			
		||||
						Markdown
 | 
			
		||||
					</button>`;
 | 
			
		||||
				} else {
 | 
			
		||||
					raw_button = html`<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						@click=${() => (this.format = 'message')}
 | 
			
		||||
					>
 | 
			
		||||
						Message
 | 
			
		||||
					</button>`;
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
			case 'md':
 | 
			
		||||
				raw_button = html`<button
 | 
			
		||||
					class="w3-button w3-theme-d1"
 | 
			
		||||
					@click=${() => (this.format = 'message')}
 | 
			
		||||
				>
 | 
			
		||||
					<a
 | 
			
		||||
						target="_top"
 | 
			
		||||
						class="w3-button w3-bar-item"
 | 
			
		||||
						href=${'#' + encodeURIComponent(this.message?.id)}
 | 
			
		||||
						>View Message</a
 | 
			
		||||
					Message
 | 
			
		||||
				</button>`;
 | 
			
		||||
				break;
 | 
			
		||||
			case 'decrypted':
 | 
			
		||||
				raw_button = html`<button
 | 
			
		||||
					class="w3-button w3-theme-d1"
 | 
			
		||||
					@click=${() => (this.format = 'raw')}
 | 
			
		||||
				>
 | 
			
		||||
					Raw
 | 
			
		||||
				</button>`;
 | 
			
		||||
				break;
 | 
			
		||||
			default:
 | 
			
		||||
				if (this.message.decrypted) {
 | 
			
		||||
					raw_button = html`<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						@click=${() => (this.format = 'decrypted')}
 | 
			
		||||
					>
 | 
			
		||||
					<button
 | 
			
		||||
						class="w3-button w3-bar-item w3-border-bottom"
 | 
			
		||||
						@click=${this.copy_id}
 | 
			
		||||
						Decrypted
 | 
			
		||||
					</button>`;
 | 
			
		||||
				} else {
 | 
			
		||||
					raw_button = html`<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						@click=${() => (this.format = 'raw')}
 | 
			
		||||
					>
 | 
			
		||||
						Copy ID
 | 
			
		||||
					</button>
 | 
			
		||||
					${this.drafts[this.message?.id] === undefined
 | 
			
		||||
						? html`
 | 
			
		||||
								<button class="w3-button w3-bar-item" @click=${this.show_reply}>
 | 
			
		||||
									↩️ Reply
 | 
			
		||||
								</button>
 | 
			
		||||
							`
 | 
			
		||||
						: undefined}
 | 
			
		||||
					<button
 | 
			
		||||
						class="w3-button w3-bar-item w3-border-bottom"
 | 
			
		||||
						@click=${this.react}
 | 
			
		||||
					>
 | 
			
		||||
						👍 React
 | 
			
		||||
					</button>
 | 
			
		||||
					${formats.map(
 | 
			
		||||
						([format, name]) => html`
 | 
			
		||||
							<button
 | 
			
		||||
								class="w3-button w3-bar-item"
 | 
			
		||||
								style=${format == this.format ? 'font-weight: bold' : ''}
 | 
			
		||||
								@click=${() => (this.format = format)}
 | 
			
		||||
							>
 | 
			
		||||
								${name}
 | 
			
		||||
							</button>
 | 
			
		||||
						`
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
						Raw
 | 
			
		||||
					</button>`;
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
		return raw_button;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_header() {
 | 
			
		||||
@@ -506,15 +438,16 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
		return html`
 | 
			
		||||
			<header class="w3-bar">
 | 
			
		||||
				<span class="w3-bar-item">
 | 
			
		||||
					${this.render_unread_icon()}<tf-user
 | 
			
		||||
						id=${this.message.author}
 | 
			
		||||
						.users=${this.users}
 | 
			
		||||
					></tf-user>
 | 
			
		||||
					<tf-user id=${this.message.author} .users=${this.users}></tf-user>
 | 
			
		||||
				</span>
 | 
			
		||||
				${is_encrypted} ${this.render_menu()}
 | 
			
		||||
				<div class="w3-bar-item w3-right" style="text-wrap: nowrap">
 | 
			
		||||
					${new Date(this.message.timestamp).toLocaleString()}
 | 
			
		||||
				</div>
 | 
			
		||||
				${is_encrypted}
 | 
			
		||||
				<span class="w3-bar-item w3-right">${this.render_raw_button()}</span>
 | 
			
		||||
				<span class="w3-bar-item w3-right" style="text-wrap: nowrap"
 | 
			
		||||
					><a target="_top" href=${'#' + encodeURIComponent(this.message.id)}
 | 
			
		||||
						>%</a
 | 
			
		||||
					>
 | 
			
		||||
					${new Date(this.message.timestamp).toLocaleString()}</span
 | 
			
		||||
				>
 | 
			
		||||
			</header>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
@@ -562,7 +495,6 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
						.expanded=${self.expanded}
 | 
			
		||||
						channel=${self.channel}
 | 
			
		||||
						channel_unread=${self.channel_unread}
 | 
			
		||||
						.recent_reactions=${self.recent_reactions}
 | 
			
		||||
					></tf-message>
 | 
			
		||||
				`
 | 
			
		||||
			)}
 | 
			
		||||
@@ -574,76 +506,32 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
		let reply =
 | 
			
		||||
			this.drafts[this.message?.id] !== undefined
 | 
			
		||||
				? html`
 | 
			
		||||
						<div class="w3-section w3-container">
 | 
			
		||||
							<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}
 | 
			
		||||
								.recent_reactions=${this.recent_reactions}
 | 
			
		||||
							></tf-compose>
 | 
			
		||||
						</div>
 | 
			
		||||
						<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>
 | 
			
		||||
					`
 | 
			
		||||
				: undefined;
 | 
			
		||||
				: html`
 | 
			
		||||
						<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
 | 
			
		||||
							Reply
 | 
			
		||||
						</button>
 | 
			
		||||
					`;
 | 
			
		||||
		return html`
 | 
			
		||||
			${reply}
 | 
			
		||||
			<footer>${this.render_children()}</footer>
 | 
			
		||||
			<div class="w3-section w3-container">
 | 
			
		||||
				${reply}
 | 
			
		||||
				<button class="w3-button w3-theme-d1" @click=${this.react}>
 | 
			
		||||
					React
 | 
			
		||||
				</button>
 | 
			
		||||
				${this.render_children()}
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content_group_by_author() {
 | 
			
		||||
		let sorted = this.message.messages
 | 
			
		||||
			.map((x) => [
 | 
			
		||||
				x.author,
 | 
			
		||||
				x.content.blocking !== undefined
 | 
			
		||||
					? x.content.blocking
 | 
			
		||||
						? 'is blocking'
 | 
			
		||||
						: 'is no longer blocking'
 | 
			
		||||
					: x.content.following !== undefined
 | 
			
		||||
						? x.content.following
 | 
			
		||||
							? 'is following'
 | 
			
		||||
							: 'is no longer following'
 | 
			
		||||
						: '',
 | 
			
		||||
				x.content.contact,
 | 
			
		||||
				x,
 | 
			
		||||
			])
 | 
			
		||||
			.sort();
 | 
			
		||||
		let result = [];
 | 
			
		||||
		let last;
 | 
			
		||||
		let group;
 | 
			
		||||
		for (let row of sorted) {
 | 
			
		||||
			if (last && last[0] == row[0] && last[1] == row[1]) {
 | 
			
		||||
				group.push(row[2]);
 | 
			
		||||
			} else {
 | 
			
		||||
				if (group) {
 | 
			
		||||
					result.push({author: last[0], action: last[1], users: group});
 | 
			
		||||
				}
 | 
			
		||||
				last = row;
 | 
			
		||||
				group = [row[2]];
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if (group) {
 | 
			
		||||
			result.push({author: last[0], action: last[1], users: group});
 | 
			
		||||
		}
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	allow_unread() {
 | 
			
		||||
		return (
 | 
			
		||||
			this.channel == '@' ||
 | 
			
		||||
			(!this.channel.startsWith('@') && !this.channel.startsWith('%'))
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_unread_icon() {
 | 
			
		||||
		return this.allow_unread() && this.message?.rowid >= this.channel_unread
 | 
			
		||||
			? html`✉️`
 | 
			
		||||
			: undefined;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let content = this.message?.content;
 | 
			
		||||
		if (this.message?.decrypted?.type == 'post') {
 | 
			
		||||
@@ -652,94 +540,29 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
		let class_background = this.class_background();
 | 
			
		||||
		let self = this;
 | 
			
		||||
		if (this.message?.type === 'contact_group') {
 | 
			
		||||
			if (this.expanded[this.expanded_key()]) {
 | 
			
		||||
				return this.render_frame(html`
 | 
			
		||||
					<div class="w3-padding">
 | 
			
		||||
						${this.message.messages.map(
 | 
			
		||||
							(x) =>
 | 
			
		||||
								html`<tf-message
 | 
			
		||||
									.message=${x}
 | 
			
		||||
									whoami=${this.whoami}
 | 
			
		||||
									.users=${this.users}
 | 
			
		||||
									.drafts=${this.drafts}
 | 
			
		||||
									.expanded=${this.expanded}
 | 
			
		||||
									channel=${this.channel}
 | 
			
		||||
									channel_unread=${this.channel_unread}
 | 
			
		||||
								></tf-message>`
 | 
			
		||||
						)}
 | 
			
		||||
					</div>
 | 
			
		||||
					<button
 | 
			
		||||
						class="w3-button w3-theme-d1 w3-block w3-bar"
 | 
			
		||||
						style="box-sizing: border-box"
 | 
			
		||||
						@click=${() => self.set_expanded(false)}
 | 
			
		||||
					>
 | 
			
		||||
						Collapse
 | 
			
		||||
					</button>
 | 
			
		||||
				`);
 | 
			
		||||
			} else {
 | 
			
		||||
				return this.render_frame(html`
 | 
			
		||||
					<div class="w3-padding">
 | 
			
		||||
						${this.content_group_by_author().map(
 | 
			
		||||
							(x) => html`
 | 
			
		||||
								<div>
 | 
			
		||||
									<tf-user id=${x.author} .users=${this.users}></tf-user>
 | 
			
		||||
									${x.action}
 | 
			
		||||
									${x.users.map(
 | 
			
		||||
										(y) => html`
 | 
			
		||||
											<tf-user
 | 
			
		||||
												id=${y}
 | 
			
		||||
												.users=${this.users}
 | 
			
		||||
												icon_only="true"
 | 
			
		||||
											></tf-user>
 | 
			
		||||
										`
 | 
			
		||||
									)}
 | 
			
		||||
								</div>
 | 
			
		||||
							`
 | 
			
		||||
						)}
 | 
			
		||||
					</div>
 | 
			
		||||
					<button
 | 
			
		||||
						class="w3-button w3-theme-d1 w3-block w3-bar"
 | 
			
		||||
						style="box-sizing: border-box"
 | 
			
		||||
						@click=${() => self.set_expanded(true)}
 | 
			
		||||
					>
 | 
			
		||||
						Expand
 | 
			
		||||
					</button>
 | 
			
		||||
				`);
 | 
			
		||||
			}
 | 
			
		||||
			return this.render_frame(
 | 
			
		||||
				html` ${this.message.messages.map(
 | 
			
		||||
					(x) =>
 | 
			
		||||
						html`<tf-message
 | 
			
		||||
							.message=${x}
 | 
			
		||||
							whoami=${this.whoami}
 | 
			
		||||
							.users=${this.users}
 | 
			
		||||
							.drafts=${this.drafts}
 | 
			
		||||
							.expanded=${this.expanded}
 | 
			
		||||
							channel=${this.channel}
 | 
			
		||||
							channel_unread=${this.channel_unread}
 | 
			
		||||
						></tf-message>`
 | 
			
		||||
				)}`
 | 
			
		||||
			);
 | 
			
		||||
		} else if (this.message.placeholder) {
 | 
			
		||||
			return this.render_frame(
 | 
			
		||||
				html`<div>
 | 
			
		||||
					<div class="w3-bar">
 | 
			
		||||
						<a
 | 
			
		||||
							class="w3-bar-item w3-panel w3-round-xlarge w3-theme-d1 w3-margin w3-button"
 | 
			
		||||
							target="_top"
 | 
			
		||||
							href=${'#' + encodeURIComponent(this.message?.id)}
 | 
			
		||||
				html`<div class="w3-padding">
 | 
			
		||||
					<p>
 | 
			
		||||
						<a target="_top" href=${'#' + encodeURIComponent(this.message.id)}
 | 
			
		||||
							>${this.message.id}</a
 | 
			
		||||
						>
 | 
			
		||||
							This message is not currently available.
 | 
			
		||||
						</a>
 | 
			
		||||
						<div class="w3-bar-item w3-right">
 | 
			
		||||
							<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}>
 | 
			
		||||
								%
 | 
			
		||||
							</button>
 | 
			
		||||
							<div
 | 
			
		||||
								class="w3-dropdown-content w3-bar-block w3-card-4 w3-theme-l1"
 | 
			
		||||
								style="right: 48px"
 | 
			
		||||
							>
 | 
			
		||||
								<a
 | 
			
		||||
									target="_top"
 | 
			
		||||
									class="w3-button w3-bar-item"
 | 
			
		||||
									href=${'#' + encodeURIComponent(this.message?.id)}
 | 
			
		||||
									>View Message</a
 | 
			
		||||
								>
 | 
			
		||||
								<button
 | 
			
		||||
									class="w3-button w3-bar-item w3-border-bottom"
 | 
			
		||||
									@click=${this.copy_id}
 | 
			
		||||
								>
 | 
			
		||||
									Copy ID
 | 
			
		||||
								</button>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
						(placeholder)
 | 
			
		||||
					</p>
 | 
			
		||||
					<div>${this.render_votes()}</div>
 | 
			
		||||
					${(this.message.child_messages || []).map(
 | 
			
		||||
						(x) => html`
 | 
			
		||||
@@ -766,7 +589,7 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
				}
 | 
			
		||||
				if (content.image !== undefined) {
 | 
			
		||||
					image = html`
 | 
			
		||||
						<div @click=${this.body_click}><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) {
 | 
			
		||||
@@ -789,45 +612,25 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
					</div>
 | 
			
		||||
				`);
 | 
			
		||||
			} else if (content.type == 'contact') {
 | 
			
		||||
				switch (this.format) {
 | 
			
		||||
					case 'message':
 | 
			
		||||
					default:
 | 
			
		||||
						return this.render_frame(html`
 | 
			
		||||
							<div class="w3-bar">
 | 
			
		||||
								<div class="w3-bar-item">
 | 
			
		||||
									<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>
 | 
			
		||||
								</div>
 | 
			
		||||
								${this.render_menu()} ${this.render_votes()}
 | 
			
		||||
								${this.render_actions()}
 | 
			
		||||
							</div>
 | 
			
		||||
						`);
 | 
			
		||||
						break;
 | 
			
		||||
					case 'raw':
 | 
			
		||||
						return this.render_frame(html`
 | 
			
		||||
							${this.render_header()}
 | 
			
		||||
							<div class="w3-container">${this.render_raw()}</div>
 | 
			
		||||
							${this.render_votes()} ${this.render_actions()}
 | 
			
		||||
						</div>
 | 
			
		||||
						`);
 | 
			
		||||
						break;
 | 
			
		||||
				}
 | 
			
		||||
				return html`
 | 
			
		||||
					<div class="w3-padding">
 | 
			
		||||
						<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>
 | 
			
		||||
					</div>
 | 
			
		||||
				`;
 | 
			
		||||
			} else if (content.type == 'post') {
 | 
			
		||||
				let self = this;
 | 
			
		||||
				let body;
 | 
			
		||||
@@ -850,14 +653,11 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
				}
 | 
			
		||||
				let content_warning = html`
 | 
			
		||||
					<div
 | 
			
		||||
						class="w3-panel w3-round-xlarge w3-theme-l4 w3"
 | 
			
		||||
						class="w3-panel w3-round-xlarge w3-theme-l4"
 | 
			
		||||
						style="cursor: pointer"
 | 
			
		||||
						@click=${(x) => this.toggle_expanded(':cw')}
 | 
			
		||||
					>
 | 
			
		||||
						<p>${content.contentWarning}</p>
 | 
			
		||||
						<p class="w3-small">
 | 
			
		||||
							${this.is_expanded(':cw') ? 'Show less' : 'Show more'}
 | 
			
		||||
						</p>
 | 
			
		||||
					</div>
 | 
			
		||||
				`;
 | 
			
		||||
				let content_html = html`
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,6 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
			expanded: {type: Object},
 | 
			
		||||
			channel: {type: String},
 | 
			
		||||
			channel_unread: {type: Number},
 | 
			
		||||
			recent_reactions: {type: Array},
 | 
			
		||||
			hash: {type: String},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -30,7 +28,6 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
		this.drafts = {};
 | 
			
		||||
		this.expanded = {};
 | 
			
		||||
		this.channel_unread = -1;
 | 
			
		||||
		this.recent_reactions = [];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	process_messages(messages) {
 | 
			
		||||
@@ -167,10 +164,7 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
			if (message?.content?.type === 'contact') {
 | 
			
		||||
				group.push(message);
 | 
			
		||||
			} else {
 | 
			
		||||
				if (group.length == 1) {
 | 
			
		||||
					result.push(group[0]);
 | 
			
		||||
					group = [];
 | 
			
		||||
				} else if (group.length > 1) {
 | 
			
		||||
				if (group.length > 0) {
 | 
			
		||||
					result.push({
 | 
			
		||||
						rowid: Math.max(...group.map((x) => x.rowid)),
 | 
			
		||||
						type: 'contact_group',
 | 
			
		||||
@@ -181,10 +175,7 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
				result.push(message);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if (group.length == 1) {
 | 
			
		||||
			result.push(group[0]);
 | 
			
		||||
			group = [];
 | 
			
		||||
		} else if (group.length > 1) {
 | 
			
		||||
		if (group.length > 0) {
 | 
			
		||||
			result.push({
 | 
			
		||||
				rowid: Math.max(...group.map((x) => x.rowid)),
 | 
			
		||||
				type: 'contact_group',
 | 
			
		||||
@@ -194,21 +185,15 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	unread_allowed() {
 | 
			
		||||
		return !this.hash?.startsWith('#%') && !this.hash?.startsWith('#@');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 unread_rowid = -1;
 | 
			
		||||
		if (this.unread_allowed()) {
 | 
			
		||||
			for (let message of final_messages) {
 | 
			
		||||
				if (message.rowid >= this.channel_unread) {
 | 
			
		||||
					unread_rowid = message.rowid;
 | 
			
		||||
				}
 | 
			
		||||
		for (let message of final_messages) {
 | 
			
		||||
			if (message.rowid >= this.channel_unread) {
 | 
			
		||||
				unread_rowid = message.rowid;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return html`
 | 
			
		||||
@@ -226,26 +211,13 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
							collapsed="true"
 | 
			
		||||
							channel=${this.channel}
 | 
			
		||||
							channel_unread=${this.channel_unread}
 | 
			
		||||
							.recent_reactions=${this.recent_reactions}
 | 
			
		||||
						></tf-message>
 | 
			
		||||
						${x.rowid == unread_rowid
 | 
			
		||||
							? html`<div style="display: flex; flex-direction: row">
 | 
			
		||||
									<div
 | 
			
		||||
										style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
 | 
			
		||||
									></div>
 | 
			
		||||
									<button
 | 
			
		||||
										style="color: #f00; padding: 8px"
 | 
			
		||||
										class="w3-button"
 | 
			
		||||
										@click=${() =>
 | 
			
		||||
											this.dispatchEvent(
 | 
			
		||||
												new Event('mark_all_read', {
 | 
			
		||||
													bubbles: true,
 | 
			
		||||
													composed: true,
 | 
			
		||||
												})
 | 
			
		||||
											)}
 | 
			
		||||
									>
 | 
			
		||||
										unread
 | 
			
		||||
									</button>
 | 
			
		||||
									<div style="color: #f00; padding: 8px">unread</div>
 | 
			
		||||
									<div
 | 
			
		||||
										style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
 | 
			
		||||
									></div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import {LitElement, html, until, 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 {styles} from './tf-styles.js';
 | 
			
		||||
@@ -11,10 +11,8 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
			id: {type: String},
 | 
			
		||||
			users: {type: Object},
 | 
			
		||||
			size: {type: Number},
 | 
			
		||||
			sequence: {type: Number},
 | 
			
		||||
			following: {type: Boolean},
 | 
			
		||||
			blocking: {type: Boolean},
 | 
			
		||||
			show_followed: {type: Boolean},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -28,7 +26,6 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
		this.id = null;
 | 
			
		||||
		this.users = {};
 | 
			
		||||
		this.size = 0;
 | 
			
		||||
		this.sequence = 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load() {
 | 
			
		||||
@@ -142,8 +139,7 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let input = document.createElement('input');
 | 
			
		||||
		input.type = 'file';
 | 
			
		||||
		input.addEventListener('change', function (event) {
 | 
			
		||||
			input.parentNode.removeChild(input);
 | 
			
		||||
		input.onchange = function (event) {
 | 
			
		||||
			let file = event.target.files[0];
 | 
			
		||||
			file
 | 
			
		||||
				.arrayBuffer()
 | 
			
		||||
@@ -158,8 +154,7 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
				.catch(function (e) {
 | 
			
		||||
					alert(e.message);
 | 
			
		||||
				});
 | 
			
		||||
		});
 | 
			
		||||
		document.body.appendChild(input);
 | 
			
		||||
		};
 | 
			
		||||
		input.click();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -167,83 +162,17 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
		navigator.clipboard.writeText(this.id);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	show_image(link) {
 | 
			
		||||
		let div = document.createElement('div');
 | 
			
		||||
		div.style.left = 0;
 | 
			
		||||
		div.style.top = 0;
 | 
			
		||||
		div.style.width = '100%';
 | 
			
		||||
		div.style.height = '100%';
 | 
			
		||||
		div.style.position = 'fixed';
 | 
			
		||||
		div.style.background = '#000';
 | 
			
		||||
		div.style.zIndex = 100;
 | 
			
		||||
		div.style.display = 'grid';
 | 
			
		||||
		let img = document.createElement('img');
 | 
			
		||||
		img.src = link;
 | 
			
		||||
		img.style.maxWidth = '100vw';
 | 
			
		||||
		img.style.maxHeight = '100vh';
 | 
			
		||||
		img.style.display = 'block';
 | 
			
		||||
		img.style.margin = 'auto';
 | 
			
		||||
		img.style.objectFit = 'contain';
 | 
			
		||||
		img.style.width = '100vw';
 | 
			
		||||
		div.appendChild(img);
 | 
			
		||||
		function image_close(event) {
 | 
			
		||||
			document.body.removeChild(div);
 | 
			
		||||
			window.removeEventListener('keydown', image_close);
 | 
			
		||||
		}
 | 
			
		||||
		div.onclick = image_close;
 | 
			
		||||
		window.addEventListener('keydown', image_close);
 | 
			
		||||
		document.body.appendChild(div);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	body_click(event) {
 | 
			
		||||
		if (event.srcElement.tagName == 'IMG') {
 | 
			
		||||
			this.show_image(event.srcElement.src);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	toggle_account_list(event) {
 | 
			
		||||
		let content = event.srcElement.nextElementSibling;
 | 
			
		||||
		this.show_followed = !this.show_followed;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load_follows() {
 | 
			
		||||
		let accounts = await tfrpc.rpc.following([this.id], 1);
 | 
			
		||||
		return html`
 | 
			
		||||
			<div class="w3-container">
 | 
			
		||||
				<button
 | 
			
		||||
					class="w3-button w3-block w3-theme-d1 followed_accounts"
 | 
			
		||||
					@click=${this.toggle_account_list}
 | 
			
		||||
				>
 | 
			
		||||
					${this.show_followed ? 'Hide' : 'Show'} Followed Accounts
 | 
			
		||||
					(${Object.keys(accounts).length})
 | 
			
		||||
				</button>
 | 
			
		||||
				<div class=${'w3-card' + (this.show_followed ? '' : ' w3-hide')}>
 | 
			
		||||
					<ul class="w3-ul w3-theme-d4 w3-border-theme">
 | 
			
		||||
						${Object.keys(accounts).map(
 | 
			
		||||
							(x) => html`
 | 
			
		||||
								<li class="w3-border-theme">
 | 
			
		||||
									<tf-user id=${x} .users=${this.users}></tf-user>
 | 
			
		||||
								</li>
 | 
			
		||||
							`
 | 
			
		||||
						)}
 | 
			
		||||
					</ul>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		this.load();
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let profile = this.users[this.id] || {};
 | 
			
		||||
		tfrpc.rpc
 | 
			
		||||
			.query(
 | 
			
		||||
				`SELECT SUM(LENGTH(content)) AS size, MAX(sequence) AS sequence FROM messages WHERE author = ?`,
 | 
			
		||||
				`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
 | 
			
		||||
				[this.id]
 | 
			
		||||
			)
 | 
			
		||||
			.then(function (result) {
 | 
			
		||||
				self.size = result[0].size;
 | 
			
		||||
				self.sequence = result[0].sequence;
 | 
			
		||||
			});
 | 
			
		||||
		let edit;
 | 
			
		||||
		let follow;
 | 
			
		||||
@@ -308,26 +237,22 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>`
 | 
			
		||||
			: null;
 | 
			
		||||
		let image = profile.image;
 | 
			
		||||
		if (typeof image == 'string' && !image.startsWith('&')) {
 | 
			
		||||
			try {
 | 
			
		||||
				image = JSON.parse(image)?.link;
 | 
			
		||||
			} catch {}
 | 
			
		||||
		}
 | 
			
		||||
		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-card-4 w3-container w3-theme-d3" style="box-sizing: border-box">
 | 
			
		||||
			<header class="w3-container">
 | 
			
		||||
				<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)} in ${this.sequence} messages)</p>
 | 
			
		||||
				<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})</p>
 | 
			
		||||
			</header>
 | 
			
		||||
			<div class="w3-container" @click=${this.body_click}>
 | 
			
		||||
			<div class="w3-container">
 | 
			
		||||
				<div class="w3-margin-bottom" style="display: flex; flex-direction: row">
 | 
			
		||||
					<input type="text" class="w3-input w3-border w3-theme-d1" style="display: flex 1 1" readonly value=${this.id}></input>
 | 
			
		||||
					<button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div style="display: flex; flex-direction: row; gap: 1em">
 | 
			
		||||
					${edit_profile}
 | 
			
		||||
					<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal">
 | 
			
		||||
					<div style="flex: 1 0 50%">
 | 
			
		||||
						${
 | 
			
		||||
							image
 | 
			
		||||
								? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>`
 | 
			
		||||
@@ -346,12 +271,8 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
					Blocked by ${profile.blocked} identities.
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			${until(this.load_follows(), html`<p>Loading accounts followed...</p>`)}
 | 
			
		||||
			<footer class="w3-container">
 | 
			
		||||
				<p>
 | 
			
		||||
					<a class="w3-button w3-theme-d1" href=${'#🔐' + (this.id != this.whoami ? this.id : '')}>
 | 
			
		||||
						Open Private Chat
 | 
			
		||||
					</a>
 | 
			
		||||
					${edit}
 | 
			
		||||
					${follow}
 | 
			
		||||
					${block}
 | 
			
		||||
 
 | 
			
		||||
@@ -41,26 +41,23 @@ class TfReactionsModalElement extends LitElement {
 | 
			
		||||
								>
 | 
			
		||||
							</header>
 | 
			
		||||
							<ul class="w3-theme-dark w3-container w3-ul">
 | 
			
		||||
								${this.votes
 | 
			
		||||
									.sort((x, y) => y.timestamp - x.timestamp)
 | 
			
		||||
									.map(
 | 
			
		||||
										(x) => html`
 | 
			
		||||
											<li style="display: flex; flex-direction: row; gap: 4px">
 | 
			
		||||
												<span style="flex-basis: 3em"
 | 
			
		||||
													>${x?.content?.vote?.expression}</span
 | 
			
		||||
												>
 | 
			
		||||
												<tf-user
 | 
			
		||||
													style="flex: 1 1"
 | 
			
		||||
													id=${x.author}
 | 
			
		||||
													.users=${this.users}
 | 
			
		||||
												></tf-user>
 | 
			
		||||
												<span
 | 
			
		||||
													style="flex-shrink: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
 | 
			
		||||
													>${new Date(x?.timestamp).toLocaleString()}</span
 | 
			
		||||
												>
 | 
			
		||||
											</li>
 | 
			
		||||
										`
 | 
			
		||||
									)}
 | 
			
		||||
								${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>
 | 
			
		||||
 
 | 
			
		||||
@@ -43,14 +43,12 @@ const tf = css`
 | 
			
		||||
		border-left: 4px solid #fff;
 | 
			
		||||
		padding: 8px;
 | 
			
		||||
		padding-left: 12px;
 | 
			
		||||
		margin-left: 0;
 | 
			
		||||
		margin-right: 0;
 | 
			
		||||
	}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
// prettier-ignore
 | 
			
		||||
const w3 = css`
 | 
			
		||||
/* W3.CSS 5.01 March 14 2025 by Jan Egil and Borge Refsnes */
 | 
			
		||||
/* 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}
 | 
			
		||||
@@ -90,7 +88,7 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
 | 
			
		||||
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
 | 
			
		||||
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
 | 
			
		||||
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}   
 | 
			
		||||
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
 | 
			
		||||
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
 | 
			
		||||
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
 | 
			
		||||
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
 | 
			
		||||
@@ -138,7 +136,7 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
 | 
			
		||||
@media (max-width:1205px){.w3-auto{max-width:95%}}
 | 
			
		||||
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
 | 
			
		||||
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}	
 | 
			
		||||
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
 | 
			
		||||
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
 | 
			
		||||
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
 | 
			
		||||
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
 | 
			
		||||
@@ -160,10 +158,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
 | 
			
		||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
 | 
			
		||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
 | 
			
		||||
 | 
			
		||||
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
 | 
			
		||||
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
 | 
			
		||||
 | 
			
		||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
 | 
			
		||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
 | 
			
		||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
 | 
			
		||||
@@ -205,9 +199,9 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.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,.w3-warning{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.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,.w3-info,.w3-primary{color:#fff!important;background-color:#2196F3!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}
 | 
			
		||||
@@ -222,24 +216,15 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.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,.w3-danger{color:#fff!important;background-color:#f44336!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,.w3-note{color:#000!important;background-color:#ffeb3b!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,.w3-secondary{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
 | 
			
		||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
 | 
			
		||||
 | 
			
		||||
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
 | 
			
		||||
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
 | 
			
		||||
.w3-emerald,.w3-hover-emerald:hover,.w3-success{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
 | 
			
		||||
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
 | 
			
		||||
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
 | 
			
		||||
 | 
			
		||||
.w3-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}
 | 
			
		||||
 
 | 
			
		||||
@@ -103,23 +103,6 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
		</div>`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_progress(name, value, max) {
 | 
			
		||||
		if (max && value != max) {
 | 
			
		||||
			return html`
 | 
			
		||||
				<div class="w3-theme-d1 w3-small">
 | 
			
		||||
					<div
 | 
			
		||||
						class="w3-container w3-theme-l1"
 | 
			
		||||
						style="width: ${Math.floor(
 | 
			
		||||
							(100.0 * value) / max
 | 
			
		||||
						)}%; text-wrap: nowrap"
 | 
			
		||||
					>
 | 
			
		||||
						${name} ${value} / ${max} (${Math.round((100.0 * value) / max)}%)
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_broadcast(connection) {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		return html`
 | 
			
		||||
@@ -171,16 +154,6 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
				: undefined}
 | 
			
		||||
			${connection.flags.one_shot ? '🔃' : undefined}
 | 
			
		||||
			<tf-user id=${connection.id} .users=${this.users}></tf-user>
 | 
			
		||||
			${this.render_progress(
 | 
			
		||||
				'recv',
 | 
			
		||||
				connection.progress.in.total - connection.progress.in.current,
 | 
			
		||||
				connection.progress.in.total
 | 
			
		||||
			)}
 | 
			
		||||
			${this.render_progress(
 | 
			
		||||
				'send',
 | 
			
		||||
				connection.progress.out.total - connection.progress.out.current,
 | 
			
		||||
				connection.progress.out.total
 | 
			
		||||
			)}
 | 
			
		||||
			${connection.tunnel !== undefined
 | 
			
		||||
				? '🚇'
 | 
			
		||||
				: html`(${connection.host}:${connection.port})`}
 | 
			
		||||
@@ -233,21 +206,6 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	toggle_accordian(id) {
 | 
			
		||||
		let element = this.renderRoot.getElementById(id);
 | 
			
		||||
		element.classList.toggle('w3-hide');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	valid_connections() {
 | 
			
		||||
		return this.connections.filter((x) => x.tunnel === undefined);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	valid_broadcasts() {
 | 
			
		||||
		return this.broadcasts
 | 
			
		||||
			.filter((x) => x.address)
 | 
			
		||||
			.filter((x) => this.connections.map((c) => c.id).indexOf(x.pubkey) == -1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		return html`
 | 
			
		||||
@@ -262,33 +220,27 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
				>
 | 
			
		||||
					Connect
 | 
			
		||||
				</button>
 | 
			
		||||
				<h2
 | 
			
		||||
					class="w3-button w3-block w3-theme-d1"
 | 
			
		||||
					@click=${() => self.toggle_accordian('connections')}
 | 
			
		||||
				>
 | 
			
		||||
					Connections (${this.valid_connections().length})
 | 
			
		||||
				</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border" id="connections">
 | 
			
		||||
					${this.valid_connections().map(
 | 
			
		||||
						(x) => html` <li class="w3-bar">${this.render_connection(x)}</li> `
 | 
			
		||||
					)}
 | 
			
		||||
				<h2>Broadcasts</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border">
 | 
			
		||||
					${this.broadcasts
 | 
			
		||||
						.filter((x) => x.address)
 | 
			
		||||
						.filter(
 | 
			
		||||
							(x) => self.connections.map((c) => c.id).indexOf(x.pubkey) == -1
 | 
			
		||||
						)
 | 
			
		||||
						.map((x) => self.render_broadcast(x))}
 | 
			
		||||
				</ul>
 | 
			
		||||
				<h2
 | 
			
		||||
					class="w3-button w3-block w3-theme-d1"
 | 
			
		||||
					@click=${() => self.toggle_accordian('broadcasts')}
 | 
			
		||||
				>
 | 
			
		||||
					Discovery (${this.valid_broadcasts().length})
 | 
			
		||||
				</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border w3-hide" id="broadcasts">
 | 
			
		||||
					${this.valid_broadcasts().map((x) => self.render_broadcast(x))}
 | 
			
		||||
				<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
 | 
			
		||||
					class="w3-button w3-block w3-theme-d1"
 | 
			
		||||
					@click=${() => self.toggle_accordian('stored_connections')}
 | 
			
		||||
				>
 | 
			
		||||
					Stored Connections (${this.stored_connections.length})
 | 
			
		||||
				</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border w3-hide" id="stored_connections">
 | 
			
		||||
				<h2>Stored Connections</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border">
 | 
			
		||||
					${this.stored_connections.map(
 | 
			
		||||
						(x) => html`
 | 
			
		||||
							<li>
 | 
			
		||||
@@ -308,12 +260,6 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
									<div class="w3-bar-item">
 | 
			
		||||
										<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
 | 
			
		||||
										<div><small>${x.address}:${x.port}</small></div>
 | 
			
		||||
										<div>
 | 
			
		||||
											<small
 | 
			
		||||
												>Last connection:
 | 
			
		||||
												${new Date(x.last_success * 1000)}</small
 | 
			
		||||
											>
 | 
			
		||||
										</div>
 | 
			
		||||
									</div>
 | 
			
		||||
								</div>
 | 
			
		||||
								${this.render_message(x)}
 | 
			
		||||
@@ -321,13 +267,8 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
						`
 | 
			
		||||
					)}
 | 
			
		||||
				</ul>
 | 
			
		||||
				<h2
 | 
			
		||||
					class="w3-button w3-block w3-theme-d1"
 | 
			
		||||
					@click=${() => self.toggle_accordian('local_accounts')}
 | 
			
		||||
				>
 | 
			
		||||
					Local Accounts (${this.identities.length})
 | 
			
		||||
				</h2>
 | 
			
		||||
				<div class="w3-container w3-hide" id="local_accounts">
 | 
			
		||||
				<h2>Local Accounts</h2>
 | 
			
		||||
				<div class="w3-container">
 | 
			
		||||
					${this.identities.map(
 | 
			
		||||
						(x) =>
 | 
			
		||||
							html`<div
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,6 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
			time_range: {type: Array},
 | 
			
		||||
			time_loading: {type: Array},
 | 
			
		||||
			private_messages: {type: Array},
 | 
			
		||||
			grouped_private_messages: {type: Object},
 | 
			
		||||
			recent_reactions: {type: Array},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -39,7 +37,6 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
		this.start_time = new Date().valueOf();
 | 
			
		||||
		this.time_range = [0, 0];
 | 
			
		||||
		this.time_loading = undefined;
 | 
			
		||||
		this.recent_reactions = [];
 | 
			
		||||
		this.loading = 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -49,73 +46,9 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
			: this.hash.substring(1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async _fetch_related_messages(messages) {
 | 
			
		||||
		let refs = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
				WITH
 | 
			
		||||
					news AS (
 | 
			
		||||
						SELECT value AS id FROM json_each(?)
 | 
			
		||||
					)
 | 
			
		||||
				SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
 | 
			
		||||
				UNION
 | 
			
		||||
				SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
 | 
			
		||||
			`,
 | 
			
		||||
			[JSON.stringify(messages.map((x) => x.id))]
 | 
			
		||||
		);
 | 
			
		||||
		let related_messages = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
				SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
				FROM messages
 | 
			
		||||
				JOIN json_each(?2) refs ON messages.id = refs.value
 | 
			
		||||
				JOIN json_each(?1) AS following ON messages.author = following.value
 | 
			
		||||
			`,
 | 
			
		||||
			[JSON.stringify(this.following), JSON.stringify(refs.map((x) => x.ref))]
 | 
			
		||||
		);
 | 
			
		||||
		let combined = [].concat(messages, related_messages);
 | 
			
		||||
		let refs2 = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
				WITH
 | 
			
		||||
					news AS (
 | 
			
		||||
						SELECT value AS id FROM json_each(?)
 | 
			
		||||
					)
 | 
			
		||||
				SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
 | 
			
		||||
				UNION
 | 
			
		||||
				SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
 | 
			
		||||
			`,
 | 
			
		||||
			[JSON.stringify(combined.map((x) => x.id))]
 | 
			
		||||
		);
 | 
			
		||||
		let t0 = new Date();
 | 
			
		||||
		let result = [].concat(
 | 
			
		||||
			combined,
 | 
			
		||||
			await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
				SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
				FROM json_each(?2) refs
 | 
			
		||||
				JOIN messages ON messages.id = refs.value
 | 
			
		||||
				JOIN json_each(?1) following ON messages.author = following.value
 | 
			
		||||
				WHERE messages.content ->> 'type' != 'post'
 | 
			
		||||
			`,
 | 
			
		||||
				[
 | 
			
		||||
					JSON.stringify(this.following),
 | 
			
		||||
					JSON.stringify(refs2.map((x) => x.ref)),
 | 
			
		||||
				]
 | 
			
		||||
			)
 | 
			
		||||
		);
 | 
			
		||||
		let t1 = new Date();
 | 
			
		||||
		console.log((t1 - t0) / 1000);
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async fetch_messages(start_time, end_time) {
 | 
			
		||||
		this.dispatchEvent(
 | 
			
		||||
			new CustomEvent('loadmessages', {
 | 
			
		||||
				bubbles: true,
 | 
			
		||||
				composed: true,
 | 
			
		||||
			})
 | 
			
		||||
		);
 | 
			
		||||
		this.time_loading = [start_time, end_time];
 | 
			
		||||
		let result;
 | 
			
		||||
		const k_max_results = 64;
 | 
			
		||||
		if (this.hash == '#@') {
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
@@ -126,7 +59,7 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
						WHERE
 | 
			
		||||
							messages.author != ?1 AND
 | 
			
		||||
							(?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4
 | 
			
		||||
						ORDER BY timestamp DESC limit ?5)
 | 
			
		||||
						ORDER BY timestamp DESC limit 20)
 | 
			
		||||
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM mentions
 | 
			
		||||
						JOIN messages_refs ON mentions.id = messages_refs.ref
 | 
			
		||||
@@ -139,7 +72,6 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
					JSON.stringify(this.following),
 | 
			
		||||
					start_time,
 | 
			
		||||
					end_time,
 | 
			
		||||
					k_max_results,
 | 
			
		||||
				]
 | 
			
		||||
			);
 | 
			
		||||
		} else if (this.hash.startsWith('#@')) {
 | 
			
		||||
@@ -149,7 +81,7 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
						selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
							FROM messages
 | 
			
		||||
							WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3
 | 
			
		||||
							ORDER BY sequence DESC LIMIT ?4
 | 
			
		||||
							ORDER BY sequence DESC LIMIT 20
 | 
			
		||||
						)
 | 
			
		||||
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM selected
 | 
			
		||||
@@ -158,7 +90,7 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT TRUE AS is_primary, * FROM selected
 | 
			
		||||
				`,
 | 
			
		||||
				[this.hash.substring(1), start_time, end_time, k_max_results]
 | 
			
		||||
				[this.hash.substring(1), start_time, end_time]
 | 
			
		||||
			);
 | 
			
		||||
		} else if (this.hash.startsWith('#%')) {
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
@@ -175,43 +107,44 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
				[this.hash.substring(1)]
 | 
			
		||||
			);
 | 
			
		||||
		} else if (this.hash.startsWith('##')) {
 | 
			
		||||
			let t0 = new Date();
 | 
			
		||||
			let initial_messages = await tfrpc.rpc.query(
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					WITH
 | 
			
		||||
						all_news AS (
 | 
			
		||||
							SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
								FROM messages
 | 
			
		||||
								JOIN json_each(?) AS following ON messages.author = following.value
 | 
			
		||||
								WHERE messages.content ->> 'channel' = ?4 AND messages.content ->> 'type' != 'vote'
 | 
			
		||||
								WHERE messages.content ->> 'channel' = ?4
 | 
			
		||||
							UNION
 | 
			
		||||
							SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
								FROM messages_refs
 | 
			
		||||
								JOIN messages ON messages.id = messages_refs.message
 | 
			
		||||
								FROM messages_fts(?5)
 | 
			
		||||
								JOIN messages ON messages.rowid = messages_fts.rowid
 | 
			
		||||
								JOIN json_each(?1) AS following ON messages.author = following.value
 | 
			
		||||
								WHERE messages_refs.ref = '#' || ?4 AND messages.content ->> 'type' != 'vote'
 | 
			
		||||
							)
 | 
			
		||||
					SELECT TRUE AS is_primary, all_news.* FROM all_news
 | 
			
		||||
						WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
 | 
			
		||||
						ORDER BY all_news.timestamp DESC LIMIT ?5
 | 
			
		||||
								JOIN json_tree(messages.content, '$.mentions') AS mention ON mention.value = '#' || ?4),
 | 
			
		||||
						news AS (SELECT * FROM all_news
 | 
			
		||||
							WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
 | 
			
		||||
							ORDER BY all_news.timestamp DESC LIMIT 20)
 | 
			
		||||
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM news
 | 
			
		||||
						JOIN messages_refs ON news.id = messages_refs.ref
 | 
			
		||||
						JOIN messages ON messages_refs.message = messages.id
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM news
 | 
			
		||||
						JOIN messages_refs ON news.id = messages_refs.message
 | 
			
		||||
						JOIN messages ON messages_refs.ref = messages.id
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT TRUE AS is_primary, news.* FROM news
 | 
			
		||||
				`,
 | 
			
		||||
				[
 | 
			
		||||
					JSON.stringify(this.following),
 | 
			
		||||
					start_time,
 | 
			
		||||
					end_time,
 | 
			
		||||
					this.hash.substring(2),
 | 
			
		||||
					k_max_results,
 | 
			
		||||
					'"#' + this.hash.substring(2).replace('"', '""') + '"',
 | 
			
		||||
				]
 | 
			
		||||
			);
 | 
			
		||||
			let t1 = new Date();
 | 
			
		||||
			result = await this._fetch_related_messages(initial_messages);
 | 
			
		||||
			let t2 = new Date();
 | 
			
		||||
			console.log(
 | 
			
		||||
				`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
 | 
			
		||||
			);
 | 
			
		||||
		} else if (this.hash.startsWith('#🔐')) {
 | 
			
		||||
			let ids =
 | 
			
		||||
				this.hash == '#🔐' ? [] : this.hash.substring('#🔐'.length).split(',');
 | 
			
		||||
		} else if (this.hash == '#🔐') {
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
@@ -220,56 +153,38 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
					WHERE
 | 
			
		||||
						(?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND
 | 
			
		||||
						json(messages.content) LIKE '"%'
 | 
			
		||||
					ORDER BY messages.rowid DESC LIMIT ?4
 | 
			
		||||
					ORDER BY messages.sequence DESC LIMIT 20
 | 
			
		||||
				`,
 | 
			
		||||
				[
 | 
			
		||||
					JSON.stringify(
 | 
			
		||||
						this.grouped_private_messages?.[JSON.stringify(ids)]?.map(
 | 
			
		||||
							(x) => x.id
 | 
			
		||||
						) ?? []
 | 
			
		||||
					),
 | 
			
		||||
					start_time,
 | 
			
		||||
					end_time,
 | 
			
		||||
					k_max_results,
 | 
			
		||||
				]
 | 
			
		||||
				[JSON.stringify(this.private_messages), start_time, end_time]
 | 
			
		||||
			);
 | 
			
		||||
			result = (await this.decrypt(result)).filter((x) => x.decrypted);
 | 
			
		||||
		} else if (this.hash == '#👍') {
 | 
			
		||||
		} else {
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					WITH votes AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM messages
 | 
			
		||||
						JOIN json_each(?1) AS following ON messages.author = following.value
 | 
			
		||||
						WHERE
 | 
			
		||||
							messages.content ->> 'type' = 'vote' AND
 | 
			
		||||
							(?2 IS NULL OR messages.timestamp >= ?2) AND messages.timestamp < ?3
 | 
			
		||||
						ORDER BY timestamp DESC limit ?4)
 | 
			
		||||
					WITH
 | 
			
		||||
						all_news AS (
 | 
			
		||||
							SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
							FROM messages
 | 
			
		||||
							JOIN json_each(?) AS following ON messages.author = following.value
 | 
			
		||||
							WHERE timestamp >= 0 AND timestamp < ?3),
 | 
			
		||||
						news AS (
 | 
			
		||||
							SELECT * FROM all_news
 | 
			
		||||
							WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
 | 
			
		||||
							ORDER BY timestamp DESC LIMIT 20
 | 
			
		||||
						)
 | 
			
		||||
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM votes
 | 
			
		||||
						JOIN messages ON messages.id = votes.content ->> '$.vote.link'
 | 
			
		||||
						FROM news
 | 
			
		||||
						JOIN messages_refs ON news.id = messages_refs.ref
 | 
			
		||||
						JOIN messages ON messages_refs.message = messages.id
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT TRUE AS is_primary, * FROM votes
 | 
			
		||||
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM news
 | 
			
		||||
						JOIN messages_refs ON news.id = messages_refs.message
 | 
			
		||||
						JOIN messages ON messages_refs.ref = messages.id
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT TRUE AS is_primary, news.* FROM news
 | 
			
		||||
				`,
 | 
			
		||||
				[JSON.stringify(this.following), start_time, end_time, k_max_results]
 | 
			
		||||
			);
 | 
			
		||||
		} else {
 | 
			
		||||
			let t0 = new Date();
 | 
			
		||||
			let initial_messages = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					SELECT TRUE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
					FROM messages
 | 
			
		||||
					JOIN json_each(?) AS following ON messages.author = following.value
 | 
			
		||||
					WHERE messages.timestamp < ?3 AND (?2 IS NULL OR messages.timestamp >= ?2) AND
 | 
			
		||||
						messages.content ->> 'type' != 'vote'
 | 
			
		||||
					ORDER BY timestamp DESC LIMIT ?4
 | 
			
		||||
				`,
 | 
			
		||||
				[JSON.stringify(this.following), start_time, end_time, k_max_results]
 | 
			
		||||
			);
 | 
			
		||||
			let t1 = new Date();
 | 
			
		||||
			result = await this._fetch_related_messages(initial_messages);
 | 
			
		||||
			let t2 = new Date();
 | 
			
		||||
			console.log(
 | 
			
		||||
				`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
 | 
			
		||||
				[JSON.stringify(this.following), start_time, end_time]
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
		this.time_loading = undefined;
 | 
			
		||||
@@ -290,24 +205,13 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
		];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	unread_allowed() {
 | 
			
		||||
		return (
 | 
			
		||||
			this.hash == '#@' ||
 | 
			
		||||
			(!this.hash.startsWith('#%') && !this.hash.startsWith('#@'))
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load_more() {
 | 
			
		||||
		this.loading++;
 | 
			
		||||
		this.loading_canceled = false;
 | 
			
		||||
		try {
 | 
			
		||||
			let more = [];
 | 
			
		||||
			let last_start_time = this.time_range[0];
 | 
			
		||||
			try {
 | 
			
		||||
				more = await this.fetch_messages(null, last_start_time);
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				console.log(e);
 | 
			
		||||
			}
 | 
			
		||||
			more = await this.fetch_messages(null, last_start_time);
 | 
			
		||||
			this.update_time_range_from_messages(
 | 
			
		||||
				more.filter((x) => x.timestamp < last_start_time)
 | 
			
		||||
			);
 | 
			
		||||
@@ -386,16 +290,12 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		this.loading++;
 | 
			
		||||
		let messages = [];
 | 
			
		||||
		let original_hash = this.hash;
 | 
			
		||||
		try {
 | 
			
		||||
			if (this._messages_hash !== this.hash) {
 | 
			
		||||
				this.messages = [];
 | 
			
		||||
				this._messages_hash = this.hash;
 | 
			
		||||
			}
 | 
			
		||||
			this._messages_following = JSON.stringify(this.following);
 | 
			
		||||
			this._private_messages =
 | 
			
		||||
				JSON.stringify(this.private_messages) +
 | 
			
		||||
				JSON.stringify(this.grouped_private_messages);
 | 
			
		||||
			this._messages_following = this.following;
 | 
			
		||||
			let now = new Date().valueOf();
 | 
			
		||||
			let start_time = now - 24 * 60 * 60 * 1000;
 | 
			
		||||
			this.start_time = start_time;
 | 
			
		||||
@@ -408,12 +308,10 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
		} finally {
 | 
			
		||||
			this.loading--;
 | 
			
		||||
		}
 | 
			
		||||
		if (this.hash == original_hash) {
 | 
			
		||||
			this.messages = this.merge_messages(this.messages, messages);
 | 
			
		||||
		}
 | 
			
		||||
		this.messages = this.merge_messages(this.messages, messages);
 | 
			
		||||
		this.time_loading = undefined;
 | 
			
		||||
		console.log(
 | 
			
		||||
			`loading ${messages.length} messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s`
 | 
			
		||||
			`loading messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s`
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -436,42 +334,12 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	close_private_chat() {
 | 
			
		||||
		this.mark_all_read();
 | 
			
		||||
		this.dispatchEvent(
 | 
			
		||||
			new CustomEvent('closeprivatechat', {
 | 
			
		||||
				bubbles: true,
 | 
			
		||||
				composed: true,
 | 
			
		||||
				detail: {
 | 
			
		||||
					key: JSON.stringify(
 | 
			
		||||
						this.hash == '#🔐'
 | 
			
		||||
							? []
 | 
			
		||||
							: this.hash.substring('#🔐'.length).split(',')
 | 
			
		||||
					),
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
		);
 | 
			
		||||
		tfrpc.rpc.setHash('#');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_close_chat_button() {
 | 
			
		||||
		if (this.hash.startsWith('#🔐')) {
 | 
			
		||||
			return html`
 | 
			
		||||
				<button class="w3-button w3-theme-d1" @click=${this.close_private_chat}>
 | 
			
		||||
					Close Chat
 | 
			
		||||
				</button>
 | 
			
		||||
			`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		if (
 | 
			
		||||
			!this.messages ||
 | 
			
		||||
			this._messages_hash !== this.hash ||
 | 
			
		||||
			this._messages_following !== JSON.stringify(this.following) ||
 | 
			
		||||
			this._private_messages !==
 | 
			
		||||
				JSON.stringify(this.private_messages) +
 | 
			
		||||
					JSON.stringify(this.grouped_private_messages)
 | 
			
		||||
			JSON.stringify(this._messages_following) !==
 | 
			
		||||
				JSON.stringify(this.following)
 | 
			
		||||
		) {
 | 
			
		||||
			console.log(
 | 
			
		||||
				`loading messages for ${this.whoami} (following ${this.following.length})`
 | 
			
		||||
@@ -482,16 +350,9 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
		if (!this.hash.startsWith('#%')) {
 | 
			
		||||
			more = html`
 | 
			
		||||
				<p>
 | 
			
		||||
					${this.unread_allowed()
 | 
			
		||||
						? html`
 | 
			
		||||
								<button
 | 
			
		||||
									class="w3-button w3-theme-d1"
 | 
			
		||||
									@click=${this.mark_all_read}
 | 
			
		||||
								>
 | 
			
		||||
									Mark All Read
 | 
			
		||||
								</button>
 | 
			
		||||
							`
 | 
			
		||||
						: undefined}
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
 | 
			
		||||
						Mark All Read
 | 
			
		||||
					</button>
 | 
			
		||||
					<button
 | 
			
		||||
						?disabled=${this.loading}
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
@@ -523,15 +384,9 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
			`;
 | 
			
		||||
		}
 | 
			
		||||
		return cache(html`
 | 
			
		||||
			${this.unread_allowed()
 | 
			
		||||
				? html`<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						@click=${this.mark_all_read}
 | 
			
		||||
					>
 | 
			
		||||
						Mark All Read
 | 
			
		||||
					</button>`
 | 
			
		||||
				: undefined}
 | 
			
		||||
			${this.render_close_chat_button()}
 | 
			
		||||
			<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
 | 
			
		||||
				Mark All Read
 | 
			
		||||
			</button>
 | 
			
		||||
			<tf-news
 | 
			
		||||
				id="news"
 | 
			
		||||
				whoami=${this.whoami}
 | 
			
		||||
@@ -540,11 +395,8 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
				.following=${this.following}
 | 
			
		||||
				.drafts=${this.drafts}
 | 
			
		||||
				.expanded=${this.expanded}
 | 
			
		||||
				hash=${this.hash}
 | 
			
		||||
				channel=${this.channel()}
 | 
			
		||||
				channel_unread=${this.channels_unread?.[this.channel()]}
 | 
			
		||||
				.recent_reactions=${this.recent_reactions}
 | 
			
		||||
				@mark_all_read=${this.mark_all_read}
 | 
			
		||||
			></tf-news>
 | 
			
		||||
			${more}
 | 
			
		||||
		`);
 | 
			
		||||
 
 | 
			
		||||
@@ -24,11 +24,6 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
			channels_latest: {type: Object},
 | 
			
		||||
			connections: {type: Array},
 | 
			
		||||
			private_messages: {type: Array},
 | 
			
		||||
			grouped_private_messages: {type: Object},
 | 
			
		||||
			recent_reactions: {type: Array},
 | 
			
		||||
			peer_exchange: {type: Boolean},
 | 
			
		||||
			is_administrator: {type: Boolean},
 | 
			
		||||
			stay_connected: {type: Boolean},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -48,11 +43,9 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
		this.channels_latest = {};
 | 
			
		||||
		this.channels = [];
 | 
			
		||||
		this.connections = [];
 | 
			
		||||
		this.recent_reactions = [];
 | 
			
		||||
		tfrpc.rpc.localStorageGet('drafts').then(function (d) {
 | 
			
		||||
			self.drafts = JSON.parse(d || '{}');
 | 
			
		||||
		});
 | 
			
		||||
		this.check_peer_exchange();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	connectedCallback() {
 | 
			
		||||
@@ -65,14 +58,6 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
		document.body.removeEventListener('keypress', this.on_keypress.bind(this));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async check_peer_exchange() {
 | 
			
		||||
		if (await tfrpc.rpc.isAdministrator()) {
 | 
			
		||||
			this.peer_exchange = await tfrpc.rpc.globalSettingsGet('peer_exchange');
 | 
			
		||||
		} else {
 | 
			
		||||
			this.peer_exchange = undefined;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	load_latest() {
 | 
			
		||||
		let news = this.shadowRoot?.getElementById('news');
 | 
			
		||||
		if (news) {
 | 
			
		||||
@@ -110,26 +95,7 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	unread_status(channel) {
 | 
			
		||||
		if (channel === undefined) {
 | 
			
		||||
			if (
 | 
			
		||||
				Object.keys(this.channels_unread).some((x) => this.unread_status(x))
 | 
			
		||||
			) {
 | 
			
		||||
				return '✉️ ';
 | 
			
		||||
			}
 | 
			
		||||
		} else if (channel?.startsWith('🔐')) {
 | 
			
		||||
			let key = JSON.stringify(channel.substring('🔐'.length).split(','));
 | 
			
		||||
			if (this.grouped_private_messages?.[key]) {
 | 
			
		||||
				let grouped_latest = Math.max(
 | 
			
		||||
					...this.grouped_private_messages?.[key]?.map((x) => x.rowid)
 | 
			
		||||
				);
 | 
			
		||||
				if (
 | 
			
		||||
					this.channels_unread[channel] === undefined ||
 | 
			
		||||
					grouped_latest > this.channels_unread[channel]
 | 
			
		||||
				) {
 | 
			
		||||
					return '✉️ ';
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else if (
 | 
			
		||||
		if (
 | 
			
		||||
			this.channels_latest[channel] &&
 | 
			
		||||
			this.channels_latest[channel] > 0 &&
 | 
			
		||||
			(this.channels_unread[channel] === undefined ||
 | 
			
		||||
@@ -170,8 +136,11 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
		return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	compare_follows(a, b) {
 | 
			
		||||
		return b[1].ts > a[1].ts ? 1 : b[1].ts < a[1].ts ? -1 : 0;
 | 
			
		||||
	compare_follows() {
 | 
			
		||||
		const now = new Date().valueOf();
 | 
			
		||||
		return function (a, b) {
 | 
			
		||||
			return (b[1].ts > now ? -1 : b[1].ts) - (a[1].ts > now ? -1 : a[1].ts);
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	suggested_follows() {
 | 
			
		||||
@@ -180,24 +149,13 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
		 ** pinned at the top.
 | 
			
		||||
		 */
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let now = new Date().valueOf();
 | 
			
		||||
		return Object.entries(this.users)
 | 
			
		||||
			.filter((x) => x[1].ts < now)
 | 
			
		||||
			.filter((x) => x[1].follow_depth > 1)
 | 
			
		||||
			.sort(self.compare_follows)
 | 
			
		||||
			.sort(self.compare_follows())
 | 
			
		||||
			.slice(0, 8)
 | 
			
		||||
			.map((x) => x[0]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async enable_peer_exchange() {
 | 
			
		||||
		await tfrpc.rpc.globalSettingsSet('peer_exchange', true);
 | 
			
		||||
		await this.check_peer_exchange();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	is_loading() {
 | 
			
		||||
		return this.shadowRoot?.getElementById('news')?.loading;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_sidebar() {
 | 
			
		||||
		return html`
 | 
			
		||||
			<div
 | 
			
		||||
@@ -211,35 +169,6 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
				>
 | 
			
		||||
					×
 | 
			
		||||
				</div>
 | 
			
		||||
				${this.is_administrator
 | 
			
		||||
					? html`
 | 
			
		||||
							<button
 | 
			
		||||
								class="w3-bar-item w3-button"
 | 
			
		||||
								@click=${() =>
 | 
			
		||||
									this.dispatchEvent(
 | 
			
		||||
										new Event('refresh', {bubbles: true, composed: true})
 | 
			
		||||
									)}
 | 
			
		||||
							>
 | 
			
		||||
								<span style="display: inline-block; width: 1.8em">↻</span>
 | 
			
		||||
								Sync now
 | 
			
		||||
							</button>
 | 
			
		||||
							<button
 | 
			
		||||
								class="w3-bar-item w3-button w3-ripple"
 | 
			
		||||
								@click=${() =>
 | 
			
		||||
									this.dispatchEvent(
 | 
			
		||||
										new Event('toggle_stay_connected', {
 | 
			
		||||
											bubbles: true,
 | 
			
		||||
											composed: true,
 | 
			
		||||
										})
 | 
			
		||||
									)}
 | 
			
		||||
							>
 | 
			
		||||
								<span style="display: inline-block; width: 1.8em"
 | 
			
		||||
									>${this.stay_connected ? '🔗' : '⛓️💥'}</span
 | 
			
		||||
								>
 | 
			
		||||
								${this.stay_connected ? 'Online mode' : 'Passive mode'}
 | 
			
		||||
							</button>
 | 
			
		||||
						`
 | 
			
		||||
					: undefined}
 | 
			
		||||
				${this.hash.startsWith('##') &&
 | 
			
		||||
				this.channels.indexOf(this.hash.substring(2)) == -1
 | 
			
		||||
					? html`
 | 
			
		||||
@@ -266,34 +195,11 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
					>${this.unread_status('@')}@mentions</a
 | 
			
		||||
				>
 | 
			
		||||
				<a
 | 
			
		||||
					href="#👍"
 | 
			
		||||
					href="#🔐"
 | 
			
		||||
					class="w3-bar-item w3-button"
 | 
			
		||||
					style=${this.hash == '#👍' ? 'font-weight: bold' : undefined}
 | 
			
		||||
					>${this.unread_status('👍')}👍votes</a
 | 
			
		||||
					style=${this.hash == '#🔐' ? 'font-weight: bold' : undefined}
 | 
			
		||||
					>${this.unread_status('🔐')}🔐private</a
 | 
			
		||||
				>
 | 
			
		||||
				${Object.keys(this?.grouped_private_messages ?? [])
 | 
			
		||||
					?.sort()
 | 
			
		||||
					?.map(
 | 
			
		||||
						(key) => html`
 | 
			
		||||
							<a
 | 
			
		||||
								href=${'#🔐' + JSON.parse(key).join(',')}
 | 
			
		||||
								class="w3-bar-item w3-button"
 | 
			
		||||
								style=${this.hash == '#🔐' + JSON.parse(key).join(',')
 | 
			
		||||
									? 'font-weight: bold'
 | 
			
		||||
									: undefined}
 | 
			
		||||
								>${this.unread_status('🔐' + JSON.parse(key).join(','))}
 | 
			
		||||
								${(key != '[]' ? JSON.parse(key) : [this.whoami]).map(
 | 
			
		||||
									(id) => html`
 | 
			
		||||
										<tf-user
 | 
			
		||||
											id=${id}
 | 
			
		||||
											nolink="true"
 | 
			
		||||
											.users=${this.users}
 | 
			
		||||
										></tf-user>
 | 
			
		||||
									`
 | 
			
		||||
								)}</a
 | 
			
		||||
							>
 | 
			
		||||
						`
 | 
			
		||||
					)}
 | 
			
		||||
				${Object.keys(this.drafts)
 | 
			
		||||
					.sort()
 | 
			
		||||
					.map(
 | 
			
		||||
@@ -317,47 +223,15 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
					`
 | 
			
		||||
				)}
 | 
			
		||||
 | 
			
		||||
				<a class="w3-bar-item w3-theme-d2 w3-button" href="#connections">
 | 
			
		||||
					<h4 style="margin: 0">Connections</h4>
 | 
			
		||||
				</a>
 | 
			
		||||
				${this.connections?.filter((x) => x.id)?.length == 0
 | 
			
		||||
					? html`
 | 
			
		||||
							<button
 | 
			
		||||
								class=${'w3-bar-item w3-button' +
 | 
			
		||||
								(this.connections?.some((x) => x.flags.one_shot)
 | 
			
		||||
									? ' w3-spin'
 | 
			
		||||
									: '')}
 | 
			
		||||
								@click=${() =>
 | 
			
		||||
									this.dispatchEvent(
 | 
			
		||||
										new Event('refresh', {bubbles: true, composed: true})
 | 
			
		||||
									)}
 | 
			
		||||
							>
 | 
			
		||||
								↻ Sync now
 | 
			
		||||
							</button>
 | 
			
		||||
							<button
 | 
			
		||||
								class=${'w3-bar-item w3-button' +
 | 
			
		||||
								(this.peer_exchange !== false ? ' w3-hide' : '')}
 | 
			
		||||
								@click=${this.enable_peer_exchange}
 | 
			
		||||
							>
 | 
			
		||||
								Enable peer exchange
 | 
			
		||||
							</button>
 | 
			
		||||
						`
 | 
			
		||||
					: undefined}
 | 
			
		||||
				<h4 class="w3-bar-item w3-theme-d2">Connections</h4>
 | 
			
		||||
				${this.connections
 | 
			
		||||
					.filter((x) => x.id)
 | 
			
		||||
					.filter((x) => x.id && !x.destroy_reason)
 | 
			
		||||
					.map(
 | 
			
		||||
						(x) => html`
 | 
			
		||||
							<tf-user
 | 
			
		||||
								class="w3-bar-item"
 | 
			
		||||
								style=${x.destroy_reason
 | 
			
		||||
									? 'border-left: 4px solid red; border-right: 4px solid red'
 | 
			
		||||
									: x.connected
 | 
			
		||||
										? x.flags?.one_shot
 | 
			
		||||
											? 'border-left: 4px solid blue; border-right: 4px solid blue'
 | 
			
		||||
											: 'border-left: 4px solid green; border-right: 4px solid green'
 | 
			
		||||
										: ''}
 | 
			
		||||
								style="max-width: 100%"
 | 
			
		||||
								id=${x.id}
 | 
			
		||||
								fallback_name=${x.host}
 | 
			
		||||
								.users=${this.users}
 | 
			
		||||
							></tf-user>
 | 
			
		||||
						`
 | 
			
		||||
@@ -411,7 +285,7 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
		return cache(html`
 | 
			
		||||
			${this.render_sidebar()}
 | 
			
		||||
			<div
 | 
			
		||||
				style="margin-left: 2in; padding: 0px; top: 0; max-height: 100%; overflow: auto; contain: layout"
 | 
			
		||||
				style="margin-left: 2in; padding: 0px; top: 0; max-height: 100%; overflow: auto"
 | 
			
		||||
				id="main"
 | 
			
		||||
				class="w3-main"
 | 
			
		||||
			>
 | 
			
		||||
@@ -436,7 +310,7 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
							class="w3-button w3-hide-large"
 | 
			
		||||
							@click=${this.show_sidebar}
 | 
			
		||||
						>
 | 
			
		||||
							${this.unread_status()}☰
 | 
			
		||||
							☰
 | 
			
		||||
						</div>
 | 
			
		||||
						Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
 | 
			
		||||
						${edit_profile}
 | 
			
		||||
@@ -449,9 +323,6 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
							.drafts=${this.drafts}
 | 
			
		||||
							@tf-draft=${this.draft}
 | 
			
		||||
							.channel=${this.channel()}
 | 
			
		||||
							.recipients=${this.hash.startsWith('#🔐')
 | 
			
		||||
								? this.hash.substring('#🔐'.length).split(',')
 | 
			
		||||
								: undefined}
 | 
			
		||||
						></tf-compose>
 | 
			
		||||
					</div>
 | 
			
		||||
					${profile}
 | 
			
		||||
@@ -468,8 +339,6 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
						.channels_unread=${this.channels_unread}
 | 
			
		||||
						.channels_latest=${this.channels_latest}
 | 
			
		||||
						.private_messages=${this.private_messages}
 | 
			
		||||
						.grouped_private_messages=${this.grouped_private_messages}
 | 
			
		||||
						.recent_reactions=${this.recent_reactions}
 | 
			
		||||
					></tf-tab-news-feed>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,10 +6,7 @@ class TfUserElement extends LitElement {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
		return {
 | 
			
		||||
			id: {type: String},
 | 
			
		||||
			fallback_name: {type: String},
 | 
			
		||||
			icon_only: {type: Boolean},
 | 
			
		||||
			users: {type: Object},
 | 
			
		||||
			nolink: {type: Boolean},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -18,8 +15,6 @@ class TfUserElement extends LitElement {
 | 
			
		||||
	constructor() {
 | 
			
		||||
		super();
 | 
			
		||||
		this.id = null;
 | 
			
		||||
		this.fallback_name = null;
 | 
			
		||||
		this.icon_only = false;
 | 
			
		||||
		this.users = {};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -35,32 +30,24 @@ class TfUserElement extends LitElement {
 | 
			
		||||
			>😎</span
 | 
			
		||||
		>`;
 | 
			
		||||
		let name = this.users?.[this.id]?.name;
 | 
			
		||||
		let name_string = name ?? this.fallback_name ?? this.id;
 | 
			
		||||
		name = this.icon_only
 | 
			
		||||
			? undefined
 | 
			
		||||
			: !this.nolink
 | 
			
		||||
				? html`<a target="_top" href=${'#' + this.id}>${name_string}</a>`
 | 
			
		||||
				: html`<span>${name_string}</span>`;
 | 
			
		||||
		name = html`<a target="_top" href=${'#' + this.id}
 | 
			
		||||
			>${name !== undefined ? name : this.id}</a
 | 
			
		||||
		>`;
 | 
			
		||||
 | 
			
		||||
		if (user) {
 | 
			
		||||
			let image_link = user.image;
 | 
			
		||||
			if (typeof image_link == 'string' && !image_link.startsWith('&')) {
 | 
			
		||||
				try {
 | 
			
		||||
					image_link = JSON.parse(image_link)?.link;
 | 
			
		||||
				} catch {}
 | 
			
		||||
			}
 | 
			
		||||
			image_link =
 | 
			
		||||
				typeof image_link == 'string' ? image_link : image_link?.link;
 | 
			
		||||
			if (image_link !== undefined) {
 | 
			
		||||
				image = html`<img
 | 
			
		||||
					class=${'w3-theme-l4 ' + shape}
 | 
			
		||||
					style="width: 2em; height: 2em; vertical-align: middle; object-fit: cover"
 | 
			
		||||
					src="/${image_link}/view"
 | 
			
		||||
					title=${name_string + ' (' + this.id + ')'}
 | 
			
		||||
				/>`;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return html` <div
 | 
			
		||||
			style=${'display: inline-block; vertical-align: middle; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis' +
 | 
			
		||||
			(this.nolink ? '' : '; font-weight: bold')}
 | 
			
		||||
			style="display: inline-block; vertical-align: middle; font-weight: bold; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis"
 | 
			
		||||
		>
 | 
			
		||||
			${image} ${name}
 | 
			
		||||
		</div>`;
 | 
			
		||||
 
 | 
			
		||||
@@ -50,9 +50,9 @@ function image(node, entering) {
 | 
			
		||||
						'</div>'
 | 
			
		||||
				);
 | 
			
		||||
				if (this.options.safe && potentiallyUnsafe(node.destination)) {
 | 
			
		||||
					this.lit('<img src="" title="');
 | 
			
		||||
					this.lit('<img src="" alt="');
 | 
			
		||||
				} else {
 | 
			
		||||
					this.lit('<img src="' + this.esc(node.destination) + '" title="');
 | 
			
		||||
					this.lit('<img src="' + this.esc(node.destination) + '" alt="');
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			this.disableTags += 1;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "💾",
 | 
			
		||||
	"previous": "&tzZFIe7Y54O4sx1QtAPdemkXh+p5qHXSG/dlS7NP6OQ=.sha256"
 | 
			
		||||
	"previous": "&mvGTlWKFR5QM/3nb4fJ2WQq0n/gNKvBmhGDkAvb8ki8=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ async function query(sql, args) {
 | 
			
		||||
 | 
			
		||||
async function get_biggest() {
 | 
			
		||||
	return query(`
 | 
			
		||||
		select author, size from messages_stats group by author order by size desc limit 10;
 | 
			
		||||
		select author, sum(length(content)) as size from messages group by author order by size desc limit 10;
 | 
			
		||||
	`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -62,14 +62,15 @@ function nice_size(bytes) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	await app.setDocument('<p style="color: #fff">Analyzing feeds...</p>');
 | 
			
		||||
	let most_follows = get_most_follows();
 | 
			
		||||
	await app.setDocument(
 | 
			
		||||
		'<p style="color: #fff">Finding the top 10 largest feeds...</p>'
 | 
			
		||||
	);
 | 
			
		||||
	let most_follows = await get_most_follows();
 | 
			
		||||
	let total = await get_total();
 | 
			
		||||
	let identities = await ssb.getAllIdentities();
 | 
			
		||||
	let following1 = await ssb.following(identities, 1);
 | 
			
		||||
	let following2 = await ssb.following(identities, 2);
 | 
			
		||||
	let biggest = await get_biggest();
 | 
			
		||||
	most_follows = await most_follows;
 | 
			
		||||
	let names = await get_names(
 | 
			
		||||
		[].concat(
 | 
			
		||||
			biggest.map((x) => x.author),
 | 
			
		||||
@@ -93,7 +94,7 @@ async function main() {
 | 
			
		||||
	}
 | 
			
		||||
	let html = `<body style="color: #000; background-color: #ddd">\n
 | 
			
		||||
		<h1>Storage Summary</h1>
 | 
			
		||||
		<h2>Top Accounts by Size</h2>
 | 
			
		||||
		<h2>Top 10 Accounts by Size</h2>
 | 
			
		||||
		<ol>`;
 | 
			
		||||
	for (let item of biggest) {
 | 
			
		||||
		html += `<li>
 | 
			
		||||
@@ -104,7 +105,7 @@ async function main() {
 | 
			
		||||
	}
 | 
			
		||||
	html += `
 | 
			
		||||
		</ol>
 | 
			
		||||
		<h2>Top Accounts by Follows</h2>
 | 
			
		||||
		<h2>Top 10 Accounts by Follows</h2>
 | 
			
		||||
		<ol>`;
 | 
			
		||||
	for (let item of most_follows) {
 | 
			
		||||
		html += `<li>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🕸",
 | 
			
		||||
	"previous": "&n7hu5b8/TsfiG6FDlCRG5nPCrIdCr96+xpIJ/aQT/uM=.sha256"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										100
									
								
								apps/web/app.js
									
									
									
									
									
								
							
							
						
						@@ -1,100 +0,0 @@
 | 
			
		||||
let g_hash;
 | 
			
		||||
 | 
			
		||||
async function query(sql, params) {
 | 
			
		||||
	let results = [];
 | 
			
		||||
	await ssb.sqlAsync(sql, params, function (row) {
 | 
			
		||||
		results.push(row);
 | 
			
		||||
	});
 | 
			
		||||
	return results;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function resolve(id) {
 | 
			
		||||
	try {
 | 
			
		||||
		let blob = await ssb.blobGet(id);
 | 
			
		||||
		if (blob) {
 | 
			
		||||
			let json;
 | 
			
		||||
			try {
 | 
			
		||||
				json = JSON.parse(utf8Decode(blob));
 | 
			
		||||
			} catch {
 | 
			
		||||
				return {id: utf8Decode(blob)};
 | 
			
		||||
			}
 | 
			
		||||
			if (json?.links) {
 | 
			
		||||
				for (let [key, value] of Object.entries(json.links)) {
 | 
			
		||||
					json.links[key] = await resolve(value);
 | 
			
		||||
				}
 | 
			
		||||
				return json;
 | 
			
		||||
			} else {
 | 
			
		||||
				return 'huh?' + json;
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			return `missing<${id}>`;
 | 
			
		||||
		}
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		return id + ': ' + e.message;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function get_names(identities) {
 | 
			
		||||
	return Object.fromEntries(
 | 
			
		||||
		(
 | 
			
		||||
			await query(
 | 
			
		||||
				`
 | 
			
		||||
		SELECT author, name FROM (
 | 
			
		||||
			SELECT
 | 
			
		||||
				messages.author,
 | 
			
		||||
				RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
 | 
			
		||||
				messages.content ->> 'name' AS name
 | 
			
		||||
			FROM messages
 | 
			
		||||
			JOIN json_each(?) AS identities ON identities.value = messages.author
 | 
			
		||||
			WHERE
 | 
			
		||||
				json_extract(messages.content, '$.type') = 'about' AND
 | 
			
		||||
				content ->> 'about' = messages.author AND name IS NOT NULL)
 | 
			
		||||
		WHERE author_rank = 1
 | 
			
		||||
	`,
 | 
			
		||||
				[JSON.stringify(identities)]
 | 
			
		||||
			)
 | 
			
		||||
		).map((x) => [x.author, x.name])
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function render(hash) {
 | 
			
		||||
	g_hash = hash;
 | 
			
		||||
	if (!hash) {
 | 
			
		||||
		let sites = await query(
 | 
			
		||||
			`
 | 
			
		||||
			SELECT site.author, site.id
 | 
			
		||||
			FROM messages site
 | 
			
		||||
			WHERE site.content ->> 'type' = 'web-init'
 | 
			
		||||
		`,
 | 
			
		||||
			[]
 | 
			
		||||
		);
 | 
			
		||||
		let names = await get_names(sites.map((x) => x.author));
 | 
			
		||||
		if (hash === g_hash) {
 | 
			
		||||
			await app.setDocument(
 | 
			
		||||
				`<ul style="background-color: #ddd">${sites.map((x) => `<li><a target="_top" href="#${encodeURIComponent(x.id)}">${names[x.author] ?? x.author} - ${x.id}</a></li>`).join('\n')}</ul>`
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		let site_id =
 | 
			
		||||
			hash.charAt(0) == '#'
 | 
			
		||||
				? decodeURIComponent(hash.substring(1))
 | 
			
		||||
				: decodeURIComponent(hash);
 | 
			
		||||
		await app.setDocument(`<html style="margin: 0; padding: 0; width: 100vw; height: 100vh; margin: 0; padding: 0">
 | 
			
		||||
			<body style="display: flex; flex-direction: column; width: 100vw; height: 100vh">
 | 
			
		||||
				<iframe src="${encodeURIComponent(site_id)}/index.html" style="flex: 1 1; border: 0; background-color: #fff"></iframe>
 | 
			
		||||
			</body>
 | 
			
		||||
		</html>`);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
core.register('message', async function message_handler(message) {
 | 
			
		||||
	if (message.event == 'hashChange') {
 | 
			
		||||
		await render(message.hash);
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	render(null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
@@ -1,63 +0,0 @@
 | 
			
		||||
async function query(sql, params) {
 | 
			
		||||
	let results = [];
 | 
			
		||||
	await ssb.sqlAsync(sql, params, function (row) {
 | 
			
		||||
		results.push(row);
 | 
			
		||||
	});
 | 
			
		||||
	return results;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function guess_content_type(name) {
 | 
			
		||||
	if (name.endsWith('.html')) {
 | 
			
		||||
		return 'text/html; charset=UTF-8';
 | 
			
		||||
	} else if (name.endsWith('.js') || name.endsWith('.mjs')) {
 | 
			
		||||
		return 'text/javascript; charset=UTF-8';
 | 
			
		||||
	} else if (name.endsWith('.css')) {
 | 
			
		||||
		return 'text/stylesheet; charset=UTF-8';
 | 
			
		||||
	} else {
 | 
			
		||||
		return 'application/binary';
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	let path = request.path.replaceAll(/(%[0-9a-fA-F]{2})/g, (x) =>
 | 
			
		||||
		String.fromCharCode(parseInt(x.substring(1), 16))
 | 
			
		||||
	);
 | 
			
		||||
	let match = path.match(/^(%.{44}\.sha256)(?:\/)?(.*)$/);
 | 
			
		||||
 | 
			
		||||
	let content_type = guess_content_type(request.path);
 | 
			
		||||
	let root = await query(
 | 
			
		||||
		`
 | 
			
		||||
		SELECT root.content ->> 'root' AS root
 | 
			
		||||
		FROM messages site
 | 
			
		||||
		JOIN messages root
 | 
			
		||||
		ON site.id = ? AND root.author = site.author AND root.content ->> 'site' = site.id
 | 
			
		||||
		ORDER BY root.sequence DESC LIMIT 1
 | 
			
		||||
	`,
 | 
			
		||||
		[match[1]]
 | 
			
		||||
	);
 | 
			
		||||
	let root_id = root[0]['root'];
 | 
			
		||||
	let last_id = root_id;
 | 
			
		||||
	let blob = await ssb.blobGet(root_id);
 | 
			
		||||
	try {
 | 
			
		||||
		for (let part of match[2]?.split('/')) {
 | 
			
		||||
			let dir = JSON.parse(utf8Decode(blob));
 | 
			
		||||
			last_id = dir?.links[part];
 | 
			
		||||
			blob = await ssb.blobGet(dir?.links[part]);
 | 
			
		||||
			content_type = guess_content_type(part);
 | 
			
		||||
		}
 | 
			
		||||
	} catch {}
 | 
			
		||||
 | 
			
		||||
	respond({
 | 
			
		||||
		status_code: 200,
 | 
			
		||||
		data: blob ? utf8Decode(blob) : `${last_id} not found`,
 | 
			
		||||
		content_type: content_type,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main().catch(function (e) {
 | 
			
		||||
	respond({
 | 
			
		||||
		status_code: 200,
 | 
			
		||||
		data: `${e.message}\n${e.stack}`,
 | 
			
		||||
		content_type: 'text/plain',
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "👋",
 | 
			
		||||
	"previous": "&5NkMRSgcMqCYF3xcLOBmaytkoxfV9zx4br7JladKPTs=.sha256"
 | 
			
		||||
	"previous": "&wAb7J6E35xEXpiXsQ6t1RaWTGIvlatUnyH8ipF6pVic=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								apps/welcome/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
			
		||||
async function main() {
 | 
			
		||||
	await app.setDocument(utf8Decode(getFile('index.html')));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 2.1 KiB  | 
| 
		 Before Width: | Height: | Size: 86 KiB  | 
@@ -28,39 +28,65 @@
 | 
			
		||||
						<b>😎 Tilde Friends</b>
 | 
			
		||||
					</h1>
 | 
			
		||||
					<h1 class="w3-xxlarge w3-text-green">
 | 
			
		||||
						<b>a Secure Scuttlebutt decentralized social network client</b>
 | 
			
		||||
						<b
 | 
			
		||||
							>the Secure Scuttlebutt decentralized social network client that's
 | 
			
		||||
							<i>fancy🎩</i></b
 | 
			
		||||
						>
 | 
			
		||||
					</h1>
 | 
			
		||||
					<p>
 | 
			
		||||
						In addition to participating in Secure Scuttlebutt, Tilde Friends is
 | 
			
		||||
						a platform for building, running, and sharing applications.
 | 
			
		||||
					</p>
 | 
			
		||||
					<a
 | 
			
		||||
						class="w3-button w3-blue w3-padding-large"
 | 
			
		||||
						href="https://www.tildefriends.net/~core/ssb/"
 | 
			
		||||
						>🦀 Try It</a
 | 
			
		||||
					>
 | 
			
		||||
					<p>
 | 
			
		||||
						Available for lots of devices:
 | 
			
		||||
						<i class="fa-brands fa-linux w3-xlarge"></i>
 | 
			
		||||
						<i class="fa-brands fa-android w3-xlarge"></i>
 | 
			
		||||
						<i class="fa-brands fa-apple w3-xlarge"></i>
 | 
			
		||||
						<i class="fa fa-mobile-screen w3-xlarge"></i>
 | 
			
		||||
						<i class="fa-brands fa-windows w3-xlarge"></i>
 | 
			
		||||
					</p>
 | 
			
		||||
					<a
 | 
			
		||||
						class="w3-button w3-black w3-padding-large"
 | 
			
		||||
						href="https://dev.tildefriends.net/cory/tildefriends/releases/latest"
 | 
			
		||||
						href="https://dev.tildefriends.net/cory/tildefriends/releases"
 | 
			
		||||
						><i class="fa fa-download"></i> Download</a
 | 
			
		||||
					>
 | 
			
		||||
					<a
 | 
			
		||||
						class="w3-button w3-black w3-padding-large"
 | 
			
		||||
						href="https://dev.tildefriends.net/cory/tildefriends"
 | 
			
		||||
						href="https://www.tildefriends.net/~core/ssb/"
 | 
			
		||||
						><i class="fa fa-link"></i> Try It</a
 | 
			
		||||
					>
 | 
			
		||||
					<a
 | 
			
		||||
						class="w3-button w3-black w3-padding-large"
 | 
			
		||||
						href="https://dev.tildefriends.net/"
 | 
			
		||||
						><i class="fa fa-mug-hot"></i> Development</a
 | 
			
		||||
					>
 | 
			
		||||
						<img src="gitea.svg" style="height: 1em; margin: 0" />
 | 
			
		||||
						Development
 | 
			
		||||
					</a>
 | 
			
		||||
					<a
 | 
			
		||||
						class="w3-button w3-black w3-padding-large"
 | 
			
		||||
						href="https://docs.tildefriends.net/"
 | 
			
		||||
						><i class="fa fa-book"></i> Documentation</a
 | 
			
		||||
					>
 | 
			
		||||
					<a
 | 
			
		||||
						class="w3-button w3-black w3-padding-large"
 | 
			
		||||
						href="https://www.tildefriends.net/~cory/tildeblog/"
 | 
			
		||||
						><i class="fa fa-solid fa-square-rss"></i> Blog</a
 | 
			
		||||
					>
 | 
			
		||||
					<p>
 | 
			
		||||
						<a
 | 
			
		||||
							class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
							href="https://f-droid.org/en/packages/com.unprompted.tildefriends.fdroid/"
 | 
			
		||||
							><img src="f-droid.svg" style="height: 2em; margin: 0" /> Get it
 | 
			
		||||
							on F-Droid</a
 | 
			
		||||
						>
 | 
			
		||||
						<a
 | 
			
		||||
							class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
							href="https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage"
 | 
			
		||||
						>
 | 
			
		||||
							<img src="appimage.svg" style="height: 2em; margin: 0" />
 | 
			
		||||
							Get Linux 64-bit AppImage
 | 
			
		||||
						</a>
 | 
			
		||||
						<a
 | 
			
		||||
							class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
							href="https://play.google.com/store/apps/details?id=com.unprompted.tildefriends"
 | 
			
		||||
						>
 | 
			
		||||
							<img src="googleplay.svg" style="height: 2em; margin: 0" />
 | 
			
		||||
							Get it on Google Play (Open Testing)
 | 
			
		||||
						</a>
 | 
			
		||||
					</p>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="w3-col l4 m6">
 | 
			
		||||
					<img src="tildefriends.png" class="w3-image w3-right w3-hide-small" />
 | 
			
		||||
@@ -78,119 +104,15 @@
 | 
			
		||||
					<h2>First-time user checklist:</h2>
 | 
			
		||||
					<ol type="1" style="text-align: left">
 | 
			
		||||
						<li>
 | 
			
		||||
							<a
 | 
			
		||||
								href="https://dev.tildefriends.net/cory/tildefriends/releases/latest"
 | 
			
		||||
							<a href="https://dev.tildefriends.net/cory/tildefriends/releases"
 | 
			
		||||
								>Download</a
 | 
			
		||||
							>
 | 
			
		||||
							Tilde Friends or use
 | 
			
		||||
							<a href="https://www.tildefriends.net/"
 | 
			
		||||
								>https://www.tildefriends.net/</a
 | 
			
		||||
							>.
 | 
			
		||||
							<div class="w3-cell-row">
 | 
			
		||||
								<div class="w3-container w3-cell w3-mobile">
 | 
			
		||||
									<h3>Mobile</h3>
 | 
			
		||||
									<p>
 | 
			
		||||
										<a
 | 
			
		||||
											class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
											href="https://f-droid.org/en/packages/com.unprompted.tildefriends.fdroid/"
 | 
			
		||||
											><img src="f-droid.svg" style="height: 2em; margin: 0" />
 | 
			
		||||
											Get it on F-Droid</a
 | 
			
		||||
										>
 | 
			
		||||
										<a
 | 
			
		||||
											class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
											href="https://play.google.com/store/apps/details?id=com.unprompted.tildefriends"
 | 
			
		||||
										>
 | 
			
		||||
											<img
 | 
			
		||||
												src="googleplay.svg"
 | 
			
		||||
												style="height: 2em; margin: 0"
 | 
			
		||||
											/>
 | 
			
		||||
											Get it on Google Play (Open Testing)
 | 
			
		||||
										</a>
 | 
			
		||||
										<a
 | 
			
		||||
											class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
											href="https://testflight.apple.com/join/tXxgtSpE"
 | 
			
		||||
										>
 | 
			
		||||
											<img src="ios.svg" style="height: 2em; margin: 0" />
 | 
			
		||||
											Get it on iOS (TestFlight)
 | 
			
		||||
										</a>
 | 
			
		||||
									</p>
 | 
			
		||||
									<p>Just launch the app.</p>
 | 
			
		||||
								</div>
 | 
			
		||||
								<div class="w3-container w3-cell w3-mobile">
 | 
			
		||||
									<h3>Web</h3>
 | 
			
		||||
									<p>
 | 
			
		||||
										<a
 | 
			
		||||
											class="w3-button w3-round-large w3-blue w3-padding-large"
 | 
			
		||||
											href="https://www.tildefriends.net/~core/ssb/"
 | 
			
		||||
											>🦀 Try It</a
 | 
			
		||||
										>
 | 
			
		||||
									</p>
 | 
			
		||||
									<p>
 | 
			
		||||
										<a href="/login?return=/~core/intro"
 | 
			
		||||
											>Register an account with tildefriends.net</a
 | 
			
		||||
										>
 | 
			
		||||
										to take it for a spin right away.
 | 
			
		||||
									</p>
 | 
			
		||||
									<h3>PeachCloud</h3>
 | 
			
		||||
									<p>
 | 
			
		||||
										Tilde Friends is also a part of 🍑☁️<a
 | 
			
		||||
											href="https://peach-docs.commoninternet.net/"
 | 
			
		||||
											>PeachCloud</a
 | 
			
		||||
										>, which is available on
 | 
			
		||||
										<a href="https://apps.yunohost.org/app/peachpub"
 | 
			
		||||
											>YunoHost</a
 | 
			
		||||
										>
 | 
			
		||||
										for accessible self-hosting.
 | 
			
		||||
									</p>
 | 
			
		||||
								</div>
 | 
			
		||||
								<div class="w3-container w3-cell w3-mobile">
 | 
			
		||||
									<h3>Desktop</h3>
 | 
			
		||||
									<p>
 | 
			
		||||
										<a
 | 
			
		||||
											class="w3-button w3-round-large w3-black w3-padding-large"
 | 
			
		||||
											href="https://dev.tildefriends.net/cory/tildefriends/releases"
 | 
			
		||||
											><i class="fa fa-download"></i> Download</a
 | 
			
		||||
										>
 | 
			
		||||
										<a
 | 
			
		||||
											class="w3-button w3-round-large w3-padding w3-blue-gray"
 | 
			
		||||
											href="https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage"
 | 
			
		||||
										>
 | 
			
		||||
											<img src="appimage.svg" style="height: 2em; margin: 0" />
 | 
			
		||||
											Get Linux 64-bit AppImage
 | 
			
		||||
										</a>
 | 
			
		||||
									</p>
 | 
			
		||||
									<p>
 | 
			
		||||
										Tilde Friends is distributed as a single executable file (or
 | 
			
		||||
										source that you can
 | 
			
		||||
										<a href="http://dev.tildefriends.net">build yourself</a>)
 | 
			
		||||
										and stores all of its data in a single
 | 
			
		||||
										file(<code>db.sqlite</code>). You can generally download the
 | 
			
		||||
										latest executable from
 | 
			
		||||
										<a
 | 
			
		||||
											href="https://dev.tildefriends.net/cory/tildefriends/releases"
 | 
			
		||||
											>releases</a
 | 
			
		||||
										>
 | 
			
		||||
										for your platform, mark it as executable (<code
 | 
			
		||||
											>chmod +x tildefriends*</code
 | 
			
		||||
										>
 | 
			
		||||
										on macOS and Linux), and run. Run with <code>-h</code> to
 | 
			
		||||
										learn more.
 | 
			
		||||
									</p>
 | 
			
		||||
									<p>
 | 
			
		||||
										Tilde Friends will run in the console and provide a web
 | 
			
		||||
										interface at
 | 
			
		||||
										<a href="http://localhost:12345/">http://localhost:12345/</a
 | 
			
		||||
										>. You will have to register a username and password to sign
 | 
			
		||||
										into your instance.
 | 
			
		||||
									</p>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
							<p>
 | 
			
		||||
								After a <a href="/~core/intro">brief introduction</a>, Tilde
 | 
			
		||||
								Friends will take you to the Secure Scuttlebutt social network
 | 
			
		||||
								app.
 | 
			
		||||
							</p>
 | 
			
		||||
						</li>
 | 
			
		||||
						<li>Create an account to identify yourself with that instance.</li>
 | 
			
		||||
						<li>
 | 
			
		||||
							Describe yourself in your profile in the <b>ssb</b> app. Give
 | 
			
		||||
							yourself a name and an avatar if you like.
 | 
			
		||||
@@ -224,11 +146,11 @@
 | 
			
		||||
		<!-- SSB Section -->
 | 
			
		||||
		<div class="w3-light-grey">
 | 
			
		||||
			<div class="w3-row-padding w3-padding-64">
 | 
			
		||||
				<div class="w3-col l4 m6 s4 w3-center">
 | 
			
		||||
				<div class="w3-col l4 m6 s4">
 | 
			
		||||
					<a href="https://scuttlebutt.nz/"
 | 
			
		||||
						><img
 | 
			
		||||
							class="w3-image"
 | 
			
		||||
							src="hermietildefriends.svg"
 | 
			
		||||
							class="w3-image w3-round-large"
 | 
			
		||||
							src="ssb.png"
 | 
			
		||||
							alt="Secure Scuttlebutt"
 | 
			
		||||
					/></a>
 | 
			
		||||
				</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="814" height="1000">
 | 
			
		||||
  <path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 660 B  | 
							
								
								
									
										
											BIN
										
									
								
								apps/welcome/ssb.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 50 KiB  | 
| 
		 Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB  | 
@@ -1,4 +1,4 @@
 | 
			
		||||
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
 | 
			
		||||
/* 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}
 | 
			
		||||
@@ -108,8 +108,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
 | 
			
		||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
 | 
			
		||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
 | 
			
		||||
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
 | 
			
		||||
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
 | 
			
		||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
 | 
			
		||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
 | 
			
		||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
 | 
			
		||||
@@ -150,7 +148,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-button:hover{color:#000!important;background-color:#ccc!important}
 | 
			
		||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
 | 
			
		||||
.w3-hover-none:hover{box-shadow:none!important}
 | 
			
		||||
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
 | 
			
		||||
/* Colors */
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
 | 
			
		||||
@@ -178,19 +175,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
 | 
			
		||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
 | 
			
		||||
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
 | 
			
		||||
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
 | 
			
		||||
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
 | 
			
		||||
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
 | 
			
		||||
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
 | 
			
		||||
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
 | 
			
		||||
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
 | 
			
		||||
.w3-danger{color:#fff!important;background-color:#dd0000!important}
 | 
			
		||||
.w3-note{color:#000!important;background-color:#fff599!important}
 | 
			
		||||
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
 | 
			
		||||
.w3-warning{color:#000!important;background-color:#ffb305!important}
 | 
			
		||||
.w3-success{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
 | 
			
		||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
 | 
			
		||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
 | 
			
		||||
@@ -248,4 +232,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.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}
 | 
			
		||||
.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}
 | 
			
		||||
							
								
								
									
										42
									
								
								apps/wiki/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										123
									
								
								core/app.js
									
									
									
									
									
								
							
							
						
						@@ -1,48 +1,53 @@
 | 
			
		||||
/**
 | 
			
		||||
 * \file
 | 
			
		||||
 * \defgroup tfapp Tilde Friends App JS
 | 
			
		||||
 * Tilde Friends server-side app wrapper.
 | 
			
		||||
 * @{
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/** \cond */
 | 
			
		||||
import * as core from './core.js';
 | 
			
		||||
 | 
			
		||||
export {App};
 | 
			
		||||
/** \endcond */
 | 
			
		||||
let g_next_id = 1;
 | 
			
		||||
let g_calls = {};
 | 
			
		||||
 | 
			
		||||
/** A sequence number of apps. */
 | 
			
		||||
let g_session_index = 0;
 | 
			
		||||
let gSessionIndex = 0;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 ** App constructor.
 | 
			
		||||
 ** @return An app instance.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function makeSessionId() {
 | 
			
		||||
	return 'session_' + (gSessionIndex++).toString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function App() {
 | 
			
		||||
	this._on_output = null;
 | 
			
		||||
	this._send_queue = [];
 | 
			
		||||
	this.calls = {};
 | 
			
		||||
	this._next_call_id = 1;
 | 
			
		||||
	return this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 ** Create a function wrapper that when called invokes a function on the app
 | 
			
		||||
 ** itself.
 | 
			
		||||
 ** @param api The function and argument names.
 | 
			
		||||
 ** @return A function.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} callback
 | 
			
		||||
 */
 | 
			
		||||
App.prototype.readOutput = function (callback) {
 | 
			
		||||
	this._on_output = callback;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} api
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
App.prototype.makeFunction = function (api) {
 | 
			
		||||
	let self = this;
 | 
			
		||||
	let result = function () {
 | 
			
		||||
		let id = self._next_call_id++;
 | 
			
		||||
		while (!id || self.calls[id]) {
 | 
			
		||||
			id = self._next_call_id++;
 | 
			
		||||
		let id = g_next_id++;
 | 
			
		||||
		while (!id || g_calls[id]) {
 | 
			
		||||
			id = g_next_id++;
 | 
			
		||||
		}
 | 
			
		||||
		let promise = new Promise(function (resolve, reject) {
 | 
			
		||||
			self.calls[id] = {resolve: resolve, reject: reject};
 | 
			
		||||
			g_calls[id] = {resolve: resolve, reject: reject};
 | 
			
		||||
		});
 | 
			
		||||
		let message = {
 | 
			
		||||
			action: 'tfrpc',
 | 
			
		||||
			message: 'tfrpc',
 | 
			
		||||
			method: api[0],
 | 
			
		||||
			params: [...arguments],
 | 
			
		||||
			id: id,
 | 
			
		||||
@@ -55,8 +60,8 @@ App.prototype.makeFunction = function (api) {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 ** Send a message to the app.
 | 
			
		||||
 ** @param message The message to send.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} message
 | 
			
		||||
 */
 | 
			
		||||
App.prototype.send = function (message) {
 | 
			
		||||
	if (this._send_queue) {
 | 
			
		||||
@@ -73,9 +78,9 @@ App.prototype.send = function (message) {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 ** App socket handler.
 | 
			
		||||
 ** @param request The HTTP request of the WebSocket connection.
 | 
			
		||||
 ** @param response The HTTP response.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} request
 | 
			
		||||
 * @param {*} response
 | 
			
		||||
 */
 | 
			
		||||
exports.app_socket = async function socket(request, response) {
 | 
			
		||||
	let process;
 | 
			
		||||
@@ -97,16 +102,10 @@ exports.app_socket = async function socket(request, response) {
 | 
			
		||||
			try {
 | 
			
		||||
				message = JSON.parse(event.data);
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				print(
 | 
			
		||||
					'WebSocket error:',
 | 
			
		||||
					error,
 | 
			
		||||
					event.data,
 | 
			
		||||
					event.data.length,
 | 
			
		||||
					event.opCode
 | 
			
		||||
				);
 | 
			
		||||
				print('ERROR', error, event.data, event.data.length, event.opCode);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			if (!process && message.action == 'hello') {
 | 
			
		||||
			if (message.action == 'hello') {
 | 
			
		||||
				let packageOwner;
 | 
			
		||||
				let packageName;
 | 
			
		||||
				let blobId;
 | 
			
		||||
@@ -123,7 +122,7 @@ exports.app_socket = async function socket(request, response) {
 | 
			
		||||
					if (!blobId) {
 | 
			
		||||
						response.send(
 | 
			
		||||
							JSON.stringify({
 | 
			
		||||
								action: 'tfrpc',
 | 
			
		||||
								message: 'tfrpc',
 | 
			
		||||
								method: 'error',
 | 
			
		||||
								params: [message.path + ' not found'],
 | 
			
		||||
								id: -1,
 | 
			
		||||
@@ -164,7 +163,7 @@ exports.app_socket = async function socket(request, response) {
 | 
			
		||||
				options.packageOwner = packageOwner;
 | 
			
		||||
				options.packageName = packageName;
 | 
			
		||||
				options.url = message.url;
 | 
			
		||||
				let sessionId = 'session_' + (g_session_index++).toString();
 | 
			
		||||
				let sessionId = makeSessionId();
 | 
			
		||||
				if (blobId) {
 | 
			
		||||
					if (message.edit_only) {
 | 
			
		||||
						response.send(
 | 
			
		||||
@@ -176,24 +175,9 @@ exports.app_socket = async function socket(request, response) {
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				if (process) {
 | 
			
		||||
					process.client_api.tfrpc = function (message) {
 | 
			
		||||
						if (message.id) {
 | 
			
		||||
							let calls = process?.app?.calls;
 | 
			
		||||
							if (calls) {
 | 
			
		||||
								let call = calls[message.id];
 | 
			
		||||
								if (call) {
 | 
			
		||||
									if (message.error !== undefined) {
 | 
			
		||||
										call.reject(message.error);
 | 
			
		||||
									} else {
 | 
			
		||||
										call.resolve(message.result);
 | 
			
		||||
									}
 | 
			
		||||
									delete calls[message.id];
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					};
 | 
			
		||||
					process.app._on_output = (message) =>
 | 
			
		||||
					process.app.readOutput(function (message) {
 | 
			
		||||
						response.send(JSON.stringify(message), 0x1);
 | 
			
		||||
					});
 | 
			
		||||
					process.app.send();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
@@ -222,13 +206,26 @@ exports.app_socket = async function socket(request, response) {
 | 
			
		||||
				if (process && process.timeout > 0) {
 | 
			
		||||
					setTimeout(ping, process.timeout);
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
			} else if (message.action == 'resetPermission') {
 | 
			
		||||
				if (process) {
 | 
			
		||||
					if (process.client_api[message.action]) {
 | 
			
		||||
						process.client_api[message.action](message);
 | 
			
		||||
					} else if (process.eventHandlers['message']) {
 | 
			
		||||
						await core.invoke(process.eventHandlers['message'], [message]);
 | 
			
		||||
					process.resetPermission(message.permission);
 | 
			
		||||
				}
 | 
			
		||||
			} else if (message.action == 'setActiveIdentity') {
 | 
			
		||||
				process.setActiveIdentity(message.identity);
 | 
			
		||||
			} else if (message.action == 'createIdentity') {
 | 
			
		||||
				await process.createIdentity();
 | 
			
		||||
			} else if (message.message == 'tfrpc') {
 | 
			
		||||
				if (message.id && g_calls[message.id]) {
 | 
			
		||||
					if (message.error !== undefined) {
 | 
			
		||||
						g_calls[message.id].reject(message.error);
 | 
			
		||||
					} else {
 | 
			
		||||
						g_calls[message.id].resolve(message.result);
 | 
			
		||||
					}
 | 
			
		||||
					delete g_calls[message.id];
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				if (process && process.eventHandlers['message']) {
 | 
			
		||||
					await core.invoke(process.eventHandlers['message'], [message]);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else if (event.opCode == 0x8) {
 | 
			
		||||
@@ -249,4 +246,4 @@ exports.app_socket = async function socket(request, response) {
 | 
			
		||||
	response.upgrade(100, {});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** @} */
 | 
			
		||||
export {App};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										495
									
								
								core/client.js
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										193
									
								
								core/core.js
									
									
									
									
									
								
							
							
						
						@@ -1,47 +1,32 @@
 | 
			
		||||
/**
 | 
			
		||||
 * \file
 | 
			
		||||
 * \defgroup tfcore Tilde Friends Core JS
 | 
			
		||||
 * Tilde Friends process management, in JavaScript.
 | 
			
		||||
 * @{
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/** \cond */
 | 
			
		||||
import * as app from './app.js';
 | 
			
		||||
import * as http from './http.js';
 | 
			
		||||
 | 
			
		||||
export {invoke, getProcessBlob};
 | 
			
		||||
/** \endcond */
 | 
			
		||||
 | 
			
		||||
/** All running processes. */
 | 
			
		||||
let gProcesses = {};
 | 
			
		||||
/** Whether stats are currently being sent. */
 | 
			
		||||
let gStatsTimer = false;
 | 
			
		||||
/** Effectively a process ID. */
 | 
			
		||||
let g_handler_index = 0;
 | 
			
		||||
/** Time between pings, in milliseconds. */
 | 
			
		||||
const k_ping_interval = 60 * 1000;
 | 
			
		||||
let kPingInterval = 60 * 1000;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Print an error.
 | 
			
		||||
 * @param error The error.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} out
 | 
			
		||||
 * @param {*} error
 | 
			
		||||
 */
 | 
			
		||||
function printError(error) {
 | 
			
		||||
function printError(out, error) {
 | 
			
		||||
	if (error.stackTrace) {
 | 
			
		||||
		print(error.fileName + ':' + error.lineNumber + ': ' + error.message);
 | 
			
		||||
		print(error.stackTrace);
 | 
			
		||||
		out.print(error.fileName + ':' + error.lineNumber + ': ' + error.message);
 | 
			
		||||
		out.print(error.stackTrace);
 | 
			
		||||
	} else {
 | 
			
		||||
		for (let [k, v] of Object.entries(error)) {
 | 
			
		||||
			print(k, v);
 | 
			
		||||
			out.print(k, v);
 | 
			
		||||
		}
 | 
			
		||||
		print(error.toString());
 | 
			
		||||
		out.print(error.toString());
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Invoke a handler.
 | 
			
		||||
 * @param handlers The handlers on which to invoke the callback.
 | 
			
		||||
 * @param argv Arguments to pass to the handlers.
 | 
			
		||||
 * @return A promise.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} handlers
 | 
			
		||||
 * @param {*} argv
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function invoke(handlers, argv) {
 | 
			
		||||
	let promises = [];
 | 
			
		||||
@@ -64,10 +49,10 @@ function invoke(handlers, argv) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Broadcast a named event to all registered apps.
 | 
			
		||||
 * @param eventName the name of the event.
 | 
			
		||||
 * @param argv Arguments to pass to the handlers.
 | 
			
		||||
 * @return A promise.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} eventName
 | 
			
		||||
 * @param {*} argv
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function broadcastEvent(eventName, argv) {
 | 
			
		||||
	let promises = [];
 | 
			
		||||
@@ -80,9 +65,9 @@ function broadcastEvent(eventName, argv) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Send a message to all other instances of the same app.
 | 
			
		||||
 * @param message The message.
 | 
			
		||||
 * @return A promise.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} message
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function broadcast(message) {
 | 
			
		||||
	let sender = this;
 | 
			
		||||
@@ -101,13 +86,10 @@ function broadcast(message) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Send a message to all instances of the same app running as the same user.
 | 
			
		||||
 * @param user The user.
 | 
			
		||||
 * @param packageOwner The owner of the app.
 | 
			
		||||
 * @param packageName The name of the app.
 | 
			
		||||
 * @param eventName The name of the event.
 | 
			
		||||
 * @param argv The arguments to pass.
 | 
			
		||||
 * @return A promise.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {String} eventName
 | 
			
		||||
 * @param {*} argv
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function broadcastAppEventToUser(
 | 
			
		||||
	user,
 | 
			
		||||
@@ -132,9 +114,10 @@ function broadcastAppEventToUser(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get user context information for a call.
 | 
			
		||||
 * @param caller The calling process.
 | 
			
		||||
 * @param process The receiving process.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} caller
 | 
			
		||||
 * @param {*} process
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function getUser(caller, process) {
 | 
			
		||||
	return {
 | 
			
		||||
@@ -147,11 +130,43 @@ function getUser(caller, process) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Send a message.
 | 
			
		||||
 * @param from The calling process.
 | 
			
		||||
 * @param to The receiving process.
 | 
			
		||||
 * @param message The message.
 | 
			
		||||
 * @return A promise.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} user
 | 
			
		||||
 * @param {*} process
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
async function getApps(user, process) {
 | 
			
		||||
	if (
 | 
			
		||||
		process.credentials &&
 | 
			
		||||
		process.credentials.session &&
 | 
			
		||||
		process.credentials.session.name
 | 
			
		||||
	) {
 | 
			
		||||
		if (user && user !== process.credentials.session.name && user !== 'core') {
 | 
			
		||||
			return {};
 | 
			
		||||
		} else if (!user) {
 | 
			
		||||
			user = process.credentials.session.name;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if (user) {
 | 
			
		||||
		let db = new Database(user);
 | 
			
		||||
		try {
 | 
			
		||||
			let names = JSON.parse(await db.get('apps'));
 | 
			
		||||
			let result = {};
 | 
			
		||||
			for (let name of names) {
 | 
			
		||||
				result[name] = await db.get('path:' + name);
 | 
			
		||||
			}
 | 
			
		||||
			return result;
 | 
			
		||||
		} catch {}
 | 
			
		||||
	}
 | 
			
		||||
	return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} from
 | 
			
		||||
 * @param {*} to
 | 
			
		||||
 * @param {*} message
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function postMessageInternal(from, to, message) {
 | 
			
		||||
	if (to.eventHandlers['message']) {
 | 
			
		||||
@@ -160,13 +175,14 @@ function postMessageInternal(from, to, message) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get or create a process for an app blob.
 | 
			
		||||
 * @param blobId The blob identifier.
 | 
			
		||||
 * @param key A unique key for the invocation.
 | 
			
		||||
 * @param options Other options.
 | 
			
		||||
 * @return The process.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} blobId
 | 
			
		||||
 * @param {*} key
 | 
			
		||||
 * @param {*} options
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
	// TODO(tasiaiso): break this down ?
 | 
			
		||||
	let process = gProcesses[key];
 | 
			
		||||
	if (!process && !(options && 'create' in options && !options.create)) {
 | 
			
		||||
		let resolveReady;
 | 
			
		||||
@@ -185,7 +201,7 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
			}
 | 
			
		||||
			process.lastActive = Date.now();
 | 
			
		||||
			process.lastPing = null;
 | 
			
		||||
			process.timeout = k_ping_interval;
 | 
			
		||||
			process.timeout = kPingInterval;
 | 
			
		||||
			process.ready = new Promise(function (resolve, reject) {
 | 
			
		||||
				resolveReady = resolve;
 | 
			
		||||
				rejectReady = reject;
 | 
			
		||||
@@ -257,6 +273,7 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
						let settings = await loadSettings();
 | 
			
		||||
						return settings?.permissions?.[user] ?? [];
 | 
			
		||||
					},
 | 
			
		||||
					apps: (user) => getApps(user, process),
 | 
			
		||||
					getSockets: getSockets,
 | 
			
		||||
					permissionTest: async function (permission) {
 | 
			
		||||
						let user = process?.credentials?.session?.name;
 | 
			
		||||
@@ -444,10 +461,10 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
					if (process.app) {
 | 
			
		||||
						process.app.makeFunction(['error'])(error);
 | 
			
		||||
					} else {
 | 
			
		||||
						printError(error);
 | 
			
		||||
						printError({print: print}, error);
 | 
			
		||||
					}
 | 
			
		||||
				} catch (e) {
 | 
			
		||||
					printError(error);
 | 
			
		||||
					printError({print: print}, error);
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
			imports.ssb = Object.fromEntries(
 | 
			
		||||
@@ -632,26 +649,17 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
					permissions: await imports.core.permissionsGranted(),
 | 
			
		||||
				});
 | 
			
		||||
			};
 | 
			
		||||
			process.client_api = {
 | 
			
		||||
				createIdentity: function () {
 | 
			
		||||
					return process.createIdentity();
 | 
			
		||||
				},
 | 
			
		||||
				resetPermission: async function resetPermission(message) {
 | 
			
		||||
					let user = process?.credentials?.session?.name;
 | 
			
		||||
					await ssb.setUserPermission(
 | 
			
		||||
						user,
 | 
			
		||||
						options?.packageOwner,
 | 
			
		||||
						options?.packageName,
 | 
			
		||||
						message.permission,
 | 
			
		||||
						undefined
 | 
			
		||||
					);
 | 
			
		||||
					return process.sendPermissions();
 | 
			
		||||
				},
 | 
			
		||||
				setActiveIdentity: function setActiveIdentity(message) {
 | 
			
		||||
					return process.setActiveIdentity(message.identity);
 | 
			
		||||
				},
 | 
			
		||||
			process.resetPermission = async function resetPermission(permission) {
 | 
			
		||||
				let user = process?.credentials?.session?.name;
 | 
			
		||||
				await ssb.setUserPermission(
 | 
			
		||||
					user,
 | 
			
		||||
					options?.packageOwner,
 | 
			
		||||
					options?.packageName,
 | 
			
		||||
					permission,
 | 
			
		||||
					undefined
 | 
			
		||||
				);
 | 
			
		||||
				return process.sendPermissions();
 | 
			
		||||
			};
 | 
			
		||||
			ssb.registerImports(imports, process);
 | 
			
		||||
			process.task.setImports(imports);
 | 
			
		||||
			process.task.activate();
 | 
			
		||||
			let source = await ssb.blobGet(blobId);
 | 
			
		||||
@@ -678,7 +686,7 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
					);
 | 
			
		||||
				}
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				printError(e);
 | 
			
		||||
				printError({print: print}, e);
 | 
			
		||||
			}
 | 
			
		||||
			broadcastEvent('onSessionBegin', [getUser(process, process)]);
 | 
			
		||||
			if (process.app) {
 | 
			
		||||
@@ -692,10 +700,14 @@ async function getProcessBlob(blobId, key, options) {
 | 
			
		||||
				sendStats();
 | 
			
		||||
			}
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			if (process?.app && process?.task?.onError) {
 | 
			
		||||
				process.task.onError(error);
 | 
			
		||||
			if (process.app) {
 | 
			
		||||
				if (process?.task?.onError) {
 | 
			
		||||
					process.task.onError(error);
 | 
			
		||||
				} else {
 | 
			
		||||
					printError({print: print}, error);
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				printError(error);
 | 
			
		||||
				printError({print: print}, error);
 | 
			
		||||
			}
 | 
			
		||||
			rejectReady(error);
 | 
			
		||||
		}
 | 
			
		||||
@@ -716,8 +728,7 @@ ssb.addEventListener('connections', function () {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Load settings from the database.
 | 
			
		||||
 * @return The settings as a key value pairs object.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 */
 | 
			
		||||
async function loadSettings() {
 | 
			
		||||
	let data = {};
 | 
			
		||||
@@ -738,7 +749,7 @@ async function loadSettings() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Send periodic stats to all clients.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 */
 | 
			
		||||
function sendStats() {
 | 
			
		||||
	let apps = Object.values(gProcesses)
 | 
			
		||||
@@ -755,16 +766,8 @@ function sendStats() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Invoke an app's handler.js.
 | 
			
		||||
 * @param response The response object.
 | 
			
		||||
 * @param app_blob_id The app's blob identifier.
 | 
			
		||||
 * @param path The request path.
 | 
			
		||||
 * @param query The request query string.
 | 
			
		||||
 * @param headers The request headers.
 | 
			
		||||
 * @param package_owner The app's owner.
 | 
			
		||||
 * @param package_name The app's name.
 | 
			
		||||
 */
 | 
			
		||||
let g_handler_index = 0;
 | 
			
		||||
 | 
			
		||||
exports.callAppHandler = async function callAppHandler(
 | 
			
		||||
	response,
 | 
			
		||||
	app_blob_id,
 | 
			
		||||
@@ -830,4 +833,4 @@ exports.callAppHandler = async function callAppHandler(
 | 
			
		||||
	response.end(answer?.data);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** @} */
 | 
			
		||||
export {invoke, getProcessBlob};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										34
									
								
								core/http.js
									
									
									
									
									
								
							
							
						
						@@ -1,14 +1,8 @@
 | 
			
		||||
/**
 | 
			
		||||
 * \file
 | 
			
		||||
 * \defgroup tfhttp Tilde Friends HTTP Client JS
 | 
			
		||||
 * Tilde Friends server-side HTTP client.
 | 
			
		||||
 * @{
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Parse a URL into protocol, host, path, and port parts.
 | 
			
		||||
 * @param url
 | 
			
		||||
 * @return An object of the URL parts.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * TODO: document so we can improve this
 | 
			
		||||
 * @param {*} url
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function parseUrl(url) {
 | 
			
		||||
	// XXX: Hack.
 | 
			
		||||
@@ -22,9 +16,9 @@ function parseUrl(url) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Parse an HTTP response into headers and body content.
 | 
			
		||||
 * @param data The response data, headers and body included.
 | 
			
		||||
 * @return headers and body data.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} data
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function parseResponse(data) {
 | 
			
		||||
	let firstLine;
 | 
			
		||||
@@ -42,15 +36,15 @@ function parseResponse(data) {
 | 
			
		||||
			headers[line.substring(colon)] = line.substring(colon + 1);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return {headers: headers, body: data};
 | 
			
		||||
	return {body: data};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Make an HTTP request.
 | 
			
		||||
 * @param url The URL.
 | 
			
		||||
 * @param options Request options.
 | 
			
		||||
 * @param allowed_hosts List of allowed hosts.
 | 
			
		||||
 * @return A promise resolved with the response headers and body.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} url
 | 
			
		||||
 * @param {*} options
 | 
			
		||||
 * @param {*} allowed_hosts
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
export function fetch(url, options, allowed_hosts) {
 | 
			
		||||
	let parsed = parseUrl(url);
 | 
			
		||||
@@ -117,5 +111,3 @@ export function fetch(url, options, allowed_hosts) {
 | 
			
		||||
			});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @} */
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,11 @@
 | 
			
		||||
/**
 | 
			
		||||
 * \file
 | 
			
		||||
 * \defgroup tfrpc Tilde Friends RPC.
 | 
			
		||||
 * Tilde Friends RPC.
 | 
			
		||||
 * @{
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/** Whether this module is being run in a web browser. */
 | 
			
		||||
const k_is_browser = get_is_browser();
 | 
			
		||||
/** Registered methods. */
 | 
			
		||||
let g_api = {};
 | 
			
		||||
/** The next method identifier. */
 | 
			
		||||
let g_next_id = 1;
 | 
			
		||||
/** Identifiers of pending calls. */
 | 
			
		||||
let g_calls = {};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Check if being called from a browser vs. server-side.
 | 
			
		||||
 * @return true if called from a browser.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function get_is_browser() {
 | 
			
		||||
	try {
 | 
			
		||||
@@ -26,30 +15,16 @@ function get_is_browser() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** \cond */
 | 
			
		||||
if (k_is_browser) {
 | 
			
		||||
	print = console.log;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (k_is_browser) {
 | 
			
		||||
	window.addEventListener('message', function (event) {
 | 
			
		||||
		call_rpc(event.data);
 | 
			
		||||
	});
 | 
			
		||||
} else {
 | 
			
		||||
	core.register('message', function (message) {
 | 
			
		||||
		call_rpc(message?.message);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export let rpc = new Proxy({}, {get: make_rpc});
 | 
			
		||||
/** \endcond */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Make a function to invoke a remote procedure.
 | 
			
		||||
 * @param target The target.
 | 
			
		||||
 * @param prop The name of the function.
 | 
			
		||||
 * @param receiver The receiver.
 | 
			
		||||
 * @return A function.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} target
 | 
			
		||||
 * @param {*} prop
 | 
			
		||||
 * @param {*} receiver
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function make_rpc(target, prop, receiver) {
 | 
			
		||||
	return function () {
 | 
			
		||||
@@ -80,8 +55,8 @@ function make_rpc(target, prop, receiver) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Send a response.
 | 
			
		||||
 * @param response The response.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} response
 | 
			
		||||
 */
 | 
			
		||||
function send(response) {
 | 
			
		||||
	if (k_is_browser) {
 | 
			
		||||
@@ -92,8 +67,8 @@ function send(response) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Invoke a remote procedure.
 | 
			
		||||
 * @param message An object describing the call.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} message
 | 
			
		||||
 */
 | 
			
		||||
function call_rpc(message) {
 | 
			
		||||
	if (message && message.message === 'tfrpc') {
 | 
			
		||||
@@ -137,12 +112,22 @@ function call_rpc(message) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (k_is_browser) {
 | 
			
		||||
	window.addEventListener('message', function (event) {
 | 
			
		||||
		call_rpc(event.data);
 | 
			
		||||
	});
 | 
			
		||||
} else {
 | 
			
		||||
	core.register('message', function (message) {
 | 
			
		||||
		call_rpc(message?.message);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export let rpc = new Proxy({}, {get: make_rpc});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Register a function that to be called remotely.
 | 
			
		||||
 * @param method The method.
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} method
 | 
			
		||||
 */
 | 
			
		||||
export function register(method) {
 | 
			
		||||
	g_api[method.name] = method;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @} */
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								core/w3.css
									
									
									
									
									
								
							
							
						
						@@ -1,4 +1,4 @@
 | 
			
		||||
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
 | 
			
		||||
/* 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}
 | 
			
		||||
@@ -108,8 +108,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
 | 
			
		||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
 | 
			
		||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
 | 
			
		||||
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
 | 
			
		||||
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
 | 
			
		||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
 | 
			
		||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
 | 
			
		||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
 | 
			
		||||
@@ -150,7 +148,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-button:hover{color:#000!important;background-color:#ccc!important}
 | 
			
		||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
 | 
			
		||||
.w3-hover-none:hover{box-shadow:none!important}
 | 
			
		||||
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
 | 
			
		||||
/* Colors */
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
 | 
			
		||||
@@ -178,19 +175,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
 | 
			
		||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
 | 
			
		||||
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
 | 
			
		||||
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
 | 
			
		||||
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
 | 
			
		||||
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
 | 
			
		||||
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
 | 
			
		||||
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
 | 
			
		||||
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
 | 
			
		||||
.w3-danger{color:#fff!important;background-color:#dd0000!important}
 | 
			
		||||
.w3-note{color:#000!important;background-color:#fff599!important}
 | 
			
		||||
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
 | 
			
		||||
.w3-warning{color:#000!important;background-color:#ffb305!important}
 | 
			
		||||
.w3-success{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
 | 
			
		||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
 | 
			
		||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
 | 
			
		||||
@@ -248,4 +232,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.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}
 | 
			
		||||
.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}
 | 
			
		||||
@@ -25,14 +25,14 @@
 | 
			
		||||
}:
 | 
			
		||||
pkgs.stdenv.mkDerivation rec {
 | 
			
		||||
  pname = "tildefriends";
 | 
			
		||||
  version = "0.0.33";
 | 
			
		||||
  version = "0.0.28";
 | 
			
		||||
 | 
			
		||||
  src = pkgs.fetchFromGitea {
 | 
			
		||||
    domain = "dev.tildefriends.net";
 | 
			
		||||
    owner = "cory";
 | 
			
		||||
    repo = "tildefriends";
 | 
			
		||||
    rev = "v${version}";
 | 
			
		||||
    hash = "sha256-9D28gmaBTRVyXhY3zZd/W9PsXA1YZt/K69hz41aVP04=";
 | 
			
		||||
    rev = "f02423d0846fefd5ab21fa4542fb77ce5714547c";
 | 
			
		||||
    hash = "sha256-QyM7wmViXJc4r8uTu4oE/HO3Z9tzNbFIX2+AOTQz9ZY=";
 | 
			
		||||
    fetchSubmodules = true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								deps/c-ares
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										2
									
								
								deps/codemirror/cm6.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										419
									
								
								deps/codemirror_src/package-lock.json
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						@@ -22,7 +22,6 @@
 | 
			
		||||
      "version": "6.18.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
 | 
			
		||||
      "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/language": "^6.0.0",
 | 
			
		||||
        "@codemirror/state": "^6.0.0",
 | 
			
		||||
@@ -31,10 +30,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/commands": {
 | 
			
		||||
      "version": "6.8.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
 | 
			
		||||
      "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "6.8.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.0.tgz",
 | 
			
		||||
      "integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/language": "^6.0.0",
 | 
			
		||||
        "@codemirror/state": "^6.4.0",
 | 
			
		||||
@@ -46,7 +44,6 @@
 | 
			
		||||
      "version": "6.3.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
 | 
			
		||||
      "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/autocomplete": "^6.0.0",
 | 
			
		||||
        "@codemirror/language": "^6.0.0",
 | 
			
		||||
@@ -59,7 +56,6 @@
 | 
			
		||||
      "version": "6.4.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz",
 | 
			
		||||
      "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/autocomplete": "^6.0.0",
 | 
			
		||||
        "@codemirror/lang-css": "^6.0.0",
 | 
			
		||||
@@ -73,10 +69,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/lang-javascript": {
 | 
			
		||||
      "version": "6.2.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
 | 
			
		||||
      "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "6.2.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.3.tgz",
 | 
			
		||||
      "integrity": "sha512-8PR3vIWg7pSu7ur8A07pGiYHgy3hHj+mRYRCSG8q+mPIrl0F02rgpGv+DsQTHRTc30rydOsf5PZ7yjKFg2Ackw==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/autocomplete": "^6.0.0",
 | 
			
		||||
        "@codemirror/language": "^6.6.0",
 | 
			
		||||
@@ -88,20 +83,18 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/lang-json": {
 | 
			
		||||
      "version": "6.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "6.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/language": "^6.0.0",
 | 
			
		||||
        "@lezer/json": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/language": {
 | 
			
		||||
      "version": "6.11.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
 | 
			
		||||
      "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "6.10.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.8.tgz",
 | 
			
		||||
      "integrity": "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/state": "^6.0.0",
 | 
			
		||||
        "@codemirror/view": "^6.23.0",
 | 
			
		||||
@@ -112,10 +105,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/lint": {
 | 
			
		||||
      "version": "6.8.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
 | 
			
		||||
      "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "6.8.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz",
 | 
			
		||||
      "integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/state": "^6.0.0",
 | 
			
		||||
        "@codemirror/view": "^6.35.0",
 | 
			
		||||
@@ -123,10 +115,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/search": {
 | 
			
		||||
      "version": "6.5.11",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
 | 
			
		||||
      "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "6.5.9",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.9.tgz",
 | 
			
		||||
      "integrity": "sha512-7DdQ9aaZMMxuWB1u6IIFWWuK9NocVZwvo4nG8QjJTS6oZGvteoLSiXw3EbVZVlO08Ri2ltO89JVInMpfcJxhtg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/state": "^6.0.0",
 | 
			
		||||
        "@codemirror/view": "^6.0.0",
 | 
			
		||||
@@ -137,16 +128,14 @@
 | 
			
		||||
      "version": "6.5.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
 | 
			
		||||
      "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@marijn/find-cluster-break": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/theme-one-dark": {
 | 
			
		||||
      "version": "6.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
 | 
			
		||||
      "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "6.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/language": "^6.0.0",
 | 
			
		||||
        "@codemirror/state": "^6.0.0",
 | 
			
		||||
@@ -155,26 +144,27 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@codemirror/view": {
 | 
			
		||||
      "version": "6.38.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
 | 
			
		||||
      "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "6.36.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.3.tgz",
 | 
			
		||||
      "integrity": "sha512-N2bilM47QWC8Hnx0rMdDxO2x2ImJ1FvZWXubwKgjeoOrWwEiFrtpA7SFHcuZ+o2Ze2VzbkgbzWVj4+V18LVkeg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/state": "^6.5.0",
 | 
			
		||||
        "crelt": "^1.0.6",
 | 
			
		||||
        "style-mod": "^4.1.0",
 | 
			
		||||
        "w3c-keyname": "^2.2.4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@jridgewell/gen-mapping": {
 | 
			
		||||
      "version": "0.3.13",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
 | 
			
		||||
      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
 | 
			
		||||
      "version": "0.3.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
 | 
			
		||||
      "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@jridgewell/sourcemap-codec": "^1.5.0",
 | 
			
		||||
        "@jridgewell/set-array": "^1.2.1",
 | 
			
		||||
        "@jridgewell/sourcemap-codec": "^1.4.10",
 | 
			
		||||
        "@jridgewell/trace-mapping": "^0.3.24"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@jridgewell/resolve-uri": {
 | 
			
		||||
@@ -182,35 +172,40 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@jridgewell/set-array": {
 | 
			
		||||
      "version": "1.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=6.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@jridgewell/source-map": {
 | 
			
		||||
      "version": "0.3.11",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
 | 
			
		||||
      "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
 | 
			
		||||
      "version": "0.3.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
 | 
			
		||||
      "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@jridgewell/gen-mapping": "^0.3.5",
 | 
			
		||||
        "@jridgewell/trace-mapping": "^0.3.25"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@jridgewell/sourcemap-codec": {
 | 
			
		||||
      "version": "1.5.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
 | 
			
		||||
      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "version": "1.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@jridgewell/trace-mapping": {
 | 
			
		||||
      "version": "0.3.30",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
 | 
			
		||||
      "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
 | 
			
		||||
      "version": "0.3.25",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
 | 
			
		||||
      "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@jridgewell/resolve-uri": "^3.1.0",
 | 
			
		||||
        "@jridgewell/sourcemap-codec": "^1.4.14"
 | 
			
		||||
@@ -219,25 +214,22 @@
 | 
			
		||||
    "node_modules/@lezer/common": {
 | 
			
		||||
      "version": "1.2.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
 | 
			
		||||
      "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@lezer/css": {
 | 
			
		||||
      "version": "1.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "1.1.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.10.tgz",
 | 
			
		||||
      "integrity": "sha512-V5/89eDapjeAkWPBpWEfQjZ1Hag3aYUUJOL8213X0dFRuXJ4BXa5NKl9USzOnaLod4AOpmVCkduir2oKwZYZtg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lezer/common": "^1.2.0",
 | 
			
		||||
        "@lezer/highlight": "^1.0.0",
 | 
			
		||||
        "@lezer/lr": "^1.3.0"
 | 
			
		||||
        "@lezer/lr": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@lezer/highlight": {
 | 
			
		||||
      "version": "1.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lezer/common": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
@@ -246,7 +238,6 @@
 | 
			
		||||
      "version": "1.3.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz",
 | 
			
		||||
      "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lezer/common": "^1.2.0",
 | 
			
		||||
        "@lezer/highlight": "^1.0.0",
 | 
			
		||||
@@ -254,10 +245,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@lezer/javascript": {
 | 
			
		||||
      "version": "1.5.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz",
 | 
			
		||||
      "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "1.4.21",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz",
 | 
			
		||||
      "integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lezer/common": "^1.2.0",
 | 
			
		||||
        "@lezer/highlight": "^1.1.3",
 | 
			
		||||
@@ -268,7 +258,6 @@
 | 
			
		||||
      "version": "1.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lezer/common": "^1.2.0",
 | 
			
		||||
        "@lezer/highlight": "^1.0.0",
 | 
			
		||||
@@ -279,7 +268,6 @@
 | 
			
		||||
      "version": "1.4.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
 | 
			
		||||
      "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lezer/common": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
@@ -287,14 +275,12 @@
 | 
			
		||||
    "node_modules/@marijn/find-cluster-break": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/plugin-node-resolve": {
 | 
			
		||||
      "version": "15.3.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz",
 | 
			
		||||
      "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@rollup/pluginutils": "^5.0.1",
 | 
			
		||||
        "@types/resolve": "1.20.2",
 | 
			
		||||
@@ -319,7 +305,6 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz",
 | 
			
		||||
      "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "serialize-javascript": "^6.0.1",
 | 
			
		||||
        "smob": "^1.0.0",
 | 
			
		||||
@@ -338,10 +323,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/pluginutils": {
 | 
			
		||||
      "version": "5.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "5.1.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
 | 
			
		||||
      "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/estree": "^1.0.0",
 | 
			
		||||
        "estree-walker": "^2.0.2",
 | 
			
		||||
@@ -360,283 +344,248 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-android-arm-eabi": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-B2wfzCJ+ps/OBzRjeds7DlJumCU3rXMxJJS1vzURyj7+KBHGONm7c9q1TfdBl4vCuNMkDvARn3PBl2wZzuR5mw==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "android"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-android-arm64": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-FGJYXvYdn8Bs6lAlBZYT5n+4x0ciEp4cmttsvKAZc/c8/JiPaQK8u0c/86vKX8lA7OY/+37lIQSe0YoAImvBAA==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "android"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-darwin-arm64": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-/9qwE/BM7ATw/W/OFEMTm3dmywbJyLQb4f4v5nmOjgYxPIGpw7HaxRi6LnD4Pjn/q7k55FGeHe1/OD02w63apA==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "darwin"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-darwin-x64": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-QkWfNbeRuzFnv2d0aPlrzcA3Ebq2mE8kX/5Pl7VdRShbPBjSnom7dbT8E3Jmhxo2RL784hyqGvR5KHavCJQciw==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "darwin"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-freebsd-arm64": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-+ToyOMYnSfV8D+ckxO6NthPln/PDNp1P6INcNypfZ7muLmEvPKXqduUiD8DlJpMMT8LxHcE5W0dK9kXfJke9Zw==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "freebsd"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-freebsd-x64": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-cGT6ey/W+sje6zywbLiqmkfkO210FgRz7tepWAzzEVgQU8Hn91JJmQWNqs55IuglG8sJdzk7XfNgmGRtcYlo1w==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "freebsd"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-9fhTJyOb275w5RofPSl8lpr4jFowd+H4oQKJ9XTYzD1JWgxdZKE8bA6d4npuiMemkecQOcigX01FNZNCYnQBdA==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-+6kCIM5Zjvz2HwPl/udgVs07tPMIp1VU2Y0c72ezjOvSvEfAIWsUgpcSDvnC7g9NrjYR6X9bZT92mZZ90TfvXw==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-arm64-gnu": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-SWuXdnsayCZL4lXoo6jn0yyAj7TTjWE4NwDVt9s7cmu6poMhtiras5c8h6Ih6Y0Zk6Z+8t/mLumvpdSPTWub2Q==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-arm64-musl": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-vDknMDqtMhrrroa5kyX6tuC0aRZZlQ+ipDfbXd2YGz5HeV2t8HOl/FDAd2ynhs7Ki5VooWiiZcCtxiZ4IjqZwQ==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-mCBkjRZWhvjtl/x+Bd4fQkWZT8canStKDxGrHlBiTnZmJnWygGcvBylzLVCZXka4dco5ymkWhZlLwKCGFF4ivw==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "loong64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-YMdz2phOTFF+Z66dQfGf0gmeDSi5DJzY5bpZyeg9CPBkV9QDzJ1yFRlmi/j7WWRf3hYIWrOaJj5jsfwgc8GTHQ==",
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "ppc64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-r0WKLSfFAK8ucG024v2yiLSJMedoWvk8yWqfNICX28NHDGeu3F/wBf8KG6mclghx4FsLePxJr/9N8rIj1PtCnw==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "riscv64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-riscv64-musl": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-IaizpPP2UQU3MNyPH1u0Xxbm73D+4OupL0bjo4Hm0496e2wg3zuvoAIhubkD1NGy9fXILEExPQy87mweujEatA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "riscv64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-s390x-gnu": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-aCM29orANR0a8wk896p6UEgIfupReupnmISz6SUwMIwTGaTI8MuKdE0OD2LvEg8ondDyZdMvnaN3bW4nFbATPA==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "s390x"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-x64-gnu": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-0Xj1vZE3cbr/wda8d/m+UeuSL+TDpuozzdD4QaSzu/xSOMK0Su5RhIkF7KVHFQsobemUNHPLEcYllL7ZTCP/Cg==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-linux-x64-musl": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-kM/orjpolfA5yxsx84kI6bnK47AAZuWxglGKcNmokw2yy9i5eHY5UAjcX45jemTJnfHAWo3/hOoRqEeeTdL5hw==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "linux"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-win32-arm64-msvc": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-cNLH4psMEsWKILW0isbpQA2OvjXLbKvnkcJFmqAptPQbtLrobiapBJVj6RoIvg6UXVp5w0wnIfd/Q56cNpF+Ew==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "arm64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "win32"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-win32-ia32-msvc": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-OiEa5lRhiANpv4SfwYVgQ3opYWi/QmPDC5ve21m8G9pf6ZO+aX1g2EEF1/IFaM1xPSP7mK0msTRXlPs6mIagkg==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "ia32"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "win32"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@rollup/rollup-win32-x64-msvc": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-IKL9mewGZ5UuuX4NQlwOmxPyqielvkAPUS2s1cl6yWjjQvyN3h5JTdVFGD5Jr5xMjRC8setOfGQDVgX8V+dkjg==",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==",
 | 
			
		||||
      "cpu": [
 | 
			
		||||
        "x64"
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "win32"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/estree": {
 | 
			
		||||
      "version": "1.0.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
 | 
			
		||||
      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "version": "1.0.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
 | 
			
		||||
      "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/resolve": {
 | 
			
		||||
      "version": "1.20.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
 | 
			
		||||
      "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/acorn": {
 | 
			
		||||
      "version": "8.15.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
 | 
			
		||||
      "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
 | 
			
		||||
      "version": "8.14.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
 | 
			
		||||
      "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "acorn": "bin/acorn"
 | 
			
		||||
      },
 | 
			
		||||
@@ -648,14 +597,12 @@
 | 
			
		||||
      "version": "1.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/codemirror": {
 | 
			
		||||
      "version": "6.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "6.0.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
 | 
			
		||||
      "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@codemirror/autocomplete": "^6.0.0",
 | 
			
		||||
        "@codemirror/commands": "^6.0.0",
 | 
			
		||||
@@ -670,20 +617,17 @@
 | 
			
		||||
      "version": "2.20.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
 | 
			
		||||
      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/crelt": {
 | 
			
		||||
      "version": "1.0.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
 | 
			
		||||
      "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/deepmerge": {
 | 
			
		||||
      "version": "4.3.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
 | 
			
		||||
      "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.10.0"
 | 
			
		||||
      }
 | 
			
		||||
@@ -691,15 +635,13 @@
 | 
			
		||||
    "node_modules/estree-walker": {
 | 
			
		||||
      "version": "2.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/fsevents": {
 | 
			
		||||
      "version": "2.3.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
 | 
			
		||||
      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
 | 
			
		||||
      "hasInstallScript": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "optional": true,
 | 
			
		||||
      "os": [
 | 
			
		||||
        "darwin"
 | 
			
		||||
@@ -712,7 +654,6 @@
 | 
			
		||||
      "version": "1.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/ljharb"
 | 
			
		||||
      }
 | 
			
		||||
@@ -721,7 +662,6 @@
 | 
			
		||||
      "version": "2.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "function-bind": "^1.1.2"
 | 
			
		||||
      },
 | 
			
		||||
@@ -733,7 +673,6 @@
 | 
			
		||||
      "version": "2.16.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
 | 
			
		||||
      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "hasown": "^2.0.2"
 | 
			
		||||
      },
 | 
			
		||||
@@ -747,20 +686,17 @@
 | 
			
		||||
    "node_modules/is-module": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/path-parse": {
 | 
			
		||||
      "version": "1.0.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
 | 
			
		||||
      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/picomatch": {
 | 
			
		||||
      "version": "4.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
 | 
			
		||||
      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "4.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      },
 | 
			
		||||
@@ -773,7 +709,6 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
 | 
			
		||||
      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "safe-buffer": "^5.1.0"
 | 
			
		||||
      }
 | 
			
		||||
@@ -782,7 +717,6 @@
 | 
			
		||||
      "version": "1.22.10",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
 | 
			
		||||
      "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "is-core-module": "^2.16.0",
 | 
			
		||||
        "path-parse": "^1.0.7",
 | 
			
		||||
@@ -799,12 +733,11 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/rollup": {
 | 
			
		||||
      "version": "4.46.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.4.tgz",
 | 
			
		||||
      "integrity": "sha512-YbxoxvoqNg9zAmw4+vzh1FkGAiZRK+LhnSrbSrSXMdZYsRPDWoshcSd/pldKRO6lWzv/e9TiJAVQyirYIeSIPQ==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "version": "4.34.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz",
 | 
			
		||||
      "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/estree": "1.0.8"
 | 
			
		||||
        "@types/estree": "1.0.6"
 | 
			
		||||
      },
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "rollup": "dist/bin/rollup"
 | 
			
		||||
@@ -814,26 +747,25 @@
 | 
			
		||||
        "npm": ">=8.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "optionalDependencies": {
 | 
			
		||||
        "@rollup/rollup-android-arm-eabi": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-android-arm64": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-darwin-arm64": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-darwin-x64": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-freebsd-arm64": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-freebsd-x64": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-linux-arm-gnueabihf": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-linux-arm-musleabihf": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-linux-arm64-gnu": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-linux-arm64-musl": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-linux-loongarch64-gnu": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-linux-ppc64-gnu": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-linux-riscv64-gnu": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-linux-riscv64-musl": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-linux-s390x-gnu": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-linux-x64-gnu": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-linux-x64-musl": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-win32-arm64-msvc": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-win32-ia32-msvc": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-win32-x64-msvc": "4.46.4",
 | 
			
		||||
        "@rollup/rollup-android-arm-eabi": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-android-arm64": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-darwin-arm64": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-darwin-x64": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-freebsd-arm64": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-freebsd-x64": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-linux-arm-gnueabihf": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-linux-arm-musleabihf": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-linux-arm64-gnu": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-linux-arm64-musl": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-linux-loongarch64-gnu": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-linux-riscv64-gnu": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-linux-s390x-gnu": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-linux-x64-gnu": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-linux-x64-musl": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-win32-arm64-msvc": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-win32-ia32-msvc": "4.34.8",
 | 
			
		||||
        "@rollup/rollup-win32-x64-msvc": "4.34.8",
 | 
			
		||||
        "fsevents": "~2.3.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
@@ -855,15 +787,13 @@
 | 
			
		||||
          "type": "consulting",
 | 
			
		||||
          "url": "https://feross.org/support"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/serialize-javascript": {
 | 
			
		||||
      "version": "6.0.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
 | 
			
		||||
      "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "BSD-3-Clause",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "randombytes": "^2.1.0"
 | 
			
		||||
      }
 | 
			
		||||
@@ -872,15 +802,13 @@
 | 
			
		||||
      "version": "1.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "dev": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/source-map": {
 | 
			
		||||
      "version": "0.6.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
 | 
			
		||||
      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "BSD-3-Clause",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=0.10.0"
 | 
			
		||||
      }
 | 
			
		||||
@@ -890,7 +818,6 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
 | 
			
		||||
      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "buffer-from": "^1.0.0",
 | 
			
		||||
        "source-map": "^0.6.0"
 | 
			
		||||
@@ -899,14 +826,12 @@
 | 
			
		||||
    "node_modules/style-mod": {
 | 
			
		||||
      "version": "4.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/supports-preserve-symlinks-flag": {
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 0.4"
 | 
			
		||||
      },
 | 
			
		||||
@@ -915,14 +840,13 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/terser": {
 | 
			
		||||
      "version": "5.43.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
 | 
			
		||||
      "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
 | 
			
		||||
      "version": "5.39.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
 | 
			
		||||
      "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "BSD-2-Clause",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@jridgewell/source-map": "^0.3.3",
 | 
			
		||||
        "acorn": "^8.14.0",
 | 
			
		||||
        "acorn": "^8.8.2",
 | 
			
		||||
        "commander": "^2.20.0",
 | 
			
		||||
        "source-map-support": "~0.5.20"
 | 
			
		||||
      },
 | 
			
		||||
@@ -936,8 +860,7 @@
 | 
			
		||||
    "node_modules/w3c-keyname": {
 | 
			
		||||
      "version": "2.2.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
 | 
			
		||||
      "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
      "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								deps/libbacktrace
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										2
									
								
								deps/libuv
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										42
									
								
								deps/lit/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										2
									
								
								deps/lit/lit-all.min.js.map
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										2
									
								
								deps/openssl_src
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										2
									
								
								deps/picohttpparser
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										2
									
								
								deps/quickjs
									
									
									
									
										vendored
									
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										2
									
								
								deps/speedscope/index.html
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -11,7 +11,7 @@
 | 
			
		||||
    <link rel="icon" type="image/x-icon" href="favicon-FOKUP5Y5.ico">
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <script src="speedscope-HCR63FMT.js"></script>
 | 
			
		||||
    <script src="speedscope-VHEG2FVF.js"></script>
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								deps/speedscope/release.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,3 +1,3 @@
 | 
			
		||||
speedscope@1.23.1
 | 
			
		||||
Mon Aug 11 11:43:09 PDT 2025
 | 
			
		||||
0cec0f82c334aed6cf19d43cabeadcda0d95e0fc
 | 
			
		||||
speedscope@1.22.2
 | 
			
		||||
Sat Feb 15 13:02:38 PST 2025
 | 
			
		||||
1c254dcb3e2b4f6d921340d20e972d9d27b788f4
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1031
									
								
								deps/sqlite/shell.c
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										4753
									
								
								deps/sqlite/sqlite3.c
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										316
									
								
								deps/sqlite/sqlite3.h
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -133,7 +133,7 @@ extern "C" {
 | 
			
		||||
**
 | 
			
		||||
** Since [version 3.6.18] ([dateof:3.6.18]),
 | 
			
		||||
** SQLite source code has been stored in the
 | 
			
		||||
** <a href="http://fossil-scm.org/">Fossil configuration management
 | 
			
		||||
** <a href="http://www.fossil-scm.org/">Fossil configuration management
 | 
			
		||||
** system</a>.  ^The SQLITE_SOURCE_ID macro evaluates to
 | 
			
		||||
** a string which identifies a particular check-in of SQLite
 | 
			
		||||
** within its configuration management system.  ^The SQLITE_SOURCE_ID
 | 
			
		||||
@@ -146,9 +146,9 @@ extern "C" {
 | 
			
		||||
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
 | 
			
		||||
** [sqlite_version()] and [sqlite_source_id()].
 | 
			
		||||
*/
 | 
			
		||||
#define SQLITE_VERSION        "3.50.4"
 | 
			
		||||
#define SQLITE_VERSION_NUMBER 3050004
 | 
			
		||||
#define SQLITE_SOURCE_ID      "2025-07-30 19:33:53 4d8adfb30e03f9cf27f800a2c1ba3c48fb4ca1b08b0f5ed59a4d5ecbf45e20a3"
 | 
			
		||||
#define SQLITE_VERSION        "3.49.1"
 | 
			
		||||
#define SQLITE_VERSION_NUMBER 3049001
 | 
			
		||||
#define SQLITE_SOURCE_ID      "2025-02-18 13:38:58 873d4e274b4988d260ba8354a9718324a1c26187a4ab4c1cc0227c03d0f10e70"
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
** CAPI3REF: Run-Time Library Version Numbers
 | 
			
		||||
@@ -1163,12 +1163,6 @@ struct sqlite3_io_methods {
 | 
			
		||||
** the value that M is to be set to. Before returning, the 32-bit signed
 | 
			
		||||
** integer is overwritten with the previous value of M.
 | 
			
		||||
**
 | 
			
		||||
** <li>[[SQLITE_FCNTL_BLOCK_ON_CONNECT]]
 | 
			
		||||
** The [SQLITE_FCNTL_BLOCK_ON_CONNECT] opcode is used to configure the
 | 
			
		||||
** VFS to block when taking a SHARED lock to connect to a wal mode database.
 | 
			
		||||
** This is used to implement the functionality associated with
 | 
			
		||||
** SQLITE_SETLK_BLOCK_ON_CONNECT.
 | 
			
		||||
**
 | 
			
		||||
** <li>[[SQLITE_FCNTL_DATA_VERSION]]
 | 
			
		||||
** The [SQLITE_FCNTL_DATA_VERSION] opcode is used to detect changes to
 | 
			
		||||
** a database file.  The argument is a pointer to a 32-bit unsigned integer.
 | 
			
		||||
@@ -1265,7 +1259,6 @@ struct sqlite3_io_methods {
 | 
			
		||||
#define SQLITE_FCNTL_CKSM_FILE              41
 | 
			
		||||
#define SQLITE_FCNTL_RESET_CACHE            42
 | 
			
		||||
#define SQLITE_FCNTL_NULL_IO                43
 | 
			
		||||
#define SQLITE_FCNTL_BLOCK_ON_CONNECT       44
 | 
			
		||||
 | 
			
		||||
/* deprecated names */
 | 
			
		||||
#define SQLITE_GET_LOCKPROXYFILE      SQLITE_FCNTL_GET_LOCKPROXYFILE
 | 
			
		||||
@@ -1996,16 +1989,13 @@ struct sqlite3_mem_methods {
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_CONFIG_LOOKASIDE]] <dt>SQLITE_CONFIG_LOOKASIDE</dt>
 | 
			
		||||
** <dd> ^(The SQLITE_CONFIG_LOOKASIDE option takes two arguments that determine
 | 
			
		||||
** the default size of [lookaside memory] on each [database connection].
 | 
			
		||||
** the default size of lookaside memory on each [database connection].
 | 
			
		||||
** The first argument is the
 | 
			
		||||
** size of each lookaside buffer slot ("sz") and the second is the number of
 | 
			
		||||
** slots allocated to each database connection ("cnt").)^
 | 
			
		||||
** ^(SQLITE_CONFIG_LOOKASIDE sets the <i>default</i> lookaside size.
 | 
			
		||||
** The [SQLITE_DBCONFIG_LOOKASIDE] option to [sqlite3_db_config()] can
 | 
			
		||||
** be used to change the lookaside configuration on individual connections.)^
 | 
			
		||||
** The [-DSQLITE_DEFAULT_LOOKASIDE] option can be used to change the
 | 
			
		||||
** default lookaside configuration at compile-time.
 | 
			
		||||
** </dd>
 | 
			
		||||
** size of each lookaside buffer slot and the second is the number of
 | 
			
		||||
** slots allocated to each database connection.)^  ^(SQLITE_CONFIG_LOOKASIDE
 | 
			
		||||
** sets the <i>default</i> lookaside size. The [SQLITE_DBCONFIG_LOOKASIDE]
 | 
			
		||||
** option to [sqlite3_db_config()] can be used to change the lookaside
 | 
			
		||||
** configuration on individual connections.)^ </dd>
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_CONFIG_PCACHE2]] <dt>SQLITE_CONFIG_PCACHE2</dt>
 | 
			
		||||
** <dd> ^(The SQLITE_CONFIG_PCACHE2 option takes a single argument which is
 | 
			
		||||
@@ -2242,50 +2232,31 @@ struct sqlite3_mem_methods {
 | 
			
		||||
** [[SQLITE_DBCONFIG_LOOKASIDE]]
 | 
			
		||||
** <dt>SQLITE_DBCONFIG_LOOKASIDE</dt>
 | 
			
		||||
** <dd> The SQLITE_DBCONFIG_LOOKASIDE option is used to adjust the
 | 
			
		||||
** configuration of the [lookaside memory allocator] within a database
 | 
			
		||||
** configuration of the lookaside memory allocator within a database
 | 
			
		||||
** connection.
 | 
			
		||||
** The arguments to the SQLITE_DBCONFIG_LOOKASIDE option are <i>not</i>
 | 
			
		||||
** in the [DBCONFIG arguments|usual format].
 | 
			
		||||
** The SQLITE_DBCONFIG_LOOKASIDE option takes three arguments, not two,
 | 
			
		||||
** so that a call to [sqlite3_db_config()] that uses SQLITE_DBCONFIG_LOOKASIDE
 | 
			
		||||
** should have a total of five parameters.
 | 
			
		||||
** <ol>
 | 
			
		||||
** <li><p>The first argument ("buf") is a
 | 
			
		||||
** ^The first argument (the third parameter to [sqlite3_db_config()] is a
 | 
			
		||||
** pointer to a memory buffer to use for lookaside memory.
 | 
			
		||||
** The first argument may be NULL in which case SQLite will allocate the
 | 
			
		||||
** lookaside buffer itself using [sqlite3_malloc()].
 | 
			
		||||
** <li><P>The second argument ("sz") is the
 | 
			
		||||
** size of each lookaside buffer slot.  Lookaside is disabled if "sz"
 | 
			
		||||
** is less than 8.  The "sz" argument should be a multiple of 8 less than
 | 
			
		||||
** 65536.  If "sz" does not meet this constraint, it is reduced in size until
 | 
			
		||||
** it does.
 | 
			
		||||
** <li><p>The third argument ("cnt") is the number of slots. Lookaside is disabled
 | 
			
		||||
** if "cnt"is less than 1.  The "cnt" value will be reduced, if necessary, so
 | 
			
		||||
** that the product of "sz" and "cnt" does not exceed 2,147,418,112.  The "cnt"
 | 
			
		||||
** parameter is usually chosen so that the product of "sz" and "cnt" is less
 | 
			
		||||
** than 1,000,000.
 | 
			
		||||
** </ol>
 | 
			
		||||
** <p>If the "buf" argument is not NULL, then it must
 | 
			
		||||
** point to a memory buffer with a size that is greater than
 | 
			
		||||
** or equal to the product of "sz" and "cnt".
 | 
			
		||||
** The buffer must be aligned to an 8-byte boundary.
 | 
			
		||||
** The lookaside memory
 | 
			
		||||
** ^The first argument after the SQLITE_DBCONFIG_LOOKASIDE verb
 | 
			
		||||
** may be NULL in which case SQLite will allocate the
 | 
			
		||||
** lookaside buffer itself using [sqlite3_malloc()]. ^The second argument is the
 | 
			
		||||
** size of each lookaside buffer slot.  ^The third argument is the number of
 | 
			
		||||
** slots.  The size of the buffer in the first argument must be greater than
 | 
			
		||||
** or equal to the product of the second and third arguments.  The buffer
 | 
			
		||||
** must be aligned to an 8-byte boundary.  ^If the second argument to
 | 
			
		||||
** SQLITE_DBCONFIG_LOOKASIDE is not a multiple of 8, it is internally
 | 
			
		||||
** rounded down to the next smaller multiple of 8.  ^(The lookaside memory
 | 
			
		||||
** configuration for a database connection can only be changed when that
 | 
			
		||||
** connection is not currently using lookaside memory, or in other words
 | 
			
		||||
** when the value returned by [SQLITE_DBSTATUS_LOOKASIDE_USED] is zero.
 | 
			
		||||
** when the "current value" returned by
 | 
			
		||||
** [sqlite3_db_status](D,[SQLITE_DBSTATUS_LOOKASIDE_USED],...) is zero.
 | 
			
		||||
** Any attempt to change the lookaside memory configuration when lookaside
 | 
			
		||||
** memory is in use leaves the configuration unchanged and returns
 | 
			
		||||
** [SQLITE_BUSY].
 | 
			
		||||
** If the "buf" argument is NULL and an attempt
 | 
			
		||||
** to allocate memory based on "sz" and "cnt" fails, then
 | 
			
		||||
** lookaside is silently disabled.
 | 
			
		||||
** <p>
 | 
			
		||||
** The [SQLITE_CONFIG_LOOKASIDE] configuration option can be used to set the
 | 
			
		||||
** default lookaside configuration at initialization.  The
 | 
			
		||||
** [-DSQLITE_DEFAULT_LOOKASIDE] option can be used to set the default lookaside
 | 
			
		||||
** configuration at compile-time.  Typical values for lookaside are 1200 for
 | 
			
		||||
** "sz" and 40 to 100 for "cnt".
 | 
			
		||||
** </dd>
 | 
			
		||||
** [SQLITE_BUSY].)^</dd>
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_DBCONFIG_ENABLE_FKEY]]
 | 
			
		||||
** <dt>SQLITE_DBCONFIG_ENABLE_FKEY</dt>
 | 
			
		||||
@@ -3022,44 +2993,6 @@ SQLITE_API int sqlite3_busy_handler(sqlite3*,int(*)(void*,int),void*);
 | 
			
		||||
*/
 | 
			
		||||
SQLITE_API int sqlite3_busy_timeout(sqlite3*, int ms);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
** CAPI3REF: Set the Setlk Timeout
 | 
			
		||||
** METHOD: sqlite3
 | 
			
		||||
**
 | 
			
		||||
** This routine is only useful in SQLITE_ENABLE_SETLK_TIMEOUT builds. If
 | 
			
		||||
** the VFS supports blocking locks, it sets the timeout in ms used by
 | 
			
		||||
** eligible locks taken on wal mode databases by the specified database
 | 
			
		||||
** handle. In non-SQLITE_ENABLE_SETLK_TIMEOUT builds, or if the VFS does
 | 
			
		||||
** not support blocking locks, this function is a no-op.
 | 
			
		||||
**
 | 
			
		||||
** Passing 0 to this function disables blocking locks altogether. Passing
 | 
			
		||||
** -1 to this function requests that the VFS blocks for a long time -
 | 
			
		||||
** indefinitely if possible. The results of passing any other negative value
 | 
			
		||||
** are undefined.
 | 
			
		||||
**
 | 
			
		||||
** Internally, each SQLite database handle store two timeout values - the
 | 
			
		||||
** busy-timeout (used for rollback mode databases, or if the VFS does not
 | 
			
		||||
** support blocking locks) and the setlk-timeout (used for blocking locks
 | 
			
		||||
** on wal-mode databases). The sqlite3_busy_timeout() method sets both
 | 
			
		||||
** values, this function sets only the setlk-timeout value. Therefore,
 | 
			
		||||
** to configure separate busy-timeout and setlk-timeout values for a single
 | 
			
		||||
** database handle, call sqlite3_busy_timeout() followed by this function.
 | 
			
		||||
**
 | 
			
		||||
** Whenever the number of connections to a wal mode database falls from
 | 
			
		||||
** 1 to 0, the last connection takes an exclusive lock on the database,
 | 
			
		||||
** then checkpoints and deletes the wal file. While it is doing this, any
 | 
			
		||||
** new connection that tries to read from the database fails with an
 | 
			
		||||
** SQLITE_BUSY error. Or, if the SQLITE_SETLK_BLOCK_ON_CONNECT flag is
 | 
			
		||||
** passed to this API, the new connection blocks until the exclusive lock
 | 
			
		||||
** has been released.
 | 
			
		||||
*/
 | 
			
		||||
SQLITE_API int sqlite3_setlk_timeout(sqlite3*, int ms, int flags);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
** CAPI3REF: Flags for sqlite3_setlk_timeout()
 | 
			
		||||
*/
 | 
			
		||||
#define SQLITE_SETLK_BLOCK_ON_CONNECT 0x01
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
** CAPI3REF: Convenience Routines For Running Queries
 | 
			
		||||
** METHOD: sqlite3
 | 
			
		||||
@@ -4079,7 +4012,7 @@ SQLITE_API sqlite3_file *sqlite3_database_file_object(const char*);
 | 
			
		||||
**
 | 
			
		||||
** The sqlite3_create_filename(D,J,W,N,P) allocates memory to hold a version of
 | 
			
		||||
** database filename D with corresponding journal file J and WAL file W and
 | 
			
		||||
** an array P of N URI Key/Value pairs.  The result from
 | 
			
		||||
** with N URI parameters key/values pairs in the array P.  The result from
 | 
			
		||||
** sqlite3_create_filename(D,J,W,N,P) is a pointer to a database filename that
 | 
			
		||||
** is safe to pass to routines like:
 | 
			
		||||
** <ul>
 | 
			
		||||
@@ -4760,7 +4693,7 @@ typedef struct sqlite3_context sqlite3_context;
 | 
			
		||||
** METHOD: sqlite3_stmt
 | 
			
		||||
**
 | 
			
		||||
** ^(In the SQL statement text input to [sqlite3_prepare_v2()] and its variants,
 | 
			
		||||
** literals may be replaced by a [parameter] that matches one of the following
 | 
			
		||||
** literals may be replaced by a [parameter] that matches one of following
 | 
			
		||||
** templates:
 | 
			
		||||
**
 | 
			
		||||
** <ul>
 | 
			
		||||
@@ -4805,7 +4738,7 @@ typedef struct sqlite3_context sqlite3_context;
 | 
			
		||||
**
 | 
			
		||||
** [[byte-order determination rules]] ^The byte-order of
 | 
			
		||||
** UTF16 input text is determined by the byte-order mark (BOM, U+FEFF)
 | 
			
		||||
** found in the first character, which is removed, or in the absence of a BOM
 | 
			
		||||
** found in first character, which is removed, or in the absence of a BOM
 | 
			
		||||
** the byte order is the native byte order of the host
 | 
			
		||||
** machine for sqlite3_bind_text16() or the byte order specified in
 | 
			
		||||
** the 6th parameter for sqlite3_bind_text64().)^
 | 
			
		||||
@@ -4825,7 +4758,7 @@ typedef struct sqlite3_context sqlite3_context;
 | 
			
		||||
** or sqlite3_bind_text16() or sqlite3_bind_text64() then
 | 
			
		||||
** that parameter must be the byte offset
 | 
			
		||||
** where the NUL terminator would occur assuming the string were NUL
 | 
			
		||||
** terminated.  If any NUL characters occur at byte offsets less than
 | 
			
		||||
** terminated.  If any NUL characters occurs at byte offsets less than
 | 
			
		||||
** the value of the fourth parameter then the resulting string value will
 | 
			
		||||
** contain embedded NULs.  The result of expressions involving strings
 | 
			
		||||
** with embedded NULs is undefined.
 | 
			
		||||
@@ -5037,7 +4970,7 @@ SQLITE_API const void *sqlite3_column_name16(sqlite3_stmt*, int N);
 | 
			
		||||
** METHOD: sqlite3_stmt
 | 
			
		||||
**
 | 
			
		||||
** ^These routines provide a means to determine the database, table, and
 | 
			
		||||
** table column that is the origin of a particular result column in a
 | 
			
		||||
** table column that is the origin of a particular result column in
 | 
			
		||||
** [SELECT] statement.
 | 
			
		||||
** ^The name of the database or table or column can be returned as
 | 
			
		||||
** either a UTF-8 or UTF-16 string.  ^The _database_ routines return
 | 
			
		||||
@@ -5175,7 +5108,7 @@ SQLITE_API const void *sqlite3_column_decltype16(sqlite3_stmt*,int);
 | 
			
		||||
** other than [SQLITE_ROW] before any subsequent invocation of
 | 
			
		||||
** sqlite3_step().  Failure to reset the prepared statement using
 | 
			
		||||
** [sqlite3_reset()] would result in an [SQLITE_MISUSE] return from
 | 
			
		||||
** sqlite3_step().  But after [version 3.6.23.1] ([dateof:3.6.23.1]),
 | 
			
		||||
** sqlite3_step().  But after [version 3.6.23.1] ([dateof:3.6.23.1],
 | 
			
		||||
** sqlite3_step() began
 | 
			
		||||
** calling [sqlite3_reset()] automatically in this circumstance rather
 | 
			
		||||
** than returning [SQLITE_MISUSE].  This is not considered a compatibility
 | 
			
		||||
@@ -5606,8 +5539,8 @@ SQLITE_API int sqlite3_reset(sqlite3_stmt *pStmt);
 | 
			
		||||
**
 | 
			
		||||
** For best security, the [SQLITE_DIRECTONLY] flag is recommended for
 | 
			
		||||
** all application-defined SQL functions that do not need to be
 | 
			
		||||
** used inside of triggers, views, CHECK constraints, or other elements of
 | 
			
		||||
** the database schema.  This flag is especially recommended for SQL
 | 
			
		||||
** used inside of triggers, view, CHECK constraints, or other elements of
 | 
			
		||||
** the database schema.  This flags is especially recommended for SQL
 | 
			
		||||
** functions that have side effects or reveal internal application state.
 | 
			
		||||
** Without this flag, an attacker might be able to modify the schema of
 | 
			
		||||
** a database file to include invocations of the function with parameters
 | 
			
		||||
@@ -5638,7 +5571,7 @@ SQLITE_API int sqlite3_reset(sqlite3_stmt *pStmt);
 | 
			
		||||
** [user-defined window functions|available here].
 | 
			
		||||
**
 | 
			
		||||
** ^(If the final parameter to sqlite3_create_function_v2() or
 | 
			
		||||
** sqlite3_create_window_function() is not NULL, then it is the destructor for
 | 
			
		||||
** sqlite3_create_window_function() is not NULL, then it is destructor for
 | 
			
		||||
** the application data pointer. The destructor is invoked when the function
 | 
			
		||||
** is deleted, either by being overloaded or when the database connection
 | 
			
		||||
** closes.)^ ^The destructor is also invoked if the call to
 | 
			
		||||
@@ -6038,7 +5971,7 @@ SQLITE_API unsigned int sqlite3_value_subtype(sqlite3_value*);
 | 
			
		||||
** METHOD: sqlite3_value
 | 
			
		||||
**
 | 
			
		||||
** ^The sqlite3_value_dup(V) interface makes a copy of the [sqlite3_value]
 | 
			
		||||
** object V and returns a pointer to that copy.  ^The [sqlite3_value] returned
 | 
			
		||||
** object D and returns a pointer to that copy.  ^The [sqlite3_value] returned
 | 
			
		||||
** is a [protected sqlite3_value] object even if the input is not.
 | 
			
		||||
** ^The sqlite3_value_dup(V) interface returns NULL if V is NULL or if a
 | 
			
		||||
** memory allocation fails. ^If V is a [pointer value], then the result
 | 
			
		||||
@@ -6076,7 +6009,7 @@ SQLITE_API void sqlite3_value_free(sqlite3_value*);
 | 
			
		||||
** allocation error occurs.
 | 
			
		||||
**
 | 
			
		||||
** ^(The amount of space allocated by sqlite3_aggregate_context(C,N) is
 | 
			
		||||
** determined by the N parameter on the first successful call.  Changing the
 | 
			
		||||
** determined by the N parameter on first successful call.  Changing the
 | 
			
		||||
** value of N in any subsequent call to sqlite3_aggregate_context() within
 | 
			
		||||
** the same aggregate function instance will not resize the memory
 | 
			
		||||
** allocation.)^  Within the xFinal callback, it is customary to set
 | 
			
		||||
@@ -6238,7 +6171,7 @@ SQLITE_API void sqlite3_set_auxdata(sqlite3_context*, int N, void*, void (*)(voi
 | 
			
		||||
**
 | 
			
		||||
** Security Warning:  These interfaces should not be exposed in scripting
 | 
			
		||||
** languages or in other circumstances where it might be possible for an
 | 
			
		||||
** attacker to invoke them.  Any agent that can invoke these interfaces
 | 
			
		||||
** an attacker to invoke them.  Any agent that can invoke these interfaces
 | 
			
		||||
** can probably also take control of the process.
 | 
			
		||||
**
 | 
			
		||||
** Database connection client data is only available for SQLite
 | 
			
		||||
@@ -6352,7 +6285,7 @@ typedef void (*sqlite3_destructor_type)(void*);
 | 
			
		||||
** pointed to by the 2nd parameter are taken as the application-defined
 | 
			
		||||
** function result.  If the 3rd parameter is non-negative, then it
 | 
			
		||||
** must be the byte offset into the string where the NUL terminator would
 | 
			
		||||
** appear if the string were NUL terminated.  If any NUL characters occur
 | 
			
		||||
** appear if the string where NUL terminated.  If any NUL characters occur
 | 
			
		||||
** in the string at a byte offset that is less than the value of the 3rd
 | 
			
		||||
** parameter, then the resulting string will contain embedded NULs and the
 | 
			
		||||
** result of expressions operating on strings with embedded NULs is undefined.
 | 
			
		||||
@@ -6410,7 +6343,7 @@ typedef void (*sqlite3_destructor_type)(void*);
 | 
			
		||||
** string and preferably a string literal. The sqlite3_result_pointer()
 | 
			
		||||
** routine is part of the [pointer passing interface] added for SQLite 3.20.0.
 | 
			
		||||
**
 | 
			
		||||
** If these routines are called from within a different thread
 | 
			
		||||
** If these routines are called from within the different thread
 | 
			
		||||
** than the one containing the application-defined function that received
 | 
			
		||||
** the [sqlite3_context] pointer, the results are undefined.
 | 
			
		||||
*/
 | 
			
		||||
@@ -6816,7 +6749,7 @@ SQLITE_API sqlite3 *sqlite3_db_handle(sqlite3_stmt*);
 | 
			
		||||
** METHOD: sqlite3
 | 
			
		||||
**
 | 
			
		||||
** ^The sqlite3_db_name(D,N) interface returns a pointer to the schema name
 | 
			
		||||
** for the N-th database on database connection D, or a NULL pointer if N is
 | 
			
		||||
** for the N-th database on database connection D, or a NULL pointer of N is
 | 
			
		||||
** out of range.  An N value of 0 means the main database file.  An N of 1 is
 | 
			
		||||
** the "temp" schema.  Larger values of N correspond to various ATTACH-ed
 | 
			
		||||
** databases.
 | 
			
		||||
@@ -6911,7 +6844,7 @@ SQLITE_API int sqlite3_txn_state(sqlite3*,const char *zSchema);
 | 
			
		||||
** <dd>The SQLITE_TXN_READ state means that the database is currently
 | 
			
		||||
** in a read transaction.  Content has been read from the database file
 | 
			
		||||
** but nothing in the database file has changed.  The transaction state
 | 
			
		||||
** will be advanced to SQLITE_TXN_WRITE if any changes occur and there are
 | 
			
		||||
** will advanced to SQLITE_TXN_WRITE if any changes occur and there are
 | 
			
		||||
** no other conflicting concurrent write transactions.  The transaction
 | 
			
		||||
** state will revert to SQLITE_TXN_NONE following a [ROLLBACK] or
 | 
			
		||||
** [COMMIT].</dd>
 | 
			
		||||
@@ -6920,7 +6853,7 @@ SQLITE_API int sqlite3_txn_state(sqlite3*,const char *zSchema);
 | 
			
		||||
** <dd>The SQLITE_TXN_WRITE state means that the database is currently
 | 
			
		||||
** in a write transaction.  Content has been written to the database file
 | 
			
		||||
** but has not yet committed.  The transaction state will change to
 | 
			
		||||
** SQLITE_TXN_NONE at the next [ROLLBACK] or [COMMIT].</dd>
 | 
			
		||||
** to SQLITE_TXN_NONE at the next [ROLLBACK] or [COMMIT].</dd>
 | 
			
		||||
*/
 | 
			
		||||
#define SQLITE_TXN_NONE  0
 | 
			
		||||
#define SQLITE_TXN_READ  1
 | 
			
		||||
@@ -7071,8 +7004,6 @@ SQLITE_API int sqlite3_autovacuum_pages(
 | 
			
		||||
**
 | 
			
		||||
** ^The second argument is a pointer to the function to invoke when a
 | 
			
		||||
** row is updated, inserted or deleted in a rowid table.
 | 
			
		||||
** ^The update hook is disabled by invoking sqlite3_update_hook()
 | 
			
		||||
** with a NULL pointer as the second parameter.
 | 
			
		||||
** ^The first argument to the callback is a copy of the third argument
 | 
			
		||||
** to sqlite3_update_hook().
 | 
			
		||||
** ^The second callback argument is one of [SQLITE_INSERT], [SQLITE_DELETE],
 | 
			
		||||
@@ -7201,7 +7132,7 @@ SQLITE_API int sqlite3_db_release_memory(sqlite3*);
 | 
			
		||||
** CAPI3REF: Impose A Limit On Heap Size
 | 
			
		||||
**
 | 
			
		||||
** These interfaces impose limits on the amount of heap memory that will be
 | 
			
		||||
** used by all database connections within a single process.
 | 
			
		||||
** by all database connections within a single process.
 | 
			
		||||
**
 | 
			
		||||
** ^The sqlite3_soft_heap_limit64() interface sets and/or queries the
 | 
			
		||||
** soft limit on the amount of heap memory that may be allocated by SQLite.
 | 
			
		||||
@@ -7259,7 +7190,7 @@ SQLITE_API int sqlite3_db_release_memory(sqlite3*);
 | 
			
		||||
** </ul>)^
 | 
			
		||||
**
 | 
			
		||||
** The circumstances under which SQLite will enforce the heap limits may
 | 
			
		||||
** change in future releases of SQLite.
 | 
			
		||||
** changes in future releases of SQLite.
 | 
			
		||||
*/
 | 
			
		||||
SQLITE_API sqlite3_int64 sqlite3_soft_heap_limit64(sqlite3_int64 N);
 | 
			
		||||
SQLITE_API sqlite3_int64 sqlite3_hard_heap_limit64(sqlite3_int64 N);
 | 
			
		||||
@@ -7374,8 +7305,8 @@ SQLITE_API int sqlite3_table_column_metadata(
 | 
			
		||||
** ^The entry point is zProc.
 | 
			
		||||
** ^(zProc may be 0, in which case SQLite will try to come up with an
 | 
			
		||||
** entry point name on its own.  It first tries "sqlite3_extension_init".
 | 
			
		||||
** If that does not work, it constructs a name "sqlite3_X_init" where
 | 
			
		||||
** X consists of the lower-case equivalent of all ASCII alphabetic
 | 
			
		||||
** If that does not work, it constructs a name "sqlite3_X_init" where the
 | 
			
		||||
** X is consists of the lower-case equivalent of all ASCII alphabetic
 | 
			
		||||
** characters in the filename from the last "/" to the first following
 | 
			
		||||
** "." and omitting any initial "lib".)^
 | 
			
		||||
** ^The sqlite3_load_extension() interface returns
 | 
			
		||||
@@ -7446,7 +7377,7 @@ SQLITE_API int sqlite3_enable_load_extension(sqlite3 *db, int onoff);
 | 
			
		||||
** ^(Even though the function prototype shows that xEntryPoint() takes
 | 
			
		||||
** no arguments and returns void, SQLite invokes xEntryPoint() with three
 | 
			
		||||
** arguments and expects an integer result as if the signature of the
 | 
			
		||||
** entry point were as follows:
 | 
			
		||||
** entry point where as follows:
 | 
			
		||||
**
 | 
			
		||||
** <blockquote><pre>
 | 
			
		||||
**    int xEntryPoint(
 | 
			
		||||
@@ -7610,7 +7541,7 @@ struct sqlite3_module {
 | 
			
		||||
** virtual table and might not be checked again by the byte code.)^ ^(The
 | 
			
		||||
** aConstraintUsage[].omit flag is an optimization hint. When the omit flag
 | 
			
		||||
** is left in its default setting of false, the constraint will always be
 | 
			
		||||
** checked separately in byte code.  If the omit flag is changed to true, then
 | 
			
		||||
** checked separately in byte code.  If the omit flag is change to true, then
 | 
			
		||||
** the constraint may or may not be checked in byte code.  In other words,
 | 
			
		||||
** when the omit flag is true there is no guarantee that the constraint will
 | 
			
		||||
** not be checked again using byte code.)^
 | 
			
		||||
@@ -7636,7 +7567,7 @@ struct sqlite3_module {
 | 
			
		||||
** The xBestIndex method may optionally populate the idxFlags field with a
 | 
			
		||||
** mask of SQLITE_INDEX_SCAN_* flags. One such flag is
 | 
			
		||||
** [SQLITE_INDEX_SCAN_HEX], which if set causes the [EXPLAIN QUERY PLAN]
 | 
			
		||||
** output to show the idxNum as hex instead of as decimal.  Another flag is
 | 
			
		||||
** output to show the idxNum has hex instead of as decimal.  Another flag is
 | 
			
		||||
** SQLITE_INDEX_SCAN_UNIQUE, which if set indicates that the query plan will
 | 
			
		||||
** return at most one row.
 | 
			
		||||
**
 | 
			
		||||
@@ -7777,7 +7708,7 @@ struct sqlite3_index_info {
 | 
			
		||||
** the implementation of the [virtual table module].   ^The fourth
 | 
			
		||||
** parameter is an arbitrary client data pointer that is passed through
 | 
			
		||||
** into the [xCreate] and [xConnect] methods of the virtual table module
 | 
			
		||||
** when a new virtual table is being created or reinitialized.
 | 
			
		||||
** when a new virtual table is be being created or reinitialized.
 | 
			
		||||
**
 | 
			
		||||
** ^The sqlite3_create_module_v2() interface has a fifth parameter which
 | 
			
		||||
** is a pointer to a destructor for the pClientData.  ^SQLite will
 | 
			
		||||
@@ -7942,7 +7873,7 @@ typedef struct sqlite3_blob sqlite3_blob;
 | 
			
		||||
** in *ppBlob. Otherwise an [error code] is returned and, unless the error
 | 
			
		||||
** code is SQLITE_MISUSE, *ppBlob is set to NULL.)^ ^This means that, provided
 | 
			
		||||
** the API is not misused, it is always safe to call [sqlite3_blob_close()]
 | 
			
		||||
** on *ppBlob after this function returns.
 | 
			
		||||
** on *ppBlob after this function it returns.
 | 
			
		||||
**
 | 
			
		||||
** This function fails with SQLITE_ERROR if any of the following are true:
 | 
			
		||||
** <ul>
 | 
			
		||||
@@ -8062,7 +7993,7 @@ SQLITE_API int sqlite3_blob_close(sqlite3_blob *);
 | 
			
		||||
**
 | 
			
		||||
** ^Returns the size in bytes of the BLOB accessible via the
 | 
			
		||||
** successfully opened [BLOB handle] in its only argument.  ^The
 | 
			
		||||
** incremental blob I/O routines can only read or overwrite existing
 | 
			
		||||
** incremental blob I/O routines can only read or overwriting existing
 | 
			
		||||
** blob content; they cannot change the size of a blob.
 | 
			
		||||
**
 | 
			
		||||
** This routine only works on a [BLOB handle] which has been created
 | 
			
		||||
@@ -8212,7 +8143,7 @@ SQLITE_API int sqlite3_vfs_unregister(sqlite3_vfs*);
 | 
			
		||||
** ^The sqlite3_mutex_alloc() routine allocates a new
 | 
			
		||||
** mutex and returns a pointer to it. ^The sqlite3_mutex_alloc()
 | 
			
		||||
** routine returns NULL if it is unable to allocate the requested
 | 
			
		||||
** mutex.  The argument to sqlite3_mutex_alloc() must be one of these
 | 
			
		||||
** mutex.  The argument to sqlite3_mutex_alloc() must one of these
 | 
			
		||||
** integer constants:
 | 
			
		||||
**
 | 
			
		||||
** <ul>
 | 
			
		||||
@@ -8445,7 +8376,7 @@ SQLITE_API int sqlite3_mutex_notheld(sqlite3_mutex*);
 | 
			
		||||
** CAPI3REF: Retrieve the mutex for a database connection
 | 
			
		||||
** METHOD: sqlite3
 | 
			
		||||
**
 | 
			
		||||
** ^This interface returns a pointer to the [sqlite3_mutex] object that
 | 
			
		||||
** ^This interface returns a pointer the [sqlite3_mutex] object that
 | 
			
		||||
** serializes access to the [database connection] given in the argument
 | 
			
		||||
** when the [threading mode] is Serialized.
 | 
			
		||||
** ^If the [threading mode] is Single-thread or Multi-thread then this
 | 
			
		||||
@@ -8568,7 +8499,7 @@ SQLITE_API int sqlite3_test_control(int op, ...);
 | 
			
		||||
** CAPI3REF: SQL Keyword Checking
 | 
			
		||||
**
 | 
			
		||||
** These routines provide access to the set of SQL language keywords
 | 
			
		||||
** recognized by SQLite.  Applications can use these routines to determine
 | 
			
		||||
** recognized by SQLite.  Applications can uses these routines to determine
 | 
			
		||||
** whether or not a specific identifier needs to be escaped (for example,
 | 
			
		||||
** by enclosing in double-quotes) so as not to confuse the parser.
 | 
			
		||||
**
 | 
			
		||||
@@ -8736,7 +8667,7 @@ SQLITE_API void sqlite3_str_reset(sqlite3_str*);
 | 
			
		||||
** content of the dynamic string under construction in X.  The value
 | 
			
		||||
** returned by [sqlite3_str_value(X)] is managed by the sqlite3_str object X
 | 
			
		||||
** and might be freed or altered by any subsequent method on the same
 | 
			
		||||
** [sqlite3_str] object.  Applications must not use the pointer returned by
 | 
			
		||||
** [sqlite3_str] object.  Applications must not used the pointer returned
 | 
			
		||||
** [sqlite3_str_value(X)] after any subsequent method call on the same
 | 
			
		||||
** object.  ^Applications may change the content of the string returned
 | 
			
		||||
** by [sqlite3_str_value(X)] as long as they do not write into any bytes
 | 
			
		||||
@@ -8822,7 +8753,7 @@ SQLITE_API int sqlite3_status64(
 | 
			
		||||
** allocation which could not be satisfied by the [SQLITE_CONFIG_PAGECACHE]
 | 
			
		||||
** buffer and where forced to overflow to [sqlite3_malloc()].  The
 | 
			
		||||
** returned value includes allocations that overflowed because they
 | 
			
		||||
** were too large (they were larger than the "sz" parameter to
 | 
			
		||||
** where too large (they were larger than the "sz" parameter to
 | 
			
		||||
** [SQLITE_CONFIG_PAGECACHE]) and allocations that overflowed because
 | 
			
		||||
** no space was left in the page cache.</dd>)^
 | 
			
		||||
**
 | 
			
		||||
@@ -8906,29 +8837,28 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
 | 
			
		||||
** [[SQLITE_DBSTATUS_LOOKASIDE_HIT]] ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_HIT</dt>
 | 
			
		||||
** <dd>This parameter returns the number of malloc attempts that were
 | 
			
		||||
** satisfied using lookaside memory. Only the high-water value is meaningful;
 | 
			
		||||
** the current value is always zero.</dd>)^
 | 
			
		||||
** the current value is always zero.)^
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE]]
 | 
			
		||||
** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE</dt>
 | 
			
		||||
** <dd>This parameter returns the number of malloc attempts that might have
 | 
			
		||||
** <dd>This parameter returns the number malloc attempts that might have
 | 
			
		||||
** been satisfied using lookaside memory but failed due to the amount of
 | 
			
		||||
** memory requested being larger than the lookaside slot size.
 | 
			
		||||
** Only the high-water value is meaningful;
 | 
			
		||||
** the current value is always zero.</dd>)^
 | 
			
		||||
** the current value is always zero.)^
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL]]
 | 
			
		||||
** ^(<dt>SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL</dt>
 | 
			
		||||
** <dd>This parameter returns the number of malloc attempts that might have
 | 
			
		||||
** <dd>This parameter returns the number malloc attempts that might have
 | 
			
		||||
** been satisfied using lookaside memory but failed due to all lookaside
 | 
			
		||||
** memory already being in use.
 | 
			
		||||
** Only the high-water value is meaningful;
 | 
			
		||||
** the current value is always zero.</dd>)^
 | 
			
		||||
** the current value is always zero.)^
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_DBSTATUS_CACHE_USED]] ^(<dt>SQLITE_DBSTATUS_CACHE_USED</dt>
 | 
			
		||||
** <dd>This parameter returns the approximate number of bytes of heap
 | 
			
		||||
** memory used by all pager caches associated with the database connection.)^
 | 
			
		||||
** ^The highwater mark associated with SQLITE_DBSTATUS_CACHE_USED is always 0.
 | 
			
		||||
** </dd>
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_DBSTATUS_CACHE_USED_SHARED]]
 | 
			
		||||
** ^(<dt>SQLITE_DBSTATUS_CACHE_USED_SHARED</dt>
 | 
			
		||||
@@ -8937,10 +8867,10 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
 | 
			
		||||
** memory used by that pager cache is divided evenly between the attached
 | 
			
		||||
** connections.)^  In other words, if none of the pager caches associated
 | 
			
		||||
** with the database connection are shared, this request returns the same
 | 
			
		||||
** value as DBSTATUS_CACHE_USED. Or, if one or more of the pager caches are
 | 
			
		||||
** value as DBSTATUS_CACHE_USED. Or, if one or more or the pager caches are
 | 
			
		||||
** shared, the value returned by this call will be smaller than that returned
 | 
			
		||||
** by DBSTATUS_CACHE_USED. ^The highwater mark associated with
 | 
			
		||||
** SQLITE_DBSTATUS_CACHE_USED_SHARED is always 0.</dd>
 | 
			
		||||
** SQLITE_DBSTATUS_CACHE_USED_SHARED is always 0.
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_DBSTATUS_SCHEMA_USED]] ^(<dt>SQLITE_DBSTATUS_SCHEMA_USED</dt>
 | 
			
		||||
** <dd>This parameter returns the approximate number of bytes of heap
 | 
			
		||||
@@ -8950,7 +8880,6 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
 | 
			
		||||
** schema memory is shared with other database connections due to
 | 
			
		||||
** [shared cache mode] being enabled.
 | 
			
		||||
** ^The highwater mark associated with SQLITE_DBSTATUS_SCHEMA_USED is always 0.
 | 
			
		||||
** </dd>
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_DBSTATUS_STMT_USED]] ^(<dt>SQLITE_DBSTATUS_STMT_USED</dt>
 | 
			
		||||
** <dd>This parameter returns the approximate number of bytes of heap
 | 
			
		||||
@@ -8987,7 +8916,7 @@ SQLITE_API int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int r
 | 
			
		||||
** been written to disk in the middle of a transaction due to the page
 | 
			
		||||
** cache overflowing. Transactions are more efficient if they are written
 | 
			
		||||
** to disk all at once. When pages spill mid-transaction, that introduces
 | 
			
		||||
** additional overhead. This parameter can be used to help identify
 | 
			
		||||
** additional overhead. This parameter can be used help identify
 | 
			
		||||
** inefficiencies that can be resolved by increasing the cache size.
 | 
			
		||||
** </dd>
 | 
			
		||||
**
 | 
			
		||||
@@ -9058,13 +8987,13 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
 | 
			
		||||
** [[SQLITE_STMTSTATUS_SORT]] <dt>SQLITE_STMTSTATUS_SORT</dt>
 | 
			
		||||
** <dd>^This is the number of sort operations that have occurred.
 | 
			
		||||
** A non-zero value in this counter may indicate an opportunity to
 | 
			
		||||
** improve performance through careful use of indices.</dd>
 | 
			
		||||
** improvement performance through careful use of indices.</dd>
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_STMTSTATUS_AUTOINDEX]] <dt>SQLITE_STMTSTATUS_AUTOINDEX</dt>
 | 
			
		||||
** <dd>^This is the number of rows inserted into transient indices that
 | 
			
		||||
** were created automatically in order to help joins run faster.
 | 
			
		||||
** A non-zero value in this counter may indicate an opportunity to
 | 
			
		||||
** improve performance by adding permanent indices that do not
 | 
			
		||||
** improvement performance by adding permanent indices that do not
 | 
			
		||||
** need to be reinitialized each time the statement is run.</dd>
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_STMTSTATUS_VM_STEP]] <dt>SQLITE_STMTSTATUS_VM_STEP</dt>
 | 
			
		||||
@@ -9073,19 +9002,19 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
 | 
			
		||||
** to 2147483647.  The number of virtual machine operations can be
 | 
			
		||||
** used as a proxy for the total work done by the prepared statement.
 | 
			
		||||
** If the number of virtual machine operations exceeds 2147483647
 | 
			
		||||
** then the value returned by this statement status code is undefined.</dd>
 | 
			
		||||
** then the value returned by this statement status code is undefined.
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_STMTSTATUS_REPREPARE]] <dt>SQLITE_STMTSTATUS_REPREPARE</dt>
 | 
			
		||||
** <dd>^This is the number of times that the prepare statement has been
 | 
			
		||||
** automatically regenerated due to schema changes or changes to
 | 
			
		||||
** [bound parameters] that might affect the query plan.</dd>
 | 
			
		||||
** [bound parameters] that might affect the query plan.
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_STMTSTATUS_RUN]] <dt>SQLITE_STMTSTATUS_RUN</dt>
 | 
			
		||||
** <dd>^This is the number of times that the prepared statement has
 | 
			
		||||
** been run.  A single "run" for the purposes of this counter is one
 | 
			
		||||
** or more calls to [sqlite3_step()] followed by a call to [sqlite3_reset()].
 | 
			
		||||
** The counter is incremented on the first [sqlite3_step()] call of each
 | 
			
		||||
** cycle.</dd>
 | 
			
		||||
** cycle.
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_STMTSTATUS_FILTER_MISS]]
 | 
			
		||||
** [[SQLITE_STMTSTATUS_FILTER HIT]]
 | 
			
		||||
@@ -9095,7 +9024,7 @@ SQLITE_API int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg);
 | 
			
		||||
** step was bypassed because a Bloom filter returned not-found.  The
 | 
			
		||||
** corresponding SQLITE_STMTSTATUS_FILTER_MISS value is the number of
 | 
			
		||||
** times that the Bloom filter returned a find, and thus the join step
 | 
			
		||||
** had to be processed as normal.</dd>
 | 
			
		||||
** had to be processed as normal.
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_STMTSTATUS_MEMUSED]] <dt>SQLITE_STMTSTATUS_MEMUSED</dt>
 | 
			
		||||
** <dd>^This is the approximate number of bytes of heap memory
 | 
			
		||||
@@ -9200,9 +9129,9 @@ struct sqlite3_pcache_page {
 | 
			
		||||
** SQLite will typically create one cache instance for each open database file,
 | 
			
		||||
** though this is not guaranteed. ^The
 | 
			
		||||
** first parameter, szPage, is the size in bytes of the pages that must
 | 
			
		||||
** be allocated by the cache.  ^szPage will always be a power of two.  ^The
 | 
			
		||||
** be allocated by the cache.  ^szPage will always a power of two.  ^The
 | 
			
		||||
** second parameter szExtra is a number of bytes of extra storage
 | 
			
		||||
** associated with each page cache entry.  ^The szExtra parameter will be
 | 
			
		||||
** associated with each page cache entry.  ^The szExtra parameter will
 | 
			
		||||
** a number less than 250.  SQLite will use the
 | 
			
		||||
** extra szExtra bytes on each page to store metadata about the underlying
 | 
			
		||||
** database page on disk.  The value passed into szExtra depends
 | 
			
		||||
@@ -9210,17 +9139,17 @@ struct sqlite3_pcache_page {
 | 
			
		||||
** ^The third argument to xCreate(), bPurgeable, is true if the cache being
 | 
			
		||||
** created will be used to cache database pages of a file stored on disk, or
 | 
			
		||||
** false if it is used for an in-memory database. The cache implementation
 | 
			
		||||
** does not have to do anything special based upon the value of bPurgeable;
 | 
			
		||||
** does not have to do anything special based with the value of bPurgeable;
 | 
			
		||||
** it is purely advisory.  ^On a cache where bPurgeable is false, SQLite will
 | 
			
		||||
** never invoke xUnpin() except to deliberately delete a page.
 | 
			
		||||
** ^In other words, calls to xUnpin() on a cache with bPurgeable set to
 | 
			
		||||
** false will always have the "discard" flag set to true.
 | 
			
		||||
** ^Hence, a cache created with bPurgeable set to false will
 | 
			
		||||
** ^Hence, a cache created with bPurgeable false will
 | 
			
		||||
** never contain any unpinned pages.
 | 
			
		||||
**
 | 
			
		||||
** [[the xCachesize() page cache method]]
 | 
			
		||||
** ^(The xCachesize() method may be called at any time by SQLite to set the
 | 
			
		||||
** suggested maximum cache-size (number of pages stored) for the cache
 | 
			
		||||
** suggested maximum cache-size (number of pages stored by) the cache
 | 
			
		||||
** instance passed as the first argument. This is the value configured using
 | 
			
		||||
** the SQLite "[PRAGMA cache_size]" command.)^  As with the bPurgeable
 | 
			
		||||
** parameter, the implementation is not required to do anything with this
 | 
			
		||||
@@ -9247,12 +9176,12 @@ struct sqlite3_pcache_page {
 | 
			
		||||
** implementation must return a pointer to the page buffer with its content
 | 
			
		||||
** intact.  If the requested page is not already in the cache, then the
 | 
			
		||||
** cache implementation should use the value of the createFlag
 | 
			
		||||
** parameter to help it determine what action to take:
 | 
			
		||||
** parameter to help it determined what action to take:
 | 
			
		||||
**
 | 
			
		||||
** <table border=1 width=85% align=center>
 | 
			
		||||
** <tr><th> createFlag <th> Behavior when page is not already in cache
 | 
			
		||||
** <tr><td> 0 <td> Do not allocate a new page.  Return NULL.
 | 
			
		||||
** <tr><td> 1 <td> Allocate a new page if it is easy and convenient to do so.
 | 
			
		||||
** <tr><td> 1 <td> Allocate a new page if it easy and convenient to do so.
 | 
			
		||||
**                 Otherwise return NULL.
 | 
			
		||||
** <tr><td> 2 <td> Make every effort to allocate a new page.  Only return
 | 
			
		||||
**                 NULL if allocating a new page is effectively impossible.
 | 
			
		||||
@@ -9269,7 +9198,7 @@ struct sqlite3_pcache_page {
 | 
			
		||||
** as its second argument.  If the third parameter, discard, is non-zero,
 | 
			
		||||
** then the page must be evicted from the cache.
 | 
			
		||||
** ^If the discard parameter is
 | 
			
		||||
** zero, then the page may be discarded or retained at the discretion of the
 | 
			
		||||
** zero, then the page may be discarded or retained at the discretion of
 | 
			
		||||
** page cache implementation. ^The page cache implementation
 | 
			
		||||
** may choose to evict unpinned pages at any time.
 | 
			
		||||
**
 | 
			
		||||
@@ -9287,7 +9216,7 @@ struct sqlite3_pcache_page {
 | 
			
		||||
** When SQLite calls the xTruncate() method, the cache must discard all
 | 
			
		||||
** existing cache entries with page numbers (keys) greater than or equal
 | 
			
		||||
** to the value of the iLimit parameter passed to xTruncate(). If any
 | 
			
		||||
** of these pages are pinned, they become implicitly unpinned, meaning that
 | 
			
		||||
** of these pages are pinned, they are implicitly unpinned, meaning that
 | 
			
		||||
** they can be safely discarded.
 | 
			
		||||
**
 | 
			
		||||
** [[the xDestroy() page cache method]]
 | 
			
		||||
@@ -9467,7 +9396,7 @@ typedef struct sqlite3_backup sqlite3_backup;
 | 
			
		||||
** external process or via a database connection other than the one being
 | 
			
		||||
** used by the backup operation, then the backup will be automatically
 | 
			
		||||
** restarted by the next call to sqlite3_backup_step(). ^If the source
 | 
			
		||||
** database is modified by using the same database connection as is used
 | 
			
		||||
** database is modified by the using the same database connection as is used
 | 
			
		||||
** by the backup operation, then the backup database is automatically
 | 
			
		||||
** updated at the same time.
 | 
			
		||||
**
 | 
			
		||||
@@ -9484,7 +9413,7 @@ typedef struct sqlite3_backup sqlite3_backup;
 | 
			
		||||
** and may not be used following a call to sqlite3_backup_finish().
 | 
			
		||||
**
 | 
			
		||||
** ^The value returned by sqlite3_backup_finish is [SQLITE_OK] if no
 | 
			
		||||
** sqlite3_backup_step() errors occurred, regardless of whether or not
 | 
			
		||||
** sqlite3_backup_step() errors occurred, regardless or whether or not
 | 
			
		||||
** sqlite3_backup_step() completed.
 | 
			
		||||
** ^If an out-of-memory condition or IO error occurred during any prior
 | 
			
		||||
** sqlite3_backup_step() call on the same [sqlite3_backup] object, then
 | 
			
		||||
@@ -9586,7 +9515,7 @@ SQLITE_API int sqlite3_backup_pagecount(sqlite3_backup *p);
 | 
			
		||||
** application receives an SQLITE_LOCKED error, it may call the
 | 
			
		||||
** sqlite3_unlock_notify() method with the blocked connection handle as
 | 
			
		||||
** the first argument to register for a callback that will be invoked
 | 
			
		||||
** when the blocking connection's current transaction is concluded. ^The
 | 
			
		||||
** when the blocking connections current transaction is concluded. ^The
 | 
			
		||||
** callback is invoked from within the [sqlite3_step] or [sqlite3_close]
 | 
			
		||||
** call that concludes the blocking connection's transaction.
 | 
			
		||||
**
 | 
			
		||||
@@ -9606,7 +9535,7 @@ SQLITE_API int sqlite3_backup_pagecount(sqlite3_backup *p);
 | 
			
		||||
** blocked connection already has a registered unlock-notify callback,
 | 
			
		||||
** then the new callback replaces the old.)^ ^If sqlite3_unlock_notify() is
 | 
			
		||||
** called with a NULL pointer as its second argument, then any existing
 | 
			
		||||
** unlock-notify callback is canceled. ^The blocked connection's
 | 
			
		||||
** unlock-notify callback is canceled. ^The blocked connections
 | 
			
		||||
** unlock-notify callback may also be canceled by closing the blocked
 | 
			
		||||
** connection using [sqlite3_close()].
 | 
			
		||||
**
 | 
			
		||||
@@ -10004,7 +9933,7 @@ SQLITE_API int sqlite3_vtab_config(sqlite3*, int op, ...);
 | 
			
		||||
** support constraints.  In this configuration (which is the default) if
 | 
			
		||||
** a call to the [xUpdate] method returns [SQLITE_CONSTRAINT], then the entire
 | 
			
		||||
** statement is rolled back as if [ON CONFLICT | OR ABORT] had been
 | 
			
		||||
** specified as part of the user's SQL statement, regardless of the actual
 | 
			
		||||
** specified as part of the users SQL statement, regardless of the actual
 | 
			
		||||
** ON CONFLICT mode specified.
 | 
			
		||||
**
 | 
			
		||||
** If X is non-zero, then the virtual table implementation guarantees
 | 
			
		||||
@@ -10038,7 +9967,7 @@ SQLITE_API int sqlite3_vtab_config(sqlite3*, int op, ...);
 | 
			
		||||
** [[SQLITE_VTAB_INNOCUOUS]]<dt>SQLITE_VTAB_INNOCUOUS</dt>
 | 
			
		||||
** <dd>Calls of the form
 | 
			
		||||
** [sqlite3_vtab_config](db,SQLITE_VTAB_INNOCUOUS) from within the
 | 
			
		||||
** [xConnect] or [xCreate] methods of a [virtual table] implementation
 | 
			
		||||
** the [xConnect] or [xCreate] methods of a [virtual table] implementation
 | 
			
		||||
** identify that virtual table as being safe to use from within triggers
 | 
			
		||||
** and views.  Conceptually, the SQLITE_VTAB_INNOCUOUS tag means that the
 | 
			
		||||
** virtual table can do no serious harm even if it is controlled by a
 | 
			
		||||
@@ -10206,7 +10135,7 @@ SQLITE_API const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
 | 
			
		||||
** </table>
 | 
			
		||||
**
 | 
			
		||||
** ^For the purposes of comparing virtual table output values to see if the
 | 
			
		||||
** values are the same value for sorting purposes, two NULL values are considered
 | 
			
		||||
** values are same value for sorting purposes, two NULL values are considered
 | 
			
		||||
** to be the same.  In other words, the comparison operator is "IS"
 | 
			
		||||
** (or "IS NOT DISTINCT FROM") and not "==".
 | 
			
		||||
**
 | 
			
		||||
@@ -10216,7 +10145,7 @@ SQLITE_API const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
 | 
			
		||||
**
 | 
			
		||||
** ^A virtual table implementation is always free to return rows in any order
 | 
			
		||||
** it wants, as long as the "orderByConsumed" flag is not set.  ^When the
 | 
			
		||||
** "orderByConsumed" flag is unset, the query planner will add extra
 | 
			
		||||
** the "orderByConsumed" flag is unset, the query planner will add extra
 | 
			
		||||
** [bytecode] to ensure that the final results returned by the SQL query are
 | 
			
		||||
** ordered correctly.  The use of the "orderByConsumed" flag and the
 | 
			
		||||
** sqlite3_vtab_distinct() interface is merely an optimization.  ^Careful
 | 
			
		||||
@@ -10313,7 +10242,7 @@ SQLITE_API int sqlite3_vtab_in(sqlite3_index_info*, int iCons, int bHandle);
 | 
			
		||||
** sqlite3_vtab_in_next(X,P) should be one of the parameters to the
 | 
			
		||||
** xFilter method which invokes these routines, and specifically
 | 
			
		||||
** a parameter that was previously selected for all-at-once IN constraint
 | 
			
		||||
** processing using the [sqlite3_vtab_in()] interface in the
 | 
			
		||||
** processing use the [sqlite3_vtab_in()] interface in the
 | 
			
		||||
** [xBestIndex|xBestIndex method].  ^(If the X parameter is not
 | 
			
		||||
** an xFilter argument that was selected for all-at-once IN constraint
 | 
			
		||||
** processing, then these routines return [SQLITE_ERROR].)^
 | 
			
		||||
@@ -10368,7 +10297,7 @@ SQLITE_API int sqlite3_vtab_in_next(sqlite3_value *pVal, sqlite3_value **ppOut);
 | 
			
		||||
** and only if *V is set to a value.  ^The sqlite3_vtab_rhs_value(P,J,V)
 | 
			
		||||
** inteface returns SQLITE_NOTFOUND if the right-hand side of the J-th
 | 
			
		||||
** constraint is not available.  ^The sqlite3_vtab_rhs_value() interface
 | 
			
		||||
** can return a result code other than SQLITE_OK or SQLITE_NOTFOUND if
 | 
			
		||||
** can return an result code other than SQLITE_OK or SQLITE_NOTFOUND if
 | 
			
		||||
** something goes wrong.
 | 
			
		||||
**
 | 
			
		||||
** The sqlite3_vtab_rhs_value() interface is usually only successful if
 | 
			
		||||
@@ -10396,8 +10325,8 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
 | 
			
		||||
** KEYWORDS: {conflict resolution mode}
 | 
			
		||||
**
 | 
			
		||||
** These constants are returned by [sqlite3_vtab_on_conflict()] to
 | 
			
		||||
** inform a [virtual table] implementation of the [ON CONFLICT] mode
 | 
			
		||||
** for the SQL statement being evaluated.
 | 
			
		||||
** inform a [virtual table] implementation what the [ON CONFLICT] mode
 | 
			
		||||
** is for the SQL statement being evaluated.
 | 
			
		||||
**
 | 
			
		||||
** Note that the [SQLITE_IGNORE] constant is also used as a potential
 | 
			
		||||
** return value from the [sqlite3_set_authorizer()] callback and that
 | 
			
		||||
@@ -10437,39 +10366,39 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
 | 
			
		||||
** [[SQLITE_SCANSTAT_EST]] <dt>SQLITE_SCANSTAT_EST</dt>
 | 
			
		||||
** <dd>^The "double" variable pointed to by the V parameter will be set to the
 | 
			
		||||
** query planner's estimate for the average number of rows output from each
 | 
			
		||||
** iteration of the X-th loop.  If the query planner's estimate was accurate,
 | 
			
		||||
** iteration of the X-th loop.  If the query planner's estimates was accurate,
 | 
			
		||||
** then this value will approximate the quotient NVISIT/NLOOP and the
 | 
			
		||||
** product of this value for all prior loops with the same SELECTID will
 | 
			
		||||
** be the NLOOP value for the current loop.</dd>
 | 
			
		||||
** be the NLOOP value for the current loop.
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_SCANSTAT_NAME]] <dt>SQLITE_SCANSTAT_NAME</dt>
 | 
			
		||||
** <dd>^The "const char *" variable pointed to by the V parameter will be set
 | 
			
		||||
** to a zero-terminated UTF-8 string containing the name of the index or table
 | 
			
		||||
** used for the X-th loop.</dd>
 | 
			
		||||
** used for the X-th loop.
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_SCANSTAT_EXPLAIN]] <dt>SQLITE_SCANSTAT_EXPLAIN</dt>
 | 
			
		||||
** <dd>^The "const char *" variable pointed to by the V parameter will be set
 | 
			
		||||
** to a zero-terminated UTF-8 string containing the [EXPLAIN QUERY PLAN]
 | 
			
		||||
** description for the X-th loop.</dd>
 | 
			
		||||
** description for the X-th loop.
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_SCANSTAT_SELECTID]] <dt>SQLITE_SCANSTAT_SELECTID</dt>
 | 
			
		||||
** <dd>^The "int" variable pointed to by the V parameter will be set to the
 | 
			
		||||
** id for the X-th query plan element. The id value is unique within the
 | 
			
		||||
** statement. The select-id is the same value as is output in the first
 | 
			
		||||
** column of an [EXPLAIN QUERY PLAN] query.</dd>
 | 
			
		||||
** column of an [EXPLAIN QUERY PLAN] query.
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_SCANSTAT_PARENTID]] <dt>SQLITE_SCANSTAT_PARENTID</dt>
 | 
			
		||||
** <dd>The "int" variable pointed to by the V parameter will be set to the
 | 
			
		||||
** id of the parent of the current query element, if applicable, or
 | 
			
		||||
** the id of the parent of the current query element, if applicable, or
 | 
			
		||||
** to zero if the query element has no parent. This is the same value as
 | 
			
		||||
** returned in the second column of an [EXPLAIN QUERY PLAN] query.</dd>
 | 
			
		||||
** returned in the second column of an [EXPLAIN QUERY PLAN] query.
 | 
			
		||||
**
 | 
			
		||||
** [[SQLITE_SCANSTAT_NCYCLE]] <dt>SQLITE_SCANSTAT_NCYCLE</dt>
 | 
			
		||||
** <dd>The sqlite3_int64 output value is set to the number of cycles,
 | 
			
		||||
** according to the processor time-stamp counter, that elapsed while the
 | 
			
		||||
** query element was being processed. This value is not available for
 | 
			
		||||
** all query elements - if it is unavailable the output variable is
 | 
			
		||||
** set to -1.</dd>
 | 
			
		||||
** set to -1.
 | 
			
		||||
** </dl>
 | 
			
		||||
*/
 | 
			
		||||
#define SQLITE_SCANSTAT_NLOOP    0
 | 
			
		||||
@@ -10510,8 +10439,8 @@ SQLITE_API int sqlite3_vtab_rhs_value(sqlite3_index_info*, int, sqlite3_value **
 | 
			
		||||
** sqlite3_stmt_scanstatus_v2() with a zeroed flags parameter.
 | 
			
		||||
**
 | 
			
		||||
** Parameter "idx" identifies the specific query element to retrieve statistics
 | 
			
		||||
** for. Query elements are numbered starting from zero. A value of -1 may
 | 
			
		||||
** retrieve statistics for the entire query. ^If idx is out of range
 | 
			
		||||
** for. Query elements are numbered starting from zero. A value of -1 may be
 | 
			
		||||
** to query for statistics regarding the entire query. ^If idx is out of range
 | 
			
		||||
** - less than -1 or greater than or equal to the total number of query
 | 
			
		||||
** elements used to implement the statement - a non-zero value is returned and
 | 
			
		||||
** the variable that pOut points to is unchanged.
 | 
			
		||||
@@ -10554,7 +10483,7 @@ SQLITE_API void sqlite3_stmt_scanstatus_reset(sqlite3_stmt*);
 | 
			
		||||
** METHOD: sqlite3
 | 
			
		||||
**
 | 
			
		||||
** ^If a write-transaction is open on [database connection] D when the
 | 
			
		||||
** [sqlite3_db_cacheflush(D)] interface is invoked, any dirty
 | 
			
		||||
** [sqlite3_db_cacheflush(D)] interface invoked, any dirty
 | 
			
		||||
** pages in the pager-cache that are not currently in use are written out
 | 
			
		||||
** to disk. A dirty page may be in use if a database cursor created by an
 | 
			
		||||
** active SQL statement is reading from it, or if it is page 1 of a database
 | 
			
		||||
@@ -10668,8 +10597,8 @@ SQLITE_API int sqlite3_db_cacheflush(sqlite3*);
 | 
			
		||||
** triggers; and so forth.
 | 
			
		||||
**
 | 
			
		||||
** When the [sqlite3_blob_write()] API is used to update a blob column,
 | 
			
		||||
** the pre-update hook is invoked with SQLITE_DELETE, because
 | 
			
		||||
** the new values are not yet available. In this case, when a
 | 
			
		||||
** the pre-update hook is invoked with SQLITE_DELETE. This is because the
 | 
			
		||||
** in this case the new values are not available. In this case, when a
 | 
			
		||||
** callback made with op==SQLITE_DELETE is actually a write using the
 | 
			
		||||
** sqlite3_blob_write() API, the [sqlite3_preupdate_blobwrite()] returns
 | 
			
		||||
** the index of the column being written. In other cases, where the
 | 
			
		||||
@@ -10922,7 +10851,7 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const c
 | 
			
		||||
** For an ordinary on-disk database file, the serialization is just a
 | 
			
		||||
** copy of the disk file.  For an in-memory database or a "TEMP" database,
 | 
			
		||||
** the serialization is the same sequence of bytes which would be written
 | 
			
		||||
** to disk if that database were backed up to disk.
 | 
			
		||||
** to disk if that database where backed up to disk.
 | 
			
		||||
**
 | 
			
		||||
** The usual case is that sqlite3_serialize() copies the serialization of
 | 
			
		||||
** the database into memory obtained from [sqlite3_malloc64()] and returns
 | 
			
		||||
@@ -10931,7 +10860,7 @@ SQLITE_API SQLITE_EXPERIMENTAL int sqlite3_snapshot_recover(sqlite3 *db, const c
 | 
			
		||||
** contains the SQLITE_SERIALIZE_NOCOPY bit, then no memory allocations
 | 
			
		||||
** are made, and the sqlite3_serialize() function will return a pointer
 | 
			
		||||
** to the contiguous memory representation of the database that SQLite
 | 
			
		||||
** is currently using for that database, or NULL if no such contiguous
 | 
			
		||||
** is currently using for that database, or NULL if the no such contiguous
 | 
			
		||||
** memory representation of the database exists.  A contiguous memory
 | 
			
		||||
** representation of the database will usually only exist if there has
 | 
			
		||||
** been a prior call to [sqlite3_deserialize(D,S,...)] with the same
 | 
			
		||||
@@ -11002,7 +10931,7 @@ SQLITE_API unsigned char *sqlite3_serialize(
 | 
			
		||||
** database is currently in a read transaction or is involved in a backup
 | 
			
		||||
** operation.
 | 
			
		||||
**
 | 
			
		||||
** It is not possible to deserialize into the TEMP database.  If the
 | 
			
		||||
** It is not possible to deserialized into the TEMP database.  If the
 | 
			
		||||
** S argument to sqlite3_deserialize(D,S,P,N,M,F) is "temp" then the
 | 
			
		||||
** function returns SQLITE_ERROR.
 | 
			
		||||
**
 | 
			
		||||
@@ -11024,7 +10953,7 @@ SQLITE_API int sqlite3_deserialize(
 | 
			
		||||
  sqlite3 *db,            /* The database connection */
 | 
			
		||||
  const char *zSchema,    /* Which DB to reopen with the deserialization */
 | 
			
		||||
  unsigned char *pData,   /* The serialized database content */
 | 
			
		||||
  sqlite3_int64 szDb,     /* Number of bytes in the deserialization */
 | 
			
		||||
  sqlite3_int64 szDb,     /* Number bytes in the deserialization */
 | 
			
		||||
  sqlite3_int64 szBuf,    /* Total size of buffer pData[] */
 | 
			
		||||
  unsigned mFlags         /* Zero or more SQLITE_DESERIALIZE_* flags */
 | 
			
		||||
);
 | 
			
		||||
@@ -11032,7 +10961,7 @@ SQLITE_API int sqlite3_deserialize(
 | 
			
		||||
/*
 | 
			
		||||
** CAPI3REF: Flags for sqlite3_deserialize()
 | 
			
		||||
**
 | 
			
		||||
** The following are allowed values for the 6th argument (the F argument) to
 | 
			
		||||
** The following are allowed values for 6th argument (the F argument) to
 | 
			
		||||
** the [sqlite3_deserialize(D,S,P,N,M,F)] interface.
 | 
			
		||||
**
 | 
			
		||||
** The SQLITE_DESERIALIZE_FREEONCLOSE means that the database serialization
 | 
			
		||||
@@ -11557,10 +11486,9 @@ SQLITE_API void sqlite3session_table_filter(
 | 
			
		||||
** is inserted while a session object is enabled, then later deleted while
 | 
			
		||||
** the same session object is disabled, no INSERT record will appear in the
 | 
			
		||||
** changeset, even though the delete took place while the session was disabled.
 | 
			
		||||
** Or, if one field of a row is updated while a session is enabled, and
 | 
			
		||||
** then another field of the same row is updated while the session is disabled,
 | 
			
		||||
** the resulting changeset will contain an UPDATE change that updates both
 | 
			
		||||
** fields.
 | 
			
		||||
** Or, if one field of a row is updated while a session is disabled, and
 | 
			
		||||
** another field of the same row is updated while the session is enabled, the
 | 
			
		||||
** resulting changeset will contain an UPDATE change that updates both fields.
 | 
			
		||||
*/
 | 
			
		||||
SQLITE_API int sqlite3session_changeset(
 | 
			
		||||
  sqlite3_session *pSession,      /* Session object */
 | 
			
		||||
@@ -11632,9 +11560,8 @@ SQLITE_API sqlite3_int64 sqlite3session_changeset_size(sqlite3_session *pSession
 | 
			
		||||
** database zFrom the contents of the two compatible tables would be
 | 
			
		||||
** identical.
 | 
			
		||||
**
 | 
			
		||||
** Unless the call to this function is a no-op as described above, it is an
 | 
			
		||||
** error if database zFrom does not exist or does not contain the required
 | 
			
		||||
** compatible table.
 | 
			
		||||
** It an error if database zFrom does not exist or does not contain the
 | 
			
		||||
** required compatible table.
 | 
			
		||||
**
 | 
			
		||||
** If the operation is successful, SQLITE_OK is returned. Otherwise, an SQLite
 | 
			
		||||
** error code. In this case, if argument pzErrMsg is not NULL, *pzErrMsg
 | 
			
		||||
@@ -11769,7 +11696,7 @@ SQLITE_API int sqlite3changeset_start_v2(
 | 
			
		||||
** The following flags may passed via the 4th parameter to
 | 
			
		||||
** [sqlite3changeset_start_v2] and [sqlite3changeset_start_v2_strm]:
 | 
			
		||||
**
 | 
			
		||||
** <dt>SQLITE_CHANGESETSTART_INVERT <dd>
 | 
			
		||||
** <dt>SQLITE_CHANGESETAPPLY_INVERT <dd>
 | 
			
		||||
**   Invert the changeset while iterating through it. This is equivalent to
 | 
			
		||||
**   inverting a changeset using sqlite3changeset_invert() before applying it.
 | 
			
		||||
**   It is an error to specify this flag with a patchset.
 | 
			
		||||
@@ -12084,6 +12011,19 @@ SQLITE_API int sqlite3changeset_concat(
 | 
			
		||||
  void **ppOut                    /* OUT: Buffer containing output changeset */
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
** CAPI3REF: Upgrade the Schema of a Changeset/Patchset
 | 
			
		||||
*/
 | 
			
		||||
SQLITE_API int sqlite3changeset_upgrade(
 | 
			
		||||
  sqlite3 *db,
 | 
			
		||||
  const char *zDb,
 | 
			
		||||
  int nIn, const void *pIn,       /* Input changeset */
 | 
			
		||||
  int *pnOut, void **ppOut        /* OUT: Inverse of input */
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
** CAPI3REF: Changegroup Handle
 | 
			
		||||
**
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								deps/sqlite/sqlite3ext.h
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -366,8 +366,6 @@ struct sqlite3_api_routines {
 | 
			
		||||
  /* Version 3.44.0 and later */
 | 
			
		||||
  void *(*get_clientdata)(sqlite3*,const char*);
 | 
			
		||||
  int (*set_clientdata)(sqlite3*, const char*, void*, void(*)(void*));
 | 
			
		||||
  /* Version 3.50.0 and later */
 | 
			
		||||
  int (*setlk_timeout)(sqlite3*,int,int);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@@ -701,8 +699,6 @@ typedef int (*sqlite3_loadext_entry)(
 | 
			
		||||
/* Version 3.44.0 and later */
 | 
			
		||||
#define sqlite3_get_clientdata         sqlite3_api->get_clientdata
 | 
			
		||||
#define sqlite3_set_clientdata         sqlite3_api->set_clientdata
 | 
			
		||||
/* Version 3.50.0 and later */
 | 
			
		||||
#define sqlite3_setlk_timeout          sqlite3_api->setlk_timeout
 | 
			
		||||
#endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */
 | 
			
		||||
 | 
			
		||||
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
# Connecting with Manyverse
 | 
			
		||||
 | 
			
		||||
Communication with [Manyverse](https://www.manyver.se/) should Just Work (tm).
 | 
			
		||||
 | 
			
		||||
This document is intended as a cheat sheet for the instances where it doesn't.
 | 
			
		||||
If your experience differs, please share so we can make things better.
 | 
			
		||||
 | 
			
		||||
## Connecting Manyverse to the tildefriends.net room
 | 
			
		||||
 | 
			
		||||
Open the `Connections` tab. This is from the desktop app, but mobile is similar.
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
Open the `Connections Panel`.
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
Use the `Add Connection` button at the bottom right to open the dialog to enter
 | 
			
		||||
a connections string to add a new connection.
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
Copy the tildefriends.net room code from https://www.tildefriends.net/~cory/room/.
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
Paste.
 | 
			
		||||
 | 
			
		||||
On mobile especially, make sure the full text is pasted without modification.
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
Click `Done`, and you should be connected successfully. tildefriends.net is
 | 
			
		||||
all things: a room, a pub, and a client, so you should be able to start replicating
 | 
			
		||||
immediately as well as find other similarly connected people with whom to establish
 | 
			
		||||
further connections.
 | 
			
		||||
 | 
			
		||||
When logged into tildefriends.net, active connections it sees can be found on
 | 
			
		||||
the `Connections` tab: https://www.tildefriends.net/~core/ssb/#connections,
 | 
			
		||||
which may indicate errors if you find yourself disconnecting.
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 43 KiB  | 
| 
		 Before Width: | Height: | Size: 27 KiB  | 
| 
		 Before Width: | Height: | Size: 48 KiB  | 
| 
		 Before Width: | Height: | Size: 25 KiB  | 
| 
		 Before Width: | Height: | Size: 136 KiB  | 
@@ -4,8 +4,7 @@
 | 
			
		||||
- run the tests
 | 
			
		||||
- format + prettier
 | 
			
		||||
- update metadata/en-US/changelogs
 | 
			
		||||
- git tag v1.2.3
 | 
			
		||||
- git tag -f latest_release
 | 
			
		||||
- git tag
 | 
			
		||||
- push
 | 
			
		||||
- make a release on gitea
 | 
			
		||||
- upload the artifacts
 | 
			
		||||
@@ -14,7 +13,7 @@
 | 
			
		||||
- upload to Apple with dist-ios on macos
 | 
			
		||||
- nix
 | 
			
		||||
  - june and december: update release version
 | 
			
		||||
  - run `nix --extra-experimental-features nix-command --extra-experimental-features flakes flake update`
 | 
			
		||||
  - run `nix flake update`
 | 
			
		||||
  - comment out the hash in default.nix
 | 
			
		||||
  - update the version
 | 
			
		||||
  - run `nix-build`
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
# Upgrading
 | 
			
		||||
 | 
			
		||||
Tilde Friends can be upgraded simply by running a new executable against an
 | 
			
		||||
existing database.
 | 
			
		||||
 | 
			
		||||
Tilde Friends writes all data to a `db.sqlite` file, either in
 | 
			
		||||
`~/.local/share/tildefriends/` or in the working directory where it is run,
 | 
			
		||||
depending on the platform and whether each one already exists. Run with
 | 
			
		||||
`tildefriends run -d DB_PATH` to specify the path to the database explicitly.
 | 
			
		||||
 | 
			
		||||
This file can be copied and moved across machines as needed like any [sqlite3
 | 
			
		||||
database](https://www.sqlite.org/onefile.html).
 | 
			
		||||
 | 
			
		||||
Schema changes and compatibility breaks have been rare, by design. In general,
 | 
			
		||||
upgrading is not expected to require any manual intervention and likely does
 | 
			
		||||
not involve any automatic migration, either. Downgrading is not well-supported
 | 
			
		||||
but will probably just work excepting rare changes that will be called out in
 | 
			
		||||
the changelog.
 | 
			
		||||
							
								
								
									
										289
									
								
								docs/usage.md
									
									
									
									
									
								
							
							
						
						@@ -1,289 +0,0 @@
 | 
			
		||||
# CLI Usage
 | 
			
		||||
 | 
			
		||||
## tildefriends -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
Usage: out/debug/tildefriends command [command-options]
 | 
			
		||||
commands:
 | 
			
		||||
  run - Run tildefriends (default).
 | 
			
		||||
  sandbox - Run a sandboxed tildefriends sandbox process (used internally).
 | 
			
		||||
  import - Import apps from file to the database.
 | 
			
		||||
  export - Export apps from the database to file.
 | 
			
		||||
  publish - Append a message to a feed.
 | 
			
		||||
  private - Append a private post message to a feed.
 | 
			
		||||
  create_invite - Create an invite.
 | 
			
		||||
  get_sequence - Get the last sequence number for a feed.
 | 
			
		||||
  get_identity - Get the server account identity.
 | 
			
		||||
  get_profile - Get profile information for the given identity.
 | 
			
		||||
  get_contacts - Get information about followed, blocked, and friend identities.
 | 
			
		||||
  has_blob - Check whether a blob is in the blob store.
 | 
			
		||||
  get_blob - Read a file from the blob store.
 | 
			
		||||
  store_blob - Write a file to the blob store.
 | 
			
		||||
  verify - Verify a feed.
 | 
			
		||||
  test - Test SSB.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends run -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends run [options]
 | 
			
		||||
 | 
			
		||||
Run tildefriends (default).
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -s, --script script        Script to run (default: core/core.js).
 | 
			
		||||
  -d, --db-path path         SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -k, --ssb-network-key key  SSB network key to use.
 | 
			
		||||
  -n, --count count          Number of instances to run.
 | 
			
		||||
  -a, --args args            Arguments of the format key=value,foo=bar,verbose=true (note: these are persisted to the database).
 | 
			
		||||
                                 code_of_conduct (default: ""): Code of conduct presented at sign-in.
 | 
			
		||||
                                 ssb_port (default: 8008): Port on which to listen for SSB secure handshake connections.
 | 
			
		||||
                                 http_local_only (default: false): Whether to bind http(s) to the loopback address.  Otherwise any.
 | 
			
		||||
                                 http_port (default: 12345): Port on which to listen for HTTP connections.
 | 
			
		||||
                                 https_port (default: 0): Port on which to listen for secure HTTP connections.
 | 
			
		||||
                                 out_http_port_file (default: ""): File to which to write bound HTTP port.
 | 
			
		||||
                                 blob_fetch_age_seconds (default: -1): Only blobs mentioned more recently than this age will be automatically fetched.
 | 
			
		||||
                                 blob_expire_age_seconds (default: -1): Blobs older than this will be automatically deleted.
 | 
			
		||||
                                 fetch_hosts (default: ""): Comma-separated list of host names to which HTTP fetch requests are allowed.  None if empty.
 | 
			
		||||
                                 http_redirect (default: ""): If connecting by HTTP and HTTPS is configured, Location header prefix (ie, "http://example.com")
 | 
			
		||||
                                 index (default: "/~core/intro/"): Default path.
 | 
			
		||||
                                 index_map (default: ""): Mappings from hostname to redirect path, one per line, as in: "www.tildefriends.net=/~core/index/"
 | 
			
		||||
                                 peer_exchange (default: false): Enable discovery of, sharing of, and connecting to internet peer strangers, including announcing this instance.
 | 
			
		||||
                                 replicator (default: true): Enable message and blob replication.
 | 
			
		||||
                                 room (default: true): Enable peers to tunnel through this instance as a room.
 | 
			
		||||
                                 room_name (default: "tilde friends tunnel"): Name of the room.
 | 
			
		||||
                                 seeds_host (default: "seeds.tildefriends.net"): Hostname for seed connections.
 | 
			
		||||
                                 account_registration (default: true): Allow registration of new accounts.
 | 
			
		||||
                                 replication_hops (default: 2): Number of hops to replicate (1 = direct follows, 2 = follows of follows, etc.).
 | 
			
		||||
                                 delete_stale_feeds (default: false): Periodically delete feeds that aren't visible from local accounts or related follows.
 | 
			
		||||
                                 talk_to_strangers (default: true): Whether connections are accepted from accounts that aren't in the replication range or otherwise already known.
 | 
			
		||||
                                 autologin (default: false): Whether mobile autologin is supported.
 | 
			
		||||
                                 broadcast (default: true): Send network discovery broadcasts.
 | 
			
		||||
                                 discovery (default: true): Receive network discovery broadcasts.
 | 
			
		||||
                                 stay_connected (default: false): Whether to attempt to keep several peer connections open.
 | 
			
		||||
  -o, --one-proc             Run everything in one process (unsafely!).
 | 
			
		||||
  -z, --zip path             Zip archive from which to load files.
 | 
			
		||||
  -v, --verbose              Log raw messages.
 | 
			
		||||
  -h, --help                 Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends sandbox -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends sandbox [options]
 | 
			
		||||
 | 
			
		||||
Run a sandboxed tildefriends sandbox process (used internally).
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -h, --help    Show this usage information.
 | 
			
		||||
  -f, --fd      File descriptor with which to communicate with parent process.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends import -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends import [options] [paths...]
 | 
			
		||||
 | 
			
		||||
Import apps from file to the database.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -u, --user user          User into whose account apps will be imported (default: "import").
 | 
			
		||||
  -d, --db-path db_path    SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -h, --help               Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends export -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends export [options] [paths...]
 | 
			
		||||
 | 
			
		||||
Export apps from the database to file.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -u, --user user          User from whose account apps will be exported (default: "core").
 | 
			
		||||
  -d, --db-path db_path    SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -h, --help               Show this usage information.
 | 
			
		||||
 | 
			
		||||
paths                      Paths of apps to export (example: /~core/ssb /~user/app).
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends publish -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends publish [options]
 | 
			
		||||
 | 
			
		||||
Append a message to a feed.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -u, --user user          User owning identity with which to publish.
 | 
			
		||||
  -i, --id identity        Identity with which to publish message.
 | 
			
		||||
  -d, --db-path db_path    SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -c, --content json       JSON content of message to publish.
 | 
			
		||||
  -h, --help               Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends private -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends private [options]
 | 
			
		||||
 | 
			
		||||
Append a private post message to a feed.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -u, --user user              User owning identity with which to publish (optional).
 | 
			
		||||
  -i, --id identity            Identity with which to publish message.
 | 
			
		||||
  -r, --recipients recipients  Recipient identities.
 | 
			
		||||
  -d, --db-path db_path        SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -t, --text text              Private post text.
 | 
			
		||||
  -h, --help                   Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends create_invite -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends create_invite [options]
 | 
			
		||||
 | 
			
		||||
Create an invite.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -d, --db-path db_path    SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -i, --identity identity  Account from which to get latest sequence number.
 | 
			
		||||
  -a, --address address    Address to which the recipient will connect.
 | 
			
		||||
  -p, --port port          Port to which the recipient will connect.
 | 
			
		||||
  -u, --use_count count    Number of times this invite may be used (default: 1).
 | 
			
		||||
  -e, --expires seconds    How long this invite is valid in seconds (-1 for indefinitely, default: 1 hour).
 | 
			
		||||
  -h, --help               Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends get_sequence -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends get_sequence [options]
 | 
			
		||||
 | 
			
		||||
Get the last sequence number for a feed.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -d, --db-path db_path    SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -i, --identity identity  Account from which to get latest sequence number.
 | 
			
		||||
  -h, --help               Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends get_identity -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends get_identity [options]
 | 
			
		||||
 | 
			
		||||
Get the server account identity.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -d, --db-path db_path    SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -h, --help               Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends get_profile -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends get_profile [options]
 | 
			
		||||
 | 
			
		||||
Get profile information for the given identity.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -d, --db-path db_path    SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -i, --identity identity  Account for which to get profile information.
 | 
			
		||||
  -h, --help               Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends get_contacts -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends get_contacts [options]
 | 
			
		||||
 | 
			
		||||
Get information about followed, blocked, and friend identities.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -d, --db-path db_path    SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -i, --identity identity  Account from which to get contact information.
 | 
			
		||||
  -h, --help               Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends has_blob -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends has_blob [options]
 | 
			
		||||
 | 
			
		||||
Check whether a blob is in the blob store.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -d, --db-path db_path    SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -b, --blob_id blob_id    ID of blob to query.
 | 
			
		||||
  -h, --help               Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends get_blob -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends get_blob [options]
 | 
			
		||||
 | 
			
		||||
Read a file from the blob store.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -d, --db-path db_path    SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -b, --blob blob_id       Blob identifier to retrieve.
 | 
			
		||||
  -o, --output file_path   Location to write the retrieved blob.
 | 
			
		||||
  -h, --help               Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends store_blob -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends store_blob [options]
 | 
			
		||||
 | 
			
		||||
Write a file to the blob store.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -d, --db-path db_path    SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -f, --file file_path     Path to file to add to the blob store.
 | 
			
		||||
  -h, --help               Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends verify -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends verify [options]
 | 
			
		||||
 | 
			
		||||
Verify a feed.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -i, --identity identity  Identity to verify.
 | 
			
		||||
  -s, --sequence sequence  Sequence number to debug.
 | 
			
		||||
  -d, --db-path db_path    SQLite database path (default: /home/cory/.local/share/tildefriends/db.sqlite).
 | 
			
		||||
  -h, --help               Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## tildefriends test -h
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Usage: out/debug/tildefriends test [options]
 | 
			
		||||
 | 
			
		||||
Test SSB.
 | 
			
		||||
 | 
			
		||||
options:
 | 
			
		||||
  -t, --tests tests      Comma-separated list of tests to run.  (default: all)
 | 
			
		||||
  -h, --help             Show this usage information.
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										8
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -20,16 +20,16 @@
 | 
			
		||||
    },
 | 
			
		||||
    "nixpkgs": {
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1753749649,
 | 
			
		||||
        "narHash": "sha256-+jkEZxs7bfOKfBIk430K+tK9IvXlwzqQQnppC2ZKFj4=",
 | 
			
		||||
        "lastModified": 1739758141,
 | 
			
		||||
        "narHash": "sha256-uq6A2L7o1/tR6VfmYhZWoVAwb3gTy7j4Jx30MIrH0rE=",
 | 
			
		||||
        "owner": "NixOS",
 | 
			
		||||
        "repo": "nixpkgs",
 | 
			
		||||
        "rev": "1f08a4df998e21f4e8be8fb6fbf61d11a1a5076a",
 | 
			
		||||
        "rev": "c618e28f70257593de75a7044438efc1c1fc0791",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
        "owner": "NixOS",
 | 
			
		||||
        "ref": "nixos-25.05",
 | 
			
		||||
        "ref": "nixos-24.11",
 | 
			
		||||
        "repo": "nixpkgs",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						@@ -2,7 +2,7 @@
 | 
			
		||||
  description = "Tilde Friends is a platform for making, running, and sharing web applications.";
 | 
			
		||||
 | 
			
		||||
  inputs = {
 | 
			
		||||
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
 | 
			
		||||
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
 | 
			
		||||
    flake-utils.url = "github:numtide/flake-utils";
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -35,5 +35,27 @@
 | 
			
		||||
            graphviz
 | 
			
		||||
          ];
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        nixosModules.default = {
 | 
			
		||||
          config,
 | 
			
		||||
          lib,
 | 
			
		||||
          ...
 | 
			
		||||
        }: let
 | 
			
		||||
          # Shorter name to access final settings a
 | 
			
		||||
          # user of hello.nix module HAS ACTUALLY SET.
 | 
			
		||||
          # cfg is a typical convention.
 | 
			
		||||
          cfg = config.services.tildefriends;
 | 
			
		||||
        in {
 | 
			
		||||
          options.services.tildefriends = {
 | 
			
		||||
            enable = lib.mkEnableOption "Enable Tilde Friends";
 | 
			
		||||
          };
 | 
			
		||||
 | 
			
		||||
          config = lib.mkIf cfg.enable {
 | 
			
		||||
            systemd.services.tildefriends = {
 | 
			
		||||
              wantedBy = ["multi-user.target"];
 | 
			
		||||
              serviceConfig.ExecStart = "${pkgs.tildefriends}/bin/tildefriends";
 | 
			
		||||
            };
 | 
			
		||||
          };
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,3 @@
 | 
			
		||||
* Allow specifying all global settings from the command-line (CLI usage changed).
 | 
			
		||||
* Replication improvements.
 | 
			
		||||
* An iOS build is on TestFlight.
 | 
			
		||||
* macOS targets are debug and release like everywhere else.
 | 
			
		||||
* Running from a subdirectory is fine.
 | 
			
		||||
@@ -7,9 +5,10 @@
 | 
			
		||||
* Invite fixes.
 | 
			
		||||
* Follow/block UI fixes.
 | 
			
		||||
* Mobile automatically logs in.
 | 
			
		||||
* Updates:
 | 
			
		||||
* Allow specifying all global settings from the command-line.
 | 
			
		||||
* UpdateS:
 | 
			
		||||
  * CodeMirror
 | 
			
		||||
  * OpenSSL 3.4.1
 | 
			
		||||
  * libbacktrace
 | 
			
		||||
  * speedscope 1.22.2
 | 
			
		||||
  * sqlite 3.49.1
 | 
			
		||||
  * speedscope 1.22.2
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
* The connections tab now shows replication progress.
 | 
			
		||||
* Replication performance and thoroughness improvements.
 | 
			
		||||
* Bind only to localhost on mobile, configurable.
 | 
			
		||||
* Request blobs referenced by referenced blobs, and improve performance of that
 | 
			
		||||
  query.
 | 
			
		||||
* Fix file upload on iOS.
 | 
			
		||||
* Add rough back/forward/refresh buttons on iOS.
 | 
			
		||||
* Other crash fixes and performance improvements.
 | 
			
		||||
* Updates:
 | 
			
		||||
  * CodeMirror
 | 
			
		||||
  * w3.css
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
* Faster loads.
 | 
			
		||||
* Replication fixes.
 | 
			
		||||
* Shutdown fixes.
 | 
			
		||||
* Consolidated message actions into a % menu.
 | 
			
		||||
* Fixed and tested handling of user permissions.
 | 
			
		||||
* Add a very work in progress "web" app.
 | 
			
		||||
* Updates:
 | 
			
		||||
  * CodeMirror
 | 
			
		||||
  * Lit 3.3.0
 | 
			
		||||
  * OpenSSL 3.5.0
 | 
			
		||||
  * c-ares 1.34.5
 | 
			
		||||
  * libbacktrace
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
* Faster loads.
 | 
			
		||||
* Minor UI tweaks.
 | 
			
		||||
* Added an intro app as part of the initial flow for first-time users.
 | 
			
		||||
* Fixed more shutdown issues.
 | 
			
		||||
* Fixed a longstanding potential database issue.
 | 
			
		||||
* Added a blob export command.
 | 
			
		||||
* Refresh blob wants for blobs that are requested over the web.
 | 
			
		||||
* Updates:
 | 
			
		||||
  * CodeMirror
 | 
			
		||||
  * QuickJS 2025-04-26
 | 
			
		||||
  * libuv 1.51.0
 | 
			
		||||
  * sqlite 3.49.2
 | 
			
		||||
  * w3.css 5.02
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
* Improve load times.
 | 
			
		||||
* Fix the messages_refs table, and make it usable for hashtags.
 | 
			
		||||
* Fix a circumstance where we would fail to call promise callbacks.
 | 
			
		||||
* Fix the private messages tab.
 | 
			
		||||
* Fix the room app.
 | 
			
		||||
* Expose followed accounts in a user's profile.
 | 
			
		||||
* Show connection status in the sidebar.
 | 
			
		||||
* Simplify placeholder messages.
 | 
			
		||||
* Only show "Mark as Read" when relevant.
 | 
			
		||||
* Treat profile images more like post images.
 | 
			
		||||
* Limit the WAL file size.
 | 
			
		||||
* Updates:
 | 
			
		||||
  * CodeMirror
 | 
			
		||||
  * sqlite 3.50.1
 | 
			
		||||
@@ -1,2 +0,0 @@
 | 
			
		||||
* Updating Android SDK+target versions.
 | 
			
		||||
* Minor UI improvements.
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
* Added an option to stay connected to a handful of peers.
 | 
			
		||||
* Load more messages at a time.
 | 
			
		||||
* Fix a set of Android not responding errors.
 | 
			
		||||
* Target Android 15 (API level 35) to meet new requirements.
 | 
			
		||||
* Support JS-less webapps.
 | 
			
		||||
* Fix unnecessary tunnel disconnects.
 | 
			
		||||
* Many small user interface tweaks.
 | 
			
		||||
* Update:
 | 
			
		||||
  * CodeMirror
 | 
			
		||||
  * OpenSSL 3.5.1
 | 
			
		||||
  * lit 3.3.1
 | 
			
		||||
  * picohttpparser
 | 
			
		||||
  * speedscope 1.23.0
 | 
			
		||||
  * sqlite 3.50.4
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
* Private messages interface overhaul in progress.
 | 
			
		||||
* Added a loading indicator.
 | 
			
		||||
* Documented the core JavaScript.
 | 
			
		||||
* Fixed @-completion.
 | 
			
		||||
* Covered up launch on Android with the splash screen.
 | 
			
		||||
* Update:
 | 
			
		||||
  * CodeMirror
 | 
			
		||||
  * OpenSSL 3.5.2
 | 
			
		||||
  * speedscope 1.23.1
 | 
			
		||||
| 
		 After Width: | Height: | Size: 275 KiB  | 
| 
		 Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 48 KiB  | 
| 
		 Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 92 KiB  |