Compare commits

..

2 Commits

Author SHA1 Message Date
d67e47ae4b nix(module): module boilerplate 2025-02-20 10:27:18 +01:00
b43b8da9ab nix (test): fix build 2025-02-20 10:17:14 +01:00
298 changed files with 14307 additions and 26491 deletions

View File

@@ -3,36 +3,6 @@ run-name: ${{ gitea.actor }} running 🚀
on: [push] on: [push]
jobs: jobs:
Build-Docs:
runs-on: ubuntu-latest
container:
image: node:trixie-slim
valid_volumes:
- '/opt/keys'
volumes:
- /opt/keys:/opt/keys
steps:
- name: Install build dependencies
run: >
apt update && apt install -y \
build-essential \
doxygen \
file \
git \
graphviz \
rsync \
unzip \
zip
- name: Get code
uses: actions/checkout@v4
with:
submodules: true
- name: Build documentation
run: |
mkdir -p out/html/ ~/.ssh/
make -j`nproc` docs
echo 'pildefriends ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3Kde5vDO0TrMBDK0IGGeNGe/XinWAZkSQ/rXxwUjt' >> ~/.ssh/known_hosts
rsync -avP --delete -e "ssh -i /opt/keys/ssh.ed25519" out/html/ tfdocs@pildefriends:docs/html/
Build-All: Build-All:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
@@ -78,8 +48,9 @@ jobs:
- name: Build documentation - name: Build documentation
run: | run: |
mkdir -p out/html/ ~/.ssh/ mkdir -p out/html/ ~/.ssh/
make -j`nproc` docs make docs
echo 'pildefriends ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3Kde5vDO0TrMBDK0IGGeNGe/XinWAZkSQ/rXxwUjt' >> ~/.ssh/known_hosts 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 - name: Setup JDK
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
@@ -88,13 +59,11 @@ jobs:
- name: Setup Android SDK - name: Setup Android SDK
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3
with: 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: Build
run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist
- name: Test Debug
run: TF_TEST_auto=0 out/debug/tildefriends test
- name: Docker build - name: Docker build
run: DOCKER_BUILDKIT=1 docker build . run: DOCKER_BUILDKIT=1 docker build .
- name: Build
run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist docs
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:

4
.gitmodules vendored
View File

@@ -19,6 +19,10 @@
[submodule "deps/picohttpparser"] [submodule "deps/picohttpparser"]
path = deps/picohttpparser path = deps/picohttpparser
url = https://github.com/h2o/picohttpparser.git url = https://github.com/h2o/picohttpparser.git
[submodule "deps/openssl_src"]
path = deps/openssl_src
url = https://github.com/openssl/openssl.git
shallow = true
[submodule "deps/c-ares"] [submodule "deps/c-ares"]
path = deps/c-ares path = deps/c-ares
url = https://github.com/c-ares/c-ares.git url = https://github.com/c-ares/c-ares.git

View File

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

View File

@@ -4,6 +4,7 @@ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
gcc \ gcc \
libc6-dev \ libc6-dev \
perl \
make make
COPY . /app COPY . /app

369
Doxyfile
View File

@@ -1,4 +1,4 @@
# Doxyfile 1.9.4 # Doxyfile 1.9.8
# This file describes the settings to be used by the documentation system # This file describes the settings to be used by the documentation system
# doxygen (www.doxygen.org) for a project. # doxygen (www.doxygen.org) for a project.
@@ -19,7 +19,8 @@
# configuration file: # configuration file:
# doxygen -x [configFile] # doxygen -x [configFile]
# Use doxygen to compare the used configuration file with the template # Use doxygen to compare the used configuration file with the template
# configuration file without replacing the environment variables: # configuration file without replacing the environment variables or CMake type
# replacement variables:
# doxygen -x_noenv [configFile] # doxygen -x_noenv [configFile]
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@@ -85,7 +86,7 @@ CREATE_SUBDIRS = NO
# level increment doubles the number of directories, resulting in 4096 # level increment doubles the number of directories, resulting in 4096
# directories at level 8 which is the default and also the maximum value. The # directories at level 8 which is the default and also the maximum value. The
# sub-directories are organized in 2 levels, the first level always has a fixed # sub-directories are organized in 2 levels, the first level always has a fixed
# numer of 16 directories. # number of 16 directories.
# Minimum value: 0, maximum value: 8, default value: 8. # Minimum value: 0, maximum value: 8, default value: 8.
# This tag requires that the tag CREATE_SUBDIRS is set to YES. # This tag requires that the tag CREATE_SUBDIRS is set to YES.
@@ -341,7 +342,7 @@ OPTIMIZE_OUTPUT_SLICE = NO
# #
# Note see also the list of default file extension mappings. # 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 # If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
# according to the Markdown format, which allows for more readable # according to the Markdown format, which allows for more readable
@@ -362,6 +363,17 @@ MARKDOWN_SUPPORT = YES
TOC_INCLUDE_HEADINGS = 5 TOC_INCLUDE_HEADINGS = 5
# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to
# generate identifiers for the Markdown headings. Note: Every identifier is
# unique.
# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a
# sequence number starting at 0 and GITHUB use the lower case version of title
# with any whitespace replaced by '-' and punctuation characters removed.
# The default value is: DOXYGEN.
# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
MARKDOWN_ID_STYLE = DOXYGEN
# When enabled doxygen tries to link words that correspond to documented # When enabled doxygen tries to link words that correspond to documented
# classes, or namespaces to their corresponding documentation. Such a link can # classes, or namespaces to their corresponding documentation. Such a link can
# be prevented in individual cases by putting a % sign in front of the word or # be prevented in individual cases by putting a % sign in front of the word or
@@ -486,6 +498,14 @@ LOOKUP_CACHE_SIZE = 0
NUM_PROC_THREADS = 1 NUM_PROC_THREADS = 1
# If the TIMESTAMP tag is set different from NO then each generated page will
# contain the date or date and time when the page was generated. Setting this to
# NO can help when comparing the output of multiple runs.
# Possible values are: YES, NO, DATETIME and DATE.
# The default value is: NO.
TIMESTAMP = NO
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Build related configuration options # Build related configuration options
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@@ -567,7 +587,8 @@ HIDE_UNDOC_MEMBERS = NO
# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all # If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
# undocumented classes that are normally visible in the class hierarchy. If set # undocumented classes that are normally visible in the class hierarchy. If set
# to NO, these classes will be included in the various overviews. This option # to NO, these classes will be included in the various overviews. This option
# has no effect if EXTRACT_ALL is enabled. # will also hide undocumented C++ concepts if enabled. This option has no effect
# if EXTRACT_ALL is enabled.
# The default value is: NO. # The default value is: NO.
HIDE_UNDOC_CLASSES = NO HIDE_UNDOC_CLASSES = NO
@@ -605,7 +626,8 @@ INTERNAL_DOCS = NO
# Windows (including Cygwin) and MacOS, users should typically set this option # Windows (including Cygwin) and MacOS, users should typically set this option
# to NO, whereas on Linux or other Unix flavors it should typically be set to # to NO, whereas on Linux or other Unix flavors it should typically be set to
# YES. # YES.
# The default value is: system dependent. # Possible values are: SYSTEM, NO and YES.
# The default value is: SYSTEM.
CASE_SENSE_NAMES = YES CASE_SENSE_NAMES = YES
@@ -857,11 +879,26 @@ WARN_IF_INCOMPLETE_DOC = YES
WARN_NO_PARAMDOC = NO WARN_NO_PARAMDOC = NO
# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about
# undocumented enumeration values. If set to NO, doxygen will accept
# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag
# will automatically be disabled.
# The default value is: NO.
WARN_IF_UNDOC_ENUM_VAL = NO
# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when # If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS # a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS
# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but # then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but
# at the end of the doxygen process doxygen will return with a non-zero status. # at the end of the doxygen process doxygen will return with a non-zero status.
# Possible values are: NO, YES and FAIL_ON_WARNINGS. # If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves
# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not
# write the warning messages in between other messages but write them at the end
# of a run, in case a WARN_LOGFILE is defined the warning messages will be
# besides being in the defined file also be shown at the end of a run, unless
# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case
# the behavior will remain as with the setting FAIL_ON_WARNINGS.
# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT.
# The default value is: NO. # The default value is: NO.
WARN_AS_ERROR = NO WARN_AS_ERROR = NO
@@ -906,22 +943,28 @@ WARN_LOGFILE =
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
# Note: If this tag is empty the current directory is searched. # Note: If this tag is empty the current directory is searched.
INPUT = README.md \ INPUT = README.md docs/ src/
core/client.js \
core/core.js \
core/tfrpc.js \
docs/ \
src/
# This tag can be used to specify the character encoding of the source files # 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 # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
# libiconv (or the iconv built into libc) for the transcoding. See the libiconv # libiconv (or the iconv built into libc) for the transcoding. See the libiconv
# documentation (see: # documentation (see:
# https://www.gnu.org/software/libiconv/) for the list of possible encodings. # https://www.gnu.org/software/libiconv/) for the list of possible encodings.
# See also: INPUT_FILE_ENCODING
# The default value is: UTF-8. # The default value is: UTF-8.
INPUT_ENCODING = UTF-8 INPUT_ENCODING = UTF-8
# This tag can be used to specify the character encoding of the source files
# that doxygen parses The INPUT_FILE_ENCODING tag can be used to specify
# character encoding on a per file pattern basis. Doxygen will compare the file
# name with each pattern and apply the encoding instead of the default
# INPUT_ENCODING) if there is a match. The character encodings are a list of the
# form: pattern=encoding (like *.php=ISO-8859-1). See cfg_input_encoding
# "INPUT_ENCODING" for further information on supported encodings.
INPUT_FILE_ENCODING =
# If the value of the INPUT tag contains directories, you can use the # If the value of the INPUT tag contains directories, you can use the
# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and # FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
# *.h) to filter out the source-files in the directories. # *.h) to filter out the source-files in the directories.
@@ -933,15 +976,14 @@ INPUT_ENCODING = UTF-8
# Note the list of default checked file patterns might differ from the list of # Note the list of default checked file patterns might differ from the list of
# default file extension mappings. # default file extension mappings.
# #
# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, # If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm,
# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, # *.cpp, *.cppm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl,
# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, # *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, *.php,
# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C # *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be
# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, # provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08,
# *.vhdl, *.ucf, *.qsf and *.ice. # *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice.
FILE_PATTERNS = *.h \ FILE_PATTERNS = *.h \
*.js \
*.md *.md
# The RECURSIVE tag can be used to specify whether or not subdirectories should # The RECURSIVE tag can be used to specify whether or not subdirectories should
@@ -980,9 +1022,6 @@ EXCLUDE_PATTERNS =
# output. The symbol name can be a fully qualified name, a word, or if the # output. The symbol name can be a fully qualified name, a word, or if the
# wildcard * is used, a substring. Examples: ANamespace, AClass, # wildcard * is used, a substring. Examples: ANamespace, AClass,
# ANamespace::AClass, ANamespace::*Test # ANamespace::AClass, ANamespace::*Test
#
# Note that the wildcards are matched against the file with absolute path, so to
# exclude all test directories use the pattern */test/*
EXCLUDE_SYMBOLS = EXCLUDE_SYMBOLS =
@@ -1010,7 +1049,7 @@ EXAMPLE_RECURSIVE = NO
# that contain images that are to be included in the documentation (see the # that contain images that are to be included in the documentation (see the
# \image command). # \image command).
IMAGE_PATH = docs/images/ IMAGE_PATH =
# The INPUT_FILTER tag can be used to specify a program that doxygen should # 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 # invoke to filter for each input file. Doxygen will invoke the filter program
@@ -1027,6 +1066,11 @@ IMAGE_PATH = docs/images/
# code is scanned, but not when the output code is generated. If lines are added # code is scanned, but not when the output code is generated. If lines are added
# or removed, the anchors will not be placed correctly. # or removed, the anchors will not be placed correctly.
# #
# Note that doxygen will use the data processed and written to standard output
# for further processing, therefore nothing else, like debug statements or used
# commands (so in case of a Windows batch file always use @echo OFF), should be
# written to standard output.
#
# Note that for custom extensions or not directly supported extensions you also # Note that for custom extensions or not directly supported extensions you also
# need to set EXTENSION_MAPPING for the extension otherwise the files are not # need to set EXTENSION_MAPPING for the extension otherwise the files are not
# properly processed by doxygen. # properly processed by doxygen.
@@ -1068,6 +1112,15 @@ FILTER_SOURCE_PATTERNS =
USE_MDFILE_AS_MAINPAGE = README.md USE_MDFILE_AS_MAINPAGE = README.md
# The Fortran standard specifies that for fixed formatted Fortran code all
# characters from position 72 are to be considered as comment. A common
# extension is to allow longer lines before the automatic comment starts. The
# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can
# be processed before the automatic comment starts.
# Minimum value: 7, maximum value: 10000, default value: 72.
FORTRAN_COMMENT_AFTER = 72
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Configuration options related to source browsing # Configuration options related to source browsing
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@@ -1205,10 +1258,11 @@ CLANG_DATABASE_PATH =
ALPHABETICAL_INDEX = YES ALPHABETICAL_INDEX = YES
# In case all classes in a project start with a common prefix, all classes will # The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes)
# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag # that should be ignored while generating the index headers. The IGNORE_PREFIX
# can be used to specify a prefix (or a list of prefixes) that should be ignored # tag works for classes, function and member names. The entity will be placed in
# while generating the index headers. # the alphabetical list under the first letter of the entity name that remains
# after removing the prefix.
# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. # This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
IGNORE_PREFIX = IGNORE_PREFIX =
@@ -1287,7 +1341,12 @@ HTML_STYLESHEET =
# Doxygen will copy the style sheet files to the output directory. # Doxygen will copy the style sheet files to the output directory.
# Note: The order of the extra style sheet files is of importance (e.g. the last # Note: The order of the extra style sheet files is of importance (e.g. the last
# style sheet in the list overrules the setting of the previous ones in the # style sheet in the list overrules the setting of the previous ones in the
# list). For an example see the documentation. # list).
# Note: Since the styling of scrollbars can currently not be overruled in
# Webkit/Chromium, the styling will be left out of the default doxygen.css if
# one or more extra stylesheets have been specified. So if scrollbar
# customization is desired it has to be added explicitly. For an example see the
# documentation.
# This tag requires that the tag GENERATE_HTML is set to YES. # This tag requires that the tag GENERATE_HTML is set to YES.
HTML_EXTRA_STYLESHEET = HTML_EXTRA_STYLESHEET =
@@ -1302,6 +1361,19 @@ HTML_EXTRA_STYLESHEET =
HTML_EXTRA_FILES = HTML_EXTRA_FILES =
# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output
# should be rendered with a dark or light theme.
# Possible values are: LIGHT always generate light mode output, DARK always
# generate dark mode output, AUTO_LIGHT automatically set the mode according to
# the user preference, use light mode if no preference is set (the default),
# AUTO_DARK automatically set the mode according to the user preference, use
# dark mode if no preference is set and TOGGLE allow to user to switch between
# light and dark mode via a button.
# The default value is: AUTO_LIGHT.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_COLORSTYLE = AUTO_LIGHT
# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
# will adjust the colors in the style sheet and background images according to # will adjust the colors in the style sheet and background images according to
# this color. Hue is specified as an angle on a color-wheel, see # this color. Hue is specified as an angle on a color-wheel, see
@@ -1332,15 +1404,6 @@ HTML_COLORSTYLE_SAT = 100
HTML_COLORSTYLE_GAMMA = 80 HTML_COLORSTYLE_GAMMA = 80
# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
# page will contain the date and time when the page was generated. Setting this
# to YES can help to show when doxygen was last run and thus if the
# documentation is up to date.
# The default value is: NO.
# This tag requires that the tag GENERATE_HTML is set to YES.
#HTML_TIMESTAMP = NO
# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
# documentation will contain a main index with vertical navigation menus that # documentation will contain a main index with vertical navigation menus that
# are dynamically created via JavaScript. If disabled, the navigation index will # are dynamically created via JavaScript. If disabled, the navigation index will
@@ -1360,6 +1423,13 @@ HTML_DYNAMIC_MENUS = YES
HTML_DYNAMIC_SECTIONS = NO HTML_DYNAMIC_SECTIONS = NO
# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be
# dynamically folded and expanded in the generated HTML source code.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.
HTML_CODE_FOLDING = YES
# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
# shown in the various tree structured indices initially; the user can expand # shown in the various tree structured indices initially; the user can expand
# and collapse entries dynamically later on. Doxygen will expand the tree to # and collapse entries dynamically later on. Doxygen will expand the tree to
@@ -1490,6 +1560,16 @@ BINARY_TOC = NO
TOC_EXPAND = NO TOC_EXPAND = NO
# The SITEMAP_URL tag is used to specify the full URL of the place where the
# generated documentation will be placed on the server by the user during the
# deployment of the documentation. The generated sitemap is called sitemap.xml
# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL
# is specified no sitemap is generated. For information about the sitemap
# protocol see https://www.sitemaps.org
# This tag requires that the tag GENERATE_HTML is set to YES.
SITEMAP_URL =
# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
@@ -1665,17 +1745,6 @@ HTML_FORMULA_FORMAT = png
FORMULA_FONTSIZE = 10 FORMULA_FONTSIZE = 10
# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
# generated for formulas are transparent PNGs. Transparent PNGs are not
# supported properly for IE 6.0, but are supported on all modern browsers.
#
# Note that when changing this option you need to delete any form_*.png files in
# the HTML output directory before the changes have effect.
# The default value is: YES.
# This tag requires that the tag GENERATE_HTML is set to YES.
#FORMULA_TRANSPARENT = YES
# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands # The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
# to create new LaTeX commands to be used in formulas as building blocks. See # to create new LaTeX commands to be used in formulas as building blocks. See
# the section "Including formulas" for details. # the section "Including formulas" for details.
@@ -1737,8 +1806,8 @@ MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@2
# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax # The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
# extension names that should be enabled during MathJax rendering. For example # extension names that should be enabled during MathJax rendering. For example
# for MathJax version 2 (see https://docs.mathjax.org/en/v2.7-latest/tex.html # for MathJax version 2 (see
# #tex-and-latex-extensions): # https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions):
# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols # MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
# For example for MathJax version 3 (see # For example for MathJax version 3 (see
# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): # http://docs.mathjax.org/en/latest/input/tex/extensions/index.html):
@@ -1989,9 +2058,16 @@ PDF_HYPERLINKS = YES
USE_PDFLATEX = YES USE_PDFLATEX = YES
# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode # The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error.
# command to the generated LaTeX files. This will instruct LaTeX to keep running # Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch
# if errors occur, instead of asking the user for help. # mode nothing is printed on the terminal, errors are scrolled as if <return> is
# hit at every error; missing files that TeX tries to input or request from
# keyboard input (\read on a not open input stream) cause the job to abort,
# NON_STOP In nonstop mode the diagnostic message will appear on the terminal,
# but there is no possibility of user interaction just like in batch mode,
# SCROLL In scroll mode, TeX will stop only for missing files to input or if
# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at
# each error, asking for user intervention.
# The default value is: NO. # The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES. # This tag requires that the tag GENERATE_LATEX is set to YES.
@@ -2012,14 +2088,6 @@ LATEX_HIDE_INDICES = NO
LATEX_BIB_STYLE = plain LATEX_BIB_STYLE = plain
# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
# page will contain the date and time when the page was generated. Setting this
# to NO can help when comparing the output of multiple runs.
# The default value is: NO.
# This tag requires that the tag GENERATE_LATEX is set to YES.
#LATEX_TIMESTAMP = NO
# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) # The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
# path from which the emoji images will be read. If a relative path is entered, # path from which the emoji images will be read. If a relative path is entered,
# it will be relative to the LATEX_OUTPUT directory. If left blank the # it will be relative to the LATEX_OUTPUT directory. If left blank the
@@ -2185,13 +2253,39 @@ DOCBOOK_OUTPUT = docbook
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an # If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures # AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures
# the structure of the code including all documentation. Note that this feature # the structure of the code including all documentation. Note that this feature
# is still experimental and incomplete at the moment. # is still experimental and incomplete at the moment.
# The default value is: NO. # The default value is: NO.
GENERATE_AUTOGEN_DEF = NO GENERATE_AUTOGEN_DEF = NO
#---------------------------------------------------------------------------
# Configuration options related to Sqlite3 output
#---------------------------------------------------------------------------
# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3
# database with symbols found by doxygen stored in tables.
# The default value is: 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
# in front of it.
# The default directory is: sqlite3.
# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
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
# will warn if an a database file is already found and not modify it.
# The default value is: YES.
# This tag requires that the tag GENERATE_SQLITE3 is set to YES.
SQLITE3_RECREATE_DB = YES
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Configuration options related to the Perl module output # Configuration options related to the Perl module output
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
@@ -2334,15 +2428,15 @@ TAGFILES =
GENERATE_TAGFILE = GENERATE_TAGFILE =
# If the ALLEXTERNALS tag is set to YES, all external class will be listed in # If the ALLEXTERNALS tag is set to YES, all external classes and namespaces
# the class index. If set to NO, only the inherited external classes will be # will be listed in the class and namespace index. If set to NO, only the
# listed. # inherited external classes will be listed.
# The default value is: NO. # The default value is: NO.
ALLEXTERNALS = NO ALLEXTERNALS = NO
# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed # If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
# in the modules index. If set to NO, only the current project's groups will be # in the topic index. If set to NO, only the current project's groups will be
# listed. # listed.
# The default value is: YES. # The default value is: YES.
@@ -2356,16 +2450,9 @@ EXTERNAL_GROUPS = YES
EXTERNAL_PAGES = YES EXTERNAL_PAGES = YES
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Configuration options related to the dot tool # Configuration options related to diagram generator tools
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# You can include diagrams made with dia in doxygen documentation. Doxygen will
# then run dia to produce the diagram and insert it in the documentation. The
# DIA_PATH tag allows you to specify the directory where the dia binary resides.
# If left empty dia is assumed to be found in the default search path.
DIA_PATH =
# If set to YES the inheritance and collaboration graphs will hide inheritance # If set to YES the inheritance and collaboration graphs will hide inheritance
# and usage relations if the target is undocumented or is not a class. # and usage relations if the target is undocumented or is not a class.
# The default value is: YES. # The default value is: YES.
@@ -2374,10 +2461,10 @@ HIDE_UNDOC_RELATIONS = YES
# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
# available from the path. This tool is part of Graphviz (see: # available from the path. This tool is part of Graphviz (see:
# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent # https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
# Bell Labs. The other options in this section have no effect if this option is # Bell Labs. The other options in this section have no effect if this option is
# set to NO # set to NO
# The default value is: NO. # The default value is: YES.
HAVE_DOT = YES HAVE_DOT = YES
@@ -2391,37 +2478,51 @@ HAVE_DOT = YES
DOT_NUM_THREADS = 0 DOT_NUM_THREADS = 0
# When you want a differently looking font in the dot files that doxygen # DOT_COMMON_ATTR is common attributes for nodes, edges and labels of
# generates you can specify the font name using DOT_FONTNAME. You need to make # subgraphs. When you want a differently looking font in the dot files that
# sure dot is able to find the font, which can be done by putting it in a # doxygen generates you can specify fontname, fontcolor and fontsize attributes.
# standard location or by setting the DOTFONTPATH environment variable or by # For details please see <a href=https://graphviz.org/doc/info/attrs.html>Node,
# setting DOT_FONTPATH to the directory containing the font. # Edge and Graph Attributes specification</a> You need to make sure dot is able
# The default value is: Helvetica. # to find the font, which can be done by putting it in a standard location or by
# setting the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the
# directory containing the font. Default graphviz fontsize is 14.
# The default value is: fontname=Helvetica,fontsize=10.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
#DOT_FONTNAME = Helvetica DOT_COMMON_ATTR = "fontname=Helvetica,fontsize=10"
# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of # DOT_EDGE_ATTR is concatenated with DOT_COMMON_ATTR. For elegant style you can
# dot graphs. # add 'arrowhead=open, arrowtail=open, arrowsize=0.5'. <a
# Minimum value: 4, maximum value: 24, default value: 10. # href=https://graphviz.org/doc/info/arrows.html>Complete documentation about
# arrows shapes.</a>
# The default value is: labelfontname=Helvetica,labelfontsize=10.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
#DOT_FONTSIZE = 10 DOT_EDGE_ATTR = "labelfontname=Helvetica,labelfontsize=10"
# By default doxygen will tell dot to use the default font as specified with # DOT_NODE_ATTR is concatenated with DOT_COMMON_ATTR. For view without boxes
# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set # around nodes set 'shape=plain' or 'shape=plaintext' <a
# the path where dot can find it using this tag. # href=https://www.graphviz.org/doc/info/shapes.html>Shapes specification</a>
# The default value is: shape=box,height=0.2,width=0.4.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
#DOT_FONTPATH = DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4"
# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a # You can set the path where dot can find font specified with fontname in
# graph for each documented class showing the direct and indirect inheritance # DOT_COMMON_ATTR and others dot attributes.
# relations. In case HAVE_DOT is set as well dot will be used to draw the graph, # This tag requires that the tag HAVE_DOT is set to YES.
# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set
# to TEXT the direct and indirect inheritance relations will be shown as texts / DOT_FONTPATH =
# links.
# Possible values are: NO, YES, TEXT and GRAPH. # If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will
# generate a graph for each documented class showing the direct and indirect
# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and
# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case
# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the
# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used.
# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance
# relations will be shown as texts / links.
# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN.
# The default value is: YES. # The default value is: YES.
CLASS_GRAPH = YES CLASS_GRAPH = YES
@@ -2429,15 +2530,21 @@ CLASS_GRAPH = YES
# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a # If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
# graph for each documented class showing the direct and indirect implementation # graph for each documented class showing the direct and indirect implementation
# dependencies (inheritance, containment, and class references variables) of the # dependencies (inheritance, containment, and class references variables) of the
# class with other documented classes. # class with other documented classes. Explicit enabling a collaboration graph,
# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the
# command \collaborationgraph. Disabling a collaboration graph can be
# accomplished by means of the command \hidecollaborationgraph.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
COLLABORATION_GRAPH = YES COLLABORATION_GRAPH = YES
# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for # If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
# groups, showing the direct groups dependencies. See also the chapter Grouping # groups, showing the direct groups dependencies. Explicit enabling a group
# in the manual. # dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means
# of the command \groupgraph. Disabling a directory graph can be accomplished by
# means of the command \hidegroupgraph. See also the chapter Grouping in the
# manual.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@@ -2497,7 +2604,9 @@ TEMPLATE_RELATIONS = NO
# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
# YES then doxygen will generate a graph for each documented file showing the # YES then doxygen will generate a graph for each documented file showing the
# direct and indirect include dependencies of the file with other documented # direct and indirect include dependencies of the file with other documented
# files. # files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO,
# can be accomplished by means of the command \includegraph. Disabling an
# include graph can be accomplished by means of the command \hideincludegraph.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@@ -2506,7 +2615,10 @@ INCLUDE_GRAPH = YES
# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
# set to YES then doxygen will generate a graph for each documented file showing # set to YES then doxygen will generate a graph for each documented file showing
# the direct and indirect include dependencies of the file with other documented # the direct and indirect include dependencies of the file with other documented
# files. # files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set
# to NO, can be accomplished by means of the command \includedbygraph. Disabling
# an included by graph can be accomplished by means of the command
# \hideincludedbygraph.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@@ -2546,7 +2658,10 @@ GRAPHICAL_HIERARCHY = YES
# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the # If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
# dependencies a directory has on other directories in a graphical way. The # dependencies a directory has on other directories in a graphical way. The
# dependency relations are determined by the #include relations between the # dependency relations are determined by the #include relations between the
# files in the directories. # files in the directories. Explicit enabling a directory graph, when
# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command
# \directorygraph. Disabling a directory graph can be accomplished by means of
# the command \hidedirectorygraph.
# The default value is: YES. # The default value is: YES.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@@ -2562,12 +2677,13 @@ DIR_GRAPH_MAX_DEPTH = 1
# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
# generated by dot. For an explanation of the image formats see the section # generated by dot. For an explanation of the image formats see the section
# output formats in the documentation of the dot tool (Graphviz (see: # output formats in the documentation of the dot tool (Graphviz (see:
# http://www.graphviz.org/)). # https://www.graphviz.org/)).
# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order # Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
# to make the SVG files visible in IE 9+ (other browsers do not have this # to make the SVG files visible in IE 9+ (other browsers do not have this
# requirement). # requirement).
# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo, # Possible values are: png, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd,
# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and # gif, gif:cairo, gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd,
# png:cairo, png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
# png:gdiplus:gdiplus. # png:gdiplus:gdiplus.
# The default value is: png. # The default value is: png.
# This tag requires that the tag HAVE_DOT is set to YES. # This tag requires that the tag HAVE_DOT is set to YES.
@@ -2599,11 +2715,12 @@ DOT_PATH =
DOTFILE_DIRS = DOTFILE_DIRS =
# The MSCFILE_DIRS tag can be used to specify one or more directories that # You can include diagrams made with dia in doxygen documentation. Doxygen will
# contain msc files that are included in the documentation (see the \mscfile # then run dia to produce the diagram and insert it in the documentation. The
# command). # DIA_PATH tag allows you to specify the directory where the dia binary resides.
# If left empty dia is assumed to be found in the default search path.
MSCFILE_DIRS = DIA_PATH =
# The DIAFILE_DIRS tag can be used to specify one or more directories that # The DIAFILE_DIRS tag can be used to specify one or more directories that
# contain dia files that are included in the documentation (see the \diafile # contain dia files that are included in the documentation (see the \diafile
@@ -2653,18 +2770,6 @@ DOT_GRAPH_MAX_NODES = 50
MAX_DOT_GRAPH_DEPTH = 0 MAX_DOT_GRAPH_DEPTH = 0
# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
# background. This is disabled by default, because dot on Windows does not seem
# to support this out of the box.
#
# Warning: Depending on the platform used, enabling this option may lead to
# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
# read).
# The default value is: NO.
# This tag requires that the tag HAVE_DOT is set to YES.
#DOT_TRANSPARENT = NO
# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output # Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
# files in one run (i.e. multiple -o and -T options on the command line). This # files in one run (i.e. multiple -o and -T options on the command line). This
# makes dot run faster, but since only newer versions of dot (>1.8.10) support # makes dot run faster, but since only newer versions of dot (>1.8.10) support
@@ -2692,3 +2797,19 @@ GENERATE_LEGEND = YES
# The default value is: YES. # The default value is: YES.
DOT_CLEANUP = YES DOT_CLEANUP = YES
# You can define message sequence charts within doxygen comments using the \msc
# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will
# use a built-in version of mscgen tool to produce the charts. Alternatively,
# the MSCGEN_TOOL tag can also specify the name an external tool. For instance,
# specifying prog as the value, doxygen will call the tool as prog -T
# <outfile_format> -o <outputfile> <inputfile>. The external tool should support
# output file formats "png", "eps", "svg", and "ismap".
MSCGEN_TOOL =
# The MSCFILE_DIRS tag can be used to specify one or more directories that
# contain msc files that are included in the documentation (see the \mscfile
# command).
MSCFILE_DIRS =

View File

@@ -16,16 +16,17 @@ MAKEFLAGS += --no-builtin-rules
## LD := Linker. ## LD := Linker.
## ANDROID_SDK := Path to the Android SDK. ## ANDROID_SDK := Path to the Android SDK.
VERSION_CODE := 49 VERSION_CODE := 33
VERSION_NUMBER := 0.2025.12 VERSION_CODE_IOS := 8
VERSION_NUMBER := 0.0.28-wip
VERSION_NAME := This program kills fascists. VERSION_NAME := This program kills fascists.
IPHONEOS_VERSION_MIN=14.5 IPHONEOS_VERSION_MIN=14.0
SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3510100.zip SQLITE_URL := https://www.sqlite.org/2025/sqlite-amalgamation-3490100.zip
BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.18.3/bundletool-all-1.18.3.jar BUNDLETOOL_URL := https://github.com/google/bundletool/releases/download/1.17.0/bundletool-all-1.17.0.jar
APPIMAGETOOL_URL := https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage APPIMAGETOOL_URL := https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
APPIMAGETOOL_MD5 := 43264887ffe43cdc02171b3463912168 out/appimagetool APPIMAGETOOL_MD5 := e989fadfc4d685fd3d6aeeb9b525d74d out/appimagetool
PROJECT = tildefriends PROJECT = tildefriends
BUILD_DIR ?= out BUILD_DIR ?= out
@@ -37,16 +38,13 @@ BUNDLETOOL = out/bundletool.jar
HAVE_WIN := HAVE_WIN :=
HAVE_CROSS_AARCH64 := HAVE_CROSS_AARCH64 :=
USE_SYSTEM_SSL :=
export SOURCE_DATE_EPOCH=1 export SOURCE_DATE_EPOCH=1
export TZ=UTC export TZ=UTC
ifeq ($(UNAME_S),Darwin) ifeq ($(UNAME_S),Darwin)
BUILD_TYPES := debug release iosdebug iosrelease iossimdebug iossimrelease 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) else ifeq ($(UNAME_S),Linux)
BUILD_TYPES := debug release BUILD_TYPES := debug release
HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1) HAVE_ANDROID = $(if $(shell which $(ANDROID_SDK)/platform-tools/adb),1)
@@ -63,10 +61,7 @@ LDFLAGS += \
-lbsd \ -lbsd \
-lnetwork \ -lnetwork \
-Wno-stringop-overflow -Wno-stringop-overflow
HAVE_ANDROID = 0 USE_SYSTEM_SSL := 1
HAVE_LINUX_IOS = 0
HAVE_LINUX_MACOS = 0
HAVE_WIN = 0
else ifeq ($(UNAME_S),OpenBSD) else ifeq ($(UNAME_S),OpenBSD)
BUILD_TYPES := debug release BUILD_TYPES := debug release
CFLAGS += \ CFLAGS += \
@@ -77,12 +72,13 @@ LDFLAGS += \
HAVE_ANDROID := HAVE_ANDROID :=
HAVE_LINUX_IOS := HAVE_LINUX_IOS :=
HAVE_LINUX_MACOS := HAVE_LINUX_MACOS :=
USE_SYSTEM_SSL := 1
else else
$(error Unexpected host platform $(UNAME_S).) $(error Unexpected host platform $(UNAME_S).)
endif endif
# Everything is set above. # Everything is set above.
$(info Building Tilde Friends $(VERSION_NUMBER) android=$(if $(HAVE_ANDROID),1,0) win=$(if $(HAVE_WIN),1,0) cross_aarch64=$(if $(HAVE_CROSS_AARCH64),1,0) cross_ios=$(if $(HAVE_LINUX_IOS),1,0) cross_macos=$(if $(HAVE_LINUX_MACOS),1,0)) $(info Building Tilde Friends $(VERSION_NUMBER) android=$(if $(HAVE_ANDROID),1,0) win=$(if $(HAVE_WIN),1,0) cross_aarch64=$(if $(HAVE_CROSS_AARCH64),1,0) cross_ios=$(if $(HAVE_LINUX_IOS),1,0) cross_macos=$(if $(HAVE_LINUX_MACOS),1,0) system_ssl=$(if $(USE_SYSTEM_SSL),1,0))
CFLAGS += \ CFLAGS += \
-std=gnu11 \ -std=gnu11 \
@@ -102,10 +98,10 @@ LDFLAGS += \
-Wno-aggressive-loop-optimizations -Wno-aggressive-loop-optimizations
ANDROID_MIN_SDK_VERSION := 24 ANDROID_MIN_SDK_VERSION := 24
ANDROID_TARGET_SDK_VERSION := 35 ANDROID_TARGET_SDK_VERSION := 34
ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/35.0.0 ANDROID_BUILD_TOOLS := $(ANDROID_SDK)/build-tools/34.0.0
ANDROID_PLATFORM := $(ANDROID_SDK)/platforms/android-$(ANDROID_TARGET_SDK_VERSION) 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 := \ ANDROID_ARMV7A_TARGETS := \
out/androiddebug-armv7a/tildefriends \ out/androiddebug-armv7a/tildefriends \
@@ -249,10 +245,7 @@ $(ANDROID_TARGETS): CFLAGS += \
-fno-asynchronous-unwind-tables \ -fno-asynchronous-unwind-tables \
-funwind-tables \ -funwind-tables \
-Wno-unknown-warning-option -Wno-unknown-warning-option
$(ANDROID_TARGETS): LDFLAGS += \ $(ANDROID_TARGETS): LDFLAGS += --sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot -fPIC
--sysroot $(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/sysroot \
-Wl,-z,max-page-size=16384 \
-fPIC
$(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og $(DEBUG_TARGETS): CFLAGS += -DDEBUG -Og
$(DEBUG_TARGETS): LDFLAGS += -Og $(DEBUG_TARGETS): LDFLAGS += -Og
$(RELEASE_TARGETS): CFLAGS += \ $(RELEASE_TARGETS): CFLAGS += \
@@ -266,12 +259,16 @@ $(WINDOWS_TARGETS): AS = $(CC)
$(WINDOWS_TARGETS): CFLAGS += \ $(WINDOWS_TARGETS): CFLAGS += \
-D_WIN32_WINNT=0x0A00 \ -D_WIN32_WINNT=0x0A00 \
-DWINVER=0x0A00 \ -DWINVER=0x0A00 \
-DNTDDI_VERSION=NTDDI_WIN10 -DNTDDI_VERSION=NTDDI_WIN10 \
-Iout/openssl/$(UNAME_S)/mingw64/usr/local/include
$(WINDOWS_TARGETS): LDFLAGS += \ $(WINDOWS_TARGETS): LDFLAGS += \
-static \ -static \
-lm -lm \
-Lout/openssl/$(UNAME_S)/mingw64/usr/local/lib
$(AARCH64_TARGETS): CC = aarch64-linux-gnu-gcc $(AARCH64_TARGETS): CC = aarch64-linux-gnu-gcc
$(AARCH64_TARGETS): AS = $(CC) $(AARCH64_TARGETS): AS = $(CC)
$(AARCH64_TARGETS): CFLAGS += -Iout/openssl/Linux/aarch64/usr/local/include
$(AARCH64_TARGETS): LDFLAGS += -Lout/openssl/Linux/aarch64/usr/local/lib
ifeq ($(UNAME_S),Darwin) ifeq ($(UNAME_S),Darwin)
$(HOST_TARGETS): CC = xcrun clang $(HOST_TARGETS): CC = xcrun clang
$(IOS_TARGETS): IOS_SYSROOT := $(shell xcrun --sdk iphoneos --show-sdk-path) $(IOS_TARGETS): IOS_SYSROOT := $(shell xcrun --sdk iphoneos --show-sdk-path)
@@ -296,12 +293,39 @@ $(ANDROID_TARGETS): AS = $(CC)
$(ANDROID_TARGETS): CFLAGS += \ $(ANDROID_TARGETS): CFLAGS += \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \ -target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
-Wno-unknown-warning-option -Wno-unknown-warning-option
$(ANDROID_ARMV7A_TARGETS): CFLAGS += -Iout/openssl/android/armeabi-v7a/usr/local/include
$(ANDROID_ARMV7A_TARGETS): LDFLAGS += -Lout/openssl/android/armeabi-v7a/usr/local/lib
$(ANDROID_ARM64_TARGETS): CFLAGS += -Iout/openssl/android/arm64-v8a/usr/local/include
$(ANDROID_ARM64_TARGETS): LDFLAGS += -Lout/openssl/android/arm64-v8a/usr/local/lib
$(ANDROID_X86_TARGETS): CFLAGS += -Iout/openssl/android/x86/usr/local/include
$(ANDROID_X86_TARGETS): CFLAGS += -Wno-atomic-alignment $(ANDROID_X86_TARGETS): CFLAGS += -Wno-atomic-alignment
$(ANDROID_X86_TARGETS): LDFLAGS += -Lout/openssl/android/x86/usr/local/lib
$(ANDROID_X86_64_TARGETS): CFLAGS += -Iout/openssl/android/x86_64/usr/local/include
$(ANDROID_X86_64_TARGETS): LDFLAGS += -Lout/openssl/android/x86_64/usr/local/lib
$(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type $(NONMACOS_TARGETS): CFLAGS += -Wno-cast-function-type
$(MACOS_TARGETS): LDFLAGS += -Wl,-dead_strip $(MACOS_TARGETS): LDFLAGS += -Wl,-dead_strip
$(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections -Wl,--as-needed $(NONMACOS_TARGETS): LDFLAGS += -Wl,--gc-sections -Wl,--as-needed
$(IOS_TARGETS): CFLAGS += -miphoneos-version-min=$(IPHONEOS_VERSION_MIN) $(IOS_TARGETS): CFLAGS += -miphoneos-version-min=$(IPHONEOS_VERSION_MIN)
$(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=$(IPHONEOS_VERSION_MIN) $(IOS_TARGETS): LDFLAGS += -miphoneos-version-min=$(IPHONEOS_VERSION_MIN)
ifeq ($(UNAME_S),Darwin)
$(IOS_TARGETS): CFLAGS += -Iout/openssl/ios/ios64-xcrun/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Lout/openssl/ios/ios64-xcrun/usr/local/lib
else
$(IOS_TARGETS): CFLAGS += -Iout/openssl/$(UNAME_S)/ios64-cross/usr/local/include
$(IOS_TARGETS): LDFLAGS += -Lout/openssl/$(UNAME_S)/ios64-cross/usr/local/lib
$(filter $(BUILD_DIR)/macosdebug-x86_64/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-x86_64/usr/local/include
$(filter $(BUILD_DIR)/macosdebug-arm/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-arm/usr/local/include
$(filter $(BUILD_DIR)/macosdebug-x86_64/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-x86_64/usr/local/lib
$(filter $(BUILD_DIR)/macosdebug-arm/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-arm/usr/local/lib
$(filter $(BUILD_DIR)/macosrelease-x86_64/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-x86_64/usr/local/include
$(filter $(BUILD_DIR)/macosrelease-arm/%,$(ALL_TARGETS)): CFLAGS += -Iout/openssl/$(UNAME_S)/macos-arm/usr/local/include
$(filter $(BUILD_DIR)/macosrelease-x86_64/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-x86_64/usr/local/lib
$(filter $(BUILD_DIR)/macosrelease-arm/%,$(ALL_TARGETS)): LDFLAGS += -Lout/openssl/$(UNAME_S)/macos-arm/usr/local/lib
endif
$(IOSSIM_TARGETS): CFLAGS += -Iout/openssl/ios/iossimulator-xcrun/usr/local/include
$(IOSSIM_TARGETS): LDFLAGS += -Lout/openssl/ios/iossimulator-xcrun/usr/local/lib
$(HOST_TARGETS): CFLAGS += -Iout/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/include
$(HOST_TARGETS): LDFLAGS += -Lout/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib
ifeq ($(UNAME_M),x86_64) ifeq ($(UNAME_M),x86_64)
ifeq ($(UNAME_S),Linux) ifeq ($(UNAME_S),Linux)
@@ -582,16 +606,15 @@ $(UV_OBJS): CFLAGS += \
-Ideps/libuv/include \ -Ideps/libuv/include \
-Ideps/libuv/src \ -Ideps/libuv/src \
-Wno-dangling-pointer \ -Wno-dangling-pointer \
-Wno-format-truncation \
-Wno-incompatible-pointer-types \ -Wno-incompatible-pointer-types \
-Wno-maybe-uninitialized \ -Wno-maybe-uninitialized \
-Wno-nonnull \
-Wno-sign-compare \ -Wno-sign-compare \
-Wno-unknown-attributes \ -Wno-unknown-attributes \
-Wno-unused-but-set-parameter \ -Wno-unused-but-set-parameter \
-Wno-unused-but-set-variable \ -Wno-unused-but-set-variable \
-Wno-unused-result \ -Wno-unused-result \
-Wno-unused-variable -Wno-unused-variable \
-Wno-nonnull
$(filter out/win%,$(UV_OBJS)): \ $(filter out/win%,$(UV_OBJS)): \
CFLAGS += \ CFLAGS += \
-Wno-cast-function-type \ -Wno-cast-function-type \
@@ -618,7 +641,6 @@ SODIUM_SOURCES := \
deps/libsodium/src/libsodium/crypto_core/hsalsa20/ref2/core_hsalsa20_ref2.c \ deps/libsodium/src/libsodium/crypto_core/hsalsa20/ref2/core_hsalsa20_ref2.c \
deps/libsodium/src/libsodium/crypto_core/salsa/ref/core_salsa_ref.c \ deps/libsodium/src/libsodium/crypto_core/salsa/ref/core_salsa_ref.c \
deps/libsodium/src/libsodium/crypto_core/softaes/softaes.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-compress-ref.c \
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-ref.c \ deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/blake2b-ref.c \
deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/generichash_blake2b.c \ deps/libsodium/src/libsodium/crypto_generichash/blake2b/ref/generichash_blake2b.c \
@@ -688,12 +710,12 @@ $(SQLITE_OBJS): CFLAGS += \
-DSQLITE_MAX_COMPOUND_SELECT=300 \ -DSQLITE_MAX_COMPOUND_SELECT=300 \
-DSQLITE_MAX_EXPR_DEPTH=40 \ -DSQLITE_MAX_EXPR_DEPTH=40 \
-DSQLITE_MAX_FUNCTION_ARG=8 \ -DSQLITE_MAX_FUNCTION_ARG=8 \
-DSQLITE_MAX_LENGTH=10485760 \ -DSQLITE_MAX_LENGTH=5242880 \
-DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \ -DSQLITE_MAX_LIKE_PATTERN_LENGTH=50 \
-DSQLITE_MAX_SQL_LENGTH=100000 \ -DSQLITE_MAX_SQL_LENGTH=100000 \
-DSQLITE_MAX_TRIGGER_DEPTH=10 \ -DSQLITE_MAX_TRIGGER_DEPTH=10 \
-DSQLITE_MAX_VARIABLE_NUMBER=100 \ -DSQLITE_MAX_VARIABLE_NUMBER=100 \
-DSQLITE_MAX_VDBE_OP=50000 \ -DSQLITE_MAX_VDBE_OP=25000 \
-DSQLITE_OMIT_DEPRECATED \ -DSQLITE_OMIT_DEPRECATED \
-DSQLITE_OMIT_DESERIALIZE \ -DSQLITE_OMIT_DESERIALIZE \
-DSQLITE_OMIT_LOAD_EXTENSION \ -DSQLITE_OMIT_LOAD_EXTENSION \
@@ -712,7 +734,7 @@ $(SQLITE_OBJS): CFLAGS += \
QUICKJS_SOURCES := \ QUICKJS_SOURCES := \
deps/quickjs/cutils.c \ deps/quickjs/cutils.c \
deps/quickjs/dtoa.c \ deps/quickjs/libbf.c \
deps/quickjs/libregexp.c \ deps/quickjs/libregexp.c \
deps/quickjs/libunicode.c \ deps/quickjs/libunicode.c \
deps/quickjs/quickjs.c deps/quickjs/quickjs.c
@@ -789,6 +811,9 @@ $(MINIUNZIP_OBJS): CFLAGS += \
LDFLAGS += \ LDFLAGS += \
-pthread \ -pthread \
-lm -lm
$(HOST_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS) $(AARCH64_TARGETS) $(filter-out $(HOST_TARGETS),$(MACOS_TARGETS)): LDFLAGS += \
-lssl \
-lcrypto
ifneq ($(UNAME_S),Haiku) ifneq ($(UNAME_S),Haiku)
ifneq ($(UNAME_S),OpenBSD) ifneq ($(UNAME_S),OpenBSD)
$(HOST_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \ $(HOST_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
@@ -796,6 +821,8 @@ $(HOST_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
endif endif
endif endif
$(WINDOWS_TARGETS): LDFLAGS += \ $(WINDOWS_TARGETS): LDFLAGS += \
-lssl \
-lcrypto \
-lcrypt32 \ -lcrypt32 \
-ldbghelp \ -ldbghelp \
-liphlpapi \ -liphlpapi \
@@ -808,15 +835,15 @@ $(WINDOWS_TARGETS): LDFLAGS += \
$(ANDROID_TARGETS): LDFLAGS += \ $(ANDROID_TARGETS): LDFLAGS += \
-target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \ -target $(ANDROID_NDK_TARGET_TRIPLE)$(ANDROID_MIN_SDK_VERSION) \
-ldl \ -ldl \
-llog -llog \
-lssl \
-lcrypto
$(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): CFLAGS += \ $(MACOS_TARGETS) $(IOS_TARGETS) $(IOSSIM_TARGETS): CFLAGS += \
-Wno-unknown-warning-option -Wno-unknown-warning-option
$(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \ $(IOS_TARGETS) $(IOSSIM_TARGETS): LDFLAGS += \
-framework Foundation \ -framework Foundation \
-framework CoreFoundation \ -framework CoreFoundation \
-framework CoreSpotlight \
-framework UIKit \ -framework UIKit \
-framework UniformTypeIdentifiers \
-framework WebKit -framework WebKit
## ##
@@ -892,7 +919,7 @@ src/ios/Info.plist : $(firstword $(MAKEFILE_LIST))
tr '\n' '^' | \ tr '\n' '^' | \
sed -r \ sed -r \
-e 's@(<key>CFBundleShortVersionString</key>\^[[:space:]]*<string>)[0-9.]*(</string>)@\1$(VERSION_NUMBER:%-wip=%)\2@' \ -e 's@(<key>CFBundleShortVersionString</key>\^[[:space:]]*<string>)[0-9.]*(</string>)@\1$(VERSION_NUMBER:%-wip=%)\2@' \
-e 's@(<key>CFBundleVersion</key>\^[[:space:]]*<string>)[[:digit:]]+(</string>)@\1$(VERSION_CODE)\2@' \ -e 's@(<key>CFBundleVersion</key>\^[[:space:]]*<string>)[[:digit:]]+(</string>)@\1$(VERSION_CODE_IOS)\2@' \
-e 's@(<key>MinimumOSVersion</key>\^[[:space:]]*<string>)[0-9.]*(</string>)@\1$(IPHONEOS_VERSION_MIN)\2@' | \ -e 's@(<key>MinimumOSVersion</key>\^[[:space:]]*<string>)[0-9.]*(</string>)@\1$(IPHONEOS_VERSION_MIN)\2@' | \
tr '^' '\n' > \ tr '^' '\n' > \
$@.tmp && mv $@.tmp $@ || rm -f $@.tmp $@.tmp && mv $@.tmp $@ || rm -f $@.tmp
@@ -979,7 +1006,7 @@ $(BUNDLETOOL):
@curl -q -L --create-dirs -o $@ $(BUNDLETOOL_URL) @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) 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 @mkdir -p out/aab/staging
@$(ANDROID_BUILD_TOOLS)/aapt2 link --proto-format -o out/aab/temporary.apk \ @$(ANDROID_BUILD_TOOLS)/aapt2 link --proto-format -o out/aab/temporary.apk \
-I $(ANDROID_PLATFORM)/android.jar \ -I $(ANDROID_PLATFORM)/android.jar \
@@ -999,11 +1026,14 @@ out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGET
@cp out/apk/classes.dex out/aab/staging/dex/ @cp out/apk/classes.dex out/aab/staging/dex/
@rm -fv out/base.zip @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/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 @cp out/androidrelease/tildefriends out/aab/staging/lib/arm64-v8a/libtildefriends.so
@$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip out/androidrelease/tildefriends -o out/aab/staging/lib/arm64-v8a/libtildefriends.so @cp out/androidrelease-armv7a/tildefriends out/aab/staging/lib/armeabi-v7a/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 @cp out/androidrelease-x86_64/tildefriends out/aab/staging/lib/x86_64/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 @cp out/androidrelease-x86/tildefriends out/aab/staging/lib/x86/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 @$(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/ @cp -r apps/ out/aab/staging/root/
@rm -rf out/aab/staging/root/apps/welcome* @rm -rf out/aab/staging/root/apps/welcome*
@cp -r core/ out/aab/staging/root/ @cp -r core/ out/aab/staging/root/
@@ -1012,12 +1042,7 @@ out/TildeFriends.aab: out/apk/classes.dex $(filter-out %debug%, $(ANDROID_TARGET
@cp -r deps/codemirror/ out/aab/staging/root/deps/ @cp -r deps/codemirror/ out/aab/staging/root/deps/
@cd out/aab/staging/; zip -r ../base.zip *; cd ../../../ @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=$@ @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_BUILD_TOOLS)/apksigner sign -ks .keys/android.jks --ks-key-alias androidKey -ks-pass pass:android --min-sdk-version=$(ANDROID_MIN_SDK_VERSION) $@
@$(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 $@
aab: out/TildeFriends.aab ## Build an Android App Bundle. aab: out/TildeFriends.aab ## Build an Android App Bundle.
.PHONY: aab .PHONY: aab
@@ -1079,12 +1104,12 @@ out/apk/TildeFriends-%.fdroid.unsigned.apk:
out/%.apk: out/apk/%.unsigned.apk out/%.apk: out/apk/%.unsigned.apk
@echo "[apksigner] $(notdir $@)" @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 out/%.zopfli.apk: out/%.apk
@echo "[zopfli] $(notdir $@)" @echo "[zopfli] $(notdir $@)"
$(ANDROID_BUILD_TOOLS)/zipalign -f -z 4 $< $@.zopfli $(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. release-apk: out/TildeFriends-arm-release.zopfli.apk out/TildeFriends-x86-release.zopfli.apk ## Build an Android release APK.
.PHONY: release-apk .PHONY: release-apk
@@ -1102,11 +1127,6 @@ releaseapkgo: out/TildeFriends-arm-release.apk ## Build, install, and run a rele
@adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity @adb shell am start com.unprompted.tildefriends/.TildeFriendsActivity
.PHONY: releaseapkgo .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. apklog: ## Display Android log output.
@adb logcat *:S tildefriends @adb logcat *:S tildefriends
.PHONY: apklog .PHONY: apklog
@@ -1133,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 -B out/zsign_build deps/zsign
@cmake --build out/zsign_build -- COLOR=0 VERBOSE=0 MAKESILENT=-s @cmake --build out/zsign_build -- COLOR=0 VERBOSE=0 MAKESILENT=-s
ifeq ($(HAVE_LINUX_IOS),1) 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)
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)
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@cp -v $(filter-out out/zsign%,$<) $@ @cp -v $(filter-out out/zsign%,$<) $@
@cp -v out/data.zip $(@D)/ @cp -v out/data.zip $(@D)/
@@ -1152,7 +1166,6 @@ out/tildefriends-%.ipa: out/tildefriends-ios%.app/tildefriends
@echo "[ipa] $@" @echo "[ipa] $@"
@rm -rf $@.tmp $@ @rm -rf $@.tmp $@
@mkdir -p $@.tmp/Payload/tildefriends.app/ @mkdir -p $@.tmp/Payload/tildefriends.app/
@cp src/ios/tildefriends512.png $@.tmp/iTunesArtwork
@cp -R $(dir $<)/* $@.tmp/Payload/tildefriends.app/ @cp -R $(dir $<)/* $@.tmp/Payload/tildefriends.app/
@cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./ @cd $@.tmp/ && zip -u ../../$@ -q -9 -r ./
@rm -rf $@.tmp/ @rm -rf $@.tmp/
@@ -1180,11 +1193,98 @@ ios%go: out/tildefriends-ios%.app/tildefriends
ideviceinstaller -i $(realpath $(dir $<)) ideviceinstaller -i $(realpath $(dir $<))
iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends ## Build, install, and run an iOS debug build. iossimdebuggo: out/tildefriends-iossimdebug.app/tildefriends ## Build, install, and run an iOS debug build.
xcrun -sdk iphoneos codesign -f -s 'Apple Distribution' --entitlements src/ios/Entitlements.plist --generate-entitlement-der out/tildefriends-iossimdebug.app
xcrun simctl install booted out/tildefriends-iossimdebug.app/ xcrun simctl install booted out/tildefriends-iossimdebug.app/
xcrun simctl launch --console booted com.unprompted.tildefriends xcrun simctl launch booted com.unprompted.tildefriends
.PHONY: iossimdebuggo .PHONY: iossimdebuggo
ANDROID_DEPS := out/openssl/android/arm64-v8a/usr/local/lib/libssl.a
$(ANDROID_DEPS):
+@export ANDROID_NDK_ROOT=$(ANDROID_NDK)
+@export BUILD_PLATFORM=android
+@export TOOLCHAIN=$(ANDROID_NDK)/toolchains/llvm/prebuilt/linux-x86_64
+@PATH="$$TOOLCHAIN/x86_64-linux-android/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=x86_64 SSL_TARGET=android-x86_64 OPTIONS="-D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
+@PATH="$$TOOLCHAIN/i686-linux-android/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=x86 SSL_TARGET=android-x86 OPTIONS="-D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
+@PATH="$$TOOLCHAIN/arm-linux-androideabi/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=armeabi-v7a SSL_TARGET=android-arm OPTIONS="--target=armv7a-linux-androideabi -Wl,--fix-cortex-a8 -D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
+@PATH="$$TOOLCHAIN/aarch64-linux-android/bin:$$TOOLCHAIN/bin:$$PATH" BUILD_TARGET=arm64-v8a SSL_TARGET=android-arm64 OPTIONS="-D__ANDROID_API__=$(ANDROID_MIN_SDK_VERSION) -Wno-macro-redefined" tools/ssl-local
$(filter $(BUILD_DIR)/android%,$(APP_OBJS)): | $(ANDROID_DEPS)
ifeq ($(UNAME_S),Linux)
ifneq ($(USE_SYSTEM_SSL),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@tools/ssl-local
$(filter $(BUILD_DIR)/debug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/release/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_CROSS_AARCH64),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/aarch64/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@OPTIONS="--cross-compile-prefix=aarch64-linux-gnu-" BUILD_TARGET=aarch64 SSL_TARGET=linux-aarch64 tools/ssl-local
$(filter $(BUILD_DIR)/armdebug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/armrelease/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_LINUX_IOS),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/ios64-cross/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@PATH=deps/ios_toolchain/target/bin:$$PATH \
BUILD_TARGET=ios64-cross \
SSL_TARGET=ios64-cross \
CROSS_COMPILE=../../deps/ios_toolchain/target/bin/arm-apple-darwin11- \
CROSS_TOP=../../deps/ios_toolchain/target \
CROSS_SDK=iPhoneOS18.2.sdk \
CC=clang \
OPTIONS=-miphoneos-version-min=$(IPHONEOS_VERSION_MIN) \
tools/ssl-local
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_LINUX_MACOS),1)
LOCAL_DEPS := out/openssl/$(UNAME_S)/macos-arm/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@PATH=../../deps/macos_toolchain/bin:$$PATH \
BUILD_TARGET=macos-arm \
SSL_TARGET=darwin64-arm64 \
CC=../../deps/macos_toolchain/bin/oa64-clang \
RANLIB=../../deps/macos_toolchain/bin/x86_64-apple-darwin24-ranlib \
AR=../../deps/macos_toolchain/bin/arm64-apple-darwin24-ar \
tools/ssl-local
$(filter $(BUILD_DIR)/macosrelease-arm/% $(BUILD_DIR)/macosdebug-arm/%,$(APP_OBJS)): | $(LOCAL_DEPS)
LOCAL_DEPS := out/openssl/$(UNAME_S)/macos-x86_64/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@PATH=../../deps/macos_toolchain/bin:$$PATH \
BUILD_TARGET=macos-x86_64 \
SSL_TARGET=darwin64-x86_64 \
CC=../../deps/macos_toolchain/bin/o64-clang \
RANLIB=../../deps/macos_toolchain/bin/x86_64-apple-darwin24-ranlib \
AR=../../deps/macos_toolchain/bin/x86_64-apple-darwin24-ar \
tools/ssl-local
$(filter $(BUILD_DIR)/macosrelease-x86_64/% $(BUILD_DIR)/macosdebug-x86_64/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
endif
ifeq ($(UNAME_S),Darwin)
LOCAL_DEPS := out/openssl/$(UNAME_S)/$(UNAME_M)/usr/local/lib/libssl.a
$(LOCAL_DEPS):
+@tools/ssl-local
$(filter $(BUILD_DIR)/debug/%,$(APP_OBJS)) $(filter $(BUILD_DIR)/release/%,$(APP_OBJS)): | $(LOCAL_DEPS)
endif
ifeq ($(HAVE_WIN),1)
WINDOWS_DEPS := out/openssl/$(UNAME_S)/mingw64/usr/local/lib/libssl.a
$(WINDOWS_DEPS):
+@BUILD_TARGET=mingw64 SSL_TARGET=mingw64 OPTIONS="--cross-compile-prefix=x86_64-w64-mingw32-" tools/ssl-local
$(filter $(BUILD_DIR)/win%,$(APP_OBJS)): | $(WINDOWS_DEPS)
endif
ifeq ($(UNAME_S),Darwin)
IOS_DEPS := out/openssl/ios/ios64-xcrun/usr/local/lib/libssl.a
$(IOS_DEPS):
+@BUILD_PLATFORM=ios BUILD_TARGET=ios64-xcrun SSL_TARGET=ios64-xcrun OPTIONS="-fPIC -Wno-macro-redefined -miphoneos-version-min=$(IPHONEOS_VERSION_MIN)" tools/ssl-local
+@BUILD_PLATFORM=ios BUILD_TARGET=iossimulator-xcrun SSL_TARGET=iossimulator-xcrun OPTIONS="-fPIC -Wno-macro-redefined" tools/ssl-local
$(filter $(BUILD_DIR)/ios%,$(APP_OBJS)): | $(IOS_DEPS)
endif
out/macos%/tildefriends: out/macos%-arm/tildefriends out/macos%-x86_64/tildefriends out/macos%/tildefriends: out/macos%-arm/tildefriends out/macos%-x86_64/tildefriends
@echo [lipo] $@ @echo [lipo] $@
@mkdir -p $(@D) @mkdir -p $(@D)
@@ -1213,26 +1313,7 @@ out/tildefriends-x86_64.AppImage: out/release/tildefriends out/data.zip
@cd out; ./appimagetool --appimage-extract; cd .. @cd out; ./appimagetool --appimage-extract; cd ..
@cd out; unset SOURCE_DATE_EPOCH; PATH=$$PATH:squashfs-root/usr/bin ARCH=x86_64 squashfs-root/usr/bin/appimagetool -u 'zsync|https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage.zsync' tildefriends.AppDir tildefriends-x86_64.AppImage; cd .. @cd out; unset SOURCE_DATE_EPOCH; PATH=$$PATH:squashfs-root/usr/bin ARCH=x86_64 squashfs-root/usr/bin/appimagetool -u 'zsync|https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage.zsync' tildefriends.AppDir tildefriends-x86_64.AppImage; cd ..
out/tildefriends-aarch64.AppImage: out/armrelease/tildefriends out/data.zip appimage: out/tildefriends-x86_64.AppImage ## Build an AppImage.
@echo "[appimage] $$@"
@rm -rf out/tildefriends_aarch64.AppDir
@mkdir -p out/tildefriends_aarch64.AppDir/usr/bin
@mkdir -p out/tildefriends_aarch64.AppDir/usr/share/applications
@mkdir -p out/tildefriends_aarch64.AppDir/usr/share/icons/hicolor/scalable/apps
@mkdir -p out/tildefriends_aarch64.AppDir/usr/share/tildefriends
@echo $(APPIMAGETOOL_MD5) > out/appimagetool.md5
@test -x out/appimagetool || curl -q -L -o out/appimagetool $(APPIMAGETOOL_URL) && md5sum -c out/appimagetool.md5 && chmod +x out/appimagetool
@echo "[Desktop Entry]\nName=tildefriends\nExec=/usr/bin/tildefriends\nIcon=/usr/share/icons/hicolor/scalable/apps/tildefriends\nType=Application\nCategories=Network" > out/tildefriends_aarch64.AppDir/tildefriends.desktop
@cp src/ios/tildefriends.svg out/tildefriends_aarch64.AppDir/usr/share/icons/hicolor/scalable/apps/
@cp src/ios/tildefriends.svg out/tildefriends_aarch64.AppDir/
@cp out/armrelease/tildefriends out/tildefriends_aarch64.AppDir/usr/bin/
@cp out/data.zip out/tildefriends_aarch64.AppDir/usr/share/tildefriends/data.zip
@echo "#!/bin/sh\n\$${APPDIR}/usr/bin/tildefriends run -z \$$APPDIR/usr/share/tildefriends/data.zip" > out/tildefriends_aarch64.AppDir/AppRun
@chmod +x out/tildefriends_aarch64.AppDir/AppRun
@cd out; ./appimagetool --appimage-extract; cd ..
@cd out; unset SOURCE_DATE_EPOCH; PATH=$$PATH:squashfs-root/usr/bin ARCH=arm_aarch64 squashfs-root/usr/bin/appimagetool -u 'zsync|https://dev.tildefriends.net/releases/tildefriends-aarch64.AppImage.zsync' tildefriends_aarch64.AppDir tildefriends-aarch64.AppImage; cd ..
appimage: out/tildefriends-x86_64.AppImage out/tildefriends-aarch64.AppImage ## Build AppImages.
.PHONY: appimage .PHONY: appimage
flatpak: out/ ## Build a flatpak. flatpak: out/ ## Build a flatpak.
@@ -1277,6 +1358,7 @@ tarball: ## Build an all-inclusive source tarball (.tar.xz).
--exclude=deps/libsodium/test \ --exclude=deps/libsodium/test \
--exclude=deps/libuv/docs \ --exclude=deps/libuv/docs \
--exclude=deps/libuv/test \ --exclude=deps/libuv/test \
--exclude=deps/speedscope/*.map \
--exclude=deps/sqlite/shell.c \ --exclude=deps/sqlite/shell.c \
--exclude=deps/zlib/contrib/vstudio \ --exclude=deps/zlib/contrib/vstudio \
--exclude=deps/zlib/doc \ --exclude=deps/zlib/doc \
@@ -1319,8 +1401,6 @@ dist:
@cp out/TildeFriends-release.fdroid.apk dist/TildeFriends-$(VERSION_NUMBER).fdroid.apk @cp out/TildeFriends-release.fdroid.apk dist/TildeFriends-$(VERSION_NUMBER).fdroid.apk
@echo "[cp] TildeFriends-x86_64-$(VERSION_NUMBER).AppImage" @echo "[cp] TildeFriends-x86_64-$(VERSION_NUMBER).AppImage"
@cp out/tildefriends-x86_64.AppImage dist/TildeFriends-x86_64-$(VERSION_NUMBER).AppImage @cp out/tildefriends-x86_64.AppImage dist/TildeFriends-x86_64-$(VERSION_NUMBER).AppImage
@echo "[cp] TildeFriends-aarch64-$(VERSION_NUMBER).AppImage"
@cp out/tildefriends-aarch64.AppImage dist/TildeFriends-aarch64-$(VERSION_NUMBER).AppImage
@echo "[cp] tildefriends-linux-$(UNAME_M)-$(VERSION_NUMBER)" @echo "[cp] tildefriends-linux-$(UNAME_M)-$(VERSION_NUMBER)"
@cp out/release/tildefriends.standalone dist/tildefriends-linux-$(UNAME_M)-$(VERSION_NUMBER) @cp out/release/tildefriends.standalone dist/tildefriends-linux-$(UNAME_M)-$(VERSION_NUMBER)
@test $(HAVE_CROSS_AARCH64) && echo "[cp] tildefriends-linux-aarch64-$(VERSION_NUMBER)" @test $(HAVE_CROSS_AARCH64) && echo "[cp] tildefriends-linux-aarch64-$(VERSION_NUMBER)"
@@ -1339,7 +1419,7 @@ dist-ios: iosrelease-app
mkdir -p out/Payload/tildefriends.app mkdir -p out/Payload/tildefriends.app
cp -avR out/tildefriends-iosrelease.app/* out/Payload/tildefriends.app/ cp -avR out/tildefriends-iosrelease.app/* out/Payload/tildefriends.app/
cp src/ios/tildefriends.png 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 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 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 .. cd out; zip -r tildefriends.ipa Payload; cd ..
@@ -1385,18 +1465,6 @@ help: ## Display this help message.
.PHONY: help .PHONY: help
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
docs: debug
docs: ## Build HTML docs. 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 @doxygen
.PHONY: docs .PHONY: docs

View File

@@ -38,6 +38,8 @@ dependencies in the right places.
### Requirements ### Requirements
System OpenSSL libraries are assumed to be available on Haiku and OpenSSL.
On MacOS, Xcode's command-line tools are expected to be available. On MacOS, Xcode's command-line tools are expected to be available.
### Build Commands ### Build Commands
@@ -53,8 +55,9 @@ standard.
## Running ## Running
By default, running the built `out/debug/tildefriends` executable will start a By default, running the built `out/debug/tildefriends` executable will start a
web server at <http://localhost:12345/>. `tildefriends -h` lists further web server at <http://localhost:12345/>. It expects to be run with the
options. 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 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 privileges. Further administration can be done in the `admin` app at

View File

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

View File

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

View File

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

View File

@@ -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} html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ /* 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} 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-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-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-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,.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-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-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-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important} .w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important} .w3-hover-none:hover{box-shadow:none!important}
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
/* Colors */ /* Colors */
.w3-amber,.w3-hover-amber:hover{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-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-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-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-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-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-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-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}

View File

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

View File

@@ -55,9 +55,6 @@ app.setDocument(`<head>
</head> </head>
<body style="color:#fff"> <body style="color:#fff">
${markdown(docs.docs.global)} ${markdown(docs.docs.global)}
<!--
${Object.keys(docs.docs).filter(x => [...treeify('', globalThis)].indexOf(x) == -1).map(x => `<p>STALE: ${x}</p>`).join('')}
-->
${[...treeify('', globalThis)].map(x => document(x)).join('\n')} ${[...treeify('', globalThis)].map(x => document(x)).join('\n')}
<a id="Database"></a> <a id="Database"></a>
${markdown(docs.docs.database)} ${markdown(docs.docs.database)}

View File

@@ -195,6 +195,51 @@ Call a function after some delay.
* *Number* **timeout** Number of milliseconds to wait before calling the callback function. * *Number* **timeout** Number of milliseconds to wait before calling the callback function.
`; `;
docs['parseHttpRequest()'] = `
Parses an HTTP request.
### Parameters
* *Uint8Array* **request** The request data. Maybe be partial or contain extra data. The return value will
indicate when and where it is complete.
* *Number* **last_length** The length of the data passed on a previous attempt for the same request, or 0 initially.
### Returns
* *Integer* **-2** if the request is incomplete.
* *Integer* **-1** if the request could not be parsed.
* *Object* An object with **bytes_parsed**, **minor_version**, **path**, and **headers** fields on successful parse.
`;
docs['parseHttpResponse()'] = `
Parses an HTTP response.
### Parameters
* *Uint8Array* **response** The response data. Maybe be partial or contain extra data. The return value will
indicate when and where it is complete.
* *Number* **last_length** The length of the data passed on a previous attempt for the same response, or 0 initially.
### Returns
* *Integer* **-2** if the response is incomplete.
* *Integer* **-1** if the response could not be parsed.
* *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse.
`;
docs['sha1Digest()'] = `
Calculates a SHA1 digest.
Completes synchronously.
### Parameters
* *String* **value** The value for which to calculate the digest.
### Returns
*String* The SHA1 digest of UTF-8 encoded \`value\`.
`;
docs['maskBytes()'] = `
Masks bytes for WebSocket communication.
Completes synchronously.
### Parameters
* *Uint8Array* **bytes** The byte array of data to mask.
* *Uint32* **mask** The mask to apply.
### Returns
*Uint32Array* The masked bytes.
`;
docs['exit()'] = ` docs['exit()'] = `
Exits the app. But why would you want to do that? Exits the app. But why would you want to do that?

View File

@@ -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} html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ /* 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} 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-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-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-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,.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-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-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-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important} .w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important} .w3-hover-none:hover{box-shadow:none!important}
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
/* Colors */ /* Colors */
.w3-amber,.w3-hover-amber:hover{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-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-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-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-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-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-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-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

@@ -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} html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ /* 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} 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-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-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-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,.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-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-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-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important} .w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important} .w3-hover-none:hover{box-shadow:none!important}
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
/* Colors */ /* Colors */
.w3-amber,.w3-hover-amber:hover{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-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-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-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-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-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-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-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}

View File

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

View File

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

View File

@@ -1,290 +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
id="scrollbox"
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" id="next0">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" id="next1">Next</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" id="next2">Next</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" id="next3">Next</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> -&gt;
<b>Core Apps</b> -&gt; <b>intro</b>. When you continue, you will
be prompted to save a setting so that you don't see this every
time.
</li>
</ul>
</div>
<footer class="w3-center w3-xlarge w3-padding">
<button class="w3-button w3-yellow" id="complete">Continue</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">
&#10094;
</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">
&#10095;
</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';
document.getElementById('scrollbox').scrollTo(0, 0);
}
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>

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -2,7 +2,6 @@ import * as tfrpc from '/tfrpc.js';
let g_database; let g_database;
let g_hash; let g_hash;
let g_sql_cache = {};
tfrpc.register(async function localStorageGet(key) { tfrpc.register(async function localStorageGet(key) {
return app.localStorageGet(key); return app.localStorageGet(key);
@@ -52,40 +51,11 @@ tfrpc.register(async function connect(token) {
tfrpc.register(async function closeConnection(id) { tfrpc.register(async function closeConnection(id) {
await ssb.closeConnection(id); await ssb.closeConnection(id);
}); });
tfrpc.register(async function query(sql, args, options) { tfrpc.register(async function query(sql, args) {
let start = new Date();
let result = []; let result = [];
let key = options?.cacheable ? JSON.stringify([sql, args]) : undefined; await ssb.sqlAsync(sql, args, function callback(row) {
let entry = key ? g_sql_cache[key] : undefined; result.push(row);
const k_ideal_count = 64; });
if (entry) {
result = entry.result;
} else {
await ssb.sqlAsync(sql, args, function callback(row) {
result.push(row);
});
if (key) {
g_sql_cache[key] = {
result: result,
time: new Date().valueOf(),
};
if (Object.keys(g_sql_cache).length > k_ideal_count * 2) {
let aged = Object.entries(g_sql_cache).map(([k, v]) => [v.time, k]);
aged.sort();
for (let i = 0; i < aged.length / 2; i++) {
delete g_sql_cache[aged[i][1]];
}
}
}
}
let end = new Date();
if (end - start > 1000) {
print(
(end - start) / 1000,
entry ? 'from cache' : 'from db',
sql.replaceAll(/\s+/g, ' ').trim()
);
}
return result; return result;
}); });
tfrpc.register(async function appendMessage(id, message) { tfrpc.register(async function appendMessage(id, message) {
@@ -106,9 +76,6 @@ tfrpc.register(function setHash(hash) {
core.register('onMessage', async function (id) { core.register('onMessage', async function (id) {
await tfrpc.rpc.notifyNewMessage(id); await tfrpc.rpc.notifyNewMessage(id);
}); });
core.register('onBlob', async function (id) {
await tfrpc.rpc.notifyNewBlob(id);
});
tfrpc.register(async function store_blob(blob) { tfrpc.register(async function store_blob(blob) {
if (Array.isArray(blob)) { if (Array.isArray(blob)) {
blob = Uint8Array.from(blob); blob = Uint8Array.from(blob);
@@ -139,15 +106,6 @@ tfrpc.register(async function sync() {
tfrpc.register(async function url() { tfrpc.register(async function url() {
return core.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 () { core.register('onBroadcastsChanged', async function () {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts()); await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());

View File

@@ -1,6 +1,6 @@
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {html, render} from './lit-all.min.js'; import {html, render} from './lit-all.min.js';
import {styles, generate_theme} from './tf-styles.js'; import {styles} from './tf-styles.js';
let g_emojis; let g_emojis;
@@ -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 json = await get_emojis();
let recent = await get_recent(author);
let div = document.createElement('div'); let div = document.createElement('div');
div.id = 'emoji_picker'; div.id = 'emoji_picker';
@@ -140,9 +155,6 @@ export async function picker(callback, anchor, author, recent) {
<style> <style>
${styles} ${styles}
</style> </style>
<style>
${generate_theme()}
</style>
<div <div
class="w3-modal" class="w3-modal"
style="display: block; box-sizing: border-box; z-index: 10" style="display: block; box-sizing: border-box; z-index: 10"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -12,14 +12,12 @@ import * as tf_tab_news from './tf-tab-news.js';
import * as tf_tab_news_feed from './tf-tab-news-feed.js'; import * as tf_tab_news_feed from './tf-tab-news-feed.js';
import * as tf_tab_search from './tf-tab-search.js'; import * as tf_tab_search from './tf-tab-search.js';
import * as tf_tab_connections from './tf-tab-connections.js'; import * as tf_tab_connections from './tf-tab-connections.js';
import * as tf_tab_query from './tf-tab-query.js';
import * as tf_tag from './tf-tag.js'; import * as tf_tag from './tf-tag.js';
import * as tf_styles from './tf-styles.js'; import * as tf_styles from './tf-styles.js';
window.addEventListener('load', function () { window.addEventListener('load', function () {
let style = document.createElement('style'); let style = document.createElement('style');
style.innerText = tf_styles.styles; style.innerText = tf_styles.styles;
Promise.resolve(tf_styles.generate_theme()).then(function (x) {
style.innerText += x;
});
document.body.appendChild(style); document.body.appendChild(style);
}); });

View File

@@ -1,6 +1,6 @@
import {LitElement, html, css, guard, until} from './lit-all.min.js'; import {LitElement, html, css, guard, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {styles, generate_theme} from './tf-styles.js'; import {styles} from './tf-styles.js';
class TfElement extends LitElement { class TfElement extends LitElement {
static get properties() { static get properties() {
@@ -11,7 +11,6 @@ class TfElement extends LitElement {
broadcasts: {type: Array}, broadcasts: {type: Array},
connections: {type: Array}, connections: {type: Array},
loading: {type: Boolean}, loading: {type: Boolean},
loading_about: {type: Number},
loaded: {type: Boolean}, loaded: {type: Boolean},
following: {type: Array}, following: {type: Array},
users: {type: Object}, users: {type: Object},
@@ -21,13 +20,7 @@ class TfElement extends LitElement {
channels_latest: {type: Object}, channels_latest: {type: Object},
guest: {type: Boolean}, guest: {type: Boolean},
url: {type: String}, url: {type: String},
private_closed: {type: Object},
private_messages: {type: Array}, 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.following = [];
this.users = {}; this.users = {};
this.loaded = false; this.loaded = false;
this.loading_about = 0;
this.channels = []; this.channels = [];
this.channels_unread = {}; this.channels_unread = {};
this.channels_latest = {}; this.channels_latest = {};
this.loading_latest = 0; this.loading_latest = 0;
this.loading_latest_scheduled = 0; this.loading_latest_scheduled = 0;
this.recent_reactions = [];
this.private_closed = {};
tfrpc.rpc.getBroadcasts().then((b) => { tfrpc.rpc.getBroadcasts().then((b) => {
self.broadcasts = b || []; self.broadcasts = b || [];
}); });
@@ -60,22 +50,10 @@ class TfElement extends LitElement {
tfrpc.rpc.getHash().then((hash) => self.set_hash(hash)); tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
tfrpc.register(function hashChanged(hash) { tfrpc.register(function hashChanged(hash) {
self.set_hash(hash); self.set_hash(hash);
self.reset_progress();
}); });
tfrpc.register(async function notifyNewMessage(id) { tfrpc.register(async function notifyNewMessage(id) {
await self.fetch_new_message(id); await self.fetch_new_message(id);
}); });
tfrpc.register(async function notifyNewBlob(id) {
window.dispatchEvent(
new CustomEvent('blob-stored', {
bubbles: true,
composed: true,
detail: {
id: id,
},
})
);
});
tfrpc.register(function set(name, value) { tfrpc.register(function set(name, value) {
if (name === 'broadcasts') { if (name === 'broadcasts') {
self.broadcasts = value; self.broadcasts = value;
@@ -91,44 +69,13 @@ class TfElement extends LitElement {
async initial_load() { async initial_load() {
let whoami = await tfrpc.rpc.getActiveIdentity(); let whoami = await tfrpc.rpc.getActiveIdentity();
let ids = (await tfrpc.rpc.getIdentities()) || []; 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.url = await tfrpc.rpc.url();
this.whoami = whoami ?? (ids.length ? ids[0] : undefined); this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
this.guest = !this.whoami?.length; this.guest = !this.whoami?.length;
this.ids = ids; this.ids = ids;
let private_closed =
(await tfrpc.rpc.databaseGet('private_closed')) ?? '{}';
this.private_closed = JSON.parse(private_closed);
await this.load_channels(); await this.load_channels();
} }
async open_private_chat(event) {
let update = {};
update[event.detail.key] = false;
this.private_closed = Object.assign(this.private_closed, update);
await tfrpc.rpc.databaseSet(
'private_closed',
JSON.stringify(this.private_closed)
);
}
async close_private_chat(event) {
let update = {};
update[
event.detail.key == '[]'
? JSON.stringify([this.whoami])
: event.detail.key
] = true;
this.private_closed = Object.assign(this.private_closed, update);
await tfrpc.rpc.databaseSet(
'private_closed',
JSON.stringify(this.private_closed)
);
}
async load_channels() { async load_channels() {
let channels = await tfrpc.rpc.query( let channels = await tfrpc.rpc.query(
` `
@@ -176,53 +123,16 @@ class TfElement extends LitElement {
} }
} }
visible_private() {
if (!this.grouped_private_messages || !this.private_closed) {
return [];
}
let self = this;
let self_key = JSON.stringify([this.whoami]);
let opened = Object.entries(this.private_closed)
.filter(([key, value]) => !value)
.map(([key, value]) => [key, []]);
return Object.fromEntries(
[...Object.entries(this.grouped_private_messages), ...opened].filter(
([key, value]) => {
let channel = '🔐' + [...new Set(JSON.parse(key))].sort().join(',');
let grouped_latest = Math.max(...value.map((x) => x.rowid));
return (
!self.private_closed[key] ||
self.channels_unread[channel] === undefined ||
grouped_latest > self.channels_unread[channel]
);
}
)
);
}
next_channel(delta) { next_channel(delta) {
let channel_names = [ let channel_names = ['', '@', '🔐', ...this.channels.map((x) => '#' + x)];
'', let index = channel_names.indexOf(this.hash.substring(1));
'@',
'👍',
'🚩',
...Object.keys(this.visible_private())
.sort()
.map((x) => '🔐' + JSON.parse(x).join(',')),
...this.channels.map((x) => '#' + x),
];
let lookup = this.hash.substring(1);
if (lookup == '🔐') {
lookup = '🔐' + this.whoami;
}
let index = channel_names.indexOf(lookup);
index = index != -1 ? index + delta : 0; index = index != -1 ? index + delta : 0;
let name = tfrpc.rpc.setHash(
channel_names[(index + channel_names.length) % channel_names.length]; '#' +
if (name == '🔐' + this.whoami) { encodeURIComponent(
name = '🔐'; channel_names[(index + channel_names.length) % channel_names.length]
} )
tfrpc.rpc.setHash('#' + encodeURIComponent(name)); );
} }
set_hash(hash) { set_hash(hash) {
@@ -231,15 +141,16 @@ class TfElement extends LitElement {
this.tab = 'search'; this.tab = 'search';
} else if (this.hash === '#connections') { } else if (this.hash === '#connections') {
this.tab = 'connections'; this.tab = 'connections';
} else if (this.hash.startsWith('#sql=')) {
this.tab = 'query';
} else { } else {
this.tab = 'news'; this.tab = 'news';
} }
} }
async fetch_about(following, users, transient) { async fetch_about(following, users) {
this.loading_about++;
let ids = Object.keys(following).sort(); let ids = Object.keys(following).sort();
const k_cache_version = 3; const k_cache_version = 1;
let cache = await tfrpc.rpc.databaseGet('about'); let cache = await tfrpc.rpc.databaseGet('about');
let original_cache = cache; let original_cache = cache;
cache = cache ? JSON.parse(cache) : {}; cache = cache ? JSON.parse(cache) : {};
@@ -247,86 +158,81 @@ class TfElement extends LitElement {
cache = { cache = {
version: k_cache_version, version: k_cache_version,
about: {}, about: {},
last_row_id: 0,
}; };
} }
let max_row_id = (
let ids_out_of_date = ids.filter( await tfrpc.rpc.query(
(x) => `
(users[x]?.seq && !cache.about[x]?.seq) || SELECT MAX(rowid) AS max_row_id FROM messages
(users[x]?.seq && users[x]?.seq > cache.about[x].seq) `,
); []
)
)[0].max_row_id;
for (let id of Object.keys(cache.about)) { for (let id of Object.keys(cache.about)) {
if (ids.indexOf(id) == -1) { if (ids.indexOf(id) == -1) {
delete cache.about[id]; delete cache.about[id];
} else {
users[id] = Object.assign(cache.about[id], users[id] || {});
} }
} }
console.log( let abouts = await tfrpc.rpc.query(
'loading about for', `
ids.length, SELECT
'accounts', messages.author, json(messages.content) AS content, messages.sequence
ids_out_of_date.length, FROM
'out of date' 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) { for (let about of abouts) {
try { let content = JSON.parse(about.content);
let rows = await tfrpc.rpc.query( if (content.about === about.author) {
` delete content.type;
SELECT all_abouts.author, json(json_group_object(all_abouts.key, all_abouts.value)) AS about delete content.about;
FROM ( cache.about[about.author] = Object.assign(
SELECT cache.about[about.author] || {},
messages.author, content
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)]
); );
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);
} }
} }
cache.last_row_id = max_row_id;
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--;
let new_cache = JSON.stringify(cache); let new_cache = JSON.stringify(cache);
if (!transient && new_cache != original_cache) { if (new_cache !== original_cache) {
let start_time = new Date(); let start_time = new Date();
tfrpc.rpc.databaseSet('about', new_cache).then(function () { tfrpc.rpc.databaseSet('about', new_cache).then(function () {
console.log('saving about took', (new Date() - start_time) / 1000); 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); return Object.assign({}, users);
} }
@@ -386,7 +292,11 @@ class TfElement extends LitElement {
ranges.push([i, Math.min(i + k_chunk_size, latest), true]); ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
} }
for (let i = cache.range[0]; i >= 0; i -= k_chunk_size) { 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 { } else {
for (let i = 0; i < latest; i += k_chunk_size) { for (let i = 0; i < latest; i += k_chunk_size) {
@@ -402,7 +312,7 @@ class TfElement extends LitElement {
messages.rowid > ?1 AND messages.rowid > ?1 AND
messages.rowid <= ?2 AND messages.rowid <= ?2 AND
json(messages.content) LIKE '"%' json(messages.content) LIKE '"%'
ORDER BY messages.rowid DESC ORDER BY sequence DESC
`, `,
[range[0], range[1]] [range[0], range[1]]
); );
@@ -428,119 +338,52 @@ class TfElement extends LitElement {
return [cache.latest, cache.messages]; return [cache.latest, cache.messages];
} }
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) { async load_channels_latest(following) {
let start_time = new Date();
let latest_private = this.get_latest_private(following); let latest_private = this.get_latest_private(following);
const k_args = [ let channels = await tfrpc.rpc.query(
JSON.stringify(this.channels), `
JSON.stringify(following), SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
'"' + this.whoami.replace('"', '""') + '"', JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value
this.whoami, JOIN json_each(?2) AS following ON messages.author = following.value
]; WHERE
let channels = ( messages.content ->> 'type' = 'post' AND
await Promise.all([ messages.content ->> 'root' IS NULL AND
tfrpc.rpc.query( messages.author != ?4
` GROUP by channel
SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages UNION
JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
JOIN json_each(?2) AS following ON messages.author = following.value JOIN json_each(?2) AS following ON messages.author = following.value
WHERE WHERE
messages.content ->> 'type' = 'post' AND messages.content ->> 'type' = 'post' AND
messages.content ->> 'root' IS NULL AND messages.content ->> 'root' IS NULL AND
messages.author != ?4 messages.author != ?4
GROUP by channel UNION
`, SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
k_args JOIN messages ON messages.rowid = messages_fts.rowid
), JOIN json_each(?2) AS following ON messages.author = following.value
tfrpc.rpc.query( WHERE messages.author != ?4
` `,
SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages [
JOIN messages_refs ON messages.id = messages_refs.message JSON.stringify(this.channels),
JOIN json_each(?1) AS channels ON messages_refs.ref = '#' || channels.value JSON.stringify(following),
JOIN json_each(?2) AS following ON messages.author = following.value '"' + this.whoami.replace('"', '""') + '"',
WHERE this.whoami,
messages.content ->> 'type' = 'post' AND ]
messages.content ->> 'root' IS NULL AND );
messages.author != ?4 this.channels_latest = Object.fromEntries(
GROUP by channel channels.map((x) => [x.channel, x.rowid])
`, );
k_args console.log('channels took', (new Date() - start_time) / 1000.0);
),
tfrpc.rpc.query(
`
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
),
tfrpc.rpc.query(
`
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
),
tfrpc.rpc.query(
`
SELECT '🚩' AS channel, MAX(messages.rowid) AS rowid FROM messages
WHERE messages.content ->> 'type' = 'flag'
`,
k_args
),
])
).flat();
let latest = {'🔐': undefined};
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 self = this; let self = this;
latest_private.then(async function (latest) { start_time = new Date();
let grouped = await self.group_private_messages(latest[1]); latest_private.then(function (latest) {
self.channels_latest = Object.assign({}, self.channels_latest, { self.channels_latest = Object.assign({}, self.channels_latest, {
'🔐': latest[0], '🔐': latest[0],
}); });
console.log('private took', (new Date() - start_time) / 1000.0);
console.log(latest);
self.private_messages = latest[1]; self.private_messages = latest[1];
self.grouped_private_messages = grouped;
}); });
} }
@@ -549,28 +392,7 @@ class TfElement extends LitElement {
this.schedule_load_latest(); 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() { schedule_load_latest() {
this.reset_progress();
if (!this.loading_latest) { if (!this.loading_latest) {
this.shadowRoot.getElementById('tf-tab-news')?.load_latest(); this.shadowRoot.getElementById('tf-tab-news')?.load_latest();
this.load(); this.load();
@@ -590,66 +412,54 @@ class TfElement extends LitElement {
[JSON.stringify(Object.keys(users))] [JSON.stringify(Object.keys(users))]
); );
for (let row of info) { for (let row of info) {
users[row.author] = Object.assign(users[row.author], { users[row.author].seq = row.max_seq;
seq: row.max_sequence, users[row.author].ts = row.max_ts;
ts: row.max_ts,
});
} }
return users; 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() { async load() {
this.loading_latest = true; this.loading_latest = true;
this.reset_progress();
try { try {
let start_time = new Date();
let whoami = this.whoami; let whoami = this.whoami;
let following = await tfrpc.rpc.following([whoami], 2); let following = await tfrpc.rpc.following([whoami], 2);
let old_users = this.users ?? {};
let users = {}; let users = {};
let by_count = []; let by_count = [];
for (let [id, v] of Object.entries(following)) { for (let [id, v] of Object.entries(following)) {
users[id] = Object.assign( users[id] = {
{ following: v.of,
following: v.of, blocking: v.ob,
blocking: v.ob, followed: v.if,
followed: v.if, blocked: v.ib,
blocked: v.ib, };
follow_depth: following[id]?.d,
},
old_users[id]
);
by_count.push({count: v.of, id: id}); by_count.push({count: v.of, id: id});
} }
let reactions = this.load_recent_reactions(); this.load_channels_latest(Object.keys(following));
let channels = this.load_channels_latest(Object.keys(following));
this.channels_unread = JSON.parse( this.channels_unread = JSON.parse(
(await tfrpc.rpc.databaseGet('unread')) ?? '{}' (await tfrpc.rpc.databaseGet('unread')) ?? '{}'
); );
this.following = Object.keys(following); 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); users = await this.fetch_user_info(users);
console.log(
'user info took',
(new Date() - start_time) / 1000.0,
'seconds'
);
this.users = users; this.users = users;
console.log(
let self = this; `load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}`
this.fetch_about(following, users).then(function (result) { );
self.users = result;
});
await reactions;
await channels;
this.whoami = whoami; this.whoami = whoami;
this.loaded = whoami; this.loaded = whoami;
} finally { } finally {
@@ -700,23 +510,13 @@ class TfElement extends LitElement {
whoami=${this.whoami} whoami=${this.whoami}
.users=${this.users} .users=${this.users}
hash=${this.hash} hash=${this.hash}
?loading=${this.loading || this.loading_about != 0} ?loading=${this.loading}
.channels=${this.channels} .channels=${this.channels}
.channels_latest=${this.channels_latest} .channels_latest=${this.channels_latest}
.channels_unread=${this.channels_unread} .channels_unread=${this.channels_unread}
@channelsetunread=${this.channel_set_unread} @channelsetunread=${this.channel_set_unread}
@refresh=${this.refresh}
@toggle_stay_connected=${this.toggle_stay_connected}
@loadmessages=${this.reset_progress}
@openprivatechat=${this.open_private_chat}
@closeprivatechat=${this.close_private_chat}
.connections=${this.connections} .connections=${this.connections}
.private_messages=${this.private_messages} .private_messages=${this.private_messages}
.visible_private_messages=${this.visible_private()}
.grouped_private_messages=${this.grouped_private_messages}
.recent_reactions=${this.recent_reactions}
?is_administrator=${this.is_administrator}
?stay_connected=${this.stay_connected}
></tf-tab-news> ></tf-tab-news>
`; `;
} else if (this.tab === 'connections') { } else if (this.tab === 'connections') {
@@ -733,19 +533,33 @@ class TfElement extends LitElement {
.following=${this.following} .following=${this.following}
whoami=${this.whoami} whoami=${this.whoami}
.users=${this.users} .users=${this.users}
query=${this.search_text()} query=${this.hash?.startsWith('#q=')
? decodeURIComponent(this.hash.substring(3))
: null}
></tf-tab-search> ></tf-tab-search>
`; `;
} else if (this.tab === 'query') {
return html`
<tf-tab-query
.following=${this.following}
whoami=${this.whoami}
.users=${this.users}
query=${this.hash?.startsWith('#sql=')
? decodeURIComponent(this.hash.substring(5))
: null}
></tf-tab-query>
`;
} }
} }
async set_tab(tab) { async set_tab(tab) {
this.tab = tab; this.tab = tab;
if (tab === 'news') { if (tab === 'news') {
this.schedule_load_latest();
await tfrpc.rpc.setHash('#'); await tfrpc.rpc.setHash('#');
} else if (tab === 'connections') { } else if (tab === 'connections') {
await tfrpc.rpc.setHash('#connections'); await tfrpc.rpc.setHash('#connections');
} else if (tab === 'query') {
await tfrpc.rpc.setHash('#sql=');
} }
} }
@@ -753,67 +567,6 @@ class TfElement extends LitElement {
tfrpc.rpc.sync(); 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');
}
}
async pick_color() {
let input = document.createElement('input');
input.type = 'color';
input.value = (await tfrpc.rpc.localStorageGet('color')) ?? '#ff0000';
input.addEventListener('change', async function () {
await tfrpc.rpc.localStorageSet('color', input.value);
window.location.reload();
});
input.click();
}
search() {
let search_text = this.renderRoot.getElementById('search_text');
if (!search_text.value.length) {
search_text.focus();
this.set_tab('search');
} else {
this.set_hash('#q=' + encodeURIComponent(search_text.value));
}
}
search_keydown(event) {
if (event.keyCode == 13) {
this.search();
}
}
search_text() {
if (this.hash.startsWith('#q=')) {
try {
return decodeURIComponent(this.hash.substring('#q='.length));
} catch {
return this.hash.substring('#q='.length);
}
}
}
async request_user(event) {
let users = {};
users[event.detail.id] = {};
users = await this.fetch_user_info(users);
if (this.users[event.detail.id]?.seq !== users[event.detail.id]?.seq) {
let self = this;
this.fetch_about(users, users, true).then(function (result) {
self.users = Object.assign({}, self.users, users);
});
}
}
render() { render() {
let self = this; let self = this;
@@ -827,19 +580,23 @@ class TfElement extends LitElement {
const k_tabs = { const k_tabs = {
'📰': 'news', '📰': 'news',
'📡': 'connections', '📡': 'connections',
'🔍': 'search',
'👩‍💻': 'query',
}; };
let tabs = html` let tabs = html`
<style>
#search_text:focus {
float: none !important;
width: 100%;
}
</style>
<div <div
class="w3-bar w3-theme-l1" class="w3-bar w3-theme-l1"
style="position: static; top: 0; z-index: 10" style="position: static; top: 0; z-index: 10"
> >
<button
class=${'w3-bar-item w3-button w3-circle w3-ripple' +
(this.connections?.some((x) => x.flags.one_shot) ? ' w3-spin' : '')}
style="width: 1.5em; height: 1.5em; padding: 8px"
@click=${this.refresh}
>
</button>
${Object.entries(k_tabs).map( ${Object.entries(k_tabs).map(
([k, v]) => html` ([k, v]) => html`
<button <button
@@ -856,39 +613,6 @@ class TfElement extends LitElement {
</button> </button>
` `
)} )}
<button
class="w3-bar-item w3-button w3-right"
@click=${this.pick_color}
>
🎨<span class="w3-hide-small">Color</span>
</button>
${
this.is_administrator
? html`
<button
class="w3-bar-item w3-button w3-circle w3-right"
@click=${this.refresh}
>
<span
style="display: inline-block"
class=${this.connections?.some((x) => x.flags.one_shot)
? ' w3-spin'
: ''}
>
</span>
</button>
<button
class="w3-bar-item w3-button w3-right"
@click=${this.toggle_stay_connected}
>
${this.stay_connected ? '🔗' : '⛓️‍💥'}
</button>
`
: undefined
}
<button class="w3-bar-item w3-button w3-right" @click=${this.search}>🔍<span class="w3-hide-small">Search</span></button>
<input type="text" class=${'w3-input w3-bar-item w3-right w3-theme-d1' + (this.tab == 'search' ? ' w3-mobile' : ' w3-hide-small')} placeholder="keywords, @id, #channel" id="search_text" @keydown=${this.search_keydown} value=${this.search_text()}></input>
</div> </div>
`; `;
let contents = this.guest let contents = this.guest
@@ -912,27 +636,11 @@ class TfElement extends LitElement {
Loading... Loading...
</div>` </div>`
: this.render_tab(); : 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` return html`
<style>
${generate_theme()}
</style>
<div <div
style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column" style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
class="w3-theme-dark" class="w3-theme-dark"
@tf-request-user=${this.request_user}
> >
${progress}
<div style="flex: 0 0">${tabs}</div> <div style="flex: 0 0">${tabs}</div>
<div style="flex: 1 1; overflow: auto; contain: layout"> <div style="flex: 1 1; overflow: auto; contain: layout">
${contents} ${contents}

View File

@@ -1,7 +1,7 @@
import {LitElement, html, unsafeHTML, live} from './lit-all.min.js'; import {LitElement, html, unsafeHTML, live} from './lit-all.min.js';
import * as tfutils from './tf-utils.js'; import * as tfutils from './tf-utils.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {styles, generate_theme} from './tf-styles.js'; import {styles} from './tf-styles.js';
import Tribute from './tribute.esm.js'; import Tribute from './tribute.esm.js';
class TfComposeElement extends LitElement { class TfComposeElement extends LitElement {
@@ -16,7 +16,6 @@ class TfComposeElement extends LitElement {
author: {type: String}, author: {type: String},
channel: {type: String}, channel: {type: String},
new_thread: {type: Boolean}, new_thread: {type: Boolean},
recipients: {type: Array},
}; };
} }
@@ -92,9 +91,7 @@ class TfComposeElement extends LitElement {
bubbles: true, bubbles: true,
composed: true, composed: true,
detail: { detail: {
id: id: this.branch,
this.branch ??
(this.recipients ? this.recipients.join(',') : undefined),
draft: draft, draft: draft,
}, },
}) })
@@ -258,12 +255,10 @@ class TfComposeElement extends LitElement {
let self = this; let self = this;
let input = document.createElement('input'); let input = document.createElement('input');
input.type = 'file'; input.type = 'file';
input.addEventListener('change', function (event) { input.onchange = function (event) {
input.parentNode.removeChild(input);
let file = event.target.files[0]; let file = event.target.files[0];
self.add_file(file); self.add_file(file);
}); };
document.body.appendChild(input);
input.click(); input.click();
} }
@@ -294,7 +289,7 @@ class TfComposeElement extends LitElement {
} }
} }
get_values() { firstUpdated() {
let values = Object.entries(this.users).map((x) => ({ let values = Object.entries(this.users).map((x) => ({
key: x[1].name ?? x[0], key: x[1].name ?? x[0],
value: x[0], value: x[0],
@@ -310,15 +305,11 @@ class TfComposeElement extends LitElement {
values values
); );
} }
return values;
}
firstUpdated() {
let tribute = new Tribute({ let tribute = new Tribute({
iframe: this.shadowRoot, iframe: this.shadowRoot,
collection: [ collection: [
{ {
values: this.get_values(), values: values,
selectTemplate: function (item) { selectTemplate: function (item) {
return item return item
? `[@${item.original.key}](${item.original.value})` ? `[@${item.original.key}](${item.original.value})`
@@ -337,7 +328,6 @@ class TfComposeElement extends LitElement {
], ],
}); });
tribute.attach(this.renderRoot.getElementById('edit')); tribute.attach(this.renderRoot.getElementById('edit'));
this._tribute = tribute;
} }
updated() { updated() {
@@ -348,7 +338,6 @@ class TfComposeElement extends LitElement {
preview.innerHTML = this.process_text(edit.innerText); preview.innerHTML = this.process_text(edit.innerText);
this.last_updated_text = edit.innerText; this.last_updated_text = edit.innerText;
} }
this._tribute.collection[0].values = this.get_values();
let encrypt = this.renderRoot.getElementById('encrypt_to'); let encrypt = this.renderRoot.getElementById('encrypt_to');
if (encrypt) { if (encrypt) {
let tribute = new Tribute({ let tribute = new Tribute({
@@ -368,7 +357,7 @@ class TfComposeElement extends LitElement {
remove_mention(id) { remove_mention(id) {
let draft = this.get_draft(); let draft = this.get_draft();
delete draft.mentions[id]; delete draft.mentions[id];
setTimeout(() => this.notify(draft), 0); setTimeout(() => this.notify(), 0);
} }
render_mention(mention) { render_mention(mention) {
@@ -455,15 +444,12 @@ class TfComposeElement extends LitElement {
self.apps = await tfrpc.rpc.apps(); self.apps = await tfrpc.rpc.apps();
} }
if (!this.apps) { if (!this.apps) {
return html`<button return html`<button class="w3-button w3-theme-d1" @click=${attach_app}>
class="w3-button w3-bar-item w3-theme-d1"
@click=${attach_app}
>
Attach App Attach App
</button>`; </button>`;
} else { } else {
return html`<button return html`<button
class="w3-button w3-bar-item w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => (this.apps = null)} @click=${() => (this.apps = null)}
> >
Discard App Discard App
@@ -484,9 +470,18 @@ class TfComposeElement extends LitElement {
if (draft.content_warning !== undefined) { if (draft.content_warning !== undefined) {
return html` return html`
<div class="w3-container w3-padding"> <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> <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> </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() { get_draft() {
let key = return this.drafts[this.branch || ''] || {};
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;
} }
update_encrypt(event) { update_encrypt(event) {
@@ -537,7 +522,7 @@ class TfComposeElement extends LitElement {
return html` return html`
<div style="display: flex; flex-direction: row; width: 100%"> <div style="display: flex; flex-direction: row; width: 100%">
<label for="encrypt_to">🔐 To:</label> <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> <button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button>
</div> </div>
<ul> <ul>
@@ -559,31 +544,6 @@ class TfComposeElement extends LitElement {
this.requestUpdate(); 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() { render() {
let self = this; let self = this;
let draft = self.get_draft(); let draft = self.get_draft();
@@ -597,15 +557,12 @@ class TfComposeElement extends LitElement {
draft.encrypt_to !== undefined draft.encrypt_to !== undefined
? undefined ? undefined
: html`<button : html`<button
class="w3-button w3-bar-item w3-theme-d1" class="w3-button w3-theme-d1"
@click=${() => this.set_encrypt([])} @click=${() => this.set_encrypt([])}
> >
🔐 Encrypt 🔐
</button>`; </button>`;
let result = html` let result = html`
<style>
${generate_theme()}
</style>
<style> <style>
.w3-input:empty::before { .w3-input:empty::before {
content: attr(placeholder); content: attr(placeholder);
@@ -624,11 +581,11 @@ class TfComposeElement extends LitElement {
: undefined} : undefined}
${this.render_encrypt()} ${this.render_encrypt()}
</header> </header>
<div class="w3-container" style="padding: 0 0 16px 0"> <div class="w3-container w3-padding-small">
<div class="w3-half"> <div class="w3-half">
<span <span
class="w3-input w3-theme-d1 w3-border" class="w3-input w3-theme-d1 w3-border"
style="resize: vertical; width: 100%; white-space: pre-wrap" style="resize: vertical; width: 100%; overflow: hidden; white-space: pre-wrap"
placeholder="Write a post here." placeholder="Write a post here."
id="edit" id="edit"
@input=${this.input} @input=${this.input}
@@ -645,7 +602,7 @@ class TfComposeElement extends LitElement {
${Object.values(draft.mentions || {}).map((x) => ${Object.values(draft.mentions || {}).map((x) =>
self.render_mention(x) self.render_mention(x)
)} )}
<footer> <footer class="w3-container">
${this.render_attach_app()} ${this.render_content_warning()} ${this.render_attach_app()} ${this.render_content_warning()}
${this.render_new_thread()} ${this.render_new_thread()}
<button <button
@@ -655,43 +612,13 @@ class TfComposeElement extends LitElement {
> >
Submit Submit
</button> </button>
<div class="w3-dropdown-click"> <button class="w3-button w3-theme-d1" @click=${this.attach}>
<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}> Attach
⚙️ </button>
</button> ${this.render_attach_app_button()} ${encrypt}
<div class="w3-dropdown-content w3-bar-block"> <button class="w3-button w3-theme-d1" @click=${this.discard}>
${this.get_draft().content_warning === undefined Discard
? html` </button>
<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>
</footer> </footer>
</div> </div>
`; `;

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import {LitElement, html, unsafeHTML, repeat, until} from './lit-all.min.js'; import {LitElement, html, unsafeHTML, repeat, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {styles, generate_theme} from './tf-styles.js'; import {styles} from './tf-styles.js';
class TfNewsElement extends LitElement { class TfNewsElement extends LitElement {
static get properties() { static get properties() {
@@ -13,8 +13,6 @@ class TfNewsElement extends LitElement {
expanded: {type: Object}, expanded: {type: Object},
channel: {type: String}, channel: {type: String},
channel_unread: {type: Number}, channel_unread: {type: Number},
recent_reactions: {type: Array},
hash: {type: String},
}; };
} }
@@ -30,13 +28,14 @@ class TfNewsElement extends LitElement {
this.drafts = {}; this.drafts = {};
this.expanded = {}; this.expanded = {};
this.channel_unread = -1; this.channel_unread = -1;
this.recent_reactions = [];
} }
process_messages(messages) { process_messages(messages) {
let self = this; let self = this;
let messages_by_id = {}; let messages_by_id = {};
console.log('processing', messages.length, 'messages');
function ensure_message(id, rowid) { function ensure_message(id, rowid) {
let found = messages_by_id[id]; let found = messages_by_id[id];
if (found) { if (found) {
@@ -57,9 +56,6 @@ class TfNewsElement extends LitElement {
} }
function link_message(message) { function link_message(message) {
if (!message.content) {
return;
}
if (message.content.type === 'vote') { if (message.content.type === 'vote') {
let parent = ensure_message(message.content.vote.link, message.rowid); let parent = ensure_message(message.content.vote.link, message.rowid);
if (!parent.votes) { if (!parent.votes) {
@@ -67,16 +63,6 @@ class TfNewsElement extends LitElement {
} }
parent.votes.push(message); parent.votes.push(message);
message.parent_message = message.content.vote.link; message.parent_message = message.content.vote.link;
} else if (message.content.type == 'flag') {
let parent = ensure_message(message.content.flag.link, message.rowid);
if (!parent.flags) {
parent.flags = [];
}
parent.flags.push(message);
parent.flags = Object.values(
Object.fromEntries(parent.flags.map((x) => [x.id, x]))
);
message.parent_message = message.content.flag.link;
} else if (message.content.type == 'post') { } else if (message.content.type == 'post') {
if (message.content.root) { if (message.content.root) {
if (typeof message.content.root === 'string') { if (typeof message.content.root === 'string') {
@@ -95,22 +81,6 @@ class TfNewsElement extends LitElement {
message.parent_message = message.content.root[0]; message.parent_message = message.content.root[0];
} }
} }
} else {
let parent_id = message.content.about?.startswith?.('%')
? message.content.about
: message.content.updates?.startsWith?.('%')
? message.content.updates
: message.content.parent?.startsWith?.('%')
? message.content.parent
: undefined;
if (parent_id) {
let parent = ensure_message(parent_id, message.rowid);
if (!parent?.refs) {
parent.refs = [];
}
parent.refs.push(message);
message.parent_message = parent_id;
}
} }
} }
@@ -133,7 +103,6 @@ class TfNewsElement extends LitElement {
message.parent_message = placeholder.parent_message; message.parent_message = placeholder.parent_message;
message.child_messages = placeholder.child_messages; message.child_messages = placeholder.child_messages;
message.votes = placeholder.votes; message.votes = placeholder.votes;
message.flags = placeholder.flags;
if ( if (
placeholder.parent_message && placeholder.parent_message &&
messages_by_id[placeholder.parent_message] messages_by_id[placeholder.parent_message]
@@ -188,80 +157,46 @@ class TfNewsElement extends LitElement {
return recursive_sort(roots, true); return recursive_sort(roots, true);
} }
group_messages(messages) { group_following(messages) {
let result = []; let result = [];
let group = []; let group = [];
let type = undefined;
for (let message of messages) { for (let message of messages) {
if ( if (message?.content?.type === 'contact') {
message?.content?.type === 'contact' ||
message?.content?.type === 'channel'
) {
if (type && message.content.type !== type) {
if (group.length == 1) {
result.push(group[0]);
group = [];
} else if (group.length > 1) {
result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: `${type}_group`,
messages: group,
});
group = [];
}
}
type = message.content.type;
group.push(message); group.push(message);
} else { } else {
if (group.length == 1) { if (group.length > 0) {
result.push(group[0]);
group = [];
} else if (group.length > 1) {
result.push({ result.push({
rowid: Math.max(...group.map((x) => x.rowid)), rowid: Math.max(...group.map((x) => x.rowid)),
type: `${type}_group`, type: 'contact_group',
messages: group, messages: group,
}); });
group = []; group = [];
} }
result.push(message); result.push(message);
type = undefined;
} }
} }
if (group.length == 1) { if (group.length > 0) {
result.push(group[0]);
group = [];
} else if (group.length > 1) {
result.push({ result.push({
rowid: Math.max(...group.map((x) => x.rowid)), rowid: Math.max(...group.map((x) => x.rowid)),
type: `${type}_group`, type: 'contact_group',
messages: group, messages: group,
}); });
} }
return result; return result;
} }
unread_allowed() {
return !this.hash?.startsWith('#%') && !this.hash?.startsWith('#@');
}
load_and_render(messages) { load_and_render(messages) {
let messages_by_id = this.process_messages(messages); let messages_by_id = this.process_messages(messages);
let final_messages = this.group_messages( let final_messages = this.group_following(
this.finalize_messages(messages_by_id) this.finalize_messages(messages_by_id)
); );
let unread_rowid = -1; let unread_rowid = -1;
if (this.unread_allowed()) { for (let message of final_messages) {
for (let message of final_messages) { if (message.rowid >= this.channel_unread) {
if (message.rowid >= this.channel_unread) { unread_rowid = message.rowid;
unread_rowid = message.rowid;
}
} }
} }
return html` return html`
<style>
${generate_theme()}
</style>
<div> <div>
${repeat( ${repeat(
final_messages, final_messages,
@@ -276,26 +211,13 @@ class TfNewsElement extends LitElement {
collapsed="true" collapsed="true"
channel=${this.channel} channel=${this.channel}
channel_unread=${this.channel_unread} channel_unread=${this.channel_unread}
.recent_reactions=${this.recent_reactions}
></tf-message> ></tf-message>
${x.rowid == unread_rowid ${x.rowid == unread_rowid
? html`<div style="display: flex; flex-direction: row"> ? html`<div style="display: flex; flex-direction: row">
<div <div
style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px" style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
></div> ></div>
<button <div style="color: #f00; padding: 8px">unread</div>
style="color: #f00; padding: 8px"
class="w3-button"
@click=${() =>
this.dispatchEvent(
new Event('mark_all_read', {
bubbles: true,
composed: true,
})
)}
>
unread
</button>
<div <div
style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px" style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
></div> ></div>

View File

@@ -1,7 +1,7 @@
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 tfrpc from '/static/tfrpc.js';
import * as tfutils from './tf-utils.js'; import * as tfutils from './tf-utils.js';
import {styles, generate_theme} from './tf-styles.js'; import {styles} from './tf-styles.js';
class TfProfileElement extends LitElement { class TfProfileElement extends LitElement {
static get properties() { static get properties() {
@@ -11,10 +11,8 @@ class TfProfileElement extends LitElement {
id: {type: String}, id: {type: String},
users: {type: Object}, users: {type: Object},
size: {type: Number}, size: {type: Number},
sequence: {type: Number},
following: {type: Boolean}, following: {type: Boolean},
blocking: {type: Boolean}, blocking: {type: Boolean},
show_followed: {type: Boolean},
}; };
} }
@@ -28,7 +26,6 @@ class TfProfileElement extends LitElement {
this.id = null; this.id = null;
this.users = {}; this.users = {};
this.size = 0; this.size = 0;
this.sequence = 0;
} }
async load() { async load() {
@@ -37,22 +34,16 @@ class TfProfileElement extends LitElement {
this.following = undefined; this.following = undefined;
this.blocking = undefined; this.blocking = undefined;
let latest = (
await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages')
)[0].latest;
let result = await tfrpc.rpc.query( let result = await tfrpc.rpc.query(
` `
SELECT json_extract(content, '$.following') AS following SELECT json_extract(content, '$.following') AS following
FROM messages WHERE author = ? AND FROM messages WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? AND json_extract(content, '$.contact') = ? AND
following IS NOT NULL AND following IS NOT NULL
messages.rowid <= ?
ORDER BY sequence DESC LIMIT 1 ORDER BY sequence DESC LIMIT 1
`, `,
[this.whoami, this.id, latest], [this.whoami, this.id]
{cacheable: true}
); );
this.following = result?.[0]?.following ?? false; this.following = result?.[0]?.following ?? false;
result = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
@@ -61,12 +52,10 @@ class TfProfileElement extends LitElement {
FROM messages WHERE author = ? AND FROM messages WHERE author = ? AND
json_extract(content, '$.type') = 'contact' AND json_extract(content, '$.type') = 'contact' AND
json_extract(content, '$.contact') = ? AND json_extract(content, '$.contact') = ? AND
blocking IS NOT NULL AND blocking IS NOT NULL
messages.rowid <= ?
ORDER BY sequence DESC LIMIT 1 ORDER BY sequence DESC LIMIT 1
`, `,
[this.whoami, this.id, latest], [this.whoami, this.id]
{cacheable: true}
); );
this.blocking = result?.[0]?.blocking ?? false; this.blocking = result?.[0]?.blocking ?? false;
} }
@@ -150,8 +139,7 @@ class TfProfileElement extends LitElement {
let self = this; let self = this;
let input = document.createElement('input'); let input = document.createElement('input');
input.type = 'file'; input.type = 'file';
input.addEventListener('change', function (event) { input.onchange = function (event) {
input.parentNode.removeChild(input);
let file = event.target.files[0]; let file = event.target.files[0];
file file
.arrayBuffer() .arrayBuffer()
@@ -166,8 +154,7 @@ class TfProfileElement extends LitElement {
.catch(function (e) { .catch(function (e) {
alert(e.message); alert(e.message);
}); });
}); };
document.body.appendChild(input);
input.click(); input.click();
} }
@@ -175,98 +162,17 @@ class TfProfileElement extends LitElement {
navigator.clipboard.writeText(this.id); 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);
delete accounts[this.id];
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>
`;
}
open_private_chat() {
let hash = '#🔐' + (this.id != this.whoami ? this.id : '');
this.dispatchEvent(
new CustomEvent('openprivatechat', {
bubbles: true,
composed: true,
detail: {
key: JSON.stringify([this.id]),
},
})
);
tfrpc.rpc.setHash(hash);
}
render() { render() {
this.load(); this.load();
let self = this; let self = this;
let profile = this.users[this.id] || {}; let profile = this.users[this.id] || {};
tfrpc.rpc tfrpc.rpc
.query( .query(
`SELECT size AS size, max_sequence AS sequence FROM messages_stats WHERE author = ?`, `SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
[this.id] [this.id]
) )
.then(function (result) { .then(function (result) {
self.size = result[0].size; self.size = result[0].size;
self.sequence = result[0].sequence;
}); });
let edit; let edit;
let follow; let follow;
@@ -274,18 +180,16 @@ class TfProfileElement extends LitElement {
if (this.id === this.whoami) { if (this.id === this.whoami) {
if (this.editing) { if (this.editing) {
edit = html` edit = html`
<div style="margin-top: 8px"> <button
<button id="save_profile"
id="save_profile" class="w3-button w3-theme-d1"
class="w3-button w3-theme-l1" @click=${this.save_edits}
@click=${this.save_edits} >
> Save Profile
Save Profile </button>
</button> <button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}> Discard
Discard </button>
</button>
</div>
`; `;
} else { } else {
edit = html`<button edit = html`<button
@@ -331,44 +235,34 @@ class TfProfileElement extends LitElement {
<div> <div>
<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button> <button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
</div> </div>
</div>` </div>`
: null; : null;
let image = profile.image; let image =
if (typeof image == 'string' && !image.startsWith('&')) { typeof profile.image == 'string' ? profile.image : profile.image?.link;
try {
image = JSON.parse(image)?.link;
} catch {}
}
image = this.editing?.image ?? image; image = this.editing?.image ?? image;
let description = this.editing?.description ?? profile.description; let description = this.editing?.description ?? profile.description;
return html` return html`<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box">
<style>${generate_theme()}</style>
<div class="w3-card-4 w3-theme-d3" style="box-sizing: border-box">
<header class="w3-container"> <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> </header>
<div class="w3-container" @click=${this.body_click}> <div class="w3-container">
<div class="w3-margin-bottom" style="display: flex; flex-direction: row"> <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> <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> <button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button>
</div> </div>
<div class=${this.editing ? 'w3-card' : ''}> <div style="display: flex; flex-direction: row; gap: 1em">
${this.editing ? html`<header class="w3-container w3-theme-l2"><h2>Editing Your Profile</h2></header>` : undefined} ${edit_profile}
<div style="display: flex; flex-direction: row; gap: 1em" class="w3-margin"> <div style="flex: 1 0 50%">
${edit_profile} ${
<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal"> image
${ ? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>`
image : html`<div>
? html`<div><img src=${'/' + image + '/view'} style="width: min(256px, 100%); height: auto"></img></div>` <div class="w3-jumbo">😎</div>
: html`<div> <div><i>Profile image not set.</i></div>
<div class="w3-jumbo">😎</div> </div>`
<div><i>Profile image not set.</i></div> }
</div>` <div>${unsafeHTML(tfutils.markdown(description))}</div>
}
<div>${unsafeHTML(tfutils.markdown(description))}</div>
</div>
</div> </div>
${this.editing ? html`<footer class="w3-container w3-theme-l2"><p>${edit}</p></footer>` : undefined}
</div> </div>
<div> <div>
Following ${profile.following} identities. Following ${profile.following} identities.
@@ -377,13 +271,9 @@ class TfProfileElement extends LitElement {
Blocked by ${profile.blocked} identities. Blocked by ${profile.blocked} identities.
</div> </div>
</div> </div>
${until(this.load_follows(), html`<p>Loading accounts followed...</p>`)}
<footer class="w3-container"> <footer class="w3-container">
<p> <p>
<button class="w3-button w3-theme-d1" @click=${this.open_private_chat} id="open_private_chat"> ${edit}
Open Private Chat
</button>
${this.editing ? undefined : edit}
${follow} ${follow}
${block} ${block}
</p> </p>

View File

@@ -1,5 +1,5 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js'; import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import {styles, generate_theme} from './tf-styles.js'; import {styles} from './tf-styles.js';
class TfReactionsModalElement extends LitElement { class TfReactionsModalElement extends LitElement {
static get properties() { static get properties() {
@@ -24,57 +24,47 @@ class TfReactionsModalElement extends LitElement {
render() { render() {
let self = this; let self = this;
return this.votes?.length return this.votes?.length
? html` <style> ? html` <div
${generate_theme()} class="w3-modal w3-animate-opacity"
</style> style="display: block; box-sizing: border-box; z-index: 10"
@click=${this.clear}
>
<div <div
class="w3-modal w3-animate-opacity" class="w3-modal-content w3-card-4 w3-theme-d1"
style="display: block; box-sizing: border-box; z-index: 10" onclick="event.stopPropagation()"
@click=${this.clear}
> >
<div <div class="w3-container w3-padding">
class="w3-modal-content w3-card-4 w3-theme-d1" <header class="w3-container">
onclick="event.stopPropagation()" <h2>Reactions</h2>
> <span class="w3-button w3-display-topright" @click=${this.clear}
<div class="w3-container w3-padding"> >&times;</span
<header class="w3-container"> >
<h2>Reactions</h2> </header>
<span <ul class="w3-theme-dark w3-container w3-ul">
class="w3-button w3-display-topright" ${this.votes.map(
@click=${this.clear} (x) => html`
>&times;</span <li class="w3-bar">
> <span class="w3-bar-item"
</header> >${x?.content?.vote?.expression}</span
<ul class="w3-theme-dark w3-container w3-ul"> >
${this.votes <tf-user
.sort((x, y) => y.timestamp - x.timestamp) class="w3-bar-item"
.map( id=${x.author}
(x) => html` .users=${this.users}
<li ></tf-user>
style="display: flex; flex-direction: row; gap: 4px" <span class="w3-bar-item w3-right"
> >${new Date(x?.timestamp).toLocaleString()}</span
<span style="flex-basis: 3em" >
>${x?.content?.vote?.expression}</span </li>
> `
<tf-user )}
style="flex: 1 1; overflow: hidden" </ul>
id=${x.author} <footer class="w3-container w3-padding">
.users=${this.users} <button class="w3-button" @click=${this.clear}>Close</button>
></tf-user> </footer>
<span
style="flex-shrink: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
>${new Date(x?.timestamp).toLocaleString()}</span
>
</li>
`
)}
</ul>
<footer class="w3-container w3-padding">
<button class="w3-button" @click=${this.clear}>Close</button>
</footer>
</div>
</div> </div>
</div>` </div>
</div>`
: undefined; : undefined;
} }
} }

View File

@@ -1,5 +1,4 @@
import {css, unsafeCSS, until} from './lit-all.min.js'; import {css, unsafeCSS} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
const tf = css` const tf = css`
img { img {
@@ -44,14 +43,12 @@ const tf = css`
border-left: 4px solid #fff; border-left: 4px solid #fff;
padding: 8px; padding: 8px;
padding-left: 12px; padding-left: 12px;
margin-left: 0;
margin-right: 0;
} }
`; `;
// prettier-ignore // prettier-ignore
const w3 = css` 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} html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ /* 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} html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
@@ -161,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-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-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-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,.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-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-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
@@ -206,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-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important} .w3-hover-none:hover{box-shadow:none!important}
/* Colors */ /* 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-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-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-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-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
@@ -223,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-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-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-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-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-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-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-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-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover,.w3-secondary{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-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-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-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-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-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
@@ -409,8 +393,16 @@ function is_dark(hex, value) {
return (r * 299 + g * 587 + b * 114) / 1000 < value; return (r * 299 + g * 587 + b * 114) / 1000 < value;
} }
export function generate(color) { function generated() {
let [r, g, b] = hex_to_rgb(color); let now = new Date();
let k_color = rgb_to_hex([
(now.getDay() * 128) / 6,
(now.getHours() * 128) / 23,
(now.getSeconds() * 128) / 59,
]);
//let k_color = '#034f84';
//let k_color = rgb_to_hex([Math.random() * 256, Math.random() * 256, Math.random() * 256]);
let [r, g, b] = hex_to_rgb(k_color);
let [h, s, l] = rgb_to_hsl(r, g, b); let [h, s, l] = rgb_to_hsl(r, g, b);
let theme1 = { let theme1 = {
@@ -454,28 +446,4 @@ export function generate(color) {
return unsafeCSS(result); return unsafeCSS(result);
} }
let g_theme; export let styles = [tf, w3, generated()];
export function generate_theme() {
return g_theme
? g_theme
: until(
tfrpc.rpc.localStorageGet('color').then(function (value) {
g_theme = generate(value ?? '#034f84');
return g_theme;
}),
generated_now()
);
}
function generated_now() {
let now = new Date();
return generate(
rgb_to_hex([
(now.getDay() * 128) / 6,
(now.getHours() * 128) / 23,
(now.getSeconds() * 128) / 59,
])
);
}
export let styles = [tf, w3];

View File

@@ -1,6 +1,6 @@
import {LitElement, html} from './lit-all.min.js'; import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {styles, generate_theme} from './tf-styles.js'; import {styles} from './tf-styles.js';
class TfTabConnectionsElement extends LitElement { class TfTabConnectionsElement extends LitElement {
static get properties() { static get properties() {
@@ -15,7 +15,6 @@ class TfTabConnectionsElement extends LitElement {
connect_attempt: {type: Object}, connect_attempt: {type: Object},
connect_message: {type: String}, connect_message: {type: String},
connect_success: {type: Boolean}, connect_success: {type: Boolean},
peer_exchange: {type: Boolean},
}; };
} }
@@ -48,20 +47,6 @@ class TfTabConnectionsElement extends LitElement {
tfrpc.rpc.getServerIdentity().then(function (identity) { tfrpc.rpc.getServerIdentity().then(function (identity) {
self.server_identity = identity; self.server_identity = identity;
}); });
this.check_peer_exchange();
}
async check_peer_exchange() {
if (await tfrpc.rpc.isAdministrator()) {
this.peer_exchange = await tfrpc.rpc.globalSettingsGet('peer_exchange');
} else {
this.peer_exchange = undefined;
}
}
async enable_peer_exchange() {
await tfrpc.rpc.globalSettingsSet('peer_exchange', true);
await this.check_peer_exchange();
} }
render_connection_summary(connection) { render_connection_summary(connection) {
@@ -118,23 +103,6 @@ class TfTabConnectionsElement extends LitElement {
</div>`; </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) { render_broadcast(connection) {
let self = this; let self = this;
return html` return html`
@@ -186,16 +154,6 @@ class TfTabConnectionsElement extends LitElement {
: undefined} : undefined}
${connection.flags.one_shot ? '🔃' : undefined} ${connection.flags.one_shot ? '🔃' : undefined}
<tf-user id=${connection.id} .users=${this.users}></tf-user> <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 ${connection.tunnel !== undefined
? '🚇' ? '🚇'
: html`(${connection.host}:${connection.port})`} : html`(${connection.host}:${connection.port})`}
@@ -248,44 +206,10 @@ 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() { render() {
let self = this; let self = this;
return html` return html`
<style>
${generate_theme()}
</style>
<div class="w3-container" style="box-sizing: border-box"> <div class="w3-container" style="box-sizing: border-box">
<div
class=${'w3-panel w3-padding w3-theme-l3' +
(this.peer_exchange !== false ? ' w3-hide' : '')}
>
<p>
Looking for connections? Enabling this option will include publicly
advertised rooms and pubs among the list of discovered connections
to help you replicate.
</p>
<button
class="w3-button w3-theme-d1"
@click=${this.enable_peer_exchange}
>
🔍🌐 Use publicly advertised peers
</button>
</div>
<h2>New Connection</h2> <h2>New Connection</h2>
<textarea class="w3-input w3-theme-d1" id="code"></textarea> <textarea class="w3-input w3-theme-d1" id="code"></textarea>
${this.render_message(this.renderRoot.getElementById('code')?.value)} ${this.render_message(this.renderRoot.getElementById('code')?.value)}
@@ -296,33 +220,27 @@ class TfTabConnectionsElement extends LitElement {
> >
Connect Connect
</button> </button>
<h2 <h2>Broadcasts</h2>
class="w3-button w3-block w3-theme-d1" <ul class="w3-ul w3-border">
@click=${() => self.toggle_accordian('connections')} ${this.broadcasts
> .filter((x) => x.address)
Connections (${this.valid_connections().length}) .filter(
</h2> (x) => self.connections.map((c) => c.id).indexOf(x.pubkey) == -1
<ul class="w3-ul w3-border" id="connections"> )
${this.valid_connections().map( .map((x) => self.render_broadcast(x))}
(x) => html` <li class="w3-bar">${this.render_connection(x)}</li> `
)}
</ul> </ul>
<h2 <h2>Connections</h2>
class="w3-button w3-block w3-theme-d1" <ul class="w3-ul w3-border">
@click=${() => self.toggle_accordian('broadcasts')} ${this.connections
> .filter((x) => x.tunnel === undefined)
Discovery (${this.valid_broadcasts().length}) .map(
</h2> (x) => html`
<ul class="w3-ul w3-border w3-hide" id="broadcasts"> <li class="w3-bar">${this.render_connection(x)}</li>
${this.valid_broadcasts().map((x) => self.render_broadcast(x))} `
)}
</ul> </ul>
<h2 <h2>Stored Connections</h2>
class="w3-button w3-block w3-theme-d1" <ul class="w3-ul w3-border">
@click=${() => self.toggle_accordian('stored_connections')}
>
Stored Connections (${this.stored_connections.length})
</h2>
<ul class="w3-ul w3-border w3-hide" id="stored_connections">
${this.stored_connections.map( ${this.stored_connections.map(
(x) => html` (x) => html`
<li> <li>
@@ -342,12 +260,6 @@ class TfTabConnectionsElement extends LitElement {
<div class="w3-bar-item"> <div class="w3-bar-item">
<tf-user id=${x.pubkey} .users=${self.users}></tf-user> <tf-user id=${x.pubkey} .users=${self.users}></tf-user>
<div><small>${x.address}:${x.port}</small></div> <div><small>${x.address}:${x.port}</small></div>
<div>
<small
>Last connection:
${new Date(x.last_success * 1000)}</small
>
</div>
</div> </div>
</div> </div>
${this.render_message(x)} ${this.render_message(x)}
@@ -355,13 +267,8 @@ class TfTabConnectionsElement extends LitElement {
` `
)} )}
</ul> </ul>
<h2 <h2>Local Accounts</h2>
class="w3-button w3-block w3-theme-d1" <div class="w3-container">
@click=${() => self.toggle_accordian('local_accounts')}
>
Local Accounts (${this.identities.length})
</h2>
<div class="w3-container w3-hide" id="local_accounts">
${this.identities.map( ${this.identities.map(
(x) => (x) =>
html`<div html`<div

View File

@@ -1,6 +1,6 @@
import {LitElement, cache, html, unsafeHTML, until} from './lit-all.min.js'; import {LitElement, cache, html, unsafeHTML, until} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {styles, generate_theme} from './tf-styles.js'; import {styles} from './tf-styles.js';
class TfTabNewsFeedElement extends LitElement { class TfTabNewsFeedElement extends LitElement {
static get properties() { static get properties() {
@@ -18,8 +18,6 @@ class TfTabNewsFeedElement extends LitElement {
time_range: {type: Array}, time_range: {type: Array},
time_loading: {type: Array}, time_loading: {type: Array},
private_messages: {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.start_time = new Date().valueOf();
this.time_range = [0, 0]; this.time_range = [0, 0];
this.time_loading = undefined; this.time_loading = undefined;
this.recent_reactions = [];
this.loading = 0; this.loading = 0;
} }
@@ -49,82 +46,20 @@ class TfTabNewsFeedElement extends LitElement {
: this.hash.substring(1); : 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 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)),
]
)
);
return result;
}
async fetch_messages(start_time, end_time) { async fetch_messages(start_time, end_time) {
this.dispatchEvent(
new CustomEvent('loadmessages', {
bubbles: true,
composed: true,
})
);
this.time_loading = [start_time, end_time]; this.time_loading = [start_time, end_time];
let result; let result;
const k_max_results = 64;
if (this.hash == '#@') { if (this.hash == '#@') {
result = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
` `
WITH mentions AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature WITH mentions AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_refs FROM messages_fts(?1)
JOIN messages ON messages.id = messages_refs.message JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?2) AS following ON messages.author = following.value JOIN json_each(?2) AS following ON messages.author = following.value
WHERE WHERE
messages_refs.ref = ?1 AND
messages.author != ?1 AND messages.author != ?1 AND
(?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4 (?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 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 FROM mentions
JOIN messages_refs ON mentions.id = messages_refs.ref JOIN messages_refs ON mentions.id = messages_refs.ref
@@ -133,11 +68,10 @@ class TfTabNewsFeedElement extends LitElement {
SELECT TRUE AS is_primary, * FROM mentions SELECT TRUE AS is_primary, * FROM mentions
`, `,
[ [
this.whoami, '"' + this.whoami.replace('"', '""') + '"',
JSON.stringify(this.following), JSON.stringify(this.following),
start_time, start_time,
end_time, end_time,
k_max_results,
] ]
); );
} else if (this.hash.startsWith('#@')) { } else if (this.hash.startsWith('#@')) {
@@ -147,7 +81,7 @@ class TfTabNewsFeedElement extends LitElement {
selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages FROM messages
WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3 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 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 FROM selected
@@ -156,7 +90,7 @@ class TfTabNewsFeedElement extends LitElement {
UNION UNION
SELECT TRUE AS is_primary, * FROM selected 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('#%')) { } else if (this.hash.startsWith('#%')) {
result = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
@@ -173,37 +107,44 @@ class TfTabNewsFeedElement extends LitElement {
[this.hash.substring(1)] [this.hash.substring(1)]
); );
} else if (this.hash.startsWith('##')) { } else if (this.hash.startsWith('##')) {
let initial_messages = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
` `
WITH WITH
all_news AS ( 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 SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages FROM messages
JOIN json_each(?) AS following ON messages.author = following.value 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 UNION
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature 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 FROM messages_fts(?5)
JOIN messages ON messages.id = messages_refs.message JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?1) AS following ON messages.author = following.value JOIN json_each(?1) AS following ON messages.author = following.value
WHERE messages_refs.ref = '#' || ?4 AND messages.content ->> 'type' != 'vote' JOIN json_tree(messages.content, '$.mentions') AS mention ON mention.value = '#' || ?4),
) news AS (SELECT * FROM all_news
SELECT TRUE AS is_primary, all_news.* FROM all_news WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3 ORDER BY all_news.timestamp DESC LIMIT 20)
ORDER BY all_news.timestamp DESC LIMIT ?5 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), JSON.stringify(this.following),
start_time, start_time,
end_time, end_time,
this.hash.substring(2), this.hash.substring(2),
k_max_results, '"#' + this.hash.substring(2).replace('"', '""') + '"',
] ]
); );
result = await this._fetch_related_messages(initial_messages); } else if (this.hash == '#🔐') {
} else if (this.hash.startsWith('#🔐')) {
let ids =
this.hash == '#🔐' ? [] : this.hash.substring('#🔐'.length).split(',');
result = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
` `
SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
@@ -212,83 +153,39 @@ class TfTabNewsFeedElement extends LitElement {
WHERE WHERE
(?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND (?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND
json(messages.content) LIKE '"%' json(messages.content) LIKE '"%'
ORDER BY messages.rowid DESC LIMIT ?4 ORDER BY messages.sequence DESC LIMIT 20
`, `,
[ [JSON.stringify(this.private_messages), start_time, end_time]
JSON.stringify(
this.grouped_private_messages?.[JSON.stringify(ids)]?.map(
(x) => x.id
) ?? []
),
start_time,
end_time,
k_max_results,
]
);
let decrypted = (await this.decrypt(result)).filter((x) => x.decrypted);
result = await this._fetch_related_messages(decrypted);
} else if (this.hash == '#👍') {
result = await tfrpc.rpc.query(
`
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)
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'
UNION
SELECT TRUE AS is_primary, * FROM votes
`,
[JSON.stringify(this.following), start_time, end_time, k_max_results]
);
} else if (this.hash == '#🚩') {
result = await tfrpc.rpc.query(
`
WITH flags AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages
WHERE
messages.content ->> 'type' = 'flag' AND
(?1 IS NULL OR messages.timestamp >= ?1) AND messages.timestamp < ?2
ORDER BY timestamp DESC limit ?3)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM flags
JOIN messages ON messages.id = flags.content ->> '$.flag.link'
UNION
SELECT TRUE AS is_primary, * FROM flags
`,
[start_time, end_time, k_max_results]
); );
result = (await this.decrypt(result)).filter((x) => x.decrypted);
} else { } else {
let initial_messages = await tfrpc.rpc.query( result = await tfrpc.rpc.query(
` `
WITH WITH
channels AS (SELECT '#' || value AS value FROM json_each(?5)) all_news AS (
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 SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages FROM messages
JOIN json_each(?1) AS following ON messages.author = following.value JOIN json_each(?) AS following ON messages.author = following.value
WHERE messages.timestamp < ?3 AND (?2 IS NULL OR messages.timestamp >= ?2) AND WHERE timestamp >= 0 AND timestamp < ?3),
messages.content ->> 'type' != 'vote' AND news AS (
(messages.content ->> 'root' IS NULL OR ( SELECT * FROM all_news
NOT EXISTS (SELECT * FROM messages root JOIN channels ON ('#' || (root.content ->> 'channel')) = channels.value WHERE root.id = messages.content ->> 'root') AND WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
NOT EXISTS (SELECT * FROM messages root JOIN messages_refs ON root.id = messages.content ->> 'root' JOIN channels ON messages_refs.message = root.id AND messages_refs.ref = channels.value) ORDER BY timestamp DESC LIMIT 20
)) AND )
(messages.content ->> 'channel' IS NULL OR ('#' || (messages.content ->> 'channel')) NOT IN (SELECT * FROM channels)) AND 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
NOT EXISTS (SELECT * FROM messages_refs JOIN channels ON messages_refs.message = messages.id AND messages_refs.ref = channels.value) FROM news
ORDER BY timestamp DESC LIMIT ?4 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]
JSON.stringify(this.following),
start_time,
end_time,
k_max_results,
JSON.stringify(Object.keys(this.channels_latest)),
]
); );
result = await this._fetch_related_messages(initial_messages);
} }
this.time_loading = undefined; this.time_loading = undefined;
return result; return result;
@@ -308,24 +205,13 @@ class TfTabNewsFeedElement extends LitElement {
]; ];
} }
unread_allowed() {
return (
this.hash == '#@' ||
(!this.hash.startsWith('#%') && !this.hash.startsWith('#@'))
);
}
async load_more() { async load_more() {
this.loading++; this.loading++;
this.loading_canceled = false; this.loading_canceled = false;
try { try {
let more = []; let more = [];
let last_start_time = this.time_range[0]; let last_start_time = this.time_range[0];
try { more = await this.fetch_messages(null, last_start_time);
more = await this.fetch_messages(null, last_start_time);
} catch (e) {
console.log(e);
}
this.update_time_range_from_messages( this.update_time_range_from_messages(
more.filter((x) => x.timestamp < last_start_time) more.filter((x) => x.timestamp < last_start_time)
); );
@@ -396,13 +282,7 @@ class TfTabNewsFeedElement extends LitElement {
) )
) )
); );
} console.log('done loading latest messages.');
make_messages_key() {
return JSON.stringify([
this.hash,
Object.keys(this.channels_latest ?? {}).filter((x) => x != '🔐'),
]);
} }
async load_messages() { async load_messages() {
@@ -410,20 +290,12 @@ class TfTabNewsFeedElement extends LitElement {
let self = this; let self = this;
this.loading++; this.loading++;
let messages = []; let messages = [];
let original_key = this.make_messages_key();
try { try {
if (this._messages_key !== original_key) { if (this._messages_hash !== this.hash) {
this.messages = []; this.messages = [];
this._messages_key = original_key; this._messages_hash = this.hash;
} }
this._messages_following = JSON.stringify(this.following); this._messages_following = this.following;
this._private_messages = JSON.stringify([
this.private_messages,
this.grouped_private_messages,
]);
this._channels_latest = JSON.stringify(
Object.keys(this.channels_latest ?? {})
);
let now = new Date().valueOf(); let now = new Date().valueOf();
let start_time = now - 24 * 60 * 60 * 1000; let start_time = now - 24 * 60 * 60 * 1000;
this.start_time = start_time; this.start_time = start_time;
@@ -436,11 +308,11 @@ class TfTabNewsFeedElement extends LitElement {
} finally { } finally {
this.loading--; this.loading--;
} }
let current_key = this.make_messages_key(); this.messages = this.merge_messages(this.messages, messages);
if (current_key === original_key) {
this.messages = this.merge_messages(this.messages, messages);
}
this.time_loading = undefined; this.time_loading = undefined;
console.log(
`loading messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s`
);
} }
mark_all_read() { mark_all_read() {
@@ -462,48 +334,15 @@ 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() { render() {
if ( if (
!this.messages || !this.messages ||
this._messages_key !== this.make_messages_key() || this._messages_hash !== this.hash ||
this._messages_following !== JSON.stringify(this.following) || JSON.stringify(this._messages_following) !==
(this.hash.startsWith('#🔐') && JSON.stringify(this.following)
this._private_messages !==
JSON.stringify([
this.private_messages,
this.grouped_private_messages,
]))
) { ) {
console.log( console.log(
`loading messages for ${this.whoami} (messages=${!this.messages},${this._messages_key != this.make_messages_key()} following=${this._messages_following !== JSON.stringify(this.following)}, private=${this._private_messages !== JSON.stringify([this.private_messages, this.grouped_private_messages])},${this.private_messages?.length},${Object.keys(this.grouped_private_messages ?? {}).length})` `loading messages for ${this.whoami} (following ${this.following.length})`
); );
this.load_messages(); this.load_messages();
} }
@@ -511,16 +350,9 @@ class TfTabNewsFeedElement extends LitElement {
if (!this.hash.startsWith('#%')) { if (!this.hash.startsWith('#%')) {
more = html` more = html`
<p> <p>
${this.unread_allowed() <button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
? html` Mark All Read
<button </button>
class="w3-button w3-theme-d1"
@click=${this.mark_all_read}
>
Mark All Read
</button>
`
: undefined}
<button <button
?disabled=${this.loading} ?disabled=${this.loading}
class="w3-button w3-theme-d1" class="w3-button w3-theme-d1"
@@ -552,18 +384,9 @@ class TfTabNewsFeedElement extends LitElement {
`; `;
} }
return cache(html` return cache(html`
<style> <button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
${generate_theme()} Mark All Read
</style> </button>
${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()}
<tf-news <tf-news
id="news" id="news"
whoami=${this.whoami} whoami=${this.whoami}
@@ -572,11 +395,8 @@ class TfTabNewsFeedElement extends LitElement {
.following=${this.following} .following=${this.following}
.drafts=${this.drafts} .drafts=${this.drafts}
.expanded=${this.expanded} .expanded=${this.expanded}
hash=${this.hash}
channel=${this.channel()} channel=${this.channel()}
channel_unread=${this.channels_unread?.[this.channel()]} channel_unread=${this.channels_unread?.[this.channel()]}
.recent_reactions=${this.recent_reactions}
@mark_all_read=${this.mark_all_read}
></tf-news> ></tf-news>
${more} ${more}
`); `);

View File

@@ -7,7 +7,7 @@ import {
until, until,
} from './lit-all.min.js'; } from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {styles, generate_theme} from './tf-styles.js'; import {styles} from './tf-styles.js';
class TfTabNewsElement extends LitElement { class TfTabNewsElement extends LitElement {
static get properties() { static get properties() {
@@ -24,12 +24,6 @@ class TfTabNewsElement extends LitElement {
channels_latest: {type: Object}, channels_latest: {type: Object},
connections: {type: Array}, connections: {type: Array},
private_messages: {type: Array}, private_messages: {type: Array},
grouped_private_messages: {type: Object},
visible_private_messages: {type: Object},
recent_reactions: {type: Array},
peer_exchange: {type: Boolean},
is_administrator: {type: Boolean},
stay_connected: {type: Boolean},
}; };
} }
@@ -49,11 +43,9 @@ class TfTabNewsElement extends LitElement {
this.channels_latest = {}; this.channels_latest = {};
this.channels = []; this.channels = [];
this.connections = []; this.connections = [];
this.recent_reactions = [];
tfrpc.rpc.localStorageGet('drafts').then(function (d) { tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}'); self.drafts = JSON.parse(d || '{}');
}); });
this.check_peer_exchange();
} }
connectedCallback() { connectedCallback() {
@@ -66,14 +58,6 @@ class TfTabNewsElement extends LitElement {
document.body.removeEventListener('keypress', this.on_keypress.bind(this)); 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() { load_latest() {
let news = this.shadowRoot?.getElementById('news'); let news = this.shadowRoot?.getElementById('news');
if (news) { if (news) {
@@ -111,26 +95,7 @@ class TfTabNewsElement extends LitElement {
} }
unread_status(channel) { unread_status(channel) {
if (channel === undefined) { if (
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 (
this.channels_latest[channel] && this.channels_latest[channel] &&
this.channels_latest[channel] > 0 && this.channels_latest[channel] > 0 &&
(this.channels_unread[channel] === undefined || (this.channels_unread[channel] === undefined ||
@@ -171,8 +136,11 @@ class TfTabNewsElement extends LitElement {
return this.hash.startsWith('##') ? this.hash.substring(2) : undefined; return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
} }
compare_follows(a, b) { compare_follows() {
return b[1].ts > a[1].ts ? 1 : b[1].ts < a[1].ts ? -1 : 0; 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() { suggested_follows() {
@@ -181,26 +149,14 @@ class TfTabNewsElement extends LitElement {
** pinned at the top. ** pinned at the top.
*/ */
let self = this; let self = this;
let now = new Date().valueOf();
return Object.entries(this.users) return Object.entries(this.users)
.filter((x) => x[1].ts < now)
.filter((x) => x[1].follow_depth > 1) .filter((x) => x[1].follow_depth > 1)
.sort(self.compare_follows) .sort(self.compare_follows())
.slice(0, 8) .slice(0, 8)
.map((x) => x[0]); .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() { render_sidebar() {
let self_key = JSON.stringify([this.whoami]);
return html` return html`
<div <div
class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left" class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left"
@@ -221,7 +177,7 @@ class TfTabNewsElement extends LitElement {
href="#" href="#"
class="w3-bar-item w3-button" class="w3-bar-item w3-button"
style="font-weight: bold" style="font-weight: bold"
>${this.hash.substring(1)}</a >${this.hash.substring(2)}</a
> >
` `
: undefined} : undefined}
@@ -239,44 +195,11 @@ class TfTabNewsElement extends LitElement {
>${this.unread_status('@')}@mentions</a >${this.unread_status('@')}@mentions</a
> >
<a <a
href="#👍" href="#🔐"
class="w3-bar-item w3-button" class="w3-bar-item w3-button"
style=${this.hash == '#👍' ? 'font-weight: bold' : undefined} style=${this.hash == '#🔐' ? 'font-weight: bold' : undefined}
>${this.unread_status('👍')}👍votes</a >${this.unread_status('🔐')}🔐private</a
> >
<a
href="#🚩"
class="w3-bar-item w3-button"
style=${this.hash == '#🚩' ? 'font-weight: bold' : undefined}
>${this.unread_status('🚩')}🚩flagged</a
>
${Object.keys(this?.visible_private_messages ?? [])
?.sort()
?.map(
(key) => html`
<a
href=${'#🔐' +
(key == self_key ? '' : JSON.parse(key).join(','))}
class="w3-bar-item w3-button"
style=${this.hash ==
'#🔐' + (key == self_key ? '' : JSON.parse(key).join(','))
? 'font-weight: bold'
: undefined}
>${this.unread_status(
'🔐' + (key == self_key ? '' : 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) ${Object.keys(this.drafts)
.sort() .sort()
.map( .map(
@@ -300,62 +223,15 @@ class TfTabNewsElement extends LitElement {
` `
)} )}
<a class="w3-bar-item w3-theme-d2 w3-button" href="#connections"> <h4 class="w3-bar-item w3-theme-d2">Connections</h4>
<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 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>
<button
class=${'w3-bar-item w3-button w3-border w3-leftbar w3-rightbar' +
(this.peer_exchange !== false ? ' w3-hide' : '')}
@click=${this.enable_peer_exchange}
>
🔍🌐 Use publicly advertised peers
</button>
`
: undefined}
${this.connections ${this.connections
.filter((x) => x.id) .filter((x) => x.id && !x.destroy_reason)
.map( .map(
(x) => html` (x) => html`
<tf-user <tf-user
class="w3-bar-item" class="w3-bar-item"
style=${x.destroy_reason style="max-width: 100%"
? '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'
: ''}
id=${x.id} id=${x.id}
fallback_name=${x.host}
.users=${this.users} .users=${this.users}
></tf-user> ></tf-user>
` `
@@ -380,14 +256,6 @@ class TfTabNewsElement extends LitElement {
`; `;
} }
recipients() {
if (this.hash == '#🔐') {
return [this.whoami];
} else if (this.hash.startsWith('#🔐')) {
return this.hash.substring('#🔐'.length).split(',');
}
}
render() { render() {
let profile = let profile =
this.hash.startsWith('#@') && this.hash != '#@' this.hash.startsWith('#@') && this.hash != '#@'
@@ -415,12 +283,9 @@ class TfTabNewsElement extends LitElement {
</div>`; </div>`;
} }
return cache(html` return cache(html`
<style>
${generate_theme()}
</style>
${this.render_sidebar()} ${this.render_sidebar()}
<div <div
style="margin-left: 2in; padding: 0px; top: 0; height: 100vh; max-height: 100%; overflow: auto; contain: layout" style="margin-left: 2in; padding: 0px; top: 0; max-height: 100%; overflow: auto"
id="main" id="main"
class="w3-main" class="w3-main"
> >
@@ -441,18 +306,13 @@ class TfTabNewsElement extends LitElement {
</p> </p>
<div> <div>
<div <div
style="width: 100%; max-width: 100%; white-space: nowrap; overflow: hidden" id="show_sidebar"
class="w3-button w3-hide-large"
@click=${this.show_sidebar}
> >
<button &#9776;
id="show_sidebar"
class="w3-button w3-hide-large"
@click=${this.show_sidebar}
>
${this.unread_status()}&#9776;
</button>
Welcome,
<tf-user id=${this.whoami} .users=${this.users}></tf-user>!
</div> </div>
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
${edit_profile} ${edit_profile}
</div> </div>
<div> <div>
@@ -463,7 +323,6 @@ class TfTabNewsElement extends LitElement {
.drafts=${this.drafts} .drafts=${this.drafts}
@tf-draft=${this.draft} @tf-draft=${this.draft}
.channel=${this.channel()} .channel=${this.channel()}
.recipients=${this.recipients()}
></tf-compose> ></tf-compose>
</div> </div>
${profile} ${profile}
@@ -480,8 +339,6 @@ class TfTabNewsElement extends LitElement {
.channels_unread=${this.channels_unread} .channels_unread=${this.channels_unread}
.channels_latest=${this.channels_latest} .channels_latest=${this.channels_latest}
.private_messages=${this.private_messages} .private_messages=${this.private_messages}
.grouped_private_messages=${this.grouped_private_messages}
.recent_reactions=${this.recent_reactions}
></tf-tab-news-feed> ></tf-tab-news-feed>
</div> </div>
</div> </div>

136
apps/ssb/tf-tab-query.js Normal file
View File

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

View File

@@ -1,6 +1,6 @@
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js'; import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {styles, generate_theme} from './tf-styles.js'; import {styles} from './tf-styles.js';
class TfTabSearchElement extends LitElement { class TfTabSearchElement extends LitElement {
static get properties() { static get properties() {
@@ -11,9 +11,6 @@ class TfTabSearchElement extends LitElement {
following: {type: Array}, following: {type: Array},
query: {type: String}, query: {type: String},
expanded: {type: Object}, expanded: {type: Object},
messages: {type: Array},
results: {type: Array},
error: {type: Object},
}; };
} }
@@ -41,40 +38,29 @@ class TfTabSearchElement extends LitElement {
search.select(); search.select();
} }
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query)); await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
this.error = undefined; let results = await tfrpc.rpc.query(
this.results = []; `
this.messages = []; SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
try { FROM messages_fts(?)
if (query.startsWith('sql:')) { JOIN messages ON messages.rowid = messages_fts.rowid
this.messages = []; JOIN json_each(?) AS following ON messages.author = following.value
this.results = await tfrpc.rpc.query( ORDER BY timestamp DESC limit 100
query.substring('sql:'.length), `,
[] ['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
); );
} else { console.log('Done.');
let results = await tfrpc.rpc.query( search = this.renderRoot.getElementById('search');
` if (search) {
SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature search.value = query;
FROM messages_fts(?) search.focus();
JOIN messages ON messages.rowid = messages_fts.rowid search.select();
JOIN json_each(?) AS following ON messages.author = following.value }
ORDER BY timestamp DESC limit 100 this.renderRoot.getElementById('news').messages = results;
`, }
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
); search_keydown(event) {
search = this.renderRoot.getElementById('search'); if (event.keyCode == 13) {
if (search) { this.query = this.renderRoot.getElementById('search').value;
search.value = query;
search.focus();
search.select();
}
this.messages = results;
}
} catch (e) {
this.messages = [];
this.results = [];
this.error = e;
console.log(e);
} }
} }
@@ -101,58 +87,18 @@ class TfTabSearchElement extends LitElement {
tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts)); tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
} }
render_results() { render() {
if (this.error) {
return html`<h2 style="color: red">${this.error.message}</h2>
<pre style="color: red">${this.error.stack}</pre>`;
} else if (this.messages?.length) {
return html`<tf-news
id="news"
whoami=${this.whoami}
.messages=${this.messages}
.users=${this.users}
.expanded=${this.expanded}
.drafts=${this.drafts}
@tf-expand=${this.on_expand}
@tf-draft=${this.draft}
></tf-news>`;
} else if (this.results?.length) {
let keys = Object.keys(this.results[0]).sort();
return html`<table style="width: 100%; max-width: 100%">
<tr>
${keys.map((key) => html`<th>${key}</th>`)}
</tr>
${this.results.map(
(row) =>
html`<tr>
${keys.map((key) => html`<td>${row[key]}</td>`)}
</tr>`
)}
</table>`;
} else {
return html`<div>No results.</div>`;
}
}
async query_results() {
if (this.query !== this.last_query) { if (this.query !== this.last_query) {
this.last_query = this.query; this.last_query = this.query;
this._query = this.search(this.query); this.search(this.query);
} }
await this._query; let self = this;
}
render() {
return html` return html`
<style> <div style="display: flex; flex-direction: row; gap: 4px">
${generate_theme()} <input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
</style> <button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
<div class="w3-padding">
${until(
this.query_results().then(this.render_results.bind(this)),
html`<p>Searching...<span class="w3-animate-fading">🦀</span></p>`
)}
</div> </div>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} .drafts=${this.drafts} @tf-expand=${this.on_expand} @tf-draft=${this.draft}></tf-news>
`; `;
} }
} }

View File

@@ -1,5 +1,5 @@
import {LitElement, html, unsafeHTML} from './lit-all.min.js'; import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import {styles, generate_theme} from './tf-styles.js'; import {styles} from './tf-styles.js';
class TfTagElement extends LitElement { class TfTagElement extends LitElement {
static get properties() { static get properties() {
@@ -17,15 +17,11 @@ class TfTagElement extends LitElement {
render() { render() {
let number = this.count ? html` (${this.count})` : undefined; let number = this.count ? html` (${this.count})` : undefined;
return html` return html`<a
<style> href=${'#' + encodeURIComponent(this.tag)}
${generate_theme()}</style class="w3-tag w3-theme-d1 w3-round-4 w3-button"
><a >${this.tag}${number}</a
href=${'#' + encodeURIComponent(this.tag)} > `;
class="w3-tag w3-theme-d1 w3-round-4 w3-button"
>${this.tag}${number}</a
>
`;
} }
} }

View File

@@ -1,15 +1,12 @@
import {LitElement, html} from './lit-all.min.js'; import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {styles, generate_theme} from './tf-styles.js'; import {styles} from './tf-styles.js';
class TfUserElement extends LitElement { class TfUserElement extends LitElement {
static get properties() { static get properties() {
return { return {
id: {type: String}, id: {type: String},
fallback_name: {type: String},
icon_only: {type: Boolean},
users: {type: Object}, users: {type: Object},
nolink: {type: Boolean},
}; };
} }
@@ -18,22 +15,11 @@ class TfUserElement extends LitElement {
constructor() { constructor() {
super(); super();
this.id = null; this.id = null;
this.fallback_name = null;
this.icon_only = false;
this.users = {}; this.users = {};
} }
render() { render() {
let user = this.users[this.id]; let user = this.users[this.id];
if (!this.users[this.id]) {
this.dispatchEvent(
new CustomEvent('tf-request-user', {
bubbles: true,
composed: true,
detail: {id: this.id},
})
);
}
let shape = let shape =
user?.follow_depth === undefined || user.follow_depth >= 2 user?.follow_depth === undefined || user.follow_depth >= 2
? 'w3-circle' ? 'w3-circle'
@@ -44,40 +30,27 @@ class TfUserElement extends LitElement {
>😎</span >😎</span
>`; >`;
let name = this.users?.[this.id]?.name; let name = this.users?.[this.id]?.name;
let name_string = name ?? this.fallback_name ?? this.id; name = html`<a target="_top" href=${'#' + this.id}
name = this.icon_only >${name !== undefined ? name : this.id}</a
? undefined >`;
: !this.nolink
? html`<a target="_top" href=${'#' + encodeURIComponent(this.id)}
>${name_string}</a
>`
: html`<span>${name_string}</span>`;
if (user) { if (user) {
let image_link = user.image; let image_link = user.image;
if (typeof image_link == 'string' && !image_link.startsWith('&')) { image_link =
try { typeof image_link == 'string' ? image_link : image_link?.link;
image_link = JSON.parse(image_link)?.link;
} catch {}
}
if (image_link !== undefined) { if (image_link !== undefined) {
image = html`<img image = html`<img
class=${'w3-theme-l4 ' + shape} class=${'w3-theme-l4 ' + shape}
style="width: 2em; height: 2em; vertical-align: middle; object-fit: cover" style="width: 2em; height: 2em; vertical-align: middle; object-fit: cover"
src="/${image_link}/view" src="/${image_link}/view"
title=${name_string + ' (' + this.id + ')'}
/>`; />`;
} }
} }
return html` <style> return html` <div
${generate_theme()} style="display: inline-block; vertical-align: middle; font-weight: bold; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis"
</style> >
<div ${image} ${name}
style=${'display: inline-block; vertical-align: middle; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis' + </div>`;
(this.nolink ? '' : '; font-weight: bold')}
>
${image} ${name}
</div>`;
} }
} }

View File

@@ -50,9 +50,9 @@ function image(node, entering) {
'</div>' '</div>'
); );
if (this.options.safe && potentiallyUnsafe(node.destination)) { if (this.options.safe && potentiallyUnsafe(node.destination)) {
this.lit('<img src="" title="'); this.lit('<img src="" alt="');
} else { } else {
this.lit('<img src="' + this.esc(node.destination) + '" title="'); this.lit('<img src="' + this.esc(node.destination) + '" alt="');
} }
} }
this.disableTags += 1; this.disableTags += 1;
@@ -104,12 +104,12 @@ export function markdown(md) {
node.destination.startsWith('@') && node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519') node.destination.endsWith('.ed25519')
) { ) {
node.destination = '#' + encodeURIComponent(node.destination); node.destination = '#' + node.destination;
} else if ( } else if (
node.destination.startsWith('%') && node.destination.startsWith('%') &&
node.destination.endsWith('.sha256') node.destination.endsWith('.sha256')
) { ) {
node.destination = '#' + encodeURIComponent(node.destination); node.destination = '#' + node.destination;
} else if ( } else if (
node.destination.startsWith('&') && node.destination.startsWith('&') &&
node.destination.endsWith('.sha256') node.destination.endsWith('.sha256')

View File

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

View File

@@ -8,7 +8,7 @@ async function query(sql, args) {
async function get_biggest() { async function get_biggest() {
return query(` 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() { async function main() {
await app.setDocument('<p style="color: #fff">Analyzing feeds...</p>'); await app.setDocument(
let most_follows = get_most_follows(); '<p style="color: #fff">Finding the top 10 largest feeds...</p>'
);
let most_follows = await get_most_follows();
let total = await get_total(); let total = await get_total();
let identities = await ssb.getAllIdentities(); let identities = await ssb.getAllIdentities();
let following1 = await ssb.following(identities, 1); let following1 = await ssb.following(identities, 1);
let following2 = await ssb.following(identities, 2); let following2 = await ssb.following(identities, 2);
let biggest = await get_biggest(); let biggest = await get_biggest();
most_follows = await most_follows;
let names = await get_names( let names = await get_names(
[].concat( [].concat(
biggest.map((x) => x.author), biggest.map((x) => x.author),
@@ -93,7 +94,7 @@ async function main() {
} }
let html = `<body style="color: #000; background-color: #ddd">\n let html = `<body style="color: #000; background-color: #ddd">\n
<h1>Storage Summary</h1> <h1>Storage Summary</h1>
<h2>Top Accounts by Size</h2> <h2>Top 10 Accounts by Size</h2>
<ol>`; <ol>`;
for (let item of biggest) { for (let item of biggest) {
html += `<li> html += `<li>
@@ -104,7 +105,7 @@ async function main() {
} }
html += ` html += `
</ol> </ol>
<h2>Top Accounts by Follows</h2> <h2>Top 10 Accounts by Follows</h2>
<ol>`; <ol>`;
for (let item of most_follows) { for (let item of most_follows) {
html += `<li> html += `<li>

View File

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

View File

@@ -1,27 +0,0 @@
async function main() {
let speedscope_js = await utf8Decode(
getFile('speedscope/speedscope-Y2522XSH.js')
);
speedscope_js = speedscope_js.replace(/alert\(`Cannot load.*?return/, '');
app.setDocument(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>speedscope</title>
<link rel="stylesheet" href="speedscope/speedscope-GHPHNKXC.css">
</head>
<body>
<script>
delete window.localStorage;
window.location.hash = '#profileURL=${core.url}../../trace';
</script>
<script>${speedscope_js}</script>
</body>
</html>
`);
}
main();

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +0,0 @@
{
"type": "tildefriends-app",
"emoji": "🕸",
"previous": "&n7hu5b8/TsfiG6FDlCRG5nPCrIdCr96+xpIJ/aQT/uM=.sha256"
}

View File

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

View File

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

View File

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

5
apps/welcome/app.js Normal file
View File

@@ -0,0 +1,5 @@
async function main() {
await app.setDocument(utf8Decode(getFile('index.html')));
}
main();

View File

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -8,10 +8,14 @@
<link rel="stylesheet" href="regular.min.css" /> <link rel="stylesheet" href="regular.min.css" />
<link rel="stylesheet" href="solid.min.css" /> <link rel="stylesheet" href="solid.min.css" />
<link rel="stylesheet" href="brands.min.css" /> <link rel="stylesheet" href="brands.min.css" />
<style> <style>
img { img {
margin-bottom: -8px; margin-bottom: -8px;
} }
.mySlides {
display: none;
}
</style> </style>
<base target="_top" /> <base target="_top" />
</head> </head>
@@ -24,39 +28,65 @@
<b>😎 Tilde Friends</b> <b>😎 Tilde Friends</b>
</h1> </h1>
<h1 class="w3-xxlarge w3-text-green"> <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> </h1>
<p> <p>
In addition to participating in Secure Scuttlebutt, Tilde Friends is In addition to participating in Secure Scuttlebutt, Tilde Friends is
a platform for building, running, and sharing applications. a platform for building, running, and sharing applications.
</p> </p>
<a <p>
class="w3-button w3-blue w3-padding-large" Available for lots of devices:
href="https://www.tildefriends.net/~core/ssb/" <i class="fa-brands fa-linux w3-xlarge"></i>
>🦀 Try It</a <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 <a
class="w3-button w3-black w3-padding-large" 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 ><i class="fa fa-download"></i> Download</a
> >
<a <a
class="w3-button w3-black w3-padding-large" 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 <a
class="w3-button w3-black w3-padding-large" class="w3-button w3-black w3-padding-large"
href="https://docs.tildefriends.net/" href="https://docs.tildefriends.net/"
><i class="fa fa-book"></i> Documentation</a ><i class="fa fa-book"></i> Documentation</a
> >
<a <p>
class="w3-button w3-black w3-padding-large" <a
href="https://www.tildefriends.net/~cory/tildeblog/" class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
><i class="fa fa-solid fa-square-rss"></i> Blog</a 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>
<div class="w3-col l4 m6"> <div class="w3-col l4 m6">
<img src="tildefriends.png" class="w3-image w3-right w3-hide-small" /> <img src="tildefriends.png" class="w3-image w3-right w3-hide-small" />
@@ -74,119 +104,15 @@
<h2>First-time user checklist:</h2> <h2>First-time user checklist:</h2>
<ol type="1" style="text-align: left"> <ol type="1" style="text-align: left">
<li> <li>
<a <a href="https://dev.tildefriends.net/cory/tildefriends/releases"
href="https://dev.tildefriends.net/cory/tildefriends/releases/latest"
>Download</a >Download</a
> >
Tilde Friends or use Tilde Friends or use
<a href="https://www.tildefriends.net/" <a href="https://www.tildefriends.net/"
>https://www.tildefriends.net/</a >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
</a>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://apps.apple.com/us/app/tilde-friends/id6742085604"
>
<img src="ios.svg" style="height: 2em; margin: 0" />
Get it on iOS
</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 x86_64 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>
<li>Create an account to identify yourself with that instance.</li>
<li> <li>
Describe yourself in your profile in the <b>ssb</b> app. Give Describe yourself in your profile in the <b>ssb</b> app. Give
yourself a name and an avatar if you like. yourself a name and an avatar if you like.
@@ -220,11 +146,11 @@
<!-- SSB Section --> <!-- SSB Section -->
<div class="w3-light-grey"> <div class="w3-light-grey">
<div class="w3-row-padding w3-padding-64"> <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/" <a href="https://scuttlebutt.nz/"
><img ><img
class="w3-image" class="w3-image w3-round-large"
src="hermietildefriends.svg" src="ssb.png"
alt="Secure Scuttlebutt" alt="Secure Scuttlebutt"
/></a> /></a>
</div> </div>
@@ -268,11 +194,11 @@
</div> </div>
<!-- Sandbox Section --> <!-- Sandbox Section -->
<div class="w3-padding-64 w3-pale-blue"> <div class="w3-padding-64 w3-grey">
<div class="w3-row-padding"> <div class="w3-row-padding">
<div class="w3-col"> <div class="w3-col">
<h1 class="w3-jumbo" style="text-align: right"> <h1 class="w3-jumbo" style="text-align: right">
<b>App Sandboxes</b> <b>Sandbox Security</b>
</h1> </h1>
<i <i
class="fa fa-road-barrier w3-right w3-jumbo w3-text-yellow" class="fa fa-road-barrier w3-right w3-jumbo w3-text-yellow"
@@ -280,7 +206,7 @@
></i> ></i>
<p> <p>
Tilde Friends tries to make sure apps can be trusted using similar Tilde Friends tries to make sure apps can be trusted using similar
techniques to web browsers and operating systems. techniques to how web browsers and operating systems do it.
</p> </p>
<p> <p>
This is all a work in progress, and it varies by platform, so don't This is all a work in progress, and it varies by platform, so don't
@@ -294,24 +220,16 @@
<!-- Technlology Section --> <!-- Technlology Section -->
<div class="w3-container w3-padding-64 w3-light-grey w3-center"> <div class="w3-container w3-padding-64 w3-light-grey w3-center">
<h1 class="w3-jumbo"><b>One Pile of Code</b></h1> <h1 class="w3-jumbo"><b>Built the Old Fashioned Way</b></h1>
<div class="w3-left-align"> <p>
<p> Tilde Friends strives to use only simple and widely adopted dependencies
Tilde Friends diverges from the Node.js web of modules from which in order to keep it easy to build for all sorts of platforms and
Secure Scuttlebutt was first developed. Here we strive to maintain a maintainable for a very long time.
single C program that works as a foundation for building a wide </p>
variety of social and other applications. <p>
</p> Though of course for building Tilde Friends apps, you are free to use
<p> whatever fits on top.
Tilde Friends uses only a handful of simple and widely adopted </p>
dependencies in order to keep it easy to build for all sorts of
platforms and maintainable for a very long time.
</p>
<p>
Though of course for building Tilde Friends apps, you are free to use
whatever works.
</p>
</div>
<div class="w3-row" style="margin-top: 64px"> <div class="w3-row" style="margin-top: 64px">
<a <a
@@ -344,6 +262,10 @@
<i class="fa fa-lock w3-text-purple w3-jumbo"></i> <i class="fa fa-lock w3-text-purple w3-jumbo"></i>
<p>libsodium</p> <p>libsodium</p>
</a> </a>
<a href="https://github.com/openssl/openssl/releases" class="w3-col s3">
<i class="fa fa-shield-halved w3-text-green w3-jumbo"></i>
<p>OpenSSL</p>
</a>
<a <a
href="https://github.com/ianlancetaylor/libbacktrace" href="https://github.com/ianlancetaylor/libbacktrace"
class="w3-col s3" class="w3-col s3"
@@ -351,13 +273,13 @@
<i class="fa fa-burst w3-text-pink w3-jumbo"></i> <i class="fa fa-burst w3-text-pink w3-jumbo"></i>
<p>libbacktrace</p> <p>libbacktrace</p>
</a> </a>
</div>
<div class="w3-row" style="margin-top: 64px">
<a href="https://codemirror.net/docs/changelog/" class="w3-col s3"> <a href="https://codemirror.net/docs/changelog/" class="w3-col s3">
<i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i> <i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i>
<p>CodeMirror</p> <p>CodeMirror</p>
</a> </a>
</div>
<div class="w3-row" style="margin-top: 64px">
<a href="https://github.com/jlfwong/speedscope/" class="w3-col s3"> <a href="https://github.com/jlfwong/speedscope/" class="w3-col s3">
<i class="fa fa-microscope w3-text-orange w3-jumbo"></i> <i class="fa fa-microscope w3-text-orange w3-jumbo"></i>
<p>Speedscope</p> <p>Speedscope</p>
@@ -370,6 +292,9 @@
<i class="fa fa-book-atlas w3-text-purple w3-jumbo"></i> <i class="fa fa-book-atlas w3-text-purple w3-jumbo"></i>
<p>c-ares</p> <p>c-ares</p>
</a> </a>
</div>
<div class="w3-row" style="margin-top: 64px">
<a href="https://www.gnu.org/software/make/" class="w3-col s3"> <a href="https://www.gnu.org/software/make/" class="w3-col s3">
<i class="fa fa-hammer w3-text-teal w3-jumbo"></i> <i class="fa fa-hammer w3-text-teal w3-jumbo"></i>
<p>GNU Make</p> <p>GNU Make</p>
@@ -381,7 +306,7 @@
<footer class="w3-container w3-padding-32 w3-blue-grey w3-center w3-xlarge"> <footer class="w3-container w3-padding-32 w3-blue-grey w3-center w3-xlarge">
<p class="w3-medium"> <p class="w3-medium">
This page and Tilde Friends itself was made by Cory mostly in coffee This page and Tilde Friends itself was made by Cory mostly in coffee
shops and a local pizza place while listening to Gary's Bangers. shops and a local pizza place.
</p> </p>
</footer> </footer>
</body> </body>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 141 KiB

View File

@@ -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} html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ /* 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} 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-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-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-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,.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-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-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-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important} .w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important} .w3-hover-none:hover{box-shadow:none!important}
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
/* Colors */ /* Colors */
.w3-amber,.w3-hover-amber:hover{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-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-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-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-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-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-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-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

249
core/app.js Normal file
View File

@@ -0,0 +1,249 @@
import * as core from './core.js';
let g_next_id = 1;
let g_calls = {};
let gSessionIndex = 0;
/**
* TODOC
* @returns
*/
function makeSessionId() {
return 'session_' + (gSessionIndex++).toString();
}
/**
* TODOC
* @returns
*/
function App() {
this._on_output = null;
this._send_queue = [];
return this;
}
/**
* 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 = g_next_id++;
while (!id || g_calls[id]) {
id = g_next_id++;
}
let promise = new Promise(function (resolve, reject) {
g_calls[id] = {resolve: resolve, reject: reject};
});
let message = {
message: 'tfrpc',
method: api[0],
params: [...arguments],
id: id,
};
self.send(message);
return promise;
};
Object.defineProperty(result, 'name', {value: api[0], writable: false});
return result;
};
/**
* TODOC
* @param {*} message
*/
App.prototype.send = function (message) {
if (this._send_queue) {
if (this._on_output) {
this._send_queue.forEach((x) => this._on_output(x));
this._send_queue = null;
} else if (message) {
this._send_queue.push(message);
}
}
if (message && this._on_output) {
this._on_output(message);
}
};
/**
* TODOC
* @param {*} request
* @param {*} response
*/
exports.app_socket = async function socket(request, response) {
let process;
let options = {};
let credentials = await httpd.auth_query(request.headers);
response.onClose = async function () {
if (process && process.task) {
process.task.kill();
}
if (process) {
process.timeout = 0;
}
};
response.onMessage = async function (event) {
if (event.opCode == 0x1 || event.opCode == 0x2) {
let message;
try {
message = JSON.parse(event.data);
} catch (error) {
print('ERROR', error, event.data, event.data.length, event.opCode);
return;
}
if (message.action == 'hello') {
let packageOwner;
let packageName;
let blobId;
let match;
let parentApp;
if (
(match = /^\/([&%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(message.path))
) {
blobId = match[1];
} else if ((match = /^\/\~([^\/]+)\/([^\/]+)\/$/.exec(message.path))) {
packageOwner = match[1];
packageName = match[2];
blobId = await new Database(packageOwner).get('path:' + packageName);
if (!blobId) {
response.send(
JSON.stringify({
message: 'tfrpc',
method: 'error',
params: [message.path + ' not found'],
id: -1,
}),
0x1
);
return;
}
if (packageOwner != 'core') {
let coreId = await new Database('core').get('path:' + packageName);
parentApp = {
path: '/~core/' + packageName + '/',
id: coreId,
};
}
}
response.send(
JSON.stringify(
Object.assign(
{
action: 'session',
credentials: credentials,
parentApp: parentApp,
id: blobId,
},
await ssb.getIdentityInfo(
credentials?.session?.name,
packageOwner,
packageName
)
)
),
0x1
);
options.api = message.api || [];
options.credentials = credentials;
options.packageOwner = packageOwner;
options.packageName = packageName;
options.url = message.url;
let sessionId = makeSessionId();
if (blobId) {
if (message.edit_only) {
response.send(
JSON.stringify({action: 'ready', edit_only: true}),
0x1
);
} else {
process = await core.getProcessBlob(blobId, sessionId, options);
}
}
if (process) {
process.app.readOutput(function (message) {
response.send(JSON.stringify(message), 0x1);
});
process.app.send();
}
let ping = function () {
let now = Date.now();
let again = true;
if (now - process.lastActive < process.timeout) {
// Active.
} else if (process.lastPing > process.lastActive) {
// We lost them.
if (process.task) {
process.task.kill();
}
again = false;
} else {
// Idle. Ping them.
response.send('', 0x9);
process.lastPing = now;
}
if (again && process.timeout) {
setTimeout(ping, process.timeout);
}
};
if (process && process.timeout > 0) {
setTimeout(ping, process.timeout);
}
} else if (message.action == 'resetPermission') {
if (process) {
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) {
// Close.
if (process && process.task) {
process.task.kill();
}
response.send(event.data, 0x8);
} else if (event.opCode == 0xa) {
// PONG
}
if (process) {
process.lastActive = Date.now();
}
};
response.upgrade(100, {});
};
export {App};

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,32 @@
/** import * as app from './app.js';
* \file import * as http from './http.js';
* \defgroup tfcore Tilde Friends Core JS
* Tilde Friends process management, in JavaScript.
* @{
*/
/** All running processes. */
let gProcesses = {}; let gProcesses = {};
/** Whether stats are currently being sent. */
let gStatsTimer = false; let gStatsTimer = false;
/** Effectively a process ID. */ let kPingInterval = 60 * 1000;
let g_handler_index = 0;
/** Whether updating accounts information is currently scheduled. */
let g_update_accounts_scheduled;
/** /**
* Invoke a handler. * TODOC
* @param handlers The handlers on which to invoke the callback. * @param {*} out
* @param argv Arguments to pass to the handlers. * @param {*} error
* @return A promise. */
function printError(out, error) {
if (error.stackTrace) {
out.print(error.fileName + ':' + error.lineNumber + ': ' + error.message);
out.print(error.stackTrace);
} else {
for (let [k, v] of Object.entries(error)) {
out.print(k, v);
}
out.print(error.toString());
}
}
/**
* TODOC
* @param {*} handlers
* @param {*} argv
* @returns
*/ */
function invoke(handlers, argv) { function invoke(handlers, argv) {
let promises = []; let promises = [];
@@ -41,10 +49,10 @@ function invoke(handlers, argv) {
} }
/** /**
* Broadcast a named event to all registered apps. * TODOC
* @param eventName the name of the event. * @param {*} eventName
* @param argv Arguments to pass to the handlers. * @param {*} argv
* @return A promise. * @returns
*/ */
function broadcastEvent(eventName, argv) { function broadcastEvent(eventName, argv) {
let promises = []; let promises = [];
@@ -57,9 +65,9 @@ function broadcastEvent(eventName, argv) {
} }
/** /**
* Send a message to all other instances of the same app. * TODOC
* @param message The message. * @param {*} message
* @return A promise. * @returns
*/ */
function broadcast(message) { function broadcast(message) {
let sender = this; let sender = this;
@@ -78,13 +86,10 @@ function broadcast(message) {
} }
/** /**
* Send a message to all instances of the same app running as the same user. * TODOC
* @param user The user. * @param {String} eventName
* @param packageOwner The owner of the app. * @param {*} argv
* @param packageName The name of the app. * @returns
* @param eventName The name of the event.
* @param argv The arguments to pass.
* @return A promise.
*/ */
function broadcastAppEventToUser( function broadcastAppEventToUser(
user, user,
@@ -109,9 +114,10 @@ function broadcastAppEventToUser(
} }
/** /**
* Get user context information for a call. * TODOC
* @param caller The calling process. * @param {*} caller
* @param process The receiving process. * @param {*} process
* @returns
*/ */
function getUser(caller, process) { function getUser(caller, process) {
return { return {
@@ -124,11 +130,43 @@ function getUser(caller, process) {
} }
/** /**
* Send a message. * TODOC
* @param from The calling process. * @param {*} user
* @param to The receiving process. * @param {*} process
* @param message The message. * @returns
* @return A promise. */
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) { function postMessageInternal(from, to, message) {
if (to.eventHandlers['message']) { if (to.eventHandlers['message']) {
@@ -137,13 +175,14 @@ function postMessageInternal(from, to, message) {
} }
/** /**
* Get or create a process for an app blob. * TODOC
* @param blobId The blob identifier. * @param {*} blobId
* @param key A unique key for the invocation. * @param {*} key
* @param options Other options. * @param {*} options
* @return The process. * @returns
*/ */
exports.getProcessBlob = async function getProcessBlob(blobId, key, options) { async function getProcessBlob(blobId, key, options) {
// TODO(tasiaiso): break this down ?
let process = gProcesses[key]; let process = gProcesses[key];
if (!process && !(options && 'create' in options && !options.create)) { if (!process && !(options && 'create' in options && !options.create)) {
let resolveReady; let resolveReady;
@@ -156,91 +195,114 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
process.task = new Task(); process.task = new Task();
process.packageOwner = options.packageOwner; process.packageOwner = options.packageOwner;
process.packageName = options.packageName; process.packageName = options.packageName;
process.url = options?.url;
process.eventHandlers = {}; process.eventHandlers = {};
if (!options?.script || options?.script === 'app.js') { if (!options?.script || options?.script === 'app.js') {
process._send_queue = []; process.app = new app.App();
process._calls = {};
process._next_call_id = 1;
/**
** Create a function wrapper that when called invokes a function on the app
** itself.
** @param api The function and argument names.
** @return A function.
*/
process.makeFunction = function (api) {
let result = function () {
let id = process._next_call_id++;
while (!id || process._calls[id]) {
id = process._next_call_id++;
}
let promise = new Promise(function (resolve, reject) {
process._calls[id] = {resolve: resolve, reject: reject};
});
let message = {
action: 'tfrpc',
method: api[0],
params: [...arguments],
id: id,
};
process.send(message);
return promise;
};
Object.defineProperty(result, 'name', {
value: api[0],
writable: false,
});
return result;
};
/**
** Send a message to the app.
** @param message The message to send.
*/
process.send = function (message) {
if (process._send_queue) {
if (process._on_output) {
process._send_queue.forEach((x) => process._on_output(x));
process._send_queue = null;
} else if (message) {
process._send_queue.push(message);
}
}
if (message && process._on_output) {
process._on_output(message);
}
};
} else {
process.makeFunction = function (api) {
return function () {};
};
} }
process.lastActive = Date.now();
process.lastPing = null;
process.timeout = kPingInterval;
process.ready = new Promise(function (resolve, reject) { process.ready = new Promise(function (resolve, reject) {
resolveReady = resolve; resolveReady = resolve;
rejectReady = reject; rejectReady = reject;
}); });
gProcesses[key] = process; gProcesses[key] = process;
process.task.onExit = function (exitCode, terminationSignal) { process.task.onExit = function (exitCode, terminationSignal) {
broadcastEvent('onSessionEnd', [getUser(process, process)]);
process.task = null; process.task = null;
delete gProcesses[key]; delete gProcesses[key];
}; };
let imports = { let imports = {
core: { core: {
broadcast: broadcast.bind(process), broadcast: broadcast.bind(process),
register: function (eventName, handler) {
if (!process.eventHandlers[eventName]) {
process.eventHandlers[eventName] = [];
}
process.eventHandlers[eventName].push(handler);
},
unregister: function (eventName, handler) {
if (process.eventHandlers[eventName]) {
let index = process.eventHandlers[eventName].indexOf(handler);
if (index != -1) {
process.eventHandlers[eventName].splice(index, 1);
}
if (process.eventHandlers[eventName].length == 0) {
delete process.eventHandlers[eventName];
}
}
},
user: getUser(process, process), user: getUser(process, process),
permissionTest: async function (permission, description) { users: async function () {
try {
return JSON.parse(await new Database('auth').get('users'));
} catch {
return [];
}
},
permissionsGranted: async function () {
let user = process?.credentials?.session?.name; let user = process?.credentials?.session?.name;
let permissions = await imports.core.permissionsGranted(); let settings = await loadSettings();
if (permissions && permissions[permission] !== undefined) { if (
if (permissions[permission]) { user &&
options?.packageOwner &&
options?.packageName &&
settings.userPermissions &&
settings.userPermissions[user] &&
settings.userPermissions[user][options.packageOwner]
) {
return settings.userPermissions[user][options.packageOwner][
options.packageName
];
}
},
allPermissionsGranted: async function () {
let user = process?.credentials?.session?.name;
let settings = await loadSettings();
if (
user &&
options?.packageOwner &&
options?.packageName &&
settings.userPermissions &&
settings.userPermissions[user]
) {
return settings.userPermissions[user];
}
},
permissionsForUser: async function (user) {
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;
let settings = await loadSettings();
if (!user || !options?.packageOwner || !options?.packageName) {
return;
} else if (
settings.userPermissions &&
settings.userPermissions[user] &&
settings.userPermissions[user][options.packageOwner] &&
settings.userPermissions[user][options.packageOwner][
options.packageName
] &&
settings.userPermissions[user][options.packageOwner][
options.packageName
][permission] !== undefined
) {
if (
settings.userPermissions[user][options.packageOwner][
options.packageName
][permission]
) {
return true; return true;
} else { } else {
throw Error(`Permission denied: ${permission}.`); throw Error(`Permission denied: ${permission}.`);
} }
} else { } else if (process.app) {
return process return process.app
.makeFunction(['requestPermission'])(permission, description) .makeFunction(['requestPermission'])(permission)
.then(async function (value) { .then(async function (value) {
if (value == 'allow') { if (value == 'allow') {
await ssb.setUserPermission( await ssb.setUserPermission(
@@ -269,28 +331,30 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
} }
throw Error(`Permission denied: ${permission}.`); throw Error(`Permission denied: ${permission}.`);
}); });
} else {
throw Error(`Permission denied: ${permission}.`);
} }
}, },
app: {
owner: options?.packageOwner,
name: options?.packageName,
},
url: options?.url,
}, },
}; };
process.sendIdentities = async function () { process.sendIdentities = async function () {
let identities = await ssb_internal.getIdentityInfo( process.app.send(
process?.credentials?.session?.name, Object.assign(
options?.packageOwner, {
options?.packageName action: 'identities',
); },
let json = JSON.stringify(identities); await ssb.getIdentityInfo(
if (process._last_sent_identities !== json) { process?.credentials?.session?.name,
process.send( options?.packageOwner,
Object.assign( options?.packageName
{
action: 'identities',
},
identities
) )
); )
process._last_sent_identities = json; );
}
}; };
process.setActiveIdentity = async function (identity) { process.setActiveIdentity = async function (identity) {
if ( if (
@@ -327,7 +391,7 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
options.packageName, options.packageName,
'setActiveIdentity', 'setActiveIdentity',
[ [
await imports.ssb.getActiveIdentity( await ssb.getActiveIdentity(
process.credentials?.session?.name, process.credentials?.session?.name,
options.packageOwner, options.packageOwner,
options.packageName options.packageName
@@ -339,11 +403,49 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
throw new Error('Must be signed-in to create an account.'); throw new Error('Must be signed-in to create an account.');
} }
}; };
if (process.credentials?.permissions?.administration) {
imports.core.globalSettingsDescriptions = async function () {
let settings = Object.assign({}, defaultGlobalSettings());
for (let [key, value] of Object.entries(await loadSettings())) {
if (settings[key]) {
settings[key].value = value;
}
}
return settings;
};
imports.core.globalSettingsGet = async function (key) {
let settings = await loadSettings();
return settings?.[key];
};
imports.core.globalSettingsSet = async function (key, value) {
await imports.core.permissionTest('set_global_setting');
print('Setting', key, value);
let settings = await loadSettings();
settings[key] = value;
await new Database('core').set('settings', JSON.stringify(settings));
print('Done.');
};
imports.core.deleteUser = async function (user) {
await imports.core.permissionTest('delete_user');
let db = new Database('auth');
db.remove('user:' + user);
let users = new Set();
let users_original = await db.get('users');
try {
users = new Set(JSON.parse(users_original));
} catch {}
users.delete(user);
users = JSON.stringify([...users].sort());
if (users !== users_original) {
await db.set('users', users);
}
};
}
if (options.api) { if (options.api) {
imports.app = {}; imports.app = {};
for (let i in options.api) { for (let i in options.api) {
let api = options.api[i]; let api = options.api[i];
imports.app[api[0]] = process.makeFunction(api); imports.app[api[0]] = process.app.makeFunction(api);
} }
} }
for (let [name, f] of Object.entries(options?.imports || {})) { for (let [name, f] of Object.entries(options?.imports || {})) {
@@ -354,12 +456,145 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
imports.app.print(...args); imports.app.print(...args);
} }
}; };
process.task.onError = process.makeFunction(['error']); process.task.onError = function (error) {
try {
if (process.app) {
process.app.makeFunction(['error'])(error);
} else {
printError({print: print}, error);
}
} catch (e) {
printError({print: print}, error);
}
};
imports.ssb = Object.fromEntries( imports.ssb = Object.fromEntries(
Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)]) Object.keys(ssb).map((key) => [key, ssb[key].bind(ssb)])
); );
imports.ssb.createIdentity = () => process.createIdentity(); imports.ssb.createIdentity = () => process.createIdentity();
imports.ssb.addIdentity = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_id_add')
).then(function () {
return ssb.addIdentity(process.credentials.session.name, id);
});
}
};
imports.ssb.deleteIdentity = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_id_delete')
).then(function () {
return ssb.deleteIdentity(process.credentials.session.name, id);
});
}
};
imports.ssb.setActiveIdentity = (id) => process.setActiveIdentity(id); imports.ssb.setActiveIdentity = (id) => process.setActiveIdentity(id);
imports.ssb.getActiveIdentity = () =>
ssb.getActiveIdentity(
process.credentials?.session?.name,
options.packageOwner,
options.packageName
);
imports.ssb.getOwnerIdentities = function () {
if (options.packageOwner) {
return ssb.getIdentities(options.packageOwner);
}
};
imports.ssb.getIdentities = function () {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.getIdentities(process.credentials.session.name);
}
};
imports.ssb.getPrivateKey = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_id_export')
).then(function () {
return ssb.getPrivateKey(process.credentials.session.name, id);
});
}
};
imports.ssb.appendMessageWithIdentity = function (id, message) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return Promise.resolve(
imports.core.permissionTest('ssb_append')
).then(function () {
return ssb.appendMessageWithIdentity(
process.credentials.session.name,
id,
message
);
});
}
};
imports.ssb.privateMessageEncrypt = function (id, recipients, message) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.privateMessageEncrypt(
process.credentials.session.name,
id,
recipients,
message
);
}
};
imports.ssb.privateMessageDecrypt = function (id, message) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.privateMessageDecrypt(
process.credentials.session.name,
id,
message
);
}
};
imports.ssb.swapWithServerIdentity = function (id) {
if (
process.credentials &&
process.credentials.session &&
process.credentials.session.name
) {
return ssb.swapWithServerIdentity(
process.credentials.session.name,
id
);
}
};
imports.ssb.addEventListener = undefined;
imports.ssb.removeEventListener = undefined;
imports.ssb.getIdentityInfo = undefined;
imports.fetch = async function (url, options) {
let settings = await loadSettings();
return http.fetch(url, options, settings?.fetch_hosts);
};
if ( if (
process.credentials && process.credentials &&
process.credentials.session && process.credentials.session &&
@@ -409,58 +644,53 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
}; };
} }
process.sendPermissions = async function sendPermissions() { process.sendPermissions = async function sendPermissions() {
process.send({ process.app.send({
action: 'permissions', action: 'permissions',
permissions: await imports.core.permissionsGranted(), permissions: await imports.core.permissionsGranted(),
}); });
}; };
process.client_api = { process.resetPermission = async function resetPermission(permission) {
createIdentity: function () { let user = process?.credentials?.session?.name;
return process.createIdentity(); await ssb.setUserPermission(
}, user,
resetPermission: async function resetPermission(message) { options?.packageOwner,
let user = process?.credentials?.session?.name; options?.packageName,
await ssb.setUserPermission( permission,
user, undefined
options?.packageOwner, );
options?.packageName, return process.sendPermissions();
message.permission,
undefined
);
return process.sendPermissions();
},
setActiveIdentity: function setActiveIdentity(message) {
return process.setActiveIdentity(message.identity);
},
}; };
ssb.registerImports(imports, process);
process.imports = imports;
process.task.setImports(imports); process.task.setImports(imports);
process.task.activate(); process.task.activate();
let source = await ssb.blobGet(blobId); let source = await ssb.blobGet(blobId);
let appSourceName = blobId; let appSourceName = blobId;
let appSource = utf8Decode(source); let appSource = utf8Decode(source);
let appObject = JSON.parse(appSource); try {
if (appObject.type == 'tildefriends-app') { let appObject = JSON.parse(appSource);
appSourceName = options?.script ?? 'app.js'; if (appObject.type == 'tildefriends-app') {
let id = appObject.files[appSourceName]; appSourceName = options?.script ?? 'app.js';
let blob = await ssb.blobGet(id); let id = appObject.files[appSourceName];
appSource = utf8Decode(blob); let blob = await ssb.blobGet(id);
await process.task.loadFile([ appSource = utf8Decode(blob);
'/tfrpc.js', await process.task.loadFile([
await File.readFile('core/tfrpc.js'), '/tfrpc.js',
]); await File.readFile('core/tfrpc.js'),
await Promise.all( ]);
Object.keys(appObject.files).map(async function (f) { await Promise.all(
await process.task.loadFile([ Object.keys(appObject.files).map(async function (f) {
f, await process.task.loadFile([
await ssb.blobGet(appObject.files[f]), f,
]); await ssb.blobGet(appObject.files[f]),
}) ]);
); })
);
}
} catch (e) {
printError({print: print}, e);
} }
if (process.send) { broadcastEvent('onSessionBegin', [getUser(process, process)]);
process.send({action: 'ready', version: version()}); if (process.app) {
process.app.send({action: 'ready', version: version()});
await process.sendPermissions(); await process.sendPermissions();
} }
await process.task.execute({name: appSourceName, source: appSource}); await process.task.execute({name: appSourceName, source: appSource});
@@ -470,62 +700,65 @@ exports.getProcessBlob = async function getProcessBlob(blobId, key, options) {
sendStats(); sendStats();
} }
} catch (error) { } catch (error) {
if (process?.task?.onError) { if (process.app) {
process.task.onError(error); if (process?.task?.onError) {
} process.task.onError(error);
if (rejectReady) { } else {
rejectReady(error); printError({print: print}, error);
}
} else {
printError({print: print}, error);
} }
rejectReady(error);
} }
} }
return process; return process;
};
/**
* Send any changed account information.
*/
function updateAccounts() {
g_update_accounts_scheduled = false;
let promises = [];
for (let process of Object.values(gProcesses)) {
promises.push(process.sendIdentities());
}
return Promise.all(promises);
} }
/** ssb.addEventListener('message', function () {
* SSB message added callback.
*/
ssb_internal.addEventListener('message', function () {
broadcastEvent('onMessage', [...arguments]); broadcastEvent('onMessage', [...arguments]);
if (!g_update_accounts_scheduled) {
setTimeout(updateAccounts, 1000);
g_update_accounts_scheduled = true;
}
}); });
ssb_internal.addEventListener('blob', function () { ssb.addEventListener('broadcasts', function () {
broadcastEvent('onBlob', [...arguments]);
});
ssb_internal.addEventListener('broadcasts', function () {
broadcastEvent('onBroadcastsChanged', []); broadcastEvent('onBroadcastsChanged', []);
}); });
ssb_internal.addEventListener('connections', function () { ssb.addEventListener('connections', function () {
broadcastEvent('onConnectionsChanged', []); broadcastEvent('onConnectionsChanged', []);
}); });
/** /**
* Send periodic stats to all clients. * TODOC
*/
async function loadSettings() {
let data = {};
try {
let settings = await new Database('core').get('settings');
if (settings) {
data = JSON.parse(settings);
}
} catch (error) {
print('Settings not found in database:', error);
}
for (let [key, value] of Object.entries(defaultGlobalSettings())) {
if (data[key] === undefined) {
data[key] = value.default_value;
}
}
return data;
}
/**
* TODOC
*/ */
function sendStats() { function sendStats() {
let apps = Object.values(gProcesses).filter((process) => process.send); let apps = Object.values(gProcesses)
.filter((process) => process.app)
.map((process) => process.app);
if (apps.length) { if (apps.length) {
let stats = getStats(); let stats = getStats();
for (let process of apps) { for (let app of apps) {
process.send({action: 'stats', stats: stats}); app.send({action: 'stats', stats: stats});
} }
setTimeout(sendStats, 1000); setTimeout(sendStats, 1000);
} else { } else {
@@ -533,16 +766,8 @@ function sendStats() {
} }
} }
/** let g_handler_index = 0;
* 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.
*/
exports.callAppHandler = async function callAppHandler( exports.callAppHandler = async function callAppHandler(
response, response,
app_blob_id, app_blob_id,
@@ -608,4 +833,4 @@ exports.callAppHandler = async function callAppHandler(
response.end(answer?.data); response.end(answer?.data);
}; };
/** @} */ export {invoke, getProcessBlob};

View File

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

113
core/http.js Normal file
View File

@@ -0,0 +1,113 @@
/**
* TODOC
* TODO: document so we can improve this
* @param {*} url
* @returns
*/
function parseUrl(url) {
// XXX: Hack.
let match = url.match(new RegExp('(\\w+)://([^/:]+)(?::(\\d+))?(.*)'));
return {
protocol: match[1],
host: match[2],
path: match[4],
port: match[3] ? parseInt(match[3]) : match[1] == 'http' ? 80 : 443,
};
}
/**
* TODOC
* @param {*} data
* @returns
*/
function parseResponse(data) {
let firstLine;
let headers = {};
while (true) {
let endLine = data.indexOf('\r\n');
let line = data.substring(0, endLine);
data = data.substring(endLine + 2);
if (!line.length) {
break;
} else if (!firstLine) {
firstLine = line;
} else {
let colon = line.indexOf(':');
headers[line.substring(colon)] = line.substring(colon + 1);
}
}
return {body: data};
}
/**
* TODOC
* @param {*} url
* @param {*} options
* @param {*} allowed_hosts
* @returns
*/
export function fetch(url, options, allowed_hosts) {
let parsed = parseUrl(url);
return new Promise(function (resolve, reject) {
if ((allowed_hosts ?? []).indexOf(parsed.host) == -1) {
throw new Error(`fetch() request to host ${parsed.host} is not allowed.`);
}
let socket = new Socket();
let buffer = new Uint8Array(0);
return socket
.connect(parsed.host, parsed.port)
.then(function () {
socket.read(function (data) {
if (data && data.length) {
let newBuffer = new Uint8Array(buffer.length + data.length);
newBuffer.set(buffer, 0);
newBuffer.set(data, buffer.length);
buffer = newBuffer;
} else {
let result = parseHttpResponse(buffer);
if (!result) {
reject(new Exception('Parse failed.'));
}
if (typeof result == 'number') {
if (result == -2) {
reject('Incomplete request.');
} else {
reject('Bad request.');
}
} else if (typeof result == 'object') {
resolve({
body: buffer.slice(result.bytes_parsed),
status: result.status,
message: result.message,
headers: result.headers,
});
} else {
reject(new Exception('Unexpected parse result.'));
}
resolve(parseResponse(utf8Decode(buffer)));
}
});
if (parsed.port == 443) {
return socket.startTls();
}
})
.then(function () {
let body =
typeof options?.body == 'string'
? utf8Encode(options.body)
: options.body || new Uint8Array(0);
let headers = utf8Encode(
`${options?.method ?? 'GET'} ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\nContent-Length: ${body.length}\r\n\r\n`
);
let fullRequest = new Uint8Array(headers.length + body.length);
fullRequest.set(headers, 0);
fullRequest.set(body, headers.length);
socket.write(fullRequest);
})
.catch(function (error) {
reject(error);
});
});
}

View File

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

View File

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

View File

@@ -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(); const k_is_browser = get_is_browser();
/** Registered methods. */
let g_api = {}; let g_api = {};
/** The next method identifier. */
let g_next_id = 1; let g_next_id = 1;
/** Identifiers of pending calls. */
let g_calls = {}; let g_calls = {};
/** /**
* Check if being called from a browser vs. server-side. * TODOC
* @return true if called from a browser. * @returns
*/ */
function get_is_browser() { function get_is_browser() {
try { try {
@@ -26,30 +15,16 @@ function get_is_browser() {
} }
} }
/** \cond */
if (k_is_browser) { if (k_is_browser) {
print = console.log; 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. * TODOC
* @param target The target. * @param {*} target
* @param prop The name of the function. * @param {*} prop
* @param receiver The receiver. * @param {*} receiver
* @return A function. * @returns
*/ */
function make_rpc(target, prop, receiver) { function make_rpc(target, prop, receiver) {
return function () { return function () {
@@ -80,8 +55,8 @@ function make_rpc(target, prop, receiver) {
} }
/** /**
* Send a response. * TODOC
* @param response The response. * @param {*} response
*/ */
function send(response) { function send(response) {
if (k_is_browser) { if (k_is_browser) {
@@ -92,8 +67,8 @@ function send(response) {
} }
/** /**
* Invoke a remote procedure. * TODOC
* @param message An object describing the call. * @param {*} message
*/ */
function call_rpc(message) { function call_rpc(message) {
if (message && message.message === 'tfrpc') { 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. * TODOC
* @param method The method. * @param {*} method
*/ */
export function register(method) { export function register(method) {
g_api[method.name] = method; g_api[method.name] = method;
} }
/** @} */

View File

@@ -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} html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ /* 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} 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-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-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-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,.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-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-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-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important} .w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important} .w3-hover-none:hover{box-shadow:none!important}
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
/* Colors */ /* Colors */
.w3-amber,.w3-hover-amber:hover{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-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-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-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-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-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-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-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}

View File

@@ -25,14 +25,14 @@
}: }:
pkgs.stdenv.mkDerivation rec { pkgs.stdenv.mkDerivation rec {
pname = "tildefriends"; pname = "tildefriends";
version = "0.2025.11"; version = "0.0.28";
src = pkgs.fetchFromGitea { src = pkgs.fetchFromGitea {
domain = "dev.tildefriends.net"; domain = "dev.tildefriends.net";
owner = "cory"; owner = "cory";
repo = "tildefriends"; repo = "tildefriends";
rev = "v${version}"; rev = "f02423d0846fefd5ab21fa4542fb77ce5714547c";
hash = "sha256-z4v4ghKOBTMv+agTUKg+HU8zfE4imluXFsozQCT4qX8="; hash = "sha256-QyM7wmViXJc4r8uTu4oE/HO3Z9tzNbFIX2+AOTQz9ZY=";
fetchSubmodules = true; fetchSubmodules = true;
}; };

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