Compare commits
	
		
			9 Commits
		
	
	
		
			v0.0.29
			...
			fae2771645
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						fae2771645
	
				 | 
					
					
						|||
| 
						
						
							
						
						2bb6d68122
	
				 | 
					
					
						|||
| 
						
						
							
						
						5c8c6e8760
	
				 | 
					
					
						|||
| 
						
						
							
						
						85ac8080f4
	
				 | 
					
					
						|||
| 
						
						
							
						
						0751699bc8
	
				 | 
					
					
						|||
| 
						
						
							
						
						5551fd2dea
	
				 | 
					
					
						|||
| 
						
						
							
						
						69b2e2a955
	
				 | 
					
					
						|||
| 
						
						
							
						
						34c7fa8312
	
				 | 
					
					
						|||
| 
						
						
							
						
						396f37ee3b
	
				 | 
					
					
						
@@ -14,7 +14,7 @@ IndentWidth: 4
 | 
			
		||||
MaxEmptyLinesToKeep: 1
 | 
			
		||||
ObjCBlockIndentWidth: 4
 | 
			
		||||
ObjCBreakBeforeNestedBlockParam: false
 | 
			
		||||
SortIncludes: true
 | 
			
		||||
SortIncludes: false
 | 
			
		||||
TabWidth: 4
 | 
			
		||||
UseTab: Always
 | 
			
		||||
...
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
.git
 | 
			
		||||
db.sqlite*
 | 
			
		||||
out/
 | 
			
		||||
.svn
 | 
			
		||||
db.*
 | 
			
		||||
out/**/*.o
 | 
			
		||||
out/**/*.d
 | 
			
		||||
NOTES.md
 | 
			
		||||
 
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
name: Build Tilde Friends
 | 
			
		||||
run-name: ${{ gitea.actor }} running 🚀
 | 
			
		||||
on: [push]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  Build-All:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    container:
 | 
			
		||||
      image: node:23-bookworm-slim
 | 
			
		||||
      valid_volumes:
 | 
			
		||||
        - '/opt/keys'
 | 
			
		||||
        - '/opt/deps'
 | 
			
		||||
      volumes:
 | 
			
		||||
        - /opt/keys:/opt/keys
 | 
			
		||||
        - /opt/deps:/opt/deps
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Install build dependencies
 | 
			
		||||
        run: >
 | 
			
		||||
          apt update && apt install -y \
 | 
			
		||||
            build-essential \
 | 
			
		||||
            clang-19 \
 | 
			
		||||
            cmake \
 | 
			
		||||
            curl \
 | 
			
		||||
            docker.io \
 | 
			
		||||
            doxygen \
 | 
			
		||||
            file \
 | 
			
		||||
            gcc-aarch64-linux-gnu \
 | 
			
		||||
            git \
 | 
			
		||||
            graphviz \
 | 
			
		||||
            libgpgme11 \
 | 
			
		||||
            libssl-dev \
 | 
			
		||||
            mingw-w64 \
 | 
			
		||||
            rsync \
 | 
			
		||||
            unzip \
 | 
			
		||||
            zip \
 | 
			
		||||
            zlib1g-dev
 | 
			
		||||
      - name: Get code
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          submodules: true
 | 
			
		||||
      - name: Setup environment
 | 
			
		||||
        run: |
 | 
			
		||||
          update-alternatives --install /usr/bin/clang clang /usr/bin/clang-19 100
 | 
			
		||||
          update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-19 100
 | 
			
		||||
          ln -s /opt/keys .keys
 | 
			
		||||
          ln -sf /opt/deps/ios_toolchain deps/
 | 
			
		||||
          ln -sf /opt/deps/macos_toolchain deps/
 | 
			
		||||
      - name: Build documentation
 | 
			
		||||
        run: |
 | 
			
		||||
          mkdir -p out/html/ ~/.ssh/
 | 
			
		||||
          make docs
 | 
			
		||||
          echo 'pildefriends ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3Kde5vDO0TrMBDK0IGGeNGe/XinWAZkSQ/rXxwUjt' >> ~/.ssh/known_hosts
 | 
			
		||||
          rsync -avP --delete -e "ssh -i /opt/keys/ssh.ed25519" out/html/ tfdocs@pildefriends:docs/html/
 | 
			
		||||
      - name: Setup JDK
 | 
			
		||||
        uses: actions/setup-java@v3
 | 
			
		||||
        with:
 | 
			
		||||
          java-version: '17'
 | 
			
		||||
          distribution: 'temurin'
 | 
			
		||||
      - name: Setup Android SDK
 | 
			
		||||
        uses: android-actions/setup-android@v3
 | 
			
		||||
        with:
 | 
			
		||||
          packages: 'tools platform-tools build-tools;34.0.0 platforms;android-34 ndk;26.3.11579264'
 | 
			
		||||
      - name: Docker build
 | 
			
		||||
        run: DOCKER_BUILDKIT=1 docker build .
 | 
			
		||||
      - name: Build
 | 
			
		||||
        run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist docs
 | 
			
		||||
      - name: Upload artifacts
 | 
			
		||||
        uses: actions/upload-artifact@v3
 | 
			
		||||
        with:
 | 
			
		||||
          name: dist
 | 
			
		||||
          path: dist/*
 | 
			
		||||
							
								
								
									
										13
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,20 +1,11 @@
 | 
			
		||||
build/
 | 
			
		||||
*.core
 | 
			
		||||
db.*
 | 
			
		||||
deps/ios_toolchain
 | 
			
		||||
deps/macos_toolchain
 | 
			
		||||
deps/ios_toolchain/
 | 
			
		||||
deps/openssl/
 | 
			
		||||
dist/
 | 
			
		||||
.flatpak-builder
 | 
			
		||||
.keys
 | 
			
		||||
**/.DS_Store
 | 
			
		||||
logs/
 | 
			
		||||
**/node_modules
 | 
			
		||||
out
 | 
			
		||||
repo/
 | 
			
		||||
result
 | 
			
		||||
*.swo
 | 
			
		||||
*.swp
 | 
			
		||||
tmp/
 | 
			
		||||
unsigned/
 | 
			
		||||
.zsign_cache/
 | 
			
		||||
NOTES.md
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							@@ -19,13 +19,3 @@
 | 
			
		||||
[submodule "deps/picohttpparser"]
 | 
			
		||||
	path = deps/picohttpparser
 | 
			
		||||
	url = https://github.com/h2o/picohttpparser.git
 | 
			
		||||
[submodule "deps/openssl_src"]
 | 
			
		||||
	path = deps/openssl_src
 | 
			
		||||
	url = https://github.com/openssl/openssl.git
 | 
			
		||||
	shallow = true
 | 
			
		||||
[submodule "deps/c-ares"]
 | 
			
		||||
	path = deps/c-ares
 | 
			
		||||
	url = https://github.com/c-ares/c-ares.git
 | 
			
		||||
[submodule "deps/zsign"]
 | 
			
		||||
	path = deps/zsign
 | 
			
		||||
	url = https://github.com/zhlynn/zsign.git
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								.markdownlint.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.markdownlint.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
default: true
 | 
			
		||||
MD010: false # Ignore tabs in code blocks
 | 
			
		||||
MD013: false # Don't wrap lines by default
 | 
			
		||||
MD046: 
 | 
			
		||||
  style: "fenced" # Force fenced code blocks
 | 
			
		||||
@@ -2,7 +2,6 @@ node_modules
 | 
			
		||||
src
 | 
			
		||||
deps
 | 
			
		||||
.clang-format
 | 
			
		||||
flake.lock
 | 
			
		||||
 | 
			
		||||
# Minified files
 | 
			
		||||
**/*.min.css
 | 
			
		||||
@@ -13,3 +12,8 @@ flake.lock
 | 
			
		||||
apps/ssb/tribute.esm.js
 | 
			
		||||
apps/api/app.js
 | 
			
		||||
**/emojis.json
 | 
			
		||||
 | 
			
		||||
# only markdownlint should deal with the documentation
 | 
			
		||||
docs/**/*.md
 | 
			
		||||
 | 
			
		||||
NOTES.md
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,19 @@
 | 
			
		||||
FROM bitnami/minideb:bookworm AS build
 | 
			
		||||
FROM bitnami/minideb:bullseye AS build
 | 
			
		||||
 | 
			
		||||
RUN apt-get update && \
 | 
			
		||||
	apt-get install -y --no-install-recommends \
 | 
			
		||||
		gcc \
 | 
			
		||||
		libc6-dev \
 | 
			
		||||
		perl \
 | 
			
		||||
		libssl-dev \
 | 
			
		||||
		make
 | 
			
		||||
 | 
			
		||||
COPY . /app
 | 
			
		||||
RUN make -C /app -j $(nproc) release
 | 
			
		||||
 | 
			
		||||
FROM bitnami/minideb:bookworm
 | 
			
		||||
FROM bitnami/minideb:bullseye
 | 
			
		||||
RUN apt-get update && \
 | 
			
		||||
	apt-get install -y --no-install-recommends \
 | 
			
		||||
		libssl1.1
 | 
			
		||||
 | 
			
		||||
COPY --from=build /app/out/release/tildefriends /app/out/release/tildefriends
 | 
			
		||||
COPY --from=build /app/apps /app/apps
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										834
									
								
								GNUmakefile
									
									
									
									
									
								
							
							
						
						
									
										834
									
								
								GNUmakefile
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
Copyright 2014 Cory McWilliams
 | 
			
		||||
Copyright 2014-2024 Cory McWilliams
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
 | 
			
		||||
this software and associated documentation files (the "Software"), to deal in
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										69
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										69
									
								
								README.md
									
									
									
									
									
								
							@@ -1,73 +1,22 @@
 | 
			
		||||
# Tilde Friends
 | 
			
		||||
 | 
			
		||||
Tilde Friends participates in the Secure Scuttlebutt decentralized social
 | 
			
		||||
network while also functioning as a platform for making, sharing, and running
 | 
			
		||||
web applications.
 | 
			
		||||
Tilde Friends is a tool for making and sharing.
 | 
			
		||||
 | 
			
		||||
A public instance lives at https://www.tildefriends.net/.
 | 
			
		||||
 | 
			
		||||
It is both a peer-to-peer social network client, participating in Secure Scuttlebutt, as well as a platform for writing and running web applications.
 | 
			
		||||
 | 
			
		||||
## Goals
 | 
			
		||||
 | 
			
		||||
1. Be the fanciest, best-maintained Secure Scuttlebutt client in town.
 | 
			
		||||
1. Make it easy to make, share, and run all sorts of applications while
 | 
			
		||||
   respecting the privacy and safety of your data.
 | 
			
		||||
 | 
			
		||||
## Getting the Source
 | 
			
		||||
 | 
			
		||||
Tilde Friends uses git submodules, so either:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git clone --recurse-submodules https://dev.tildefriends.net/cory/tildefriends.git
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
or:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
git clone https://dev.tildefriends.net/cory/tildefriends.git
 | 
			
		||||
cd tildefriends
 | 
			
		||||
git submodule update --init --recursive
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The `.tar.xz` source releases are all-inclusive.
 | 
			
		||||
 | 
			
		||||
## Building
 | 
			
		||||
 | 
			
		||||
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. It's possible
 | 
			
		||||
to build for Android, iOS, and Windows on Linux, if you have the right
 | 
			
		||||
dependencies in the right places.
 | 
			
		||||
 | 
			
		||||
### Requirements
 | 
			
		||||
 | 
			
		||||
System OpenSSL libraries are assumed to be available on Haiku and OpenSSL.
 | 
			
		||||
 | 
			
		||||
On MacOS, Xcode's command-line tools are expected to be available.
 | 
			
		||||
 | 
			
		||||
### Build Commands
 | 
			
		||||
 | 
			
		||||
Run `make` with no arguments to see available build targets and options. `make
 | 
			
		||||
debug` is a good place to start.
 | 
			
		||||
 | 
			
		||||
To build in docker, `docker build .`.
 | 
			
		||||
 | 
			
		||||
`make format` and `make prettier` will normalize formatting to the coding
 | 
			
		||||
standard.
 | 
			
		||||
 | 
			
		||||
## Running
 | 
			
		||||
 | 
			
		||||
By default, running the built `out/debug/tildefriends` executable will start a
 | 
			
		||||
web server at <http://localhost:12345/>. It expects to be run with the
 | 
			
		||||
repository root as the current working directory. `tildefriends -h` lists
 | 
			
		||||
further options.
 | 
			
		||||
 | 
			
		||||
The first user to create an account and log in will be granted administrative
 | 
			
		||||
privileges. Further administration can be done in the `admin` app at
 | 
			
		||||
<http://localhost:12345/~core/admin/>.
 | 
			
		||||
1. Make it easy and fun to run all sorts of web applications.
 | 
			
		||||
2. Provide security that is easy to understand and protects your data.
 | 
			
		||||
3. Make creating and sharing web applications accessible to anyone with a browser.
 | 
			
		||||
 | 
			
		||||
## Documentation
 | 
			
		||||
 | 
			
		||||
Docs live here: <https://docs.tildefriends.net/>.
 | 
			
		||||
Docs are a work in progress in the `docs` folder, or alternatively in Tilde Friends: <https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
 | 
			
		||||
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
All code unless otherwise noted in is provided under the
 | 
			
		||||
[MIT](https://opensource.org/licenses/MIT) license.
 | 
			
		||||
All code, documentation and assets unless otherwise noted in is provided under the
 | 
			
		||||
[MIT](https://opensource.org/licenses/MIT/) license.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🎛",
 | 
			
		||||
	"previous": "&kmKNyb/uaXNb24gCinJtfS8iWx4cLUWdtl0y2DwEUas=.sha256"
 | 
			
		||||
	"previous": "&vrpS/vE7n588iYv1p8HafDxHB+YDHTrtUbJiu9nGA9I=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,7 @@
 | 
			
		||||
		<script>
 | 
			
		||||
			const g_data = $data;
 | 
			
		||||
		</script>
 | 
			
		||||
		<link rel="stylesheet" href="w3.css" />
 | 
			
		||||
		<!-- prettier-ignore -->
 | 
			
		||||
		<link rel="stylesheet" href="w3.css"></link>
 | 
			
		||||
		<style>
 | 
			
		||||
			/* 2018 Valiant Poppy */
 | 
			
		||||
			.w3-theme-l5 {color:#000 !important; background-color:#fbf3f3 !important}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,55 +27,31 @@ function global_settings_set(key, value) {
 | 
			
		||||
		});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function title_case(name) {
 | 
			
		||||
	return name
 | 
			
		||||
		.split('_')
 | 
			
		||||
		.map((x) => x.charAt(0).toUpperCase() + x.substring(1))
 | 
			
		||||
		.join(' ');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.addEventListener('load', function () {
 | 
			
		||||
	const permission_template = (permission) => html` <code>${permission}</code>`;
 | 
			
		||||
	function input_template(key, description) {
 | 
			
		||||
		if (description.type === 'boolean') {
 | 
			
		||||
			return html`
 | 
			
		||||
				<li class="w3-row">
 | 
			
		||||
					<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
 | 
			
		||||
					<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${key}</label>
 | 
			
		||||
					<div class="w3-quarter w3-padding">${description.description}</div>
 | 
			
		||||
					<div class="w3-quarter w3-padding w3-center"><input class="w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input></div>
 | 
			
		||||
					<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstChild.checked)}>Set</button>
 | 
			
		||||
					<input class="w3-quarter w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
 | 
			
		||||
					<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
 | 
			
		||||
				</li>
 | 
			
		||||
			`;
 | 
			
		||||
		} else if (description.type === 'textarea') {
 | 
			
		||||
			return html`
 | 
			
		||||
				<li class="w3-row">
 | 
			
		||||
					<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold"
 | 
			
		||||
						>${title_case(key)}</label
 | 
			
		||||
					>
 | 
			
		||||
					<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${key}</label>
 | 
			
		||||
					<div class="w3-rest w3-padding">${description.description}</div>
 | 
			
		||||
					<textarea
 | 
			
		||||
						class="w3-input"
 | 
			
		||||
						style="vertical-align: top; resize: vertical"
 | 
			
		||||
						id=${'gs_' + key}
 | 
			
		||||
					>
 | 
			
		||||
${description.value}</textarea
 | 
			
		||||
					>
 | 
			
		||||
					<button
 | 
			
		||||
						class="w3-button w3-right w3-quarter w3-theme-action"
 | 
			
		||||
						@click=${(e) =>
 | 
			
		||||
							global_settings_set(
 | 
			
		||||
								key,
 | 
			
		||||
								e.srcElement.previousElementSibling.value
 | 
			
		||||
							)}
 | 
			
		||||
					>
 | 
			
		||||
						Set
 | 
			
		||||
					</button>
 | 
			
		||||
					<textarea class="w3-input" style="vertical-align: top; resize: vertical" id=${'gs_' + key}>${description.value}</textarea>
 | 
			
		||||
					<button class="w3-button w3-right w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
 | 
			
		||||
				</li>
 | 
			
		||||
			`;
 | 
			
		||||
		} else if (description.type != 'hidden') {
 | 
			
		||||
		} else {
 | 
			
		||||
			return html`
 | 
			
		||||
				<li class="w3-row">
 | 
			
		||||
					<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label>
 | 
			
		||||
					<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${key}</label>
 | 
			
		||||
					<div class="w3-quarter w3-padding">${description.description}</div>
 | 
			
		||||
					<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input>
 | 
			
		||||
					<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
 | 
			
		||||
@@ -85,17 +61,13 @@ ${description.value}</textarea
 | 
			
		||||
	}
 | 
			
		||||
	const user_template = (user, permissions) => html`
 | 
			
		||||
		<li class="w3-card w3-margin">
 | 
			
		||||
			<button
 | 
			
		||||
				class="w3-button w3-theme-action"
 | 
			
		||||
				@click=${(e) => delete_user(user)}
 | 
			
		||||
			>
 | 
			
		||||
				Delete
 | 
			
		||||
			</button>
 | 
			
		||||
			<button class="w3-button w3-theme-action" @click=${(e) => delete_user(user)}>Delete</button>
 | 
			
		||||
			${user}: ${permissions.map((x) => permission_template(x))}
 | 
			
		||||
		</li>
 | 
			
		||||
	`;
 | 
			
		||||
	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">
 | 
			
		||||
				${Object.entries(users).map((u) => user_template(u[0], u[1]))}
 | 
			
		||||
			</ul>`;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
/* W3.CSS 5.01 March 14 2025 by Jan Egil and Borge Refsnes */
 | 
			
		||||
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
 | 
			
		||||
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
 | 
			
		||||
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
 | 
			
		||||
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
 | 
			
		||||
@@ -108,10 +108,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
 | 
			
		||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
 | 
			
		||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
 | 
			
		||||
 | 
			
		||||
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
 | 
			
		||||
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
 | 
			
		||||
 | 
			
		||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
 | 
			
		||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
 | 
			
		||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
 | 
			
		||||
@@ -153,9 +149,9 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
 | 
			
		||||
.w3-hover-none:hover{box-shadow:none!important}
 | 
			
		||||
/* Colors */
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover,.w3-warning{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
 | 
			
		||||
.w3-blue,.w3-hover-blue:hover,.w3-info,.w3-primary{color:#fff!important;background-color:#2196F3!important}
 | 
			
		||||
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
 | 
			
		||||
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
 | 
			
		||||
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
 | 
			
		||||
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
 | 
			
		||||
@@ -170,24 +166,15 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
 | 
			
		||||
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
 | 
			
		||||
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
 | 
			
		||||
.w3-red,.w3-hover-red:hover,.w3-danger{color:#fff!important;background-color:#f44336!important}
 | 
			
		||||
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
 | 
			
		||||
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
 | 
			
		||||
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
 | 
			
		||||
.w3-yellow,.w3-hover-yellow:hover,.w3-note{color:#000!important;background-color:#ffeb3b!important}
 | 
			
		||||
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
 | 
			
		||||
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
 | 
			
		||||
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
 | 
			
		||||
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover,.w3-secondary{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
 | 
			
		||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
 | 
			
		||||
 | 
			
		||||
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
 | 
			
		||||
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
 | 
			
		||||
.w3-emerald,.w3-hover-emerald:hover,.w3-success{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
 | 
			
		||||
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
 | 
			
		||||
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
 | 
			
		||||
 | 
			
		||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
 | 
			
		||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
 | 
			
		||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
 | 
			
		||||
@@ -245,4 +232,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
 | 
			
		||||
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
 | 
			
		||||
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
 | 
			
		||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
 | 
			
		||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "📜",
 | 
			
		||||
	"previous": "&BEf0nraBdHk/+PWqx6tOSu5rheWVaxaL7orAOz3285M=.sha256"
 | 
			
		||||
	"previous": "&miGORZ8BwjHg2YO0t4bms6SI28XWPYqnqOZ8u9zsbZc=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ function* treeify(prefix, o) {
 | 
			
		||||
 | 
			
		||||
function markdown(md) {
 | 
			
		||||
	let parsed = new commonmark.Parser().parse(md ?? '*undocumented*');
 | 
			
		||||
	return new commonmark.HtmlRenderer({safe: true}).render(parsed);
 | 
			
		||||
	return new commonmark.HtmlRenderer().render(parsed);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function document(api) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "💻",
 | 
			
		||||
	"previous": "&sFRTDn/RpxP1NJeECXHrXKwCRUJsEOEDVaCMPl50zpM=.sha256"
 | 
			
		||||
	"previous": "&icsPplXHgmpkbNWyo/WTvRdT/A8BXxW4lJixOtP4ueQ=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,10 @@ async function fetch_info(apps) {
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
async function fetch_shared_apps() {
 | 
			
		||||
	let messages = {};
 | 
			
		||||
 | 
			
		||||
@@ -65,17 +69,17 @@ async function main() {
 | 
			
		||||
	const stylesheet = `
 | 
			
		||||
		body {
 | 
			
		||||
			color: whitesmoke;
 | 
			
		||||
			margin: 8px;
 | 
			
		||||
			font-family: sans-serif;
 | 
			
		||||
			margin: 16px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.iconbox {
 | 
			
		||||
		.container {
 | 
			
		||||
			display: grid;
 | 
			
		||||
			grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.iconbox::after {
 | 
			
		||||
			content: "";
 | 
			
		||||
			flex: auto;
 | 
			
		||||
			grid-template-columns: repeat(auto-fill, 64px);
 | 
			
		||||
			gap: 1em;
 | 
			
		||||
			justify-content: space-around;
 | 
			
		||||
			background-color: #ffffff10;
 | 
			
		||||
			border: 2px solid #073642;
 | 
			
		||||
			border-radius: 8px;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		.app {
 | 
			
		||||
@@ -97,28 +101,16 @@ async function main() {
 | 
			
		||||
	`;
 | 
			
		||||
 | 
			
		||||
	const body = `
 | 
			
		||||
		<h1>Welcome to Tilde Friends</h1>
 | 
			
		||||
		<h1 style="text-shadow: #808080 0 0 10px;">Welcome to Tilde Friends.</h1>
 | 
			
		||||
 | 
			
		||||
		<div class="w3-card-4 w3-margin-top">
 | 
			
		||||
			<header class="w3-container w3-light-blue">
 | 
			
		||||
				<h2>Your Apps</h2>
 | 
			
		||||
			</header>
 | 
			
		||||
			<div id="apps" class="w3-indigo iconbox"></div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<h2>your apps</h2>
 | 
			
		||||
		<div id="apps" class="container"></div>
 | 
			
		||||
 | 
			
		||||
		<div class="w3-card-4 w3-margin-top">
 | 
			
		||||
			<header class="w3-container w3-light-blue">
 | 
			
		||||
				<h2>Shared Apps</h2>
 | 
			
		||||
			</header>
 | 
			
		||||
			<div id="shared_apps" class="w3-indigo iconbox"></div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<h2>shared apps</h2>
 | 
			
		||||
		<div id="shared_apps" class="container"></div>
 | 
			
		||||
 | 
			
		||||
		<div class="w3-card-4 w3-margin-top">
 | 
			
		||||
			<header class="w3-container w3-light-blue">
 | 
			
		||||
				<h2>Core Apps</h2>
 | 
			
		||||
			</header>
 | 
			
		||||
			<div id="core_apps" class="w3-indigo iconbox"></div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<h2>core apps</h2>
 | 
			
		||||
		<div id="core_apps" class="container"></div>
 | 
			
		||||
	`;
 | 
			
		||||
 | 
			
		||||
	const script = `
 | 
			
		||||
@@ -134,13 +126,9 @@ async function main() {
 | 
			
		||||
 | 
			
		||||
			// For each app in the provided list
 | 
			
		||||
			for (let app of Object.keys(apps).sort()) {
 | 
			
		||||
 | 
			
		||||
				// Create the item
 | 
			
		||||
				let inline = document.createElement('div');
 | 
			
		||||
				inline.style.display = 'inline-block';
 | 
			
		||||
				inline.classList.add('w3-button');
 | 
			
		||||
				list.appendChild(inline);
 | 
			
		||||
				let div = document.createElement('div');
 | 
			
		||||
				inline.appendChild(div);
 | 
			
		||||
				let div = list.appendChild(document.createElement('div'));
 | 
			
		||||
				div.classList.add('app');
 | 
			
		||||
 | 
			
		||||
				// The app's icon
 | 
			
		||||
@@ -173,13 +161,12 @@ async function main() {
 | 
			
		||||
	<!DOCTYPE html>
 | 
			
		||||
	<html>
 | 
			
		||||
		<head>
 | 
			
		||||
			<link type="text/css" rel="stylesheet" href="w3.css"></link>
 | 
			
		||||
			<style>
 | 
			
		||||
				${stylesheet}
 | 
			
		||||
			</style>
 | 
			
		||||
		</head>
 | 
			
		||||
 | 
			
		||||
		<body class="w3-darkgray">
 | 
			
		||||
		<body>
 | 
			
		||||
			${body}
 | 
			
		||||
		</body>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										248
									
								
								apps/apps/w3.css
									
									
									
									
									
								
							
							
						
						
									
										248
									
								
								apps/apps/w3.css
									
									
									
									
									
								
							@@ -1,248 +0,0 @@
 | 
			
		||||
/* W3.CSS 5.01 March 14 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}
 | 
			
		||||
/* Colors */
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover,.w3-warning{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
 | 
			
		||||
.w3-blue,.w3-hover-blue:hover,.w3-info,.w3-primary{color:#fff!important;background-color:#2196F3!important}
 | 
			
		||||
.w3-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,.w3-danger{color:#fff!important;background-color:#f44336!important}
 | 
			
		||||
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
 | 
			
		||||
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
 | 
			
		||||
.w3-yellow,.w3-hover-yellow:hover,.w3-note{color:#000!important;background-color:#ffeb3b!important}
 | 
			
		||||
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
 | 
			
		||||
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
 | 
			
		||||
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover,.w3-secondary{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
 | 
			
		||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
 | 
			
		||||
 | 
			
		||||
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
 | 
			
		||||
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
 | 
			
		||||
.w3-emerald,.w3-hover-emerald:hover,.w3-success{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
 | 
			
		||||
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
 | 
			
		||||
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
 | 
			
		||||
 | 
			
		||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
 | 
			
		||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
 | 
			
		||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
 | 
			
		||||
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
 | 
			
		||||
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
 | 
			
		||||
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
 | 
			
		||||
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
 | 
			
		||||
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
 | 
			
		||||
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
 | 
			
		||||
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
 | 
			
		||||
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
 | 
			
		||||
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
 | 
			
		||||
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
 | 
			
		||||
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
 | 
			
		||||
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
 | 
			
		||||
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
 | 
			
		||||
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
 | 
			
		||||
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
 | 
			
		||||
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
 | 
			
		||||
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
 | 
			
		||||
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
 | 
			
		||||
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
 | 
			
		||||
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
 | 
			
		||||
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
 | 
			
		||||
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
 | 
			
		||||
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
 | 
			
		||||
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
 | 
			
		||||
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
 | 
			
		||||
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
 | 
			
		||||
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
 | 
			
		||||
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
 | 
			
		||||
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
 | 
			
		||||
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
 | 
			
		||||
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
 | 
			
		||||
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
 | 
			
		||||
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
 | 
			
		||||
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
 | 
			
		||||
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
 | 
			
		||||
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
 | 
			
		||||
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
 | 
			
		||||
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
 | 
			
		||||
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
 | 
			
		||||
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
 | 
			
		||||
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
 | 
			
		||||
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
 | 
			
		||||
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
 | 
			
		||||
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
 | 
			
		||||
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
 | 
			
		||||
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
 | 
			
		||||
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
 | 
			
		||||
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
 | 
			
		||||
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
 | 
			
		||||
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
 | 
			
		||||
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
 | 
			
		||||
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
 | 
			
		||||
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
 | 
			
		||||
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
 | 
			
		||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🪵",
 | 
			
		||||
	"previous": "&3jabNEk6W2uolzTvfXX6fcWF50N3501vtgZ6ZxFVJ1s=.sha256"
 | 
			
		||||
	"previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -52,8 +52,8 @@ export async function get_blog_message(id) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function markdown(md) {
 | 
			
		||||
	let reader = new commonmark.Parser();
 | 
			
		||||
	let writer = new commonmark.HtmlRenderer({safe: true});
 | 
			
		||||
	let reader = new commonmark.Parser({safe: true});
 | 
			
		||||
	let writer = new commonmark.HtmlRenderer();
 | 
			
		||||
	let parsed = reader.parse(md || '');
 | 
			
		||||
	let walker = parsed.walker();
 | 
			
		||||
	let event, node;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								apps/blog/commonmark.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								apps/blog/commonmark.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										42
									
								
								apps/blog/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										42
									
								
								apps/blog/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,5 +1,4 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "💽",
 | 
			
		||||
	"previous": "&uQzkIe/Aj8yNhLKe3hEq+5fEJsGwIUx8NVBjJKwoV2U=.sha256"
 | 
			
		||||
	"emoji": "💽"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -51,19 +51,6 @@ async function key_list(db) {
 | 
			
		||||
	app.setDocument(doc);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function load() {
 | 
			
		||||
	if (core.user?.credentials?.session) {
 | 
			
		||||
		database_list();
 | 
			
		||||
	} else {
 | 
			
		||||
		app.setDocument(`<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<body style="background: #888">
 | 
			
		||||
	<h1>Must be signed in to examine databases.</h1>
 | 
			
		||||
</body>
 | 
			
		||||
</html>`);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
core.register('message', async function (message) {
 | 
			
		||||
	if (message.event == 'hashChange') {
 | 
			
		||||
		let hash = message.hash.substring(1);
 | 
			
		||||
@@ -75,9 +62,9 @@ core.register('message', async function (message) {
 | 
			
		||||
		} else if (hash.length) {
 | 
			
		||||
			key_list(await database(hash.split(':').slice(1).join(':')));
 | 
			
		||||
		} else {
 | 
			
		||||
			load();
 | 
			
		||||
			database_list();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
load();
 | 
			
		||||
database_list();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "➡️",
 | 
			
		||||
	"previous": "&YDDSzbRD8NFAykYlZnk4r4hAK5qXjT5LmKE6rhS1s+A=.sha256"
 | 
			
		||||
	"emoji": "➡️"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ async function contacts_internal(id, last_row_id, following, max_row_id) {
 | 
			
		||||
	result.blocking = result.blocking || {};
 | 
			
		||||
	let contacts = await query(
 | 
			
		||||
		`
 | 
			
		||||
				SELECT json(content) AS content FROM messages
 | 
			
		||||
				SELECT content FROM messages
 | 
			
		||||
				WHERE author = ? AND
 | 
			
		||||
				rowid > ? AND
 | 
			
		||||
				rowid <= ? AND
 | 
			
		||||
@@ -189,6 +189,50 @@ async function fetch_about(db, ids, users) {
 | 
			
		||||
	return Object.assign({}, users);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getAbout(db, id) {
 | 
			
		||||
	if (g_about_cache[id]) {
 | 
			
		||||
		return g_about_cache[id];
 | 
			
		||||
	}
 | 
			
		||||
	let o = await db.get(id + ':about');
 | 
			
		||||
	const k_version = 4;
 | 
			
		||||
	let f = o ? JSON.parse(o) : o;
 | 
			
		||||
	if (!f || f.version != k_version) {
 | 
			
		||||
		f = {about: {}, sequence: 0, version: k_version};
 | 
			
		||||
	}
 | 
			
		||||
	await ssb.sqlAsync(
 | 
			
		||||
		'SELECT ' +
 | 
			
		||||
			'  sequence, ' +
 | 
			
		||||
			'  content ' +
 | 
			
		||||
			'FROM messages ' +
 | 
			
		||||
			'WHERE ' +
 | 
			
		||||
			'  author = ?1 AND ' +
 | 
			
		||||
			'  sequence > ?2 AND ' +
 | 
			
		||||
			"  json_extract(content, '$.type') = 'about' AND " +
 | 
			
		||||
			"  json_extract(content, '$.about') = ?1 " +
 | 
			
		||||
			'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
 | 
			
		||||
			'ORDER BY sequence',
 | 
			
		||||
		[id, f.sequence],
 | 
			
		||||
		function (row) {
 | 
			
		||||
			f.sequence = row.sequence;
 | 
			
		||||
			if (row.content) {
 | 
			
		||||
				let about = {};
 | 
			
		||||
				try {
 | 
			
		||||
					about = JSON.parse(row.content);
 | 
			
		||||
				} catch {}
 | 
			
		||||
				delete about.about;
 | 
			
		||||
				delete about.type;
 | 
			
		||||
				f.about = Object.assign(f.about, about);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	);
 | 
			
		||||
	let j = JSON.stringify(f);
 | 
			
		||||
	if (o != j) {
 | 
			
		||||
		await db.set(id + ':about', j);
 | 
			
		||||
	}
 | 
			
		||||
	g_about_cache[id] = f.about;
 | 
			
		||||
	return f.about;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getSize(db, id) {
 | 
			
		||||
	let size = 0;
 | 
			
		||||
	await ssb.sqlAsync(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🪪",
 | 
			
		||||
	"previous": "&5kw/2PgcySwOYCmAkjHTR2xTkIx3i7UjQmtQ8MfgWw8=.sha256"
 | 
			
		||||
	"previous": "&de7q4A59auHP/34bXgeNH05JZoxsGr5TjwXPvehWH30=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,5 @@
 | 
			
		||||
import * as tfrpc from '/tfrpc.js';
 | 
			
		||||
 | 
			
		||||
const is_admin = core.user?.credentials?.permissions?.administration;
 | 
			
		||||
 | 
			
		||||
tfrpc.register(async function get_private_key(id) {
 | 
			
		||||
	return bip39Words(await ssb.getPrivateKey(id));
 | 
			
		||||
});
 | 
			
		||||
@@ -17,13 +15,9 @@ tfrpc.register(async function delete_id(id) {
 | 
			
		||||
tfrpc.register(async function reload() {
 | 
			
		||||
	await main();
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function make_server(id) {
 | 
			
		||||
	return await ssb.swapWithServerIdentity(id);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	let ids = await ssb.getIdentities();
 | 
			
		||||
	let server_id = await ssb.getServerIdentity();
 | 
			
		||||
	await app.setDocument(
 | 
			
		||||
		`
 | 
			
		||||
		<head>
 | 
			
		||||
@@ -84,7 +78,7 @@ async function main() {
 | 
			
		||||
					alert('Successfully created: ' + id);
 | 
			
		||||
					await tfrpc.rpc.reload();
 | 
			
		||||
				} catch (e) {
 | 
			
		||||
					alert('Error creating identity: ' + e.message);
 | 
			
		||||
					alert('Error creating identity: ' + e);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			handler.hide_id = function hide_id(event, element) {
 | 
			
		||||
@@ -104,16 +98,6 @@ async function main() {
 | 
			
		||||
					alert('Error deleting ID: ' + e);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			handler.make_server = async function make_server(event) {
 | 
			
		||||
				let id = event.srcElement.dataset.id;
 | 
			
		||||
				try {
 | 
			
		||||
					if (confirm('Are you sure you want to make "' + id + '" the server identity?\\n\\nFor it to take effect, you will need to both sign in again and restart Tilde Friends.')) {
 | 
			
		||||
						await tfrpc.rpc.make_server(id);
 | 
			
		||||
					}
 | 
			
		||||
				} catch (e) {
 | 
			
		||||
					alert('Error making server ID: ' + e);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		</script>
 | 
			
		||||
		<header class="w3-theme w3-padding"><h1>SSB Identity Management</h1></header>
 | 
			
		||||
		<div class="w3-card-4 w3-margin">
 | 
			
		||||
@@ -132,19 +116,16 @@ async function main() {
 | 
			
		||||
		<div class="w3-card-4 w3-margin">
 | 
			
		||||
			<header class="w3-container w3-theme-l2"><h2>Identities</h2></header>
 | 
			
		||||
			<ul class="w3-ul">` +
 | 
			
		||||
			(ids ?? [])
 | 
			
		||||
				.map(
 | 
			
		||||
					(
 | 
			
		||||
						id
 | 
			
		||||
					) => `<li style="overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis">
 | 
			
		||||
				ids
 | 
			
		||||
					.map(
 | 
			
		||||
						(id) => `<li style="overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis">
 | 
			
		||||
				<button onclick="handler.export_id(event)" data-id="${id}" class="w3-button w3-theme">Export Identity</button>
 | 
			
		||||
				<button onclick="handler.delete_id(event)" data-id="${id}" class="w3-button w3-theme">Delete Identity</button>
 | 
			
		||||
				${is_admin && id != server_id ? `<button onclick="handler.make_server(event)" data-id="${id}" class="w3-button w3-theme">Make Server Identity</button>` : ''}
 | 
			
		||||
				${id}${id == server_id ? ' <div class="w3-tag w3-theme-l4 w3-round">🖥 local server</div>' : ''}
 | 
			
		||||
				${id}
 | 
			
		||||
			</li>`
 | 
			
		||||
				)
 | 
			
		||||
				.join('\n') +
 | 
			
		||||
			`	</ul>
 | 
			
		||||
					)
 | 
			
		||||
					.join('\n') +
 | 
			
		||||
				`	</ul>
 | 
			
		||||
		</div>
 | 
			
		||||
	</body>`
 | 
			
		||||
	);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
/* W3.CSS 5.01 March 14 2025 by Jan Egil and Borge Refsnes */
 | 
			
		||||
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
 | 
			
		||||
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
 | 
			
		||||
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
 | 
			
		||||
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
 | 
			
		||||
@@ -108,10 +108,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
 | 
			
		||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
 | 
			
		||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
 | 
			
		||||
 | 
			
		||||
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
 | 
			
		||||
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
 | 
			
		||||
 | 
			
		||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
 | 
			
		||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
 | 
			
		||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
 | 
			
		||||
@@ -153,9 +149,9 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
 | 
			
		||||
.w3-hover-none:hover{box-shadow:none!important}
 | 
			
		||||
/* Colors */
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover,.w3-warning{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
 | 
			
		||||
.w3-blue,.w3-hover-blue:hover,.w3-info,.w3-primary{color:#fff!important;background-color:#2196F3!important}
 | 
			
		||||
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
 | 
			
		||||
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
 | 
			
		||||
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
 | 
			
		||||
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
 | 
			
		||||
@@ -170,24 +166,15 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
 | 
			
		||||
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
 | 
			
		||||
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
 | 
			
		||||
.w3-red,.w3-hover-red:hover,.w3-danger{color:#fff!important;background-color:#f44336!important}
 | 
			
		||||
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
 | 
			
		||||
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
 | 
			
		||||
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
 | 
			
		||||
.w3-yellow,.w3-hover-yellow:hover,.w3-note{color:#000!important;background-color:#ffeb3b!important}
 | 
			
		||||
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
 | 
			
		||||
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
 | 
			
		||||
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
 | 
			
		||||
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover,.w3-secondary{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
 | 
			
		||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
 | 
			
		||||
 | 
			
		||||
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
 | 
			
		||||
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
 | 
			
		||||
.w3-emerald,.w3-hover-emerald:hover,.w3-success{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
 | 
			
		||||
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
 | 
			
		||||
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
 | 
			
		||||
 | 
			
		||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
 | 
			
		||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
 | 
			
		||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
 | 
			
		||||
@@ -245,4 +232,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
 | 
			
		||||
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
 | 
			
		||||
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
 | 
			
		||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
 | 
			
		||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🦟",
 | 
			
		||||
	"previous": "&O0huuEgL/UQC9bkUfhPOyZFo9eRiz+koOkba6QwCGNA=.sha256"
 | 
			
		||||
	"previous": "&cUqvSDUls3jn0haD85LPFAGdkc8wFuy347TtATNcJgg=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -67,6 +67,9 @@ tfrpc.register(function getHash(id, message) {
 | 
			
		||||
tfrpc.register(function setHash(hash) {
 | 
			
		||||
	return app.setHash(hash);
 | 
			
		||||
});
 | 
			
		||||
ssb.addEventListener('message', async function (id) {
 | 
			
		||||
	await tfrpc.rpc.notifyNewMessage(id);
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function store_blob(blob) {
 | 
			
		||||
	if (Array.isArray(blob)) {
 | 
			
		||||
		blob = Uint8Array.from(blob);
 | 
			
		||||
@@ -88,12 +91,10 @@ tfrpc.register(function getActiveIdentity() {
 | 
			
		||||
tfrpc.register(async function try_decrypt(id, content) {
 | 
			
		||||
	return await ssb.privateMessageDecrypt(id, content);
 | 
			
		||||
});
 | 
			
		||||
core.register('onMessage', async function (id) {
 | 
			
		||||
	await tfrpc.rpc.notifyNewMessage(id);
 | 
			
		||||
});
 | 
			
		||||
core.register('onBroadcastsChanged', async function () {
 | 
			
		||||
ssb.addEventListener('broadcasts', async function () {
 | 
			
		||||
	await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
core.register('onConnectionsChanged', async function () {
 | 
			
		||||
	await tfrpc.rpc.set('connections', await ssb.connections());
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								apps/issues/commonmark.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								apps/issues/commonmark.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										42
									
								
								apps/issues/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										42
									
								
								apps/issues/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,11 +1,5 @@
 | 
			
		||||
import * as linkify from './commonmark-linkify.js';
 | 
			
		||||
 | 
			
		||||
var reUnsafeProtocol = /^javascript:|vbscript:|file:|data:/i;
 | 
			
		||||
var reSafeDataProtocol = /^data:image\/(?:png|gif|jpeg|webp)/i;
 | 
			
		||||
var potentiallyUnsafe = function (url) {
 | 
			
		||||
	return reUnsafeProtocol.test(url) && !reSafeDataProtocol.test(url);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function image(node, entering) {
 | 
			
		||||
	if (
 | 
			
		||||
		node.firstChild?.type === 'text' &&
 | 
			
		||||
@@ -67,8 +61,8 @@ function image(node, entering) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function markdown(md) {
 | 
			
		||||
	var reader = new commonmark.Parser();
 | 
			
		||||
	var writer = new commonmark.HtmlRenderer({safe: true});
 | 
			
		||||
	var reader = new commonmark.Parser({safe: true});
 | 
			
		||||
	var writer = new commonmark.HtmlRenderer();
 | 
			
		||||
	writer.image = image;
 | 
			
		||||
	var parsed = reader.parse(md || '');
 | 
			
		||||
	parsed = linkify.transform(parsed);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "📝",
 | 
			
		||||
	"previous": "&5LpOTEnor/rYFk3axyfmmehAoq9aEwNQRH4jwNhRQ7o=.sha256"
 | 
			
		||||
	"previous": "&b//KqE4Vx6kOSBRODK1p/8wjOLKZJ+CBB5IkaBt5YsM=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@ function new_message() {
 | 
			
		||||
	return g_new_message_promise;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
core.register('onMessage', function (id) {
 | 
			
		||||
ssb.addEventListener('message', function (id) {
 | 
			
		||||
	let resolve = g_new_message_resolve;
 | 
			
		||||
	g_new_message_promise = new Promise(function (resolve, reject) {
 | 
			
		||||
		g_new_message_resolve = resolve;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								apps/journal/commonmark.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								apps/journal/commonmark.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										42
									
								
								apps/journal/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										42
									
								
								apps/journal/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -18,8 +18,8 @@ class TfJournalEntryElement extends LitElement {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	markdown(md) {
 | 
			
		||||
		var reader = new commonmark.Parser();
 | 
			
		||||
		var writer = new commonmark.HtmlRenderer({safe: true});
 | 
			
		||||
		var reader = new commonmark.Parser({safe: true});
 | 
			
		||||
		var writer = new commonmark.HtmlRenderer();
 | 
			
		||||
		var parsed = reader.parse(md || '');
 | 
			
		||||
		return writer.render(parsed);
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
async function main() {
 | 
			
		||||
	print(core.url);
 | 
			
		||||
	let host = core.url.match(/.*?\/\/([^:/]*)/)[1];
 | 
			
		||||
	let port = await ssb.port();
 | 
			
		||||
	let host = core.url.match(/.*\/\/(.*?)\//)[1];
 | 
			
		||||
	let id = (await ssb.getServerIdentity()).substring(1);
 | 
			
		||||
	let room = `net:${host}:${port}~shs:${id}:SSB+Room+SK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
 | 
			
		||||
	let room = `net:${host}:${ssb.port}~shs:${id}:SSB+Room+SK3TLYC2T86EHQCUHBUHASCASE18JBV24=`;
 | 
			
		||||
	await app.setDocument(`
 | 
			
		||||
		<body style="color: #fff">
 | 
			
		||||
			<h1>Server</h1>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								apps/sneaker/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										42
									
								
								apps/sneaker/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🦀",
 | 
			
		||||
	"previous": "&wOd/+1l5wpywBlfxC1Lm0i+HhYidrgSfrn9LRX7qy2w=.sha256"
 | 
			
		||||
	"emoji": "🐌",
 | 
			
		||||
	"previous": "&vEaOZjrNb0u9rhNqrQ8eU9TlOFlo4HsgW6hbI7VdIT0=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,9 @@ tfrpc.register(async function createIdentity() {
 | 
			
		||||
tfrpc.register(async function getServerIdentity() {
 | 
			
		||||
	return ssb.getServerIdentity();
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function setServerFollowingMe(id, following) {
 | 
			
		||||
	return ssb.setServerFollowingMe(id, following);
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function getIdentities() {
 | 
			
		||||
	return ssb.getIdentities();
 | 
			
		||||
});
 | 
			
		||||
@@ -73,7 +76,7 @@ tfrpc.register(function getHash(id, message) {
 | 
			
		||||
tfrpc.register(function setHash(hash) {
 | 
			
		||||
	return app.setHash(hash);
 | 
			
		||||
});
 | 
			
		||||
core.register('onMessage', async function (id) {
 | 
			
		||||
ssb.addEventListener('message', async function (id) {
 | 
			
		||||
	await tfrpc.rpc.notifyNewMessage(id);
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function store_blob(blob) {
 | 
			
		||||
@@ -100,14 +103,7 @@ tfrpc.register(async function encrypt(id, recipients, content) {
 | 
			
		||||
tfrpc.register(async function getActiveIdentity() {
 | 
			
		||||
	return await ssb.getActiveIdentity();
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function sync() {
 | 
			
		||||
	return await ssb.sync();
 | 
			
		||||
});
 | 
			
		||||
tfrpc.register(async function url() {
 | 
			
		||||
	return core.url;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
core.register('onBroadcastsChanged', async function () {
 | 
			
		||||
ssb.addEventListener('broadcasts', async function () {
 | 
			
		||||
	await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ function textNode(text) {
 | 
			
		||||
function linkNode(text, link) {
 | 
			
		||||
	const linkNode = new commonmark.Node('link', undefined);
 | 
			
		||||
	if (link.startsWith('#')) {
 | 
			
		||||
		linkNode.destination = `#${encodeURIComponent(link)}`;
 | 
			
		||||
		linkNode.destination = `#q=${encodeURIComponent(link)}`;
 | 
			
		||||
	} else {
 | 
			
		||||
		linkNode.destination = link;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								apps/ssb/commonmark.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								apps/ssb/commonmark.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,6 +1,4 @@
 | 
			
		||||
import * as tfrpc from '/static/tfrpc.js';
 | 
			
		||||
import {html, render} from './lit-all.min.js';
 | 
			
		||||
import {styles} from './tf-styles.js';
 | 
			
		||||
 | 
			
		||||
let g_emojis;
 | 
			
		||||
 | 
			
		||||
@@ -37,12 +35,15 @@ export async function picker(callback, anchor, author) {
 | 
			
		||||
	div.style.color = '#000';
 | 
			
		||||
	div.style.background = '#fff';
 | 
			
		||||
	div.style.border = '1px solid #000';
 | 
			
		||||
	div.style.display = 'flex';
 | 
			
		||||
	div.style.display = 'block';
 | 
			
		||||
	div.style.position = 'absolute';
 | 
			
		||||
	div.style.minWidth = 'min(16em, 90vw)';
 | 
			
		||||
	div.style.width = 'min(16em, 90vw)';
 | 
			
		||||
	div.style.maxWidth = 'min(16em, 90vw)';
 | 
			
		||||
	div.style.maxHeight = '16em';
 | 
			
		||||
	div.style.overflow = 'scroll';
 | 
			
		||||
	div.style.fontWeight = 'bold';
 | 
			
		||||
	div.style.fontSize = 'xx-large';
 | 
			
		||||
	div.style.flex = '1 1';
 | 
			
		||||
	div.style.flexDirection = 'column';
 | 
			
		||||
	let input = document.createElement('input');
 | 
			
		||||
	input.type = 'text';
 | 
			
		||||
	input.style.display = 'block';
 | 
			
		||||
@@ -52,12 +53,19 @@ export async function picker(callback, anchor, author) {
 | 
			
		||||
	input.style.position = 'relative';
 | 
			
		||||
	div.appendChild(input);
 | 
			
		||||
	let list = document.createElement('div');
 | 
			
		||||
	list.style.overflow = 'scroll';
 | 
			
		||||
	div.appendChild(list);
 | 
			
		||||
	div.addEventListener('mousedown', function (event) {
 | 
			
		||||
		event.stopPropagation();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	function cleanup() {
 | 
			
		||||
		console.log('emoji cleanup');
 | 
			
		||||
		div.parentElement.removeChild(div);
 | 
			
		||||
		window.removeEventListener('keydown', key_down);
 | 
			
		||||
		console.log('removing click');
 | 
			
		||||
		document.body.removeEventListener('mousedown', cleanup);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function key_down(event) {
 | 
			
		||||
		if (event.key == 'Escape') {
 | 
			
		||||
			cleanup();
 | 
			
		||||
@@ -145,42 +153,13 @@ export async function picker(callback, anchor, author) {
 | 
			
		||||
	}
 | 
			
		||||
	refresh();
 | 
			
		||||
	input.oninput = refresh;
 | 
			
		||||
	let parent = document.createElement('div');
 | 
			
		||||
	function cleanup() {
 | 
			
		||||
		parent.parentElement.removeChild(parent);
 | 
			
		||||
		window.removeEventListener('keydown', key_down);
 | 
			
		||||
		document.body.removeEventListener('mousedown', cleanup);
 | 
			
		||||
	}
 | 
			
		||||
	let modal = html`
 | 
			
		||||
		<style>
 | 
			
		||||
			${styles}
 | 
			
		||||
		</style>
 | 
			
		||||
		<div
 | 
			
		||||
			class="w3-modal"
 | 
			
		||||
			style="display: block; box-sizing: border-box; z-index: 10"
 | 
			
		||||
		>
 | 
			
		||||
			<div class="w3-modal-content w3-card-4">
 | 
			
		||||
				<div
 | 
			
		||||
					class="w3-content w3-theme-d1"
 | 
			
		||||
					style="display: flex; flex-direction: column; max-height: 80vh"
 | 
			
		||||
				>
 | 
			
		||||
					<header class="w3-container" style="flex: 0 0">
 | 
			
		||||
						<h1>Choose a Reaction</h1>
 | 
			
		||||
						<span class="w3-button w3-display-topright" @click=${cleanup}
 | 
			
		||||
							>×</span
 | 
			
		||||
						>
 | 
			
		||||
					</header>
 | 
			
		||||
					${div}
 | 
			
		||||
					<footer class="w3-container w3-padding" style="flex: 0 0">
 | 
			
		||||
						<button class="w3-button" @click=${cleanup}>Close</button>
 | 
			
		||||
					</footer>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	`;
 | 
			
		||||
	document.body.appendChild(parent);
 | 
			
		||||
	render(modal, parent);
 | 
			
		||||
	document.body.appendChild(div);
 | 
			
		||||
	div.style.position = 'fixed';
 | 
			
		||||
	div.style.top = '50%';
 | 
			
		||||
	div.style.left = '50%';
 | 
			
		||||
	div.style.transform = 'translate(-50%, -50%)';
 | 
			
		||||
	input.focus();
 | 
			
		||||
	console.log('adding click');
 | 
			
		||||
	document.body.addEventListener('mousedown', cleanup);
 | 
			
		||||
	window.addEventListener('keydown', key_down);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										42
									
								
								apps/ssb/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										42
									
								
								apps/ssb/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -8,16 +8,10 @@ import * as tf_compose from './tf-compose.js';
 | 
			
		||||
import * as tf_news from './tf-news.js';
 | 
			
		||||
import * as tf_profile from './tf-profile.js';
 | 
			
		||||
import * as tf_reactions_modal from './tf-reactions-modal.js';
 | 
			
		||||
import * as tf_tab_mentions from './tf-tab-mentions.js';
 | 
			
		||||
import * as tf_tab_news from './tf-tab-news.js';
 | 
			
		||||
import * as tf_tab_news_feed from './tf-tab-news-feed.js';
 | 
			
		||||
import * as tf_tab_search from './tf-tab-search.js';
 | 
			
		||||
import * as tf_tab_connections from './tf-tab-connections.js';
 | 
			
		||||
import * as tf_tab_query from './tf-tab-query.js';
 | 
			
		||||
import * as tf_tag from './tf-tag.js';
 | 
			
		||||
import * as tf_styles from './tf-styles.js';
 | 
			
		||||
 | 
			
		||||
window.addEventListener('load', function () {
 | 
			
		||||
	let style = document.createElement('style');
 | 
			
		||||
	style.innerText = tf_styles.styles;
 | 
			
		||||
	document.body.appendChild(style);
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ class TfElement extends LitElement {
 | 
			
		||||
		return {
 | 
			
		||||
			whoami: {type: String},
 | 
			
		||||
			hash: {type: String},
 | 
			
		||||
			unread: {type: Array},
 | 
			
		||||
			tab: {type: String},
 | 
			
		||||
			broadcasts: {type: Array},
 | 
			
		||||
			connections: {type: Array},
 | 
			
		||||
@@ -15,12 +16,7 @@ class TfElement extends LitElement {
 | 
			
		||||
			following: {type: Array},
 | 
			
		||||
			users: {type: Object},
 | 
			
		||||
			ids: {type: Array},
 | 
			
		||||
			channels: {type: Array},
 | 
			
		||||
			channels_unread: {type: Object},
 | 
			
		||||
			channels_latest: {type: Object},
 | 
			
		||||
			guest: {type: Boolean},
 | 
			
		||||
			url: {type: String},
 | 
			
		||||
			private_messages: {type: Array},
 | 
			
		||||
			tags: {type: Array},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -30,17 +26,14 @@ class TfElement extends LitElement {
 | 
			
		||||
		super();
 | 
			
		||||
		let self = this;
 | 
			
		||||
		this.hash = '#';
 | 
			
		||||
		this.unread = [];
 | 
			
		||||
		this.tab = 'news';
 | 
			
		||||
		this.broadcasts = [];
 | 
			
		||||
		this.connections = [];
 | 
			
		||||
		this.following = [];
 | 
			
		||||
		this.users = {};
 | 
			
		||||
		this.loaded = false;
 | 
			
		||||
		this.channels = [];
 | 
			
		||||
		this.channels_unread = {};
 | 
			
		||||
		this.channels_latest = {};
 | 
			
		||||
		this.loading_latest = 0;
 | 
			
		||||
		this.loading_latest_scheduled = 0;
 | 
			
		||||
		this.tags = [];
 | 
			
		||||
		tfrpc.rpc.getBroadcasts().then((b) => {
 | 
			
		||||
			self.broadcasts = b || [];
 | 
			
		||||
		});
 | 
			
		||||
@@ -69,78 +62,18 @@ class TfElement extends LitElement {
 | 
			
		||||
	async initial_load() {
 | 
			
		||||
		let whoami = await tfrpc.rpc.getActiveIdentity();
 | 
			
		||||
		let ids = (await tfrpc.rpc.getIdentities()) || [];
 | 
			
		||||
		this.url = await tfrpc.rpc.url();
 | 
			
		||||
		this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
 | 
			
		||||
		this.guest = !this.whoami?.length;
 | 
			
		||||
		this.ids = ids;
 | 
			
		||||
		await this.load_channels();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load_channels() {
 | 
			
		||||
		let channels = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
			SELECT
 | 
			
		||||
				content ->> 'channel' AS channel,
 | 
			
		||||
				content ->> 'subscribed' AS subscribed
 | 
			
		||||
			FROM
 | 
			
		||||
				messages
 | 
			
		||||
			WHERE
 | 
			
		||||
				author = ? AND
 | 
			
		||||
				content ->> 'type' = 'channel'
 | 
			
		||||
			ORDER BY sequence
 | 
			
		||||
		`,
 | 
			
		||||
			[this.whoami]
 | 
			
		||||
		);
 | 
			
		||||
		let channel_map = {};
 | 
			
		||||
		for (let row of channels) {
 | 
			
		||||
			if (row.subscribed) {
 | 
			
		||||
				channel_map[row.channel] = true;
 | 
			
		||||
			} else {
 | 
			
		||||
				delete channel_map[row.channel];
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		this.channels = Object.keys(channel_map).sort();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	connectedCallback() {
 | 
			
		||||
		super.connectedCallback();
 | 
			
		||||
		this._keydown = this.keydown.bind(this);
 | 
			
		||||
		window.addEventListener('keydown', this._keydown);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	disconnectedCallback() {
 | 
			
		||||
		super.disconnectedCallback();
 | 
			
		||||
		window.removeEventListener('keydown', this._keydown);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	keydown(event) {
 | 
			
		||||
		if (event.altKey && event.key == 'ArrowUp') {
 | 
			
		||||
			this.next_channel(-1);
 | 
			
		||||
			event.preventDefault();
 | 
			
		||||
		} else if (event.altKey && event.key == 'ArrowDown') {
 | 
			
		||||
			this.next_channel(1);
 | 
			
		||||
			event.preventDefault();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	next_channel(delta) {
 | 
			
		||||
		let channel_names = ['', '@', '🔐', ...this.channels.map((x) => '#' + x)];
 | 
			
		||||
		let index = channel_names.indexOf(this.hash.substring(1));
 | 
			
		||||
		index = index != -1 ? index + delta : 0;
 | 
			
		||||
		tfrpc.rpc.setHash(
 | 
			
		||||
			'#' +
 | 
			
		||||
				encodeURIComponent(
 | 
			
		||||
					channel_names[(index + channel_names.length) % channel_names.length]
 | 
			
		||||
				)
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	set_hash(hash) {
 | 
			
		||||
		this.hash = decodeURIComponent(hash || '#');
 | 
			
		||||
		this.hash = hash || '#';
 | 
			
		||||
		if (this.hash.startsWith('#q=')) {
 | 
			
		||||
			this.tab = 'search';
 | 
			
		||||
		} else if (this.hash === '#connections') {
 | 
			
		||||
			this.tab = 'connections';
 | 
			
		||||
		} else if (this.hash === '#mentions') {
 | 
			
		||||
			this.tab = 'mentions';
 | 
			
		||||
		} else if (this.hash.startsWith('#sql=')) {
 | 
			
		||||
			this.tab = 'query';
 | 
			
		||||
		} else {
 | 
			
		||||
@@ -148,11 +81,9 @@ class TfElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async fetch_about(following, users) {
 | 
			
		||||
		let ids = Object.keys(following).sort();
 | 
			
		||||
	async fetch_about(ids, users) {
 | 
			
		||||
		const k_cache_version = 1;
 | 
			
		||||
		let cache = await tfrpc.rpc.databaseGet('about');
 | 
			
		||||
		let original_cache = cache;
 | 
			
		||||
		cache = cache ? JSON.parse(cache) : {};
 | 
			
		||||
		if (cache.version !== k_cache_version) {
 | 
			
		||||
			cache = {
 | 
			
		||||
@@ -164,8 +95,8 @@ class TfElement extends LitElement {
 | 
			
		||||
		let max_row_id = (
 | 
			
		||||
			await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					SELECT MAX(rowid) AS max_row_id FROM messages
 | 
			
		||||
				`,
 | 
			
		||||
			SELECT MAX(rowid) AS max_row_id FROM messages
 | 
			
		||||
		`,
 | 
			
		||||
				[]
 | 
			
		||||
			)
 | 
			
		||||
		)[0].max_row_id;
 | 
			
		||||
@@ -178,7 +109,7 @@ class TfElement extends LitElement {
 | 
			
		||||
		let abouts = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
				SELECT
 | 
			
		||||
					messages.author, json(messages.content) AS content, messages.sequence
 | 
			
		||||
					messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
				FROM
 | 
			
		||||
					messages,
 | 
			
		||||
					json_each(?1) AS following
 | 
			
		||||
@@ -189,7 +120,7 @@ class TfElement extends LitElement {
 | 
			
		||||
					json_extract(messages.content, '$.type') = 'about'
 | 
			
		||||
				UNION
 | 
			
		||||
				SELECT
 | 
			
		||||
					messages.author, json(messages.content) AS content, messages.sequence
 | 
			
		||||
					messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
				FROM
 | 
			
		||||
					messages,
 | 
			
		||||
					json_each(?2) AS following
 | 
			
		||||
@@ -218,20 +149,10 @@ class TfElement extends LitElement {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		cache.last_row_id = max_row_id;
 | 
			
		||||
		let new_cache = JSON.stringify(cache);
 | 
			
		||||
		if (new_cache !== original_cache) {
 | 
			
		||||
			let start_time = new Date();
 | 
			
		||||
			tfrpc.rpc.databaseSet('about', new_cache).then(function () {
 | 
			
		||||
				console.log('saving about took', (new Date() - start_time) / 1000);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		await tfrpc.rpc.databaseSet('about', JSON.stringify(cache));
 | 
			
		||||
		users = users || {};
 | 
			
		||||
		for (let id of Object.keys(cache.about)) {
 | 
			
		||||
			users[id] = Object.assign(
 | 
			
		||||
				{follow_depth: following[id]?.d},
 | 
			
		||||
				users[id] || {},
 | 
			
		||||
				cache.about[id]
 | 
			
		||||
			);
 | 
			
		||||
			users[id] = Object.assign(users[id] || {}, cache.about[id]);
 | 
			
		||||
		}
 | 
			
		||||
		return Object.assign({}, users);
 | 
			
		||||
	}
 | 
			
		||||
@@ -246,15 +167,10 @@ class TfElement extends LitElement {
 | 
			
		||||
			`,
 | 
			
		||||
			[JSON.stringify(this.following), id]
 | 
			
		||||
		);
 | 
			
		||||
		for (let message of messages) {
 | 
			
		||||
			if (
 | 
			
		||||
				message.author == this.whoami &&
 | 
			
		||||
				JSON.parse(message.content)?.type == 'channel'
 | 
			
		||||
			) {
 | 
			
		||||
				this.load_channels();
 | 
			
		||||
			}
 | 
			
		||||
		if (messages && messages.length) {
 | 
			
		||||
			this.unread = [...this.unread, ...messages];
 | 
			
		||||
			this.unread = this.unread.slice(this.unread.length - 1024);
 | 
			
		||||
		}
 | 
			
		||||
		this.schedule_load_latest();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async _handle_whoami_changed(event) {
 | 
			
		||||
@@ -269,234 +185,70 @@ class TfElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async get_latest_private(following) {
 | 
			
		||||
		const k_version = 1;
 | 
			
		||||
		// { "version": 1, "range": [1234, 5678], messages: [ "%1.sha256", "%2.sha256", ... ], latest: rowid }
 | 
			
		||||
		let cache = JSON.parse(
 | 
			
		||||
			(await tfrpc.rpc.databaseGet(`private:${this.whoami}`)) ?? '{}'
 | 
			
		||||
		);
 | 
			
		||||
		if (cache.version !== k_version) {
 | 
			
		||||
			cache = {
 | 
			
		||||
				version: k_version,
 | 
			
		||||
				messages: [],
 | 
			
		||||
				range: [],
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
		let latest = (
 | 
			
		||||
			await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages')
 | 
			
		||||
		)[0].latest;
 | 
			
		||||
		let ranges = [];
 | 
			
		||||
		const k_chunk_size = 512;
 | 
			
		||||
		if (cache.range.length) {
 | 
			
		||||
			for (let i = cache.range[1]; i < latest; i += k_chunk_size) {
 | 
			
		||||
				ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
 | 
			
		||||
			}
 | 
			
		||||
			for (let i = cache.range[0]; i >= 0; i -= k_chunk_size) {
 | 
			
		||||
				ranges.push([
 | 
			
		||||
					Math.max(i - k_chunk_size, 0),
 | 
			
		||||
					Math.min(cache.range[0], i + k_chunk_size),
 | 
			
		||||
					false,
 | 
			
		||||
				]);
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			for (let i = 0; i < latest; i += k_chunk_size) {
 | 
			
		||||
				ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
 | 
			
		||||
	async create_identity() {
 | 
			
		||||
		if (confirm('Are you sure you want to create a new identity?')) {
 | 
			
		||||
			await tfrpc.rpc.createIdentity();
 | 
			
		||||
			this.ids = (await tfrpc.rpc.getIdentities()) || [];
 | 
			
		||||
			if (this.ids && !this.whoami) {
 | 
			
		||||
				this.whoami = this.ids[0];
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		for (let range of ranges) {
 | 
			
		||||
			let messages = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					SELECT messages.rowid, messages.id, json(content) AS content
 | 
			
		||||
						FROM messages
 | 
			
		||||
						WHERE
 | 
			
		||||
							messages.rowid > ?1 AND
 | 
			
		||||
							messages.rowid <= ?2 AND
 | 
			
		||||
							json(messages.content) LIKE '"%'
 | 
			
		||||
						ORDER BY sequence DESC
 | 
			
		||||
					`,
 | 
			
		||||
				[range[0], range[1]]
 | 
			
		||||
			);
 | 
			
		||||
			messages = (await this.decrypt(messages)).filter((x) => x.decrypted);
 | 
			
		||||
			if (messages.length) {
 | 
			
		||||
				cache.latest = Math.max(
 | 
			
		||||
					cache.latest ?? 0,
 | 
			
		||||
					...messages.map((x) => x.rowid)
 | 
			
		||||
				);
 | 
			
		||||
				if (range[2]) {
 | 
			
		||||
					cache.messages = [...cache.messages, ...messages.map((x) => x.id)];
 | 
			
		||||
				} else {
 | 
			
		||||
					cache.messages = [...messages.map((x) => x.id), ...cache.messages];
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			cache.range[0] = Math.min(cache.range[0] ?? range[0], range[0]);
 | 
			
		||||
			cache.range[1] = Math.max(cache.range[1] ?? range[1], range[1]);
 | 
			
		||||
			await tfrpc.rpc.databaseSet(
 | 
			
		||||
				`private:${this.whoami}`,
 | 
			
		||||
				JSON.stringify(cache)
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
		return [cache.latest, cache.messages];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load_channels_latest(following) {
 | 
			
		||||
		let start_time = new Date();
 | 
			
		||||
		let latest_private = this.get_latest_private(following);
 | 
			
		||||
		let channels = await tfrpc.rpc.query(
 | 
			
		||||
	async load_recent_tags() {
 | 
			
		||||
		let start = new Date();
 | 
			
		||||
		this.tags = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
			SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages
 | 
			
		||||
			JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value
 | 
			
		||||
			JOIN json_each(?2) AS following ON messages.author = following.value
 | 
			
		||||
			WHERE
 | 
			
		||||
				messages.content ->> 'type' = 'post' AND
 | 
			
		||||
				messages.content ->> 'root' IS NULL AND
 | 
			
		||||
				messages.author != ?4
 | 
			
		||||
			GROUP by channel
 | 
			
		||||
			UNION
 | 
			
		||||
			SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages
 | 
			
		||||
			JOIN json_each(?2) AS following ON messages.author = following.value
 | 
			
		||||
			WHERE
 | 
			
		||||
				messages.content ->> 'type' = 'post' AND
 | 
			
		||||
				messages.content ->> 'root' IS NULL AND
 | 
			
		||||
				messages.author != ?4
 | 
			
		||||
			UNION
 | 
			
		||||
			SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
 | 
			
		||||
			JOIN messages ON messages.rowid = messages_fts.rowid
 | 
			
		||||
			JOIN json_each(?2) AS following ON messages.author = following.value
 | 
			
		||||
			WHERE messages.author != ?4
 | 
			
		||||
			WITH
 | 
			
		||||
				recent AS (SELECT id, json(content) AS content FROM messages
 | 
			
		||||
					WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
 | 
			
		||||
					ORDER BY timestamp DESC LIMIT 1024),
 | 
			
		||||
				recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
 | 
			
		||||
					FROM recent
 | 
			
		||||
					WHERE json_extract(content, '$.channel') IS NOT NULL),
 | 
			
		||||
				recent_mentions AS (SELECT recent.id, json_extract(mention.value, '$.link') AS tag
 | 
			
		||||
					FROM recent, json_each(recent.content, '$.mentions') AS mention
 | 
			
		||||
					WHERE json_valid(mention.value) AND tag LIKE '#%'),
 | 
			
		||||
				combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
 | 
			
		||||
				by_message AS (SELECT DISTINCT id, tag FROM combined)
 | 
			
		||||
			SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
 | 
			
		||||
		`,
 | 
			
		||||
			[
 | 
			
		||||
				JSON.stringify(this.channels),
 | 
			
		||||
				JSON.stringify(following),
 | 
			
		||||
				'"' + this.whoami.replace('"', '""') + '"',
 | 
			
		||||
				this.whoami,
 | 
			
		||||
			]
 | 
			
		||||
			[new Date() - 7 * 24 * 60 * 60 * 1000]
 | 
			
		||||
		);
 | 
			
		||||
		this.channels_latest = Object.fromEntries(
 | 
			
		||||
			channels.map((x) => [x.channel, x.rowid])
 | 
			
		||||
		);
 | 
			
		||||
		console.log('channels took', (new Date() - start_time) / 1000.0);
 | 
			
		||||
		let self = this;
 | 
			
		||||
		start_time = new Date();
 | 
			
		||||
		latest_private.then(function (latest) {
 | 
			
		||||
			self.channels_latest = Object.assign({}, self.channels_latest, {
 | 
			
		||||
				'🔐': latest[0],
 | 
			
		||||
			});
 | 
			
		||||
			console.log('private took', (new Date() - start_time) / 1000.0);
 | 
			
		||||
			console.log(latest);
 | 
			
		||||
			self.private_messages = latest[1];
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_schedule_load_latest_timer() {
 | 
			
		||||
		--this.loading_latest_scheduled;
 | 
			
		||||
		this.schedule_load_latest();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	schedule_load_latest() {
 | 
			
		||||
		if (!this.loading_latest) {
 | 
			
		||||
			this.shadowRoot.getElementById('tf-tab-news')?.load_latest();
 | 
			
		||||
			this.load();
 | 
			
		||||
		} else if (!this.loading_latest_scheduled) {
 | 
			
		||||
			this.loading_latest_scheduled++;
 | 
			
		||||
			setTimeout(this._schedule_load_latest_timer.bind(this), 5000);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async fetch_user_info(users) {
 | 
			
		||||
		let info = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
				SELECT messages_stats.author, messages_stats.max_sequence, messages_stats.max_timestamp AS max_ts FROM messages_stats
 | 
			
		||||
				JOIN json_each(?) AS following
 | 
			
		||||
				ON messages_stats.author = following.value
 | 
			
		||||
			`,
 | 
			
		||||
			[JSON.stringify(Object.keys(users))]
 | 
			
		||||
		);
 | 
			
		||||
		for (let row of info) {
 | 
			
		||||
			users[row.author].seq = row.max_seq;
 | 
			
		||||
			users[row.author].ts = row.max_ts;
 | 
			
		||||
		}
 | 
			
		||||
		return users;
 | 
			
		||||
		console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load() {
 | 
			
		||||
		this.loading_latest = true;
 | 
			
		||||
		try {
 | 
			
		||||
			let start_time = new Date();
 | 
			
		||||
			let whoami = this.whoami;
 | 
			
		||||
			let following = await tfrpc.rpc.following([whoami], 2);
 | 
			
		||||
			let users = {};
 | 
			
		||||
			let by_count = [];
 | 
			
		||||
			for (let [id, v] of Object.entries(following)) {
 | 
			
		||||
				users[id] = {
 | 
			
		||||
					following: v.of,
 | 
			
		||||
					blocking: v.ob,
 | 
			
		||||
					followed: v.if,
 | 
			
		||||
					blocked: v.ib,
 | 
			
		||||
				};
 | 
			
		||||
				by_count.push({count: v.of, id: id});
 | 
			
		||||
			}
 | 
			
		||||
			this.load_channels_latest(Object.keys(following));
 | 
			
		||||
			this.channels_unread = JSON.parse(
 | 
			
		||||
				(await tfrpc.rpc.databaseGet('unread')) ?? '{}'
 | 
			
		||||
			);
 | 
			
		||||
			this.following = Object.keys(following);
 | 
			
		||||
			let about_start_time = new Date();
 | 
			
		||||
			users = await this.fetch_about(following, users);
 | 
			
		||||
			console.log(
 | 
			
		||||
				'about took',
 | 
			
		||||
				(new Date() - about_start_time) / 1000.0,
 | 
			
		||||
				'seconds for',
 | 
			
		||||
				Object.keys(users).length,
 | 
			
		||||
				'users'
 | 
			
		||||
			);
 | 
			
		||||
			start_time = new Date();
 | 
			
		||||
			users = await this.fetch_user_info(users);
 | 
			
		||||
			console.log(
 | 
			
		||||
				'user info took',
 | 
			
		||||
				(new Date() - start_time) / 1000.0,
 | 
			
		||||
				'seconds'
 | 
			
		||||
			);
 | 
			
		||||
			this.users = users;
 | 
			
		||||
			console.log(
 | 
			
		||||
				`load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}`
 | 
			
		||||
			);
 | 
			
		||||
			this.whoami = whoami;
 | 
			
		||||
			this.loaded = whoami;
 | 
			
		||||
		} finally {
 | 
			
		||||
			this.loading_latest = false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	channel_set_unread(event) {
 | 
			
		||||
		this.channels_unread[event.detail.channel ?? ''] = event.detail.unread;
 | 
			
		||||
		this.channels_unread = Object.assign({}, this.channels_unread);
 | 
			
		||||
		tfrpc.rpc.databaseSet('unread', JSON.stringify(this.channels_unread));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async decrypt(messages) {
 | 
			
		||||
		let whoami = this.whoami;
 | 
			
		||||
		return Promise.all(
 | 
			
		||||
			messages.map(async function (message) {
 | 
			
		||||
				let content;
 | 
			
		||||
				try {
 | 
			
		||||
					content = JSON.parse(message?.content);
 | 
			
		||||
				} catch {}
 | 
			
		||||
				if (typeof content === 'string') {
 | 
			
		||||
					let decrypted;
 | 
			
		||||
					try {
 | 
			
		||||
						decrypted = await tfrpc.rpc.try_decrypt(whoami, content);
 | 
			
		||||
					} catch {}
 | 
			
		||||
					if (decrypted) {
 | 
			
		||||
						try {
 | 
			
		||||
							message.decrypted = JSON.parse(decrypted);
 | 
			
		||||
						} catch {
 | 
			
		||||
							message.decrypted = decrypted;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				return message;
 | 
			
		||||
			})
 | 
			
		||||
		let tags = this.load_recent_tags();
 | 
			
		||||
		let following = await tfrpc.rpc.following([whoami], 2);
 | 
			
		||||
		let users = {};
 | 
			
		||||
		let by_count = [];
 | 
			
		||||
		for (let [id, v] of Object.entries(following)) {
 | 
			
		||||
			users[id] = {
 | 
			
		||||
				following: v.of,
 | 
			
		||||
				blocking: v.ob,
 | 
			
		||||
				followed: v.if,
 | 
			
		||||
				blocked: v.ib,
 | 
			
		||||
			};
 | 
			
		||||
			by_count.push({count: v.of, id: id});
 | 
			
		||||
		}
 | 
			
		||||
		console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
 | 
			
		||||
		let start_time = new Date();
 | 
			
		||||
		users = await this.fetch_about(Object.keys(following).sort(), users);
 | 
			
		||||
		console.log(
 | 
			
		||||
			'about took',
 | 
			
		||||
			(new Date() - start_time) / 1000.0,
 | 
			
		||||
			'seconds for',
 | 
			
		||||
			Object.keys(users).length,
 | 
			
		||||
			'users'
 | 
			
		||||
		);
 | 
			
		||||
		this.following = Object.keys(following);
 | 
			
		||||
		this.users = users;
 | 
			
		||||
		await tags;
 | 
			
		||||
		console.log(`load finished ${whoami} => ${this.whoami}`);
 | 
			
		||||
		this.whoami = whoami;
 | 
			
		||||
		this.loaded = whoami;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_tab() {
 | 
			
		||||
@@ -510,13 +262,8 @@ class TfElement extends LitElement {
 | 
			
		||||
					whoami=${this.whoami}
 | 
			
		||||
					.users=${this.users}
 | 
			
		||||
					hash=${this.hash}
 | 
			
		||||
					?loading=${this.loading}
 | 
			
		||||
					.channels=${this.channels}
 | 
			
		||||
					.channels_latest=${this.channels_latest}
 | 
			
		||||
					.channels_unread=${this.channels_unread}
 | 
			
		||||
					@channelsetunread=${this.channel_set_unread}
 | 
			
		||||
					.connections=${this.connections}
 | 
			
		||||
					.private_messages=${this.private_messages}
 | 
			
		||||
					.unread=${this.unread}
 | 
			
		||||
					@refresh=${() => (this.unread = [])}
 | 
			
		||||
				></tf-tab-news>
 | 
			
		||||
			`;
 | 
			
		||||
		} else if (this.tab === 'connections') {
 | 
			
		||||
@@ -527,6 +274,14 @@ class TfElement extends LitElement {
 | 
			
		||||
					.broadcasts=${this.broadcasts}
 | 
			
		||||
				></tf-tab-connections>
 | 
			
		||||
			`;
 | 
			
		||||
		} else if (this.tab === 'mentions') {
 | 
			
		||||
			return html`
 | 
			
		||||
				<tf-tab-mentions
 | 
			
		||||
					.following=${this.following}
 | 
			
		||||
					whoami=${this.whoami}
 | 
			
		||||
					.users="${this.users}}"
 | 
			
		||||
				></tf-tab-mentions>
 | 
			
		||||
			`;
 | 
			
		||||
		} else if (this.tab === 'search') {
 | 
			
		||||
			return html`
 | 
			
		||||
				<tf-tab-search
 | 
			
		||||
@@ -558,15 +313,13 @@ class TfElement extends LitElement {
 | 
			
		||||
			await tfrpc.rpc.setHash('#');
 | 
			
		||||
		} else if (tab === 'connections') {
 | 
			
		||||
			await tfrpc.rpc.setHash('#connections');
 | 
			
		||||
		} else if (tab === 'mentions') {
 | 
			
		||||
			await tfrpc.rpc.setHash('#mentions');
 | 
			
		||||
		} else if (tab === 'query') {
 | 
			
		||||
			await tfrpc.rpc.setHash('#sql=');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	refresh() {
 | 
			
		||||
		tfrpc.rpc.sync();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let self = this;
 | 
			
		||||
 | 
			
		||||
@@ -580,69 +333,47 @@ class TfElement extends LitElement {
 | 
			
		||||
		const k_tabs = {
 | 
			
		||||
			'📰': 'news',
 | 
			
		||||
			'📡': 'connections',
 | 
			
		||||
			'@': 'mentions',
 | 
			
		||||
			'🔍': 'search',
 | 
			
		||||
			'👩💻': 'query',
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		let tabs = html`
 | 
			
		||||
			<div
 | 
			
		||||
				class="w3-bar w3-theme-l1"
 | 
			
		||||
				style="position: static; top: 0; z-index: 10"
 | 
			
		||||
			>
 | 
			
		||||
				<button
 | 
			
		||||
					class=${'w3-bar-item w3-button w3-circle w3-ripple' +
 | 
			
		||||
					(this.connections?.some((x) => x.flags.one_shot) ? ' w3-spin' : '')}
 | 
			
		||||
					style="width: 1.5em; height: 1.5em; padding: 8px"
 | 
			
		||||
					@click=${this.refresh}
 | 
			
		||||
				>
 | 
			
		||||
					↻
 | 
			
		||||
				</button>
 | 
			
		||||
			<div class="w3-bar w3-theme-l1">
 | 
			
		||||
				${Object.entries(k_tabs).map(
 | 
			
		||||
					([k, v]) => html`
 | 
			
		||||
						<button
 | 
			
		||||
							title=${v}
 | 
			
		||||
							class="w3-bar-item w3-padding w3-hover-theme tab ${self.tab == v
 | 
			
		||||
							class="w3-bar-item w3-padding-large w3-hover-theme tab ${self.tab ==
 | 
			
		||||
							v
 | 
			
		||||
								? 'w3-theme-l2'
 | 
			
		||||
								: 'w3-theme-l1'}"
 | 
			
		||||
							@click=${() => self.set_tab(v)}
 | 
			
		||||
						>
 | 
			
		||||
							${k}
 | 
			
		||||
							<span class=${self.tab == v ? '' : 'w3-hide-small'}
 | 
			
		||||
								>${v.charAt(0).toUpperCase() + v.substring(1)}</span
 | 
			
		||||
							>
 | 
			
		||||
						</button>
 | 
			
		||||
					`
 | 
			
		||||
				)}
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
		let contents = this.guest
 | 
			
		||||
			? html`<div
 | 
			
		||||
					class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge w3-container"
 | 
			
		||||
				>
 | 
			
		||||
					<p>⚠️🦀 Must be logged in to Tilde Friends to scuttle here. 🦀⚠️</p>
 | 
			
		||||
					<footer class="w3-center">
 | 
			
		||||
						<a
 | 
			
		||||
							class="w3-button w3-theme-d1"
 | 
			
		||||
							href=${`/login?return=${encodeURIComponent(this.url)}`}
 | 
			
		||||
							>Login</a
 | 
			
		||||
						>
 | 
			
		||||
					</footer>
 | 
			
		||||
				</div>`
 | 
			
		||||
			: !this.loaded || this.loading
 | 
			
		||||
				? html`<div
 | 
			
		||||
						class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge"
 | 
			
		||||
					>
 | 
			
		||||
						<span class="w3-spin" style="display: inline-block">🦀</span>
 | 
			
		||||
						Loading...
 | 
			
		||||
					</div>`
 | 
			
		||||
				: this.render_tab();
 | 
			
		||||
		let contents = !this.loaded
 | 
			
		||||
			? this.loading
 | 
			
		||||
				? html`<div class="w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge">
 | 
			
		||||
					Loading...
 | 
			
		||||
				</div>
 | 
			
		||||
				${this.render_tab()}`
 | 
			
		||||
				: html`<div>Select or create an identity.</div>`
 | 
			
		||||
			: this.render_tab();
 | 
			
		||||
		return html`
 | 
			
		||||
			<div
 | 
			
		||||
				style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column"
 | 
			
		||||
				style="width: 100vw; min-height: 100vh; height: 100%"
 | 
			
		||||
				class="w3-theme-dark"
 | 
			
		||||
			>
 | 
			
		||||
				<div style="flex: 0 0">${tabs}</div>
 | 
			
		||||
				<div style="flex: 1 1; overflow: auto; contain: layout">
 | 
			
		||||
				${tabs}
 | 
			
		||||
				<div style="padding: 8px">
 | 
			
		||||
					${this.tags.map(
 | 
			
		||||
						(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
 | 
			
		||||
					)}
 | 
			
		||||
					${contents}
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,6 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
			apps: {type: Object},
 | 
			
		||||
			drafts: {type: Object},
 | 
			
		||||
			author: {type: String},
 | 
			
		||||
			channel: {type: String},
 | 
			
		||||
			new_thread: {type: Boolean},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -29,7 +27,6 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		this.apps = undefined;
 | 
			
		||||
		this.drafts = {};
 | 
			
		||||
		this.author = undefined;
 | 
			
		||||
		this.new_thread = false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	process_text(text) {
 | 
			
		||||
@@ -79,9 +76,15 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		let preview = this.renderRoot.getElementById('preview');
 | 
			
		||||
		preview.innerHTML = this.process_text(edit.innerText);
 | 
			
		||||
		let content_warning = this.renderRoot.getElementById('content_warning');
 | 
			
		||||
		let content_warning_preview = this.renderRoot.getElementById(
 | 
			
		||||
			'content_warning_preview'
 | 
			
		||||
		);
 | 
			
		||||
		if (content_warning && content_warning_preview) {
 | 
			
		||||
			content_warning_preview.innerText = content_warning.value;
 | 
			
		||||
		}
 | 
			
		||||
		let draft = this.get_draft();
 | 
			
		||||
		draft.text = edit.innerText;
 | 
			
		||||
		draft.content_warning = content_warning?.value;
 | 
			
		||||
		draft.content_warning = content_warning?.innerText;
 | 
			
		||||
		setTimeout(() => this.notify(draft), 0);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -183,13 +186,6 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		event.preventDefault();
 | 
			
		||||
		document.execCommand(
 | 
			
		||||
			'insertText',
 | 
			
		||||
			false,
 | 
			
		||||
			event.clipboardData.getData('text/plain')
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async submit() {
 | 
			
		||||
@@ -199,26 +195,11 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		let message = {
 | 
			
		||||
			type: 'post',
 | 
			
		||||
			text: edit.innerText,
 | 
			
		||||
			channel: this.channel,
 | 
			
		||||
		};
 | 
			
		||||
		if (this.root || this.branch) {
 | 
			
		||||
			message.root = this.new_thread ? (this.branch ?? this.root) : this.root;
 | 
			
		||||
			message.root = this.root;
 | 
			
		||||
			message.branch = this.branch;
 | 
			
		||||
		}
 | 
			
		||||
		let reply = Object.fromEntries(
 | 
			
		||||
			(
 | 
			
		||||
				await tfrpc.rpc.query(
 | 
			
		||||
					`
 | 
			
		||||
				SELECT messages.id, messages.author FROM messages
 | 
			
		||||
				JOIN json_each(?) AS refs ON messages.id = refs.value
 | 
			
		||||
			`,
 | 
			
		||||
					[JSON.stringify([this.root, this.branch])]
 | 
			
		||||
				)
 | 
			
		||||
			).map((row) => [row.id, row.author])
 | 
			
		||||
		);
 | 
			
		||||
		if (Object.keys(reply).length) {
 | 
			
		||||
			message.reply = reply;
 | 
			
		||||
		}
 | 
			
		||||
		if (Object.values(draft.mentions || {}).length) {
 | 
			
		||||
			message.mentions = Object.values(draft.mentions);
 | 
			
		||||
		}
 | 
			
		||||
@@ -240,8 +221,12 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
			console.log('encrypted as', message);
 | 
			
		||||
		}
 | 
			
		||||
		try {
 | 
			
		||||
			await tfrpc.rpc.appendMessage(this.whoami, message);
 | 
			
		||||
			self.notify(undefined);
 | 
			
		||||
			await tfrpc.rpc.appendMessage(this.whoami, message).then(function () {
 | 
			
		||||
				edit.innerText = '';
 | 
			
		||||
				self.input();
 | 
			
		||||
				self.notify(undefined);
 | 
			
		||||
				self.requestUpdate();
 | 
			
		||||
			});
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			alert(error.message);
 | 
			
		||||
		}
 | 
			
		||||
@@ -255,12 +240,10 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let input = document.createElement('input');
 | 
			
		||||
		input.type = 'file';
 | 
			
		||||
		input.addEventListener('change', function (event) {
 | 
			
		||||
			input.parentNode.removeChild(input);
 | 
			
		||||
		input.onchange = function (event) {
 | 
			
		||||
			let file = event.target.files[0];
 | 
			
		||||
			self.add_file(file);
 | 
			
		||||
		});
 | 
			
		||||
		document.body.appendChild(input);
 | 
			
		||||
		};
 | 
			
		||||
		input.click();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -270,9 +253,9 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		try {
 | 
			
		||||
			let rows = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
				SELECT json(messages.content) AS content FROM messages_fts(?)
 | 
			
		||||
				SELECT json(messages.content) FROM messages_fts(?)
 | 
			
		||||
				JOIN messages ON messages.rowid = messages_fts.rowid
 | 
			
		||||
				WHERE json(messages.content) LIKE ?
 | 
			
		||||
				WHERE messages.content LIKE ?
 | 
			
		||||
				ORDER BY timestamp DESC LIMIT 10
 | 
			
		||||
			`,
 | 
			
		||||
				['"' + text.replace('"', '""') + '"', `%%`]
 | 
			
		||||
@@ -308,23 +291,18 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
		let tribute = new Tribute({
 | 
			
		||||
			iframe: this.shadowRoot,
 | 
			
		||||
			collection: [
 | 
			
		||||
				{
 | 
			
		||||
					values: values,
 | 
			
		||||
					selectTemplate: function (item) {
 | 
			
		||||
						return item
 | 
			
		||||
							? `[@${item.original.key}](${item.original.value})`
 | 
			
		||||
							: undefined;
 | 
			
		||||
						return item ? `[@${item.original.key}](${item.original.value})` : undefined;
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					trigger: '&',
 | 
			
		||||
					values: this.autocomplete,
 | 
			
		||||
					selectTemplate: function (item) {
 | 
			
		||||
						return item
 | 
			
		||||
							? ``
 | 
			
		||||
							: undefined;
 | 
			
		||||
						return item ? `` : undefined;
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			],
 | 
			
		||||
@@ -343,7 +321,6 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		let encrypt = this.renderRoot.getElementById('encrypt_to');
 | 
			
		||||
		if (encrypt) {
 | 
			
		||||
			let tribute = new Tribute({
 | 
			
		||||
				iframe: this.shadowRoot,
 | 
			
		||||
				values: Object.entries(this.users).map((x) => ({
 | 
			
		||||
					key: x[1].name,
 | 
			
		||||
					value: x[0],
 | 
			
		||||
@@ -476,7 +453,7 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
						<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=${this.input} @change=${this.change} value=${draft.content_warning}></input>
 | 
			
		||||
				</div>
 | 
			
		||||
			`;
 | 
			
		||||
		} else {
 | 
			
		||||
@@ -487,20 +464,6 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_new_thread() {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		if (
 | 
			
		||||
			this.root !== undefined &&
 | 
			
		||||
			this.branch !== undefined &&
 | 
			
		||||
			this.root != this.branch
 | 
			
		||||
		) {
 | 
			
		||||
			return html`
 | 
			
		||||
				<input type="checkbox" class="w3-check w3-theme-d1" id="new_thread" @change=${() => (self.new_thread = !self.new_thread)} ?checked=${self.new_thread}></input>
 | 
			
		||||
				<label for="new_thread">New Thread</label>
 | 
			
		||||
			`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	get_draft() {
 | 
			
		||||
		return this.drafts[this.branch || ''] || {};
 | 
			
		||||
	}
 | 
			
		||||
@@ -565,24 +528,11 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
						🔐
 | 
			
		||||
					</button>`;
 | 
			
		||||
		let result = html`
 | 
			
		||||
			<style>
 | 
			
		||||
				.w3-input:empty::before {
 | 
			
		||||
					content: attr(placeholder);
 | 
			
		||||
				}
 | 
			
		||||
				.w3-input:empty:focus::before {
 | 
			
		||||
					content: '';
 | 
			
		||||
				}
 | 
			
		||||
			</style>
 | 
			
		||||
			<div
 | 
			
		||||
				class="w3-card-4 w3-theme-d4 w3-padding w3-margin-top w3-margin-bottom"
 | 
			
		||||
				class="w3-card-4 w3-theme-d4 w3-padding-small"
 | 
			
		||||
				style="box-sizing: border-box"
 | 
			
		||||
			>
 | 
			
		||||
				<header class="w3-container">
 | 
			
		||||
					${this.channel !== undefined
 | 
			
		||||
						? html`<p>To #${this.channel}:</p>`
 | 
			
		||||
						: undefined}
 | 
			
		||||
					${this.render_encrypt()}
 | 
			
		||||
				</header>
 | 
			
		||||
				${this.render_encrypt()}
 | 
			
		||||
				<div class="w3-container w3-padding-small">
 | 
			
		||||
					<div class="w3-half">
 | 
			
		||||
						<span
 | 
			
		||||
@@ -592,36 +542,29 @@ class TfComposeElement extends LitElement {
 | 
			
		||||
							id="edit"
 | 
			
		||||
							@input=${this.input}
 | 
			
		||||
							@paste=${this.paste}
 | 
			
		||||
							contenteditable="plaintext-only"
 | 
			
		||||
							contenteditable
 | 
			
		||||
							.innerText=${live(draft.text ?? '')}
 | 
			
		||||
						></span>
 | 
			
		||||
							></span>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="w3-half w3-container">
 | 
			
		||||
					<div class="w3-half w3-padding">
 | 
			
		||||
						${content_warning}
 | 
			
		||||
						<p id="preview"></p>
 | 
			
		||||
						<div id="preview"></div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				${Object.values(draft.mentions || {}).map((x) =>
 | 
			
		||||
					self.render_mention(x)
 | 
			
		||||
				)}
 | 
			
		||||
				<footer class="w3-container">
 | 
			
		||||
					${this.render_attach_app()} ${this.render_content_warning()}
 | 
			
		||||
					${this.render_new_thread()}
 | 
			
		||||
					<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						id="submit"
 | 
			
		||||
						@click=${this.submit}
 | 
			
		||||
					>
 | 
			
		||||
						Submit
 | 
			
		||||
					</button>
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.attach}>
 | 
			
		||||
						Attach
 | 
			
		||||
					</button>
 | 
			
		||||
					${this.render_attach_app_button()} ${encrypt}
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.discard}>
 | 
			
		||||
						Discard
 | 
			
		||||
					</button>
 | 
			
		||||
				</footer>
 | 
			
		||||
				${this.render_attach_app()} ${this.render_content_warning()}
 | 
			
		||||
				<button class="w3-button w3-theme-d1" id="submit" @click=${this.submit}>
 | 
			
		||||
					Submit
 | 
			
		||||
				</button>
 | 
			
		||||
				<button class="w3-button w3-theme-d1" @click=${this.attach}>
 | 
			
		||||
					Attach
 | 
			
		||||
				</button>
 | 
			
		||||
				${this.render_attach_app_button()} ${encrypt}
 | 
			
		||||
				<button class="w3-button w3-theme-d1" @click=${this.discard}>
 | 
			
		||||
					Discard
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
		return result;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import {LitElement, html, repeat, render, unsafeHTML} from './lit-all.min.js';
 | 
			
		||||
import {LitElement, html, render, unsafeHTML} from './lit-all.min.js';
 | 
			
		||||
import * as tfrpc from '/static/tfrpc.js';
 | 
			
		||||
import * as tfutils from './tf-utils.js';
 | 
			
		||||
import * as emojis from './emojis.js';
 | 
			
		||||
@@ -14,8 +14,6 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
			format: {type: String},
 | 
			
		||||
			blog_data: {type: String},
 | 
			
		||||
			expanded: {type: Object},
 | 
			
		||||
			channel: {type: String},
 | 
			
		||||
			channel_unread: {type: Number},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -30,7 +28,6 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
		this.drafts = {};
 | 
			
		||||
		this.format = 'message';
 | 
			
		||||
		this.expanded = {};
 | 
			
		||||
		this.channel_unread = -1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	show_reply() {
 | 
			
		||||
@@ -76,34 +73,22 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if (this.message?.votes?.length) {
 | 
			
		||||
			return html` <div class="w3-container">
 | 
			
		||||
				<div
 | 
			
		||||
					class="w3-button w3-bar w3-padding-small"
 | 
			
		||||
					@click=${this.show_reactions}
 | 
			
		||||
				>
 | 
			
		||||
					${(this.message.votes || []).map(
 | 
			
		||||
						(vote) => html`
 | 
			
		||||
							<span
 | 
			
		||||
								class="w3-bar-item w3-padding-small"
 | 
			
		||||
								title="${this.users[vote.author]?.name ??
 | 
			
		||||
								vote.author} ${new Date(vote.timestamp)}"
 | 
			
		||||
							>
 | 
			
		||||
								${normalize_expression(vote.content.vote.expression)}
 | 
			
		||||
							</span>
 | 
			
		||||
						`
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
			return html`<div class="w3-button" @click=${this.show_reactions}>
 | 
			
		||||
				${(this.message.votes || []).map(
 | 
			
		||||
					(vote) => html`
 | 
			
		||||
						<span
 | 
			
		||||
							title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
 | 
			
		||||
								vote.timestamp
 | 
			
		||||
							)}"
 | 
			
		||||
						>
 | 
			
		||||
							${normalize_expression(vote.content.vote.expression)}
 | 
			
		||||
						</span>
 | 
			
		||||
					`
 | 
			
		||||
				)}
 | 
			
		||||
			</div>`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_json(value) {
 | 
			
		||||
		let json = JSON.stringify(value, null, 2);
 | 
			
		||||
		return html`
 | 
			
		||||
			<pre style="white-space: pre-wrap; overflow-wrap: anywhere">${json}</pre>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_raw() {
 | 
			
		||||
		let raw = {
 | 
			
		||||
			id: this.message?.id,
 | 
			
		||||
@@ -115,24 +100,36 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
			content: this.message?.content,
 | 
			
		||||
			signature: this.message?.signature,
 | 
			
		||||
		};
 | 
			
		||||
		return this.render_json(raw);
 | 
			
		||||
		return html`<div style="white-space: pre-wrap">
 | 
			
		||||
			${JSON.stringify(raw, null, 2)}
 | 
			
		||||
		</div>`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	vote(emoji) {
 | 
			
		||||
		let reaction = emoji;
 | 
			
		||||
		let message = this.message.id;
 | 
			
		||||
		tfrpc.rpc
 | 
			
		||||
			.appendMessage(this.whoami, {
 | 
			
		||||
				type: 'vote',
 | 
			
		||||
				vote: {
 | 
			
		||||
					link: message,
 | 
			
		||||
					value: 1,
 | 
			
		||||
					expression: reaction,
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
			.catch(function (error) {
 | 
			
		||||
				alert(error?.message);
 | 
			
		||||
			});
 | 
			
		||||
		if (
 | 
			
		||||
			confirm(
 | 
			
		||||
				'Are you sure you want to react with ' +
 | 
			
		||||
					reaction +
 | 
			
		||||
					' to ' +
 | 
			
		||||
					message +
 | 
			
		||||
					'?'
 | 
			
		||||
			)
 | 
			
		||||
		) {
 | 
			
		||||
			tfrpc.rpc
 | 
			
		||||
				.appendMessage(this.whoami, {
 | 
			
		||||
					type: 'vote',
 | 
			
		||||
					vote: {
 | 
			
		||||
						link: message,
 | 
			
		||||
						value: 1,
 | 
			
		||||
						expression: reaction,
 | 
			
		||||
					},
 | 
			
		||||
				})
 | 
			
		||||
				.catch(function (error) {
 | 
			
		||||
					alert(error?.message);
 | 
			
		||||
				});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	react(event) {
 | 
			
		||||
@@ -175,7 +172,7 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
			event.srcElement.classList.contains('img_caption')
 | 
			
		||||
		) {
 | 
			
		||||
			let next = event.srcElement.nextSibling;
 | 
			
		||||
			if (next.style.display != 'none') {
 | 
			
		||||
			if (next.style.display == 'block') {
 | 
			
		||||
				next.style.display = 'none';
 | 
			
		||||
			} else {
 | 
			
		||||
				next.style.display = 'block';
 | 
			
		||||
@@ -185,7 +182,7 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
 | 
			
		||||
	render_mention(mention) {
 | 
			
		||||
		if (!mention?.link || typeof mention.link != 'string') {
 | 
			
		||||
			return this.render_json(mention);
 | 
			
		||||
			return html` <pre>${JSON.stringify(mention)}</pre>`;
 | 
			
		||||
		} else if (
 | 
			
		||||
			mention?.link?.startsWith('&') &&
 | 
			
		||||
			mention?.type?.startsWith('image/')
 | 
			
		||||
@@ -226,7 +223,7 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
				>${mention.name}</a
 | 
			
		||||
			>`;
 | 
			
		||||
		} else if (mention.link?.startsWith('#')) {
 | 
			
		||||
			return html` <a href=${'#' + encodeURIComponent('#' + mention.link)}
 | 
			
		||||
			return html` <a href=${'#q=' + encodeURIComponent(mention.link)}
 | 
			
		||||
				>${mention.link}</a
 | 
			
		||||
			>`;
 | 
			
		||||
		} else if (
 | 
			
		||||
@@ -236,22 +233,23 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
		) {
 | 
			
		||||
			return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
 | 
			
		||||
		} else {
 | 
			
		||||
			return this.render_json(mention);
 | 
			
		||||
			return html` <pre style="white-space: pre-wrap">
 | 
			
		||||
${JSON.stringify(mention, null, 2)}</pre
 | 
			
		||||
			>`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_mentions() {
 | 
			
		||||
		let mentions = this.message?.content?.mentions || [];
 | 
			
		||||
		mentions = mentions.filter(
 | 
			
		||||
			(x) =>
 | 
			
		||||
				this.message?.content?.text?.indexOf(
 | 
			
		||||
					typeof x === 'string' ? x : x.link
 | 
			
		||||
				) === -1
 | 
			
		||||
			(x) => this.message?.content?.text?.indexOf(x.link) === -1
 | 
			
		||||
		);
 | 
			
		||||
		if (mentions.length) {
 | 
			
		||||
			let self = this;
 | 
			
		||||
			return html`
 | 
			
		||||
				<fieldset style="padding: 0.5em; border: 1px solid black">
 | 
			
		||||
				<fieldset
 | 
			
		||||
					style="padding: 0.5em; border: 1px solid black"
 | 
			
		||||
				>
 | 
			
		||||
					<legend>Mentions</legend>
 | 
			
		||||
					${mentions.map((x) => self.render_mention(x))}
 | 
			
		||||
				</fieldset>
 | 
			
		||||
@@ -303,9 +301,7 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
						@click=${() => self.set_expanded(false)}
 | 
			
		||||
					>
 | 
			
		||||
						Collapse</button
 | 
			
		||||
					>${repeat(
 | 
			
		||||
						this.message.child_messages || [],
 | 
			
		||||
						(x) => x.id,
 | 
			
		||||
					>${(this.message.child_messages || []).map(
 | 
			
		||||
						(x) =>
 | 
			
		||||
							html`<tf-message
 | 
			
		||||
								.message=${x}
 | 
			
		||||
@@ -313,29 +309,12 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
								.users=${this.users}
 | 
			
		||||
								.drafts=${this.drafts}
 | 
			
		||||
								.expanded=${this.expanded}
 | 
			
		||||
								channel=${this.channel}
 | 
			
		||||
								channel_unread=${this.channel_unread}
 | 
			
		||||
							></tf-message>`
 | 
			
		||||
					)}`;
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			return undefined;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mark_unread() {
 | 
			
		||||
		this.dispatchEvent(
 | 
			
		||||
			new CustomEvent('channelsetunread', {
 | 
			
		||||
				bubbles: true,
 | 
			
		||||
				composed: true,
 | 
			
		||||
				detail: {
 | 
			
		||||
					channel: this.channel,
 | 
			
		||||
					unread: this.message.rowid,
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_channels() {
 | 
			
		||||
		let content = this.message?.content;
 | 
			
		||||
		if (this?.messsage?.decrypted?.type == 'post') {
 | 
			
		||||
@@ -355,38 +334,29 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
		return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	class_background() {
 | 
			
		||||
		return this.message?.decrypted
 | 
			
		||||
			? 'w3-pale-red'
 | 
			
		||||
			: this.message?.rowid >= this.channel_unread
 | 
			
		||||
				? 'w3-theme-d2'
 | 
			
		||||
				: 'w3-theme-d4';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	get_content() {
 | 
			
		||||
	render() {
 | 
			
		||||
		let content = this.message?.content;
 | 
			
		||||
		if (this.message?.decrypted?.type == 'post') {
 | 
			
		||||
			content = this.message.decrypted;
 | 
			
		||||
		}
 | 
			
		||||
		return content;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_raw_button() {
 | 
			
		||||
		let content = this.get_content();
 | 
			
		||||
		let class_background = this.message?.decrypted
 | 
			
		||||
			? 'w3-pale-red'
 | 
			
		||||
			: 'w3-theme-d4';
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let raw_button;
 | 
			
		||||
		switch (this.format) {
 | 
			
		||||
			case 'raw':
 | 
			
		||||
				if (content?.type == 'post' || content?.type == 'blog') {
 | 
			
		||||
					raw_button = html`<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						@click=${() => (this.format = 'md')}
 | 
			
		||||
						@click=${() => (self.format = 'md')}
 | 
			
		||||
					>
 | 
			
		||||
						Markdown
 | 
			
		||||
					</button>`;
 | 
			
		||||
				} else {
 | 
			
		||||
					raw_button = html`<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						@click=${() => (this.format = 'message')}
 | 
			
		||||
						@click=${() => (self.format = 'message')}
 | 
			
		||||
					>
 | 
			
		||||
						Message
 | 
			
		||||
					</button>`;
 | 
			
		||||
@@ -395,7 +365,7 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
			case 'md':
 | 
			
		||||
				raw_button = html`<button
 | 
			
		||||
					class="w3-button w3-theme-d1"
 | 
			
		||||
					@click=${() => (this.format = 'message')}
 | 
			
		||||
					@click=${() => (self.format = 'message')}
 | 
			
		||||
				>
 | 
			
		||||
					Message
 | 
			
		||||
				</button>`;
 | 
			
		||||
@@ -403,7 +373,7 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
			case 'decrypted':
 | 
			
		||||
				raw_button = html`<button
 | 
			
		||||
					class="w3-button w3-theme-d1"
 | 
			
		||||
					@click=${() => (this.format = 'raw')}
 | 
			
		||||
					@click=${() => (self.format = 'raw')}
 | 
			
		||||
				>
 | 
			
		||||
					Raw
 | 
			
		||||
				</button>`;
 | 
			
		||||
@@ -412,136 +382,55 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
				if (this.message.decrypted) {
 | 
			
		||||
					raw_button = html`<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						@click=${() => (this.format = 'decrypted')}
 | 
			
		||||
						@click=${() => (self.format = 'decrypted')}
 | 
			
		||||
					>
 | 
			
		||||
						Decrypted
 | 
			
		||||
					</button>`;
 | 
			
		||||
				} else {
 | 
			
		||||
					raw_button = html`<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						@click=${() => (this.format = 'raw')}
 | 
			
		||||
						@click=${() => (self.format = 'raw')}
 | 
			
		||||
					>
 | 
			
		||||
						Raw
 | 
			
		||||
					</button>`;
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
		return raw_button;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_header() {
 | 
			
		||||
		let is_encrypted = this.message?.decrypted
 | 
			
		||||
			? html`<span class="w3-bar-item">🔓</span>`
 | 
			
		||||
			: typeof this.message?.content == 'string'
 | 
			
		||||
				? html`<span class="w3-bar-item">🔒</span>`
 | 
			
		||||
				: undefined;
 | 
			
		||||
		return html`
 | 
			
		||||
			<header class="w3-bar">
 | 
			
		||||
				<span class="w3-bar-item">
 | 
			
		||||
					<tf-user id=${this.message.author} .users=${this.users}></tf-user>
 | 
			
		||||
				</span>
 | 
			
		||||
				${is_encrypted}
 | 
			
		||||
				<span class="w3-bar-item w3-right">${this.render_raw_button()}</span>
 | 
			
		||||
				<span class="w3-bar-item w3-right" style="text-wrap: nowrap"
 | 
			
		||||
					><a target="_top" href=${'#' + encodeURIComponent(this.message.id)}
 | 
			
		||||
						>%</a
 | 
			
		||||
					>
 | 
			
		||||
					${new Date(this.message.timestamp).toLocaleString()}</span
 | 
			
		||||
		function small_frame(inner) {
 | 
			
		||||
			let body;
 | 
			
		||||
			return html`
 | 
			
		||||
				<div
 | 
			
		||||
					class="w3-card-4 w3-theme-d4 w3-border-theme"
 | 
			
		||||
					style="margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
 | 
			
		||||
				>
 | 
			
		||||
			</header>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_frame(inner) {
 | 
			
		||||
		return html`
 | 
			
		||||
			<style>
 | 
			
		||||
				code {
 | 
			
		||||
					white-space: pre-wrap;
 | 
			
		||||
					overflow-wrap: break-word;
 | 
			
		||||
				}
 | 
			
		||||
				div {
 | 
			
		||||
					overflow-wrap: anywhere;
 | 
			
		||||
				}
 | 
			
		||||
				img {
 | 
			
		||||
					max-width: 100%;
 | 
			
		||||
					height: auto;
 | 
			
		||||
					display: block;
 | 
			
		||||
				}
 | 
			
		||||
			</style>
 | 
			
		||||
			<div
 | 
			
		||||
				class="w3-card-4 ${this.class_background()} w3-border-theme w3-margin-top"
 | 
			
		||||
				style="overflow: auto; overflow-wrap: anywhere; display: block; max-width: 100%"
 | 
			
		||||
			>
 | 
			
		||||
				${inner}
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_small_frame(inner) {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		return this.render_frame(html`
 | 
			
		||||
			${self.render_header()}
 | 
			
		||||
			${self.format == 'raw'
 | 
			
		||||
				? html`<div class="w3-container">${self.render_raw()}</div>`
 | 
			
		||||
				: inner}
 | 
			
		||||
			${self.render_votes()}
 | 
			
		||||
			${(self.message.child_messages || []).map(
 | 
			
		||||
				(x) => html`
 | 
			
		||||
					<tf-message
 | 
			
		||||
						.message=${x}
 | 
			
		||||
						whoami=${self.whoami}
 | 
			
		||||
						.users=${self.users}
 | 
			
		||||
						.drafts=${self.drafts}
 | 
			
		||||
						.expanded=${self.expanded}
 | 
			
		||||
						channel=${self.channel}
 | 
			
		||||
						channel_unread=${self.channel_unread}
 | 
			
		||||
					></tf-message>
 | 
			
		||||
				`
 | 
			
		||||
			)}
 | 
			
		||||
		`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_actions() {
 | 
			
		||||
		let content = this.get_content();
 | 
			
		||||
		let reply =
 | 
			
		||||
			this.drafts[this.message?.id] !== undefined
 | 
			
		||||
				? html`
 | 
			
		||||
						<tf-compose
 | 
			
		||||
							whoami=${this.whoami}
 | 
			
		||||
							.users=${this.users}
 | 
			
		||||
							root=${content.root || this.message.id}
 | 
			
		||||
							branch=${this.message.id}
 | 
			
		||||
							.drafts=${this.drafts}
 | 
			
		||||
							@tf-discard=${this.discard_reply}
 | 
			
		||||
							author=${this.message.author}
 | 
			
		||||
						></tf-compose>
 | 
			
		||||
					`
 | 
			
		||||
				: html`
 | 
			
		||||
						<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
 | 
			
		||||
							Reply
 | 
			
		||||
						</button>
 | 
			
		||||
					`;
 | 
			
		||||
		return html`
 | 
			
		||||
			<div class="w3-section w3-container">
 | 
			
		||||
				${reply}
 | 
			
		||||
				<button class="w3-button w3-theme-d1" @click=${this.react}>
 | 
			
		||||
					React
 | 
			
		||||
				</button>
 | 
			
		||||
				${this.render_children()}
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let content = this.message?.content;
 | 
			
		||||
		if (this.message?.decrypted?.type == 'post') {
 | 
			
		||||
			content = this.message.decrypted;
 | 
			
		||||
					<tf-user id=${self.message.author} .users=${self.users}></tf-user>
 | 
			
		||||
					<span style="padding-right: 8px"
 | 
			
		||||
						><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(
 | 
			
		||||
							self.message.timestamp
 | 
			
		||||
						).toLocaleString()}</span
 | 
			
		||||
					>
 | 
			
		||||
					${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
 | 
			
		||||
					${self.render_votes()}
 | 
			
		||||
					${(self.message.child_messages || []).map(
 | 
			
		||||
						(x) => html`
 | 
			
		||||
							<tf-message
 | 
			
		||||
								.message=${x}
 | 
			
		||||
								whoami=${self.whoami}
 | 
			
		||||
								.users=${self.users}
 | 
			
		||||
								.drafts=${self.drafts}
 | 
			
		||||
								.expanded=${self.expanded}
 | 
			
		||||
							></tf-message>
 | 
			
		||||
						`
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
			`;
 | 
			
		||||
		}
 | 
			
		||||
		let class_background = this.class_background();
 | 
			
		||||
		let self = this;
 | 
			
		||||
		if (this.message?.type === 'contact_group') {
 | 
			
		||||
			return this.render_frame(
 | 
			
		||||
				html` ${this.message.messages.map(
 | 
			
		||||
			return html` <div
 | 
			
		||||
				class="w3-card-4 w3-theme-d4 w3-border-theme"
 | 
			
		||||
				style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
 | 
			
		||||
			>
 | 
			
		||||
				${this.message.messages.map(
 | 
			
		||||
					(x) =>
 | 
			
		||||
						html`<tf-message
 | 
			
		||||
							.message=${x}
 | 
			
		||||
@@ -549,37 +438,30 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
							.users=${this.users}
 | 
			
		||||
							.drafts=${this.drafts}
 | 
			
		||||
							.expanded=${this.expanded}
 | 
			
		||||
							channel=${this.channel}
 | 
			
		||||
							channel_unread=${this.channel_unread}
 | 
			
		||||
						></tf-message>`
 | 
			
		||||
				)}`
 | 
			
		||||
			);
 | 
			
		||||
				)}
 | 
			
		||||
			</div>`;
 | 
			
		||||
		} else if (this.message.placeholder) {
 | 
			
		||||
			return this.render_frame(
 | 
			
		||||
				html`<div class="w3-padding">
 | 
			
		||||
					<p>
 | 
			
		||||
						<a target="_top" href=${'#' + encodeURIComponent(this.message.id)}
 | 
			
		||||
							>${this.message.id}</a
 | 
			
		||||
						>
 | 
			
		||||
						(placeholder)
 | 
			
		||||
					</p>
 | 
			
		||||
					<div>${this.render_votes()}</div>
 | 
			
		||||
					${(this.message.child_messages || []).map(
 | 
			
		||||
						(x) => html`
 | 
			
		||||
							<tf-message
 | 
			
		||||
								.message=${x}
 | 
			
		||||
								whoami=${this.whoami}
 | 
			
		||||
								.users=${this.users}
 | 
			
		||||
								.drafts=${this.drafts}
 | 
			
		||||
								.expanded=${this.expanded}
 | 
			
		||||
								channel=${this.channel}
 | 
			
		||||
								channel_unread=${this.channel_unread}
 | 
			
		||||
							></tf-message>
 | 
			
		||||
						`
 | 
			
		||||
					)}
 | 
			
		||||
				</div>`
 | 
			
		||||
			);
 | 
			
		||||
		} else if (typeof content?.type === 'string') {
 | 
			
		||||
			return html` <div
 | 
			
		||||
				class="w3-card-4 w3-theme-d4 w3-border-theme"
 | 
			
		||||
				style="margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
 | 
			
		||||
			>
 | 
			
		||||
				<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
 | 
			
		||||
				(placeholder)
 | 
			
		||||
				<div>${this.render_votes()}</div>
 | 
			
		||||
				${(this.message.child_messages || []).map(
 | 
			
		||||
					(x) => html`
 | 
			
		||||
						<tf-message
 | 
			
		||||
							.message=${x}
 | 
			
		||||
							whoami=${this.whoami}
 | 
			
		||||
							.users=${this.users}
 | 
			
		||||
							.drafts=${this.drafts}
 | 
			
		||||
							.expanded=${this.expanded}
 | 
			
		||||
						></tf-message>
 | 
			
		||||
					`
 | 
			
		||||
				)}
 | 
			
		||||
			</div>`;
 | 
			
		||||
		} else if (typeof (content?.type === 'string')) {
 | 
			
		||||
			if (content.type == 'about') {
 | 
			
		||||
				let name;
 | 
			
		||||
				let image;
 | 
			
		||||
@@ -606,14 +488,10 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
								Updated profile for
 | 
			
		||||
								<tf-user id=${content.about} .users=${this.users}></tf-user>.
 | 
			
		||||
							</div>`;
 | 
			
		||||
				return this.render_small_frame(html`
 | 
			
		||||
					<div class="w3-container">
 | 
			
		||||
						<p>${update} ${name} ${image} ${description}</p>
 | 
			
		||||
					</div>
 | 
			
		||||
				`);
 | 
			
		||||
				return small_frame(html` ${update} ${name} ${image} ${description} `);
 | 
			
		||||
			} else if (content.type == 'contact') {
 | 
			
		||||
				return html`
 | 
			
		||||
					<div class="w3-padding">
 | 
			
		||||
					<div>
 | 
			
		||||
						<tf-user id=${this.message.author} .users=${this.users}></tf-user>
 | 
			
		||||
						is
 | 
			
		||||
						${content.blocking === true
 | 
			
		||||
@@ -632,6 +510,24 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
					</div>
 | 
			
		||||
				`;
 | 
			
		||||
			} else if (content.type == 'post') {
 | 
			
		||||
				let reply =
 | 
			
		||||
					this.drafts[this.message?.id] !== undefined
 | 
			
		||||
						? html`
 | 
			
		||||
								<tf-compose
 | 
			
		||||
									whoami=${this.whoami}
 | 
			
		||||
									.users=${this.users}
 | 
			
		||||
									root=${content.root || this.message.id}
 | 
			
		||||
									branch=${this.message.id}
 | 
			
		||||
									.drafts=${this.drafts}
 | 
			
		||||
									@tf-discard=${this.discard_reply}
 | 
			
		||||
									author=${this.message.author}
 | 
			
		||||
								></tf-compose>
 | 
			
		||||
							`
 | 
			
		||||
						: html`
 | 
			
		||||
								<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
 | 
			
		||||
									Reply
 | 
			
		||||
								</button>
 | 
			
		||||
							`;
 | 
			
		||||
				let self = this;
 | 
			
		||||
				let body;
 | 
			
		||||
				switch (this.format) {
 | 
			
		||||
@@ -648,7 +544,11 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
						body = unsafeHTML(tfutils.markdown(content.text));
 | 
			
		||||
						break;
 | 
			
		||||
					case 'decrypted':
 | 
			
		||||
						body = this.render_json(content);
 | 
			
		||||
						body = html`<pre
 | 
			
		||||
							style="white-space: pre-wrap; overflow-wrap: anywhere"
 | 
			
		||||
						>
 | 
			
		||||
${JSON.stringify(content, null, 2)}</pre
 | 
			
		||||
						>`;
 | 
			
		||||
						break;
 | 
			
		||||
				}
 | 
			
		||||
				let content_warning = html`
 | 
			
		||||
@@ -670,22 +570,90 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
						? html` ${content_warning} ${content_html} `
 | 
			
		||||
						: content_warning
 | 
			
		||||
					: content_html;
 | 
			
		||||
				return this.render_frame(html`
 | 
			
		||||
					${this.render_header()}
 | 
			
		||||
					<div class="w3-container">${payload}</div>
 | 
			
		||||
					${this.render_votes()} ${this.render_actions()}
 | 
			
		||||
				</div>
 | 
			
		||||
				`);
 | 
			
		||||
			} else if (content.type === 'issue') {
 | 
			
		||||
				return this.render_frame(html`
 | 
			
		||||
					${this.render_header()} ${content.text} ${this.render_votes()}
 | 
			
		||||
					<footer class="w3-container">
 | 
			
		||||
						<button class="w3-button w3-theme-d1" @click=${this.react}>
 | 
			
		||||
							React
 | 
			
		||||
						</button>
 | 
			
		||||
				let is_encrypted = this.message?.decrypted
 | 
			
		||||
					? html`<span style="align-self: center">🔓</span>`
 | 
			
		||||
					: undefined;
 | 
			
		||||
				return html`
 | 
			
		||||
					<style>
 | 
			
		||||
						code {
 | 
			
		||||
							white-space: pre-wrap;
 | 
			
		||||
							overflow-wrap: break-word;
 | 
			
		||||
						}
 | 
			
		||||
						div {
 | 
			
		||||
							overflow-wrap: anywhere;
 | 
			
		||||
						}
 | 
			
		||||
						img {
 | 
			
		||||
							max-width: 100%;
 | 
			
		||||
							height: auto;
 | 
			
		||||
							display: block;
 | 
			
		||||
						}
 | 
			
		||||
					</style>
 | 
			
		||||
					<div
 | 
			
		||||
						class="w3-card-4 ${class_background} w3-border-theme"
 | 
			
		||||
						style="margin-top: 8px; padding: 16px"
 | 
			
		||||
					>
 | 
			
		||||
						<div style="display: flex; flex-direction: row">
 | 
			
		||||
							<tf-user id=${this.message.author} .users=${this.users}></tf-user>
 | 
			
		||||
							${is_encrypted}
 | 
			
		||||
							<span style="flex: 1"></span>
 | 
			
		||||
							<span style="padding-right: 8px"
 | 
			
		||||
								><a target="_top" href=${'#' + self.message.id}>%</a>
 | 
			
		||||
								${new Date(this.message.timestamp).toLocaleString()}</span
 | 
			
		||||
							>
 | 
			
		||||
							<span>${raw_button}</span>
 | 
			
		||||
						</div>
 | 
			
		||||
						${payload} ${this.render_votes()}
 | 
			
		||||
						<p>
 | 
			
		||||
							${reply}
 | 
			
		||||
							<button class="w3-button w3-theme-d1" @click=${this.react}>
 | 
			
		||||
								React
 | 
			
		||||
							</button>
 | 
			
		||||
						</p>
 | 
			
		||||
						${this.render_children()}
 | 
			
		||||
					</footer>
 | 
			
		||||
				`);
 | 
			
		||||
					</div>
 | 
			
		||||
				`;
 | 
			
		||||
			} else if (content.type === 'issue') {
 | 
			
		||||
				let is_encrypted = this.message?.decrypted
 | 
			
		||||
					? html`<span style="align-self: center">🔓</span>`
 | 
			
		||||
					: undefined;
 | 
			
		||||
				return html`
 | 
			
		||||
					<style>
 | 
			
		||||
						code {
 | 
			
		||||
							white-space: pre-wrap;
 | 
			
		||||
							overflow-wrap: break-word;
 | 
			
		||||
						}
 | 
			
		||||
						div {
 | 
			
		||||
							overflow-wrap: anywhere;
 | 
			
		||||
						}
 | 
			
		||||
						img {
 | 
			
		||||
							max-width: 100%;
 | 
			
		||||
							height: auto;
 | 
			
		||||
							display: block;
 | 
			
		||||
						}
 | 
			
		||||
					</style>
 | 
			
		||||
					<div
 | 
			
		||||
						class="w3-card-4 ${class_background} w3-border-theme"
 | 
			
		||||
						style="margin-top: 8px; padding: 16px"
 | 
			
		||||
					>
 | 
			
		||||
						<div style="display: flex; flex-direction: row">
 | 
			
		||||
							<tf-user id=${this.message.author} .users=${this.users}></tf-user>
 | 
			
		||||
							${is_encrypted}
 | 
			
		||||
							<span style="flex: 1"></span>
 | 
			
		||||
							<span style="padding-right: 8px"
 | 
			
		||||
								><a target="_top" href=${'#' + self.message.id}>%</a>
 | 
			
		||||
								${new Date(this.message.timestamp).toLocaleString()}</span
 | 
			
		||||
							>
 | 
			
		||||
							<span>${raw_button}</span>
 | 
			
		||||
						</div>
 | 
			
		||||
						${content.text} ${this.render_votes()}
 | 
			
		||||
						<p>
 | 
			
		||||
							<button class="w3-button w3-theme-d1" @click=${this.react}>
 | 
			
		||||
								React
 | 
			
		||||
							</button>
 | 
			
		||||
						</p>
 | 
			
		||||
						${this.render_children()}
 | 
			
		||||
					</div>
 | 
			
		||||
				`;
 | 
			
		||||
			} else if (content.type === 'blog') {
 | 
			
		||||
				let self = this;
 | 
			
		||||
				tfrpc.rpc.get_blob(content.blog).then(function (data) {
 | 
			
		||||
@@ -721,20 +689,72 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
						`;
 | 
			
		||||
						break;
 | 
			
		||||
				}
 | 
			
		||||
				return this.render_frame(html`
 | 
			
		||||
					${this.render_header()}
 | 
			
		||||
					<div>${body}</div>
 | 
			
		||||
					${this.render_mentions()} ${this.render_votes()}
 | 
			
		||||
					${this.render_actions()}
 | 
			
		||||
				`);
 | 
			
		||||
				let reply =
 | 
			
		||||
					this.drafts[this.message?.id] !== undefined
 | 
			
		||||
						? html`
 | 
			
		||||
								<tf-compose
 | 
			
		||||
									whoami=${this.whoami}
 | 
			
		||||
									.users=${this.users}
 | 
			
		||||
									root=${content.root || this.message.id}
 | 
			
		||||
									branch=${this.message.id}
 | 
			
		||||
									.drafts=${this.drafts}
 | 
			
		||||
									@tf-discard=${this.discard_reply}
 | 
			
		||||
									author=${this.message.author}
 | 
			
		||||
								></tf-compose>
 | 
			
		||||
							`
 | 
			
		||||
						: html`
 | 
			
		||||
								<button class="w3-button w3-theme-d1" @click=${this.show_reply}>
 | 
			
		||||
									Reply
 | 
			
		||||
								</button>
 | 
			
		||||
							`;
 | 
			
		||||
				return html`
 | 
			
		||||
					<style>
 | 
			
		||||
						code {
 | 
			
		||||
							white-space: pre-wrap;
 | 
			
		||||
							overflow-wrap: break-word;
 | 
			
		||||
						}
 | 
			
		||||
						div {
 | 
			
		||||
							overflow-wrap: anywhere;
 | 
			
		||||
						}
 | 
			
		||||
						img {
 | 
			
		||||
							max-width: 100%;
 | 
			
		||||
							height: auto;
 | 
			
		||||
							display: block;
 | 
			
		||||
						}
 | 
			
		||||
					</style>
 | 
			
		||||
					<div
 | 
			
		||||
						class="w3-card-4 w3-theme-d4 w3-border-theme"
 | 
			
		||||
						style="margin-top: 8px; padding: 16px"
 | 
			
		||||
					>
 | 
			
		||||
						<div style="display: flex; flex-direction: row">
 | 
			
		||||
							<tf-user id=${this.message.author} .users=${this.users}></tf-user>
 | 
			
		||||
							<span style="flex: 1"></span>
 | 
			
		||||
							<span style="padding-right: 8px"
 | 
			
		||||
								><a target="_top" href=${'#' + self.message.id}>%</a>
 | 
			
		||||
								${new Date(this.message.timestamp).toLocaleString()}</span
 | 
			
		||||
							>
 | 
			
		||||
							<span>${raw_button}</span>
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						<div>${body}</div>
 | 
			
		||||
						${this.render_mentions()}
 | 
			
		||||
						<div>
 | 
			
		||||
							${reply}
 | 
			
		||||
							<button class="w3-button w3-theme-d1" @click=${this.react}>
 | 
			
		||||
								React
 | 
			
		||||
							</button>
 | 
			
		||||
						</div>
 | 
			
		||||
						${this.render_votes()} ${this.render_children()}
 | 
			
		||||
					</div>
 | 
			
		||||
				`;
 | 
			
		||||
			} else if (content.type === 'pub') {
 | 
			
		||||
				return this.render_small_frame(
 | 
			
		||||
				return small_frame(
 | 
			
		||||
					html` <style>
 | 
			
		||||
							span {
 | 
			
		||||
								overflow-wrap: anywhere;
 | 
			
		||||
							}
 | 
			
		||||
						</style>
 | 
			
		||||
						<div class="w3-padding">
 | 
			
		||||
						<span>
 | 
			
		||||
							<div>
 | 
			
		||||
								🍻
 | 
			
		||||
								<tf-user
 | 
			
		||||
@@ -743,47 +763,38 @@ class TfMessageElement extends LitElement {
 | 
			
		||||
								></tf-user>
 | 
			
		||||
							</div>
 | 
			
		||||
							<pre>${content.address.host}:${content.address.port}</pre>
 | 
			
		||||
						</div>`
 | 
			
		||||
						</span>`
 | 
			
		||||
				);
 | 
			
		||||
			} else if (content.type === 'channel') {
 | 
			
		||||
				return this.render_small_frame(html`
 | 
			
		||||
					<div class="w3-container">
 | 
			
		||||
						<p>
 | 
			
		||||
							${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
 | 
			
		||||
							<a href=${'#' + encodeURIComponent('#' + content.channel)}
 | 
			
		||||
								>#${content.channel}</a
 | 
			
		||||
							>
 | 
			
		||||
						</p>
 | 
			
		||||
				return small_frame(html`
 | 
			
		||||
					<div>
 | 
			
		||||
						${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
 | 
			
		||||
						<a href=${'#q=' + encodeURIComponent('#' + content.channel)}
 | 
			
		||||
							>#${content.channel}</a
 | 
			
		||||
						>
 | 
			
		||||
					</div>
 | 
			
		||||
				`);
 | 
			
		||||
			} else if (typeof this.message.content == 'string') {
 | 
			
		||||
				if (this.message?.decrypted) {
 | 
			
		||||
					if (this.format == 'decrypted') {
 | 
			
		||||
						return this.render_small_frame(
 | 
			
		||||
							html`<span class="w3-container">🔓</span> ${this.render_json(
 | 
			
		||||
									this.message.decrypted
 | 
			
		||||
								)}`
 | 
			
		||||
						return small_frame(
 | 
			
		||||
							html`<span>🔓</span>
 | 
			
		||||
								<pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`
 | 
			
		||||
						);
 | 
			
		||||
					} else {
 | 
			
		||||
						return this.render_small_frame(
 | 
			
		||||
							html`<span class="w3-container">🔓</span>
 | 
			
		||||
								<div class="w3-container">${this.message.decrypted.type}</div>`
 | 
			
		||||
						return small_frame(
 | 
			
		||||
							html`<span>🔓</span>
 | 
			
		||||
								<div>${this.message.decrypted.type}</div>`
 | 
			
		||||
						);
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					return this.render_small_frame();
 | 
			
		||||
					return small_frame(html`<span>🔒</span>`);
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				return this.render_small_frame(
 | 
			
		||||
					html`<div class="w3-container">
 | 
			
		||||
						<p><b>type</b>: ${content.type}</p>
 | 
			
		||||
					</div>`
 | 
			
		||||
				);
 | 
			
		||||
				return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
 | 
			
		||||
			}
 | 
			
		||||
		} else if (typeof this.message.content == 'string') {
 | 
			
		||||
			return this.render_small_frame();
 | 
			
		||||
		} else {
 | 
			
		||||
			return this.render_small_frame(this.render_raw());
 | 
			
		||||
			return small_frame(this.render_raw());
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import {LitElement, html, unsafeHTML, repeat, until} from './lit-all.min.js';
 | 
			
		||||
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
 | 
			
		||||
import * as tfrpc from '/static/tfrpc.js';
 | 
			
		||||
import {styles} from './tf-styles.js';
 | 
			
		||||
 | 
			
		||||
@@ -11,8 +11,6 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
			following: {type: Array},
 | 
			
		||||
			drafts: {type: Object},
 | 
			
		||||
			expanded: {type: Object},
 | 
			
		||||
			channel: {type: String},
 | 
			
		||||
			channel_unread: {type: Number},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -27,7 +25,6 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
		this.following = [];
 | 
			
		||||
		this.drafts = {};
 | 
			
		||||
		this.expanded = {};
 | 
			
		||||
		this.channel_unread = -1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	process_messages(messages) {
 | 
			
		||||
@@ -36,13 +33,12 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
 | 
			
		||||
		console.log('processing', messages.length, 'messages');
 | 
			
		||||
 | 
			
		||||
		function ensure_message(id, rowid) {
 | 
			
		||||
		function ensure_message(id) {
 | 
			
		||||
			let found = messages_by_id[id];
 | 
			
		||||
			if (found) {
 | 
			
		||||
				return found;
 | 
			
		||||
			} else {
 | 
			
		||||
				let added = {
 | 
			
		||||
					rowid: rowid,
 | 
			
		||||
					id: id,
 | 
			
		||||
					placeholder: true,
 | 
			
		||||
					content: '"placeholder"',
 | 
			
		||||
@@ -57,7 +53,7 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
 | 
			
		||||
		function link_message(message) {
 | 
			
		||||
			if (message.content.type === 'vote') {
 | 
			
		||||
				let parent = ensure_message(message.content.vote.link, message.rowid);
 | 
			
		||||
				let parent = ensure_message(message.content.vote.link);
 | 
			
		||||
				if (!parent.votes) {
 | 
			
		||||
					parent.votes = [];
 | 
			
		||||
				}
 | 
			
		||||
@@ -66,14 +62,14 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
			} else if (message.content.type == 'post') {
 | 
			
		||||
				if (message.content.root) {
 | 
			
		||||
					if (typeof message.content.root === 'string') {
 | 
			
		||||
						let m = ensure_message(message.content.root, message.rowid);
 | 
			
		||||
						let m = ensure_message(message.content.root);
 | 
			
		||||
						if (!m.child_messages) {
 | 
			
		||||
							m.child_messages = [];
 | 
			
		||||
						}
 | 
			
		||||
						m.child_messages.push(message);
 | 
			
		||||
						message.parent_message = message.content.root;
 | 
			
		||||
					} else {
 | 
			
		||||
						let m = ensure_message(message.content.root[0], message.rowid);
 | 
			
		||||
						let m = ensure_message(message.content.root[0]);
 | 
			
		||||
						if (!m.child_messages) {
 | 
			
		||||
							m.child_messages = [];
 | 
			
		||||
						}
 | 
			
		||||
@@ -166,7 +162,6 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
			} else {
 | 
			
		||||
				if (group.length > 0) {
 | 
			
		||||
					result.push({
 | 
			
		||||
						rowid: Math.max(...group.map((x) => x.rowid)),
 | 
			
		||||
						type: 'contact_group',
 | 
			
		||||
						messages: group,
 | 
			
		||||
					});
 | 
			
		||||
@@ -175,13 +170,6 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
				result.push(message);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if (group.length > 0) {
 | 
			
		||||
			result.push({
 | 
			
		||||
				rowid: Math.max(...group.map((x) => x.rowid)),
 | 
			
		||||
				type: 'contact_group',
 | 
			
		||||
				messages: group,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -190,40 +178,18 @@ class TfNewsElement extends LitElement {
 | 
			
		||||
		let final_messages = this.group_following(
 | 
			
		||||
			this.finalize_messages(messages_by_id)
 | 
			
		||||
		);
 | 
			
		||||
		let unread_rowid = -1;
 | 
			
		||||
		for (let message of final_messages) {
 | 
			
		||||
			if (message.rowid >= this.channel_unread) {
 | 
			
		||||
				unread_rowid = message.rowid;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return html`
 | 
			
		||||
			<div>
 | 
			
		||||
				${repeat(
 | 
			
		||||
					final_messages,
 | 
			
		||||
					(x) => x.id,
 | 
			
		||||
					(x) => html`
 | 
			
		||||
						<tf-message
 | 
			
		||||
			<div style="display: flex; flex-direction: column">
 | 
			
		||||
				${final_messages.map(
 | 
			
		||||
					(x) =>
 | 
			
		||||
						html`<tf-message
 | 
			
		||||
							.message=${x}
 | 
			
		||||
							whoami=${this.whoami}
 | 
			
		||||
							.users=${this.users}
 | 
			
		||||
							.drafts=${this.drafts}
 | 
			
		||||
							.expanded=${this.expanded}
 | 
			
		||||
							collapsed="true"
 | 
			
		||||
							channel=${this.channel}
 | 
			
		||||
							channel_unread=${this.channel_unread}
 | 
			
		||||
						></tf-message>
 | 
			
		||||
						${x.rowid == unread_rowid
 | 
			
		||||
							? html`<div style="display: flex; flex-direction: row">
 | 
			
		||||
									<div
 | 
			
		||||
										style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
 | 
			
		||||
									></div>
 | 
			
		||||
									<div style="color: #f00; padding: 8px">unread</div>
 | 
			
		||||
									<div
 | 
			
		||||
										style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
 | 
			
		||||
									></div>
 | 
			
		||||
								</div>`
 | 
			
		||||
							: undefined}
 | 
			
		||||
					`
 | 
			
		||||
						></tf-message>`
 | 
			
		||||
				)}
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
			id: {type: String},
 | 
			
		||||
			users: {type: Object},
 | 
			
		||||
			size: {type: Number},
 | 
			
		||||
			server_follows_me: {type: Boolean},
 | 
			
		||||
			following: {type: Boolean},
 | 
			
		||||
			blocking: {type: Boolean},
 | 
			
		||||
		};
 | 
			
		||||
@@ -26,6 +27,7 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
		this.id = null;
 | 
			
		||||
		this.users = {};
 | 
			
		||||
		this.size = 0;
 | 
			
		||||
		this.server_follows_me = undefined;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load() {
 | 
			
		||||
@@ -61,8 +63,27 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async initial_load() {
 | 
			
		||||
		this.server_follows_me = undefined;
 | 
			
		||||
		let server_id = await tfrpc.rpc.getServerIdentity();
 | 
			
		||||
		let followed = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
			SELECT json_extract(content, '$.following') AS following
 | 
			
		||||
			FROM messages
 | 
			
		||||
			WHERE author = ? AND
 | 
			
		||||
			json_extract(content, '$.type') = 'contact' AND
 | 
			
		||||
			json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
 | 
			
		||||
		`,
 | 
			
		||||
			[server_id, this.whoami]
 | 
			
		||||
		);
 | 
			
		||||
		let is_followed = false;
 | 
			
		||||
		for (let row of followed) {
 | 
			
		||||
			is_followed = row.following != 0;
 | 
			
		||||
		}
 | 
			
		||||
		this.server_follows_me = is_followed;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	modify(change) {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		tfrpc.rpc
 | 
			
		||||
			.appendMessage(
 | 
			
		||||
				this.whoami,
 | 
			
		||||
@@ -74,10 +95,6 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
					change
 | 
			
		||||
				)
 | 
			
		||||
			)
 | 
			
		||||
			.then(function () {
 | 
			
		||||
				self._follow_whoami = undefined;
 | 
			
		||||
				self.load();
 | 
			
		||||
			})
 | 
			
		||||
			.catch(function (error) {
 | 
			
		||||
				alert(error?.message);
 | 
			
		||||
			});
 | 
			
		||||
@@ -139,8 +156,7 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let input = document.createElement('input');
 | 
			
		||||
		input.type = 'file';
 | 
			
		||||
		input.addEventListener('change', function (event) {
 | 
			
		||||
			input.parentNode.removeChild(input);
 | 
			
		||||
		input.onchange = function (event) {
 | 
			
		||||
			let file = event.target.files[0];
 | 
			
		||||
			file
 | 
			
		||||
				.arrayBuffer()
 | 
			
		||||
@@ -155,16 +171,31 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
				.catch(function (e) {
 | 
			
		||||
					alert(e.message);
 | 
			
		||||
				});
 | 
			
		||||
		});
 | 
			
		||||
		document.body.appendChild(input);
 | 
			
		||||
		};
 | 
			
		||||
		input.click();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	copy_id() {
 | 
			
		||||
		navigator.clipboard.writeText(this.id);
 | 
			
		||||
	async server_follow_me(follow) {
 | 
			
		||||
		try {
 | 
			
		||||
			await tfrpc.rpc.setServerFollowingMe(this.whoami, follow);
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			console.log(e);
 | 
			
		||||
		}
 | 
			
		||||
		try {
 | 
			
		||||
			await this.initial_load();
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			console.log(e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		if (
 | 
			
		||||
			this.id == this.whoami &&
 | 
			
		||||
			this.editing &&
 | 
			
		||||
			this.server_follows_me === undefined
 | 
			
		||||
		) {
 | 
			
		||||
			this.initial_load();
 | 
			
		||||
		}
 | 
			
		||||
		this.load();
 | 
			
		||||
		let self = this;
 | 
			
		||||
		let profile = this.users[this.id] || {};
 | 
			
		||||
@@ -181,24 +212,33 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
		let block;
 | 
			
		||||
		if (this.id === this.whoami) {
 | 
			
		||||
			if (this.editing) {
 | 
			
		||||
				edit = html`
 | 
			
		||||
					<button
 | 
			
		||||
						id="save_profile"
 | 
			
		||||
				let server_follow;
 | 
			
		||||
				if (this.server_follows_me === true) {
 | 
			
		||||
					server_follow = html`<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						@click=${this.save_edits}
 | 
			
		||||
						@click=${() => this.server_follow_me(false)}
 | 
			
		||||
					>
 | 
			
		||||
						Server, Stop Following Me
 | 
			
		||||
					</button>`;
 | 
			
		||||
				} else if (this.server_follows_me === false) {
 | 
			
		||||
					server_follow = html`<button
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						@click=${() => this.server_follow_me(true)}
 | 
			
		||||
					>
 | 
			
		||||
						Server, Follow Me
 | 
			
		||||
					</button>`;
 | 
			
		||||
				}
 | 
			
		||||
				edit = html`
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.save_edits}>
 | 
			
		||||
						Save Profile
 | 
			
		||||
					</button>
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.discard_edits}>
 | 
			
		||||
						Discard
 | 
			
		||||
					</button>
 | 
			
		||||
					${server_follow}
 | 
			
		||||
				`;
 | 
			
		||||
			} else {
 | 
			
		||||
				edit = html`<button
 | 
			
		||||
					id="edit_profile"
 | 
			
		||||
					class="w3-button w3-theme-d1"
 | 
			
		||||
					@click=${this.edit}
 | 
			
		||||
				>
 | 
			
		||||
				edit = html`<button class="w3-button w3-theme-d1" @click=${this.edit}>
 | 
			
		||||
					Edit Profile
 | 
			
		||||
				</button>`;
 | 
			
		||||
			}
 | 
			
		||||
@@ -224,18 +264,20 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
		let edit_profile = this.editing
 | 
			
		||||
			? html`
 | 
			
		||||
			<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
 | 
			
		||||
				<div>
 | 
			
		||||
					<label for="name">Name:</label>
 | 
			
		||||
					<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))} placeholder="Choose a name"></input>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div><label for="description">Description:</label></div>
 | 
			
		||||
				<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))} placeholder="Tell people a little bit about yourself here, if you like.">${this.editing.description}</textarea>
 | 
			
		||||
				<div>
 | 
			
		||||
					<label for="public_web_hosting">Public Web Hosting:</label>
 | 
			
		||||
					<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div>
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
 | 
			
		||||
				<div class="w3-container">
 | 
			
		||||
					<div>
 | 
			
		||||
						<label for="name">Name:</label>
 | 
			
		||||
						<input class="w3-input w3-theme-d1" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div><label for="description">Description:</label></div>
 | 
			
		||||
					<textarea class="w3-input w3-theme-d1" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
 | 
			
		||||
					<div>
 | 
			
		||||
						<label for="public_web_hosting">Public Web Hosting:</label>
 | 
			
		||||
						<input class="w3-check w3-theme-d1" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${(event) => (self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked}))}></input>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div>
 | 
			
		||||
						<button class="w3-button w3-theme-d1" @click=${this.attach_image}>Attach Image</button>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>`
 | 
			
		||||
			: null;
 | 
			
		||||
@@ -243,43 +285,26 @@ class TfProfileElement extends LitElement {
 | 
			
		||||
			typeof profile.image == 'string' ? profile.image : profile.image?.link;
 | 
			
		||||
		image = this.editing?.image ?? image;
 | 
			
		||||
		let description = this.editing?.description ?? profile.description;
 | 
			
		||||
		return html`<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box">
 | 
			
		||||
			<header class="w3-container">
 | 
			
		||||
				<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})</p>
 | 
			
		||||
			</header>
 | 
			
		||||
			<div class="w3-container">
 | 
			
		||||
				<div class="w3-margin-bottom" style="display: flex; flex-direction: row">
 | 
			
		||||
					<input type="text" class="w3-input w3-border w3-theme-d1" style="display: flex 1 1" readonly value=${this.id}></input>
 | 
			
		||||
					<button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div style="display: flex; flex-direction: row; gap: 1em">
 | 
			
		||||
					${edit_profile}
 | 
			
		||||
					<div style="flex: 1 0 50%">
 | 
			
		||||
						${
 | 
			
		||||
							image
 | 
			
		||||
								? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>`
 | 
			
		||||
								: html`<div>
 | 
			
		||||
										<div class="w3-jumbo">😎</div>
 | 
			
		||||
										<div><i>Profile image not set.</i></div>
 | 
			
		||||
									</div>`
 | 
			
		||||
						}
 | 
			
		||||
						<div>${unsafeHTML(tfutils.markdown(description))}</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div>
 | 
			
		||||
					Following ${profile.following} identities.
 | 
			
		||||
					Followed by ${profile.followed} identities.
 | 
			
		||||
					Blocking ${profile.blocking} identities.
 | 
			
		||||
					Blocked by ${profile.blocked} identities.
 | 
			
		||||
		return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
 | 
			
		||||
			<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
 | 
			
		||||
			<div style="display: flex; flex-direction: row; gap: 1em">
 | 
			
		||||
				${edit_profile}
 | 
			
		||||
				<div style="flex: 1 0 50%">
 | 
			
		||||
					<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
 | 
			
		||||
					<div>${unsafeHTML(tfutils.markdown(description))}</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<footer class="w3-container">
 | 
			
		||||
				<p>
 | 
			
		||||
					${edit}
 | 
			
		||||
					${follow}
 | 
			
		||||
					${block}
 | 
			
		||||
				</p>
 | 
			
		||||
			</footer>
 | 
			
		||||
			<div>
 | 
			
		||||
				Following ${profile.following} identities.
 | 
			
		||||
				Followed by ${profile.followed} identities.
 | 
			
		||||
				Blocking ${profile.blocking} identities.
 | 
			
		||||
				Blocked by ${profile.blocked} identities.
 | 
			
		||||
			</div>
 | 
			
		||||
			<div>
 | 
			
		||||
				${edit}
 | 
			
		||||
				${follow}
 | 
			
		||||
				${block}
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,13 +26,9 @@ class TfReactionsModalElement extends LitElement {
 | 
			
		||||
		return this.votes?.length
 | 
			
		||||
			? html` <div
 | 
			
		||||
					class="w3-modal w3-animate-opacity"
 | 
			
		||||
					style="display: block; box-sizing: border-box; z-index: 10"
 | 
			
		||||
					@click=${this.clear}
 | 
			
		||||
					style="display: block; box-sizing: border-box"
 | 
			
		||||
				>
 | 
			
		||||
					<div
 | 
			
		||||
						class="w3-modal-content w3-card-4 w3-theme-d1"
 | 
			
		||||
						onclick="event.stopPropagation()"
 | 
			
		||||
					>
 | 
			
		||||
					<div class="w3-modal-content w3-card-4 w3-theme-d1">
 | 
			
		||||
						<div class="w3-container w3-padding">
 | 
			
		||||
							<header class="w3-container">
 | 
			
		||||
								<h2>Reactions</h2>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import {css, unsafeCSS} from './lit-all.min.js';
 | 
			
		||||
import {css} from './lit-all.min.js';
 | 
			
		||||
 | 
			
		||||
const tf = css`
 | 
			
		||||
	img {
 | 
			
		||||
@@ -48,7 +48,7 @@ const tf = css`
 | 
			
		||||
 | 
			
		||||
// prettier-ignore
 | 
			
		||||
const w3 = css`
 | 
			
		||||
/* W3.CSS 5.01 March 14 2025 by Jan Egil and Borge Refsnes */
 | 
			
		||||
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
 | 
			
		||||
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
 | 
			
		||||
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
 | 
			
		||||
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
 | 
			
		||||
@@ -88,7 +88,7 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
 | 
			
		||||
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
 | 
			
		||||
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
 | 
			
		||||
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}   
 | 
			
		||||
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
 | 
			
		||||
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
 | 
			
		||||
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
 | 
			
		||||
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
 | 
			
		||||
@@ -136,7 +136,7 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
 | 
			
		||||
@media (max-width:1205px){.w3-auto{max-width:95%}}
 | 
			
		||||
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
 | 
			
		||||
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}	
 | 
			
		||||
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
 | 
			
		||||
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
 | 
			
		||||
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
 | 
			
		||||
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
 | 
			
		||||
@@ -158,10 +158,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
 | 
			
		||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
 | 
			
		||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
 | 
			
		||||
 | 
			
		||||
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
 | 
			
		||||
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
 | 
			
		||||
 | 
			
		||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
 | 
			
		||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
 | 
			
		||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
 | 
			
		||||
@@ -203,9 +199,9 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
 | 
			
		||||
.w3-hover-none:hover{box-shadow:none!important}
 | 
			
		||||
/* Colors */
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover,.w3-warning{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
 | 
			
		||||
.w3-blue,.w3-hover-blue:hover,.w3-info,.w3-primary{color:#fff!important;background-color:#2196F3!important}
 | 
			
		||||
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
 | 
			
		||||
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
 | 
			
		||||
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
 | 
			
		||||
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
 | 
			
		||||
@@ -220,24 +216,15 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
 | 
			
		||||
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
 | 
			
		||||
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
 | 
			
		||||
.w3-red,.w3-hover-red:hover,.w3-danger{color:#fff!important;background-color:#f44336!important}
 | 
			
		||||
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
 | 
			
		||||
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
 | 
			
		||||
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
 | 
			
		||||
.w3-yellow,.w3-hover-yellow:hover,.w3-note{color:#000!important;background-color:#ffeb3b!important}
 | 
			
		||||
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
 | 
			
		||||
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
 | 
			
		||||
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
 | 
			
		||||
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover,.w3-secondary{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
 | 
			
		||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
 | 
			
		||||
 | 
			
		||||
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
 | 
			
		||||
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
 | 
			
		||||
.w3-emerald,.w3-hover-emerald:hover,.w3-success{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
 | 
			
		||||
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
 | 
			
		||||
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
 | 
			
		||||
 | 
			
		||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
 | 
			
		||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
 | 
			
		||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
 | 
			
		||||
@@ -298,165 +285,30 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
function rgb_to_hsl(r, g, b) {
 | 
			
		||||
	let min,
 | 
			
		||||
		max,
 | 
			
		||||
		i,
 | 
			
		||||
		l,
 | 
			
		||||
		s,
 | 
			
		||||
		maxcolor,
 | 
			
		||||
		h,
 | 
			
		||||
		rgb = [];
 | 
			
		||||
	rgb[0] = r / 255;
 | 
			
		||||
	rgb[1] = g / 255;
 | 
			
		||||
	rgb[2] = b / 255;
 | 
			
		||||
	min = rgb[0];
 | 
			
		||||
	max = rgb[0];
 | 
			
		||||
	maxcolor = 0;
 | 
			
		||||
	for (i = 0; i < rgb.length - 1; i++) {
 | 
			
		||||
		if (rgb[i + 1] <= min) {
 | 
			
		||||
			min = rgb[i + 1];
 | 
			
		||||
		}
 | 
			
		||||
		if (rgb[i + 1] >= max) {
 | 
			
		||||
			max = rgb[i + 1];
 | 
			
		||||
			maxcolor = i + 1;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if (maxcolor == 0) {
 | 
			
		||||
		h = (rgb[1] - rgb[2]) / (max - min);
 | 
			
		||||
	}
 | 
			
		||||
	if (maxcolor == 1) {
 | 
			
		||||
		h = 2 + (rgb[2] - rgb[0]) / (max - min);
 | 
			
		||||
	}
 | 
			
		||||
	if (maxcolor == 2) {
 | 
			
		||||
		h = 4 + (rgb[0] - rgb[1]) / (max - min);
 | 
			
		||||
	}
 | 
			
		||||
	if (isNaN(h)) {
 | 
			
		||||
		h = 0;
 | 
			
		||||
	}
 | 
			
		||||
	h = h * 60;
 | 
			
		||||
	if (h < 0) {
 | 
			
		||||
		h = h + 360;
 | 
			
		||||
	}
 | 
			
		||||
	l = (min + max) / 2;
 | 
			
		||||
	if (min == max) {
 | 
			
		||||
		s = 0;
 | 
			
		||||
	} else {
 | 
			
		||||
		if (l < 0.5) {
 | 
			
		||||
			s = (max - min) / (max + min);
 | 
			
		||||
		} else {
 | 
			
		||||
			s = (max - min) / (2 - max - min);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	s = s;
 | 
			
		||||
	return [h, s, l];
 | 
			
		||||
}
 | 
			
		||||
// prettier-ignore
 | 
			
		||||
const w3_2016_riverside = css`
 | 
			
		||||
.w3-theme-l5 {color:#000 !important; background-color:#f4f6f9 !important}
 | 
			
		||||
.w3-theme-l4 {color:#000 !important; background-color:#d9e1ec !important}
 | 
			
		||||
.w3-theme-l3 {color:#000 !important; background-color:#b4c3d8 !important}
 | 
			
		||||
.w3-theme-l2 {color:#fff !important; background-color:#8ea6c5 !important}
 | 
			
		||||
.w3-theme-l1 {color:#fff !important; background-color:#6888b1 !important}
 | 
			
		||||
.w3-theme-d1 {color:#fff !important; background-color:#456185 !important}
 | 
			
		||||
.w3-theme-d2 {color:#fff !important; background-color:#3d5676 !important}
 | 
			
		||||
.w3-theme-d3 {color:#fff !important; background-color:#354b68 !important}
 | 
			
		||||
.w3-theme-d4 {color:#fff !important; background-color:#2e4059 !important}
 | 
			
		||||
.w3-theme-d5 {color:#fff !important; background-color:#26364a !important}
 | 
			
		||||
 | 
			
		||||
function hex_to_rgb(hex) {
 | 
			
		||||
	if (hex.charAt(0) == '#') {
 | 
			
		||||
		hex = hex.substring(1);
 | 
			
		||||
	}
 | 
			
		||||
	return [
 | 
			
		||||
		parseInt(hex.substring(0, 2), 16),
 | 
			
		||||
		parseInt(hex.substring(2, 4), 16),
 | 
			
		||||
		parseInt(hex.substring(4, 6), 16),
 | 
			
		||||
	];
 | 
			
		||||
}
 | 
			
		||||
.w3-theme-light {color:#000 !important; background-color:#f4f6f9 !important}
 | 
			
		||||
.w3-theme-dark {color:#fff !important; background-color:#26364a !important}
 | 
			
		||||
.w3-theme-action {color:#fff !important; background-color:#26364a !important}
 | 
			
		||||
 | 
			
		||||
function hsl_to_rgb(hue, sat, light) {
 | 
			
		||||
	let t2;
 | 
			
		||||
	hue /= 60;
 | 
			
		||||
	if (light <= 0.5) {
 | 
			
		||||
		t2 = light * (sat + 1);
 | 
			
		||||
	} else {
 | 
			
		||||
		t2 = light + sat - light * sat;
 | 
			
		||||
	}
 | 
			
		||||
	let t1 = light * 2 - t2;
 | 
			
		||||
	return [
 | 
			
		||||
		hue_to_rgb(t1, t2, hue + 2) * 255,
 | 
			
		||||
		hue_to_rgb(t1, t2, hue) * 255,
 | 
			
		||||
		hue_to_rgb(t1, t2, hue - 2) * 255,
 | 
			
		||||
	];
 | 
			
		||||
}
 | 
			
		||||
function hue_to_rgb(t1, t2, hue) {
 | 
			
		||||
	if (hue < 0) {
 | 
			
		||||
		hue += 6;
 | 
			
		||||
	}
 | 
			
		||||
	if (hue >= 6) {
 | 
			
		||||
		hue -= 6;
 | 
			
		||||
	}
 | 
			
		||||
	if (hue < 1) {
 | 
			
		||||
		return (t2 - t1) * hue + t1;
 | 
			
		||||
	} else if (hue < 3) {
 | 
			
		||||
		return t2;
 | 
			
		||||
	} else if (hue < 4) {
 | 
			
		||||
		return (t2 - t1) * (4 - hue) + t1;
 | 
			
		||||
	} else {
 | 
			
		||||
		return t1;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
.w3-theme {color:#fff !important; background-color:#4c6a92 !important}
 | 
			
		||||
.w3-text-theme {color:#4c6a92 !important}
 | 
			
		||||
.w3-border-theme {border-color:#4c6a92 !important}
 | 
			
		||||
 | 
			
		||||
function rgb_to_hex(rgb) {
 | 
			
		||||
	const hex_pair = (x) => Math.floor(x).toString(16).padStart(2, '0');
 | 
			
		||||
	return `#${hex_pair(rgb[0])}${hex_pair(rgb[1])}${hex_pair(rgb[2])}`;
 | 
			
		||||
}
 | 
			
		||||
.w3-hover-theme:hover {color:#fff !important; background-color:#4c6a92 !important}
 | 
			
		||||
.w3-hover-text-theme:hover {color:#4c6a92 !important}
 | 
			
		||||
.w3-hover-border-theme:hover {border-color:#4c6a92 !important}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
function is_dark(hex, value) {
 | 
			
		||||
	let [r, g, b] = hex_to_rgb(hex);
 | 
			
		||||
	return (r * 299 + g * 587 + b * 114) / 1000 < value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generated() {
 | 
			
		||||
	let now = new Date();
 | 
			
		||||
	let k_color = rgb_to_hex([
 | 
			
		||||
		(now.getDay() * 128) / 6,
 | 
			
		||||
		(now.getHours() * 128) / 23,
 | 
			
		||||
		(now.getSeconds() * 128) / 59,
 | 
			
		||||
	]);
 | 
			
		||||
	//let k_color = '#034f84';
 | 
			
		||||
	//let k_color = rgb_to_hex([Math.random() * 256, Math.random() * 256, Math.random() * 256]);
 | 
			
		||||
	let [r, g, b] = hex_to_rgb(k_color);
 | 
			
		||||
	let [h, s, l] = rgb_to_hsl(r, g, b);
 | 
			
		||||
 | 
			
		||||
	let theme1 = {
 | 
			
		||||
		l5: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 4.7)),
 | 
			
		||||
		l4: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 4)),
 | 
			
		||||
		l3: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 3)),
 | 
			
		||||
		l2: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 2)),
 | 
			
		||||
		l1: rgb_to_hex(hsl_to_rgb(h, s, l + ((1.0 - l) / 5) * 1)),
 | 
			
		||||
		d0: rgb_to_hex(hsl_to_rgb(h, s, l)),
 | 
			
		||||
		d1: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 0.5)),
 | 
			
		||||
		d2: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 1)),
 | 
			
		||||
		d3: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 1.5)),
 | 
			
		||||
		d4: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 2)),
 | 
			
		||||
		d5: rgb_to_hex(hsl_to_rgb(h, s, l - (l / 5) * 2.5)),
 | 
			
		||||
	};
 | 
			
		||||
	for (let [k, v] of Object.entries(theme1)) {
 | 
			
		||||
		theme1['t' + k] = is_dark(v, 165) ? '#fff' : '#000';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let result = `
 | 
			
		||||
		.w3-theme-l5 {color: ${theme1.tl5} !important; background-color: ${theme1.l5} !important}
 | 
			
		||||
		.w3-theme-l4 {color: ${theme1.tl4} !important; background-color: ${theme1.l4} !important}
 | 
			
		||||
		.w3-theme-l3 {color: ${theme1.tl3} !important; background-color: ${theme1.l3} !important}
 | 
			
		||||
		.w3-theme-l2 {color: ${theme1.tl2} !important; background-color: ${theme1.l2} !important}
 | 
			
		||||
		.w3-theme-l1 {color: ${theme1.tl1} !important; background-color: ${theme1.l1} !important}
 | 
			
		||||
		.w3-theme-d1 {color: ${theme1.td1} !important; background-color: ${theme1.d1} !important}
 | 
			
		||||
		.w3-theme-d2 {color: ${theme1.td2} !important; background-color: ${theme1.d2} !important}
 | 
			
		||||
		.w3-theme-d3 {color: ${theme1.td3} !important; background-color: ${theme1.d3} !important}
 | 
			
		||||
		.w3-theme-d4 {color: ${theme1.td4} !important; background-color: ${theme1.d4} !important}
 | 
			
		||||
		.w3-theme-d5 {color: ${theme1.td5} !important; background-color: ${theme1.d5} !important}
 | 
			
		||||
		.w3-theme-light {color: ${theme1.tl5} !important; background-color: ${theme1.l5} !important}
 | 
			
		||||
		.w3-theme-dark {color: ${theme1.td5} !important; background-color: ${theme1.d5} !important}
 | 
			
		||||
		.w3-theme-action {color: ${theme1.td5} !important; background-color: ${theme1.d5} !important}
 | 
			
		||||
		.w3-theme {color: ${theme1.td0} !important; background-color: ${theme1.d0} !important}
 | 
			
		||||
		.w3-text-theme {color: ${theme1.d0} !important}
 | 
			
		||||
		.w3-border-theme {border-color: ${theme1.d0} !important}
 | 
			
		||||
		.w3-hover-theme:hover {color: ${theme1.td0} !important; background-color: ${theme1.d0} !important}
 | 
			
		||||
		.w3-hover-text-theme:hover {color: ${theme1.d0} !important}
 | 
			
		||||
		.w3-hover-border-theme:hover {border-color: ${theme1.d0} !important}
 | 
			
		||||
	`;
 | 
			
		||||
	return unsafeCSS(result);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export let styles = [tf, w3, generated()];
 | 
			
		||||
export let styles = [tf, w3, w3_2016_riverside];
 | 
			
		||||
 
 | 
			
		||||
@@ -7,46 +7,28 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
		return {
 | 
			
		||||
			broadcasts: {type: Array},
 | 
			
		||||
			identities: {type: Array},
 | 
			
		||||
			my_identities: {type: Array},
 | 
			
		||||
			connections: {type: Array},
 | 
			
		||||
			stored_connections: {type: Array},
 | 
			
		||||
			users: {type: Object},
 | 
			
		||||
			server_identity: {type: String},
 | 
			
		||||
			connect_attempt: {type: Object},
 | 
			
		||||
			connect_message: {type: String},
 | 
			
		||||
			connect_success: {type: Boolean},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static styles = styles;
 | 
			
		||||
 | 
			
		||||
	static k_broadcast_emojis = {
 | 
			
		||||
		discovery: '🏓',
 | 
			
		||||
		room: '🚪',
 | 
			
		||||
		peer_exchange: '🕸',
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	constructor() {
 | 
			
		||||
		super();
 | 
			
		||||
		let self = this;
 | 
			
		||||
		this.broadcasts = [];
 | 
			
		||||
		this.identities = [];
 | 
			
		||||
		this.my_identities = [];
 | 
			
		||||
		this.connections = [];
 | 
			
		||||
		this.stored_connections = [];
 | 
			
		||||
		this.users = {};
 | 
			
		||||
		tfrpc.rpc.getIdentities().then(function (identities) {
 | 
			
		||||
			self.my_identities = identities || [];
 | 
			
		||||
		});
 | 
			
		||||
		tfrpc.rpc.getAllIdentities().then(function (identities) {
 | 
			
		||||
			self.identities = identities || [];
 | 
			
		||||
		});
 | 
			
		||||
		tfrpc.rpc.getStoredConnections().then(function (connections) {
 | 
			
		||||
			self.stored_connections = connections || [];
 | 
			
		||||
		});
 | 
			
		||||
		tfrpc.rpc.getServerIdentity().then(function (identity) {
 | 
			
		||||
			self.server_identity = identity;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_connection_summary(connection) {
 | 
			
		||||
@@ -91,53 +73,19 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_message(connection) {
 | 
			
		||||
		return html`<div
 | 
			
		||||
			?hidden=${this.connect_message === undefined ||
 | 
			
		||||
			this.connect_attempt != connection}
 | 
			
		||||
			style="cursor: pointer"
 | 
			
		||||
			class=${'w3-panel ' + (this.connect_success ? 'w3-green' : 'w3-red')}
 | 
			
		||||
			@click=${() => (this.connect_attempt = undefined)}
 | 
			
		||||
		>
 | 
			
		||||
			<p>${this.connect_message}</p>
 | 
			
		||||
		</div>`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_progress(name, value, max) {
 | 
			
		||||
		if (max && value != max) {
 | 
			
		||||
			return html`
 | 
			
		||||
				<div class="w3-theme-d1 w3-small">
 | 
			
		||||
					<div
 | 
			
		||||
						class="w3-container w3-theme-l1"
 | 
			
		||||
						style="width: ${Math.floor(
 | 
			
		||||
							(100.0 * value) / max
 | 
			
		||||
						)}%; text-wrap: nowrap"
 | 
			
		||||
					>
 | 
			
		||||
						${name} ${value} / ${max} (${Math.round((100.0 * value) / max)}%)
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_broadcast(connection) {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		return html`
 | 
			
		||||
			<li>
 | 
			
		||||
				<div class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap">
 | 
			
		||||
					<button
 | 
			
		||||
						class="w3-bar-item w3-button w3-theme-d1"
 | 
			
		||||
						@click=${() => self.connect(connection)}
 | 
			
		||||
					>
 | 
			
		||||
						Connect
 | 
			
		||||
					</button>
 | 
			
		||||
					<div class="w3-bar-item">
 | 
			
		||||
						${TfTabConnectionsElement.k_broadcast_emojis[connection.origin]}
 | 
			
		||||
						<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
 | 
			
		||||
						${this.render_connection_summary(connection)}
 | 
			
		||||
					</div>
 | 
			
		||||
			<li class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap">
 | 
			
		||||
				<button
 | 
			
		||||
					class="w3-bar-item w3-button w3-theme-d1"
 | 
			
		||||
					@click=${() => tfrpc.rpc.connect(connection)}
 | 
			
		||||
				>
 | 
			
		||||
					Connect
 | 
			
		||||
				</button>
 | 
			
		||||
				<div class="w3-bar-item">
 | 
			
		||||
					<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
 | 
			
		||||
					${this.render_connection_summary(connection)}
 | 
			
		||||
				</div>
 | 
			
		||||
				${this.render_message(connection)}
 | 
			
		||||
			</li>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
@@ -148,200 +96,92 @@ class TfTabConnectionsElement extends LitElement {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_connection(connection) {
 | 
			
		||||
		let requests = Object.values(
 | 
			
		||||
			connection.requests.reduce(function (accumulator, value) {
 | 
			
		||||
				let key = `${value.name}:${Math.sign(value.request_number)}`;
 | 
			
		||||
				if (!accumulator[key]) {
 | 
			
		||||
					accumulator[key] = Object.assign({count: 0}, value);
 | 
			
		||||
				}
 | 
			
		||||
				accumulator[key].count++;
 | 
			
		||||
				return accumulator;
 | 
			
		||||
			}, {})
 | 
			
		||||
		);
 | 
			
		||||
		return html`
 | 
			
		||||
			${connection.connected
 | 
			
		||||
				? html`
 | 
			
		||||
						<button
 | 
			
		||||
							class="w3-button w3-theme-d1"
 | 
			
		||||
							@click=${() => tfrpc.rpc.closeConnection(connection.id)}
 | 
			
		||||
						>
 | 
			
		||||
							Close
 | 
			
		||||
						</button>
 | 
			
		||||
					`
 | 
			
		||||
				: undefined}
 | 
			
		||||
			${connection.flags.one_shot ? '🔃' : undefined}
 | 
			
		||||
			<button
 | 
			
		||||
				class="w3-button w3-theme-d1"
 | 
			
		||||
				@click=${() => tfrpc.rpc.closeConnection(connection.id)}
 | 
			
		||||
			>
 | 
			
		||||
				Close
 | 
			
		||||
			</button>
 | 
			
		||||
			<tf-user id=${connection.id} .users=${this.users}></tf-user>
 | 
			
		||||
			${this.render_progress(
 | 
			
		||||
				'recv',
 | 
			
		||||
				connection.progress.in.total - connection.progress.in.current,
 | 
			
		||||
				connection.progress.in.total
 | 
			
		||||
			)}
 | 
			
		||||
			${this.render_progress(
 | 
			
		||||
				'send',
 | 
			
		||||
				connection.progress.out.total - connection.progress.out.current,
 | 
			
		||||
				connection.progress.out.total
 | 
			
		||||
			)}
 | 
			
		||||
			${connection.tunnel !== undefined
 | 
			
		||||
				? '🚇'
 | 
			
		||||
				: html`(${connection.host}:${connection.port})`}
 | 
			
		||||
			<div>
 | 
			
		||||
				${requests.map(
 | 
			
		||||
					(x) => html`
 | 
			
		||||
						<span
 | 
			
		||||
							class=${'w3-tag w3-small ' +
 | 
			
		||||
							(x.active ? 'w3-theme-l3' : 'w3-theme-d3')}
 | 
			
		||||
							>${x.request_number > 0 ? '🟩' : '🟥'} ${x.name}
 | 
			
		||||
							<span
 | 
			
		||||
								class="w3-badge w3-white"
 | 
			
		||||
								style=${x.count > 1 ? undefined : 'display: none'}
 | 
			
		||||
								>${x.count}</span
 | 
			
		||||
							></span
 | 
			
		||||
						>
 | 
			
		||||
					`
 | 
			
		||||
				)}
 | 
			
		||||
			</div>
 | 
			
		||||
			<div>${connection.requests.map(x => html`
 | 
			
		||||
				<span class="w3-tag w3-small">${x.request_number > 0 ? '🟩' : '🟥'} ${x.name}</span>
 | 
			
		||||
			`)}</div>
 | 
			
		||||
			<ul>
 | 
			
		||||
				${this.connections
 | 
			
		||||
					.filter((x) => x.tunnel === this.connections.indexOf(connection))
 | 
			
		||||
					.map((x) => html`<li>${this.render_connection(x)}</li>`)}
 | 
			
		||||
				${this.render_room_peers(connection.id)}
 | 
			
		||||
			</ul>
 | 
			
		||||
			<div ?hidden=${!connection.destroy_reason} class="w3-panel w3-red">
 | 
			
		||||
				<p>${connection.destroy_reason}</p>
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	connect(address) {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		self.connect_attempt = address;
 | 
			
		||||
		self.connect_message = undefined;
 | 
			
		||||
		self.connect_success = false;
 | 
			
		||||
		tfrpc.rpc
 | 
			
		||||
			.connect(address)
 | 
			
		||||
			.then(function () {
 | 
			
		||||
				if (self.connect_attempt == address) {
 | 
			
		||||
					self.connect_message = 'Connected.';
 | 
			
		||||
					self.connect_success = true;
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			.catch(function (error) {
 | 
			
		||||
				if (self.connect_attempt == address) {
 | 
			
		||||
					self.connect_message = 'Error: ' + error;
 | 
			
		||||
					self.connect_success = false;
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	toggle_accordian(id) {
 | 
			
		||||
		let element = this.renderRoot.getElementById(id);
 | 
			
		||||
		element.classList.toggle('w3-hide');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	valid_connections() {
 | 
			
		||||
		return this.connections.filter((x) => x.tunnel === undefined);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	valid_broadcasts() {
 | 
			
		||||
		return this.broadcasts
 | 
			
		||||
			.filter((x) => x.address)
 | 
			
		||||
			.filter((x) => this.connections.map((c) => c.id).indexOf(x.pubkey) == -1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		return html`
 | 
			
		||||
			<div class="w3-container" style="box-sizing: border-box">
 | 
			
		||||
				<h2>New Connection</h2>
 | 
			
		||||
				<textarea class="w3-input w3-theme-d1" id="code"></textarea>
 | 
			
		||||
				${this.render_message(this.renderRoot.getElementById('code')?.value)}
 | 
			
		||||
				<button
 | 
			
		||||
					class="w3-button w3-theme-d1"
 | 
			
		||||
					@click=${() =>
 | 
			
		||||
						self.connect(self.renderRoot.getElementById('code')?.value)}
 | 
			
		||||
						tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
 | 
			
		||||
				>
 | 
			
		||||
					Connect
 | 
			
		||||
				</button>
 | 
			
		||||
				<h2
 | 
			
		||||
					class="w3-button w3-block w3-theme-d1"
 | 
			
		||||
					@click=${() => self.toggle_accordian('connections')}
 | 
			
		||||
				>
 | 
			
		||||
					Connections (${this.valid_connections().length})
 | 
			
		||||
				</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border" id="connections">
 | 
			
		||||
					${this.valid_connections().map(
 | 
			
		||||
						(x) => html` <li class="w3-bar">${this.render_connection(x)}</li> `
 | 
			
		||||
					)}
 | 
			
		||||
				<h2>Broadcasts</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border">
 | 
			
		||||
					${this.broadcasts
 | 
			
		||||
						.filter((x) => x.address)
 | 
			
		||||
						.map((x) => self.render_broadcast(x))}
 | 
			
		||||
				</ul>
 | 
			
		||||
				<h2
 | 
			
		||||
					class="w3-button w3-block w3-theme-d1"
 | 
			
		||||
					@click=${() => self.toggle_accordian('broadcasts')}
 | 
			
		||||
				>
 | 
			
		||||
					Broadcasts (${this.valid_broadcasts().length})
 | 
			
		||||
				</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border w3-hide" id="broadcasts">
 | 
			
		||||
					${this.valid_broadcasts().map((x) => self.render_broadcast(x))}
 | 
			
		||||
				<h2>Connections</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border">
 | 
			
		||||
					${this.connections
 | 
			
		||||
						.filter((x) => x.tunnel === undefined)
 | 
			
		||||
						.map(
 | 
			
		||||
							(x) => html`
 | 
			
		||||
								<li class="w3-bar">${this.render_connection(x)}</li>
 | 
			
		||||
							`
 | 
			
		||||
						)}
 | 
			
		||||
				</ul>
 | 
			
		||||
				<h2
 | 
			
		||||
					class="w3-button w3-block w3-theme-d1"
 | 
			
		||||
					@click=${() => self.toggle_accordian('stored_connections')}
 | 
			
		||||
				>
 | 
			
		||||
					Stored Connections (${this.stored_connections.length})
 | 
			
		||||
				</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border w3-hide" id="stored_connections">
 | 
			
		||||
				<h2>Stored Connections</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border">
 | 
			
		||||
					${this.stored_connections.map(
 | 
			
		||||
						(x) => html`
 | 
			
		||||
							<li>
 | 
			
		||||
								<div class="w3-bar">
 | 
			
		||||
									<button
 | 
			
		||||
										class="w3-bar-item w3-button w3-theme-d1"
 | 
			
		||||
										@click=${() => self.forget_stored_connection(x)}
 | 
			
		||||
									>
 | 
			
		||||
										Forget
 | 
			
		||||
									</button>
 | 
			
		||||
									<button
 | 
			
		||||
										class="w3-bar-item w3-button w3-theme-d1"
 | 
			
		||||
										@click=${() => this.connect(x)}
 | 
			
		||||
									>
 | 
			
		||||
										Connect
 | 
			
		||||
									</button>
 | 
			
		||||
									<div class="w3-bar-item">
 | 
			
		||||
										<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
 | 
			
		||||
										<div><small>${x.address}:${x.port}</small></div>
 | 
			
		||||
									</div>
 | 
			
		||||
							<li class="w3-bar">
 | 
			
		||||
								<button
 | 
			
		||||
									class="w3-bar-item w3-button w3-theme-d1"
 | 
			
		||||
									@click=${() => self.forget_stored_connection(x)}
 | 
			
		||||
								>
 | 
			
		||||
									Forget
 | 
			
		||||
								</button>
 | 
			
		||||
								<button
 | 
			
		||||
									class="w3-bar-item w3-button w3-theme-d1"
 | 
			
		||||
									@click=${() => tfrpc.rpc.connect(x)}
 | 
			
		||||
								>
 | 
			
		||||
									Connect
 | 
			
		||||
								</button>
 | 
			
		||||
								<div class="w3-bar-item">
 | 
			
		||||
									<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
 | 
			
		||||
									<div><small>${x.address}:${x.port}</small></div>
 | 
			
		||||
								</div>
 | 
			
		||||
								${this.render_message(x)}
 | 
			
		||||
							</li>
 | 
			
		||||
						`
 | 
			
		||||
					)}
 | 
			
		||||
				</ul>
 | 
			
		||||
				<h2
 | 
			
		||||
					class="w3-button w3-block w3-theme-d1"
 | 
			
		||||
					@click=${() => self.toggle_accordian('local_accounts')}
 | 
			
		||||
				>
 | 
			
		||||
					Local Accounts (${this.identities.length})
 | 
			
		||||
				</h2>
 | 
			
		||||
				<div class="w3-container w3-hide" id="local_accounts">
 | 
			
		||||
				<h2>Local Accounts</h2>
 | 
			
		||||
				<ul class="w3-ul w3-border">
 | 
			
		||||
					${this.identities.map(
 | 
			
		||||
						(x) =>
 | 
			
		||||
							html`<div
 | 
			
		||||
								class="w3-tag w3-round w3-theme-l3"
 | 
			
		||||
								style="padding: 4px; margin: 2px; max-width: 100%; text-wrap: nowrap; overflow: hidden"
 | 
			
		||||
							>
 | 
			
		||||
								${x == this.server_identity
 | 
			
		||||
									? html`<div class="w3-tag w3-medium w3-round w3-theme-l1">
 | 
			
		||||
											🖥 local server
 | 
			
		||||
										</div>`
 | 
			
		||||
									: undefined}
 | 
			
		||||
								${this.my_identities.indexOf(x) != -1
 | 
			
		||||
									? html`<div class="w3-tag w3-medium w3-round w3-theme-d1">
 | 
			
		||||
											😎 you
 | 
			
		||||
										</div>`
 | 
			
		||||
									: undefined}
 | 
			
		||||
							html`<li class="w3-bar">
 | 
			
		||||
								<tf-user id=${x} .users=${this.users}></tf-user>
 | 
			
		||||
							</div>`
 | 
			
		||||
							</li>`
 | 
			
		||||
					)}
 | 
			
		||||
				</div>
 | 
			
		||||
				</ul>
 | 
			
		||||
			</div>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										78
									
								
								apps/ssb/tf-tab-mentions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								apps/ssb/tf-tab-mentions.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
import {LitElement, html, unsafeHTML} from './lit-all.min.js';
 | 
			
		||||
import * as tfrpc from '/static/tfrpc.js';
 | 
			
		||||
import {styles} from './tf-styles.js';
 | 
			
		||||
 | 
			
		||||
class TfTabMentionsElement extends LitElement {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
		return {
 | 
			
		||||
			whoami: {type: String},
 | 
			
		||||
			users: {type: Object},
 | 
			
		||||
			following: {type: Array},
 | 
			
		||||
			expanded: {type: Object},
 | 
			
		||||
			messages: {type: Array},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static styles = styles;
 | 
			
		||||
 | 
			
		||||
	constructor() {
 | 
			
		||||
		super();
 | 
			
		||||
		let self = this;
 | 
			
		||||
		this.whoami = null;
 | 
			
		||||
		this.users = {};
 | 
			
		||||
		this.following = [];
 | 
			
		||||
		this.expanded = {};
 | 
			
		||||
		this.messages = [];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load() {
 | 
			
		||||
		console.log('Loading...', this.whoami);
 | 
			
		||||
		let results = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
				SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
				FROM messages_fts(?)
 | 
			
		||||
				JOIN messages ON messages.rowid = messages_fts.rowid
 | 
			
		||||
				JOIN json_each(?) AS following ON messages.author = following.value
 | 
			
		||||
				WHERE messages.author != ?
 | 
			
		||||
				ORDER BY timestamp DESC limit 20
 | 
			
		||||
			`,
 | 
			
		||||
			[
 | 
			
		||||
				'"' + this.whoami.replace('"', '""') + '"',
 | 
			
		||||
				JSON.stringify(this.following),
 | 
			
		||||
				this.whoami,
 | 
			
		||||
			]
 | 
			
		||||
		);
 | 
			
		||||
		console.log('Done.');
 | 
			
		||||
		this.messages = results;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	on_expand(event) {
 | 
			
		||||
		if (event.detail.expanded) {
 | 
			
		||||
			let expand = {};
 | 
			
		||||
			expand[event.detail.id] = true;
 | 
			
		||||
			this.expanded = Object.assign({}, this.expanded, expand);
 | 
			
		||||
		} else {
 | 
			
		||||
			delete this.expanded[event.detail.id];
 | 
			
		||||
			this.expanded = Object.assign({}, this.expanded);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let self = this;
 | 
			
		||||
		if (!this.loading) {
 | 
			
		||||
			this.loading = true;
 | 
			
		||||
			this.load();
 | 
			
		||||
		}
 | 
			
		||||
		return html`
 | 
			
		||||
			<tf-news
 | 
			
		||||
				id="news"
 | 
			
		||||
				whoami=${this.whoami}
 | 
			
		||||
				.messages=${this.messages}
 | 
			
		||||
				.users=${this.users}
 | 
			
		||||
				.expanded=${this.expanded}
 | 
			
		||||
				@tf-expand=${this.on_expand}
 | 
			
		||||
			></tf-news>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
customElements.define('tf-tab-mentions', TfTabMentionsElement);
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import {LitElement, cache, html, unsafeHTML, until} from './lit-all.min.js';
 | 
			
		||||
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
 | 
			
		||||
import * as tfrpc from '/static/tfrpc.js';
 | 
			
		||||
import {styles} from './tf-styles.js';
 | 
			
		||||
 | 
			
		||||
@@ -12,12 +12,6 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
			messages: {type: Array},
 | 
			
		||||
			drafts: {type: Object},
 | 
			
		||||
			expanded: {type: Object},
 | 
			
		||||
			channels_unread: {type: Object},
 | 
			
		||||
			channels_latest: {type: Object},
 | 
			
		||||
			loading: {type: Number},
 | 
			
		||||
			time_range: {type: Array},
 | 
			
		||||
			time_loading: {type: Array},
 | 
			
		||||
			private_messages: {type: Array},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -32,200 +26,112 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
		this.following = [];
 | 
			
		||||
		this.drafts = {};
 | 
			
		||||
		this.expanded = {};
 | 
			
		||||
		this.channels_unread = {};
 | 
			
		||||
		this.channels_latest = {};
 | 
			
		||||
		this.start_time = new Date().valueOf();
 | 
			
		||||
		this.time_range = [0, 0];
 | 
			
		||||
		this.time_loading = undefined;
 | 
			
		||||
		this.loading = 0;
 | 
			
		||||
		this.start_time = new Date().valueOf() - 24 * 60 * 60 * 1000;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	channel() {
 | 
			
		||||
		return this.hash.startsWith('##')
 | 
			
		||||
			? this.hash.substring(2)
 | 
			
		||||
			: this.hash.substring(1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async fetch_messages(start_time, end_time) {
 | 
			
		||||
		this.time_loading = [start_time, end_time];
 | 
			
		||||
		let result;
 | 
			
		||||
		if (this.hash == '#@') {
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
	async fetch_messages() {
 | 
			
		||||
		if (this.hash.startsWith('#@')) {
 | 
			
		||||
			let r = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					WITH mentions AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM messages_fts(?1)
 | 
			
		||||
						JOIN messages ON messages.rowid = messages_fts.rowid
 | 
			
		||||
						JOIN json_each(?2) AS following ON messages.author = following.value
 | 
			
		||||
						WHERE
 | 
			
		||||
							messages.author != ?1 AND
 | 
			
		||||
							(?3 IS NULL OR messages.timestamp >= ?3) AND messages.timestamp < ?4
 | 
			
		||||
						ORDER BY timestamp DESC limit 20)
 | 
			
		||||
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM mentions
 | 
			
		||||
						JOIN messages_refs ON mentions.id = messages_refs.ref
 | 
			
		||||
					WITH mine AS (SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
						FROM messages
 | 
			
		||||
						WHERE messages.author = ?
 | 
			
		||||
						ORDER BY sequence DESC
 | 
			
		||||
						LIMIT 20)
 | 
			
		||||
					SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM mine
 | 
			
		||||
						JOIN messages_refs ON mine.id = messages_refs.ref
 | 
			
		||||
						JOIN messages ON messages_refs.message = messages.id
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT TRUE AS is_primary, * FROM mentions
 | 
			
		||||
					SELECT * FROM mine
 | 
			
		||||
				`,
 | 
			
		||||
				[
 | 
			
		||||
					'"' + this.whoami.replace('"', '""') + '"',
 | 
			
		||||
					JSON.stringify(this.following),
 | 
			
		||||
					start_time,
 | 
			
		||||
					end_time,
 | 
			
		||||
				]
 | 
			
		||||
			);
 | 
			
		||||
		} else if (this.hash.startsWith('#@')) {
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					WITH
 | 
			
		||||
						selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
							FROM messages
 | 
			
		||||
							WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3
 | 
			
		||||
							ORDER BY sequence DESC LIMIT 20
 | 
			
		||||
						)
 | 
			
		||||
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM selected
 | 
			
		||||
						JOIN messages_refs ON selected.id = messages_refs.ref
 | 
			
		||||
						JOIN messages ON messages_refs.message = messages.id
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT TRUE AS is_primary, * FROM selected
 | 
			
		||||
				`,
 | 
			
		||||
				[this.hash.substring(1), start_time, end_time]
 | 
			
		||||
				[this.hash.substring(1)]
 | 
			
		||||
			);
 | 
			
		||||
			return r;
 | 
			
		||||
		} else if (this.hash.startsWith('#%')) {
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
			return await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					SELECT TRUE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
					SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
					FROM messages
 | 
			
		||||
					WHERE messages.id = ?1
 | 
			
		||||
					WHERE id = ?1
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT FALSE AS is_primary, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
					SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
					FROM messages JOIN messages_refs
 | 
			
		||||
					ON messages.id = messages_refs.message
 | 
			
		||||
					WHERE messages_refs.ref = ?1
 | 
			
		||||
				`,
 | 
			
		||||
				[this.hash.substring(1)]
 | 
			
		||||
			);
 | 
			
		||||
		} else if (this.hash.startsWith('##')) {
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					WITH
 | 
			
		||||
						all_news AS (
 | 
			
		||||
							SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
								FROM messages
 | 
			
		||||
								JOIN json_each(?) AS following ON messages.author = following.value
 | 
			
		||||
								WHERE messages.content ->> 'channel' = ?4
 | 
			
		||||
							UNION
 | 
			
		||||
							SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
								FROM messages_fts(?5)
 | 
			
		||||
								JOIN messages ON messages.rowid = messages_fts.rowid
 | 
			
		||||
								JOIN json_each(?1) AS following ON messages.author = following.value
 | 
			
		||||
								JOIN json_tree(messages.content, '$.mentions') AS mention ON mention.value = '#' || ?4),
 | 
			
		||||
						news AS (SELECT * FROM all_news
 | 
			
		||||
							WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
 | 
			
		||||
							ORDER BY all_news.timestamp DESC LIMIT 20)
 | 
			
		||||
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM news
 | 
			
		||||
						JOIN messages_refs ON news.id = messages_refs.ref
 | 
			
		||||
						JOIN messages ON messages_refs.message = messages.id
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM news
 | 
			
		||||
						JOIN messages_refs ON news.id = messages_refs.message
 | 
			
		||||
						JOIN messages ON messages_refs.ref = messages.id
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT TRUE AS is_primary, news.* FROM news
 | 
			
		||||
				`,
 | 
			
		||||
				[
 | 
			
		||||
					JSON.stringify(this.following),
 | 
			
		||||
					start_time,
 | 
			
		||||
					end_time,
 | 
			
		||||
					this.hash.substring(2),
 | 
			
		||||
					'"#' + this.hash.substring(2).replace('"', '""') + '"',
 | 
			
		||||
				]
 | 
			
		||||
			);
 | 
			
		||||
		} else if (this.hash == '#🔐') {
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
 | 
			
		||||
					FROM messages
 | 
			
		||||
					JOIN json_each(?1) AS private_messages ON messages.id = private_messages.value
 | 
			
		||||
					WHERE
 | 
			
		||||
						(?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND
 | 
			
		||||
						json(messages.content) LIKE '"%'
 | 
			
		||||
					ORDER BY messages.sequence DESC LIMIT 20
 | 
			
		||||
				`,
 | 
			
		||||
				[JSON.stringify(this.private_messages), start_time, end_time]
 | 
			
		||||
			);
 | 
			
		||||
			result = (await this.decrypt(result)).filter((x) => x.decrypted);
 | 
			
		||||
		} else {
 | 
			
		||||
			result = await tfrpc.rpc.query(
 | 
			
		||||
				`
 | 
			
		||||
					WITH
 | 
			
		||||
						all_news AS (
 | 
			
		||||
							SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
							FROM messages
 | 
			
		||||
							JOIN json_each(?) AS following ON messages.author = following.value
 | 
			
		||||
							WHERE timestamp >= 0 AND timestamp < ?3),
 | 
			
		||||
						news AS (
 | 
			
		||||
							SELECT * FROM all_news
 | 
			
		||||
							WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
 | 
			
		||||
							ORDER BY timestamp DESC LIMIT 20
 | 
			
		||||
						)
 | 
			
		||||
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM news
 | 
			
		||||
						JOIN messages_refs ON news.id = messages_refs.ref
 | 
			
		||||
						JOIN messages ON messages_refs.message = messages.id
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM news
 | 
			
		||||
						JOIN messages_refs ON news.id = messages_refs.message
 | 
			
		||||
						JOIN messages ON messages_refs.ref = messages.id
 | 
			
		||||
					UNION
 | 
			
		||||
					SELECT TRUE AS is_primary, news.* FROM news
 | 
			
		||||
				`,
 | 
			
		||||
				[JSON.stringify(this.following), start_time, end_time]
 | 
			
		||||
			);
 | 
			
		||||
			let promises = [];
 | 
			
		||||
			const k_following_limit = 256;
 | 
			
		||||
			for (let i = 0; i < this.following.length; i += k_following_limit) {
 | 
			
		||||
				promises.push(
 | 
			
		||||
					tfrpc.rpc.query(
 | 
			
		||||
						`
 | 
			
		||||
						WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
						FROM messages
 | 
			
		||||
						JOIN json_each(?) AS following ON messages.author = following.value
 | 
			
		||||
						WHERE messages.timestamp > ? AND messages.timestamp < ?
 | 
			
		||||
						ORDER BY messages.timestamp DESC)
 | 
			
		||||
						SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
							FROM news
 | 
			
		||||
							JOIN messages_refs ON news.id = messages_refs.ref
 | 
			
		||||
							JOIN messages ON messages_refs.message = messages.id
 | 
			
		||||
						UNION
 | 
			
		||||
						SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
							FROM news
 | 
			
		||||
							JOIN messages_refs ON news.id = messages_refs.message
 | 
			
		||||
							JOIN messages ON messages_refs.ref = messages.id
 | 
			
		||||
						UNION
 | 
			
		||||
						SELECT news.* FROM news
 | 
			
		||||
					`,
 | 
			
		||||
						[
 | 
			
		||||
							JSON.stringify(this.following.slice(i, i + k_following_limit)),
 | 
			
		||||
							this.start_time,
 | 
			
		||||
							/*
 | 
			
		||||
							 ** Don't show messages more than a day into the future to prevent
 | 
			
		||||
							 ** messages with far-future timestamps from staying at the top forever.
 | 
			
		||||
							 */
 | 
			
		||||
							new Date().valueOf() + 24 * 60 * 60 * 1000,
 | 
			
		||||
						]
 | 
			
		||||
					)
 | 
			
		||||
				);
 | 
			
		||||
			}
 | 
			
		||||
			return [].concat(...(await Promise.all(promises)));
 | 
			
		||||
		}
 | 
			
		||||
		this.time_loading = undefined;
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	update_time_range_from_messages(messages) {
 | 
			
		||||
		let only_primary = messages.filter((x) => x.is_primary);
 | 
			
		||||
		this.time_range = [
 | 
			
		||||
			only_primary.reduce(
 | 
			
		||||
				(accumulator, current) => Math.min(accumulator, current.timestamp),
 | 
			
		||||
				this.time_range[0]
 | 
			
		||||
			),
 | 
			
		||||
			only_primary.reduce(
 | 
			
		||||
				(accumulator, current) => Math.max(accumulator, current.timestamp),
 | 
			
		||||
				this.time_range[1]
 | 
			
		||||
			),
 | 
			
		||||
		];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load_more() {
 | 
			
		||||
		this.loading++;
 | 
			
		||||
		this.loading_canceled = false;
 | 
			
		||||
		try {
 | 
			
		||||
			let more = [];
 | 
			
		||||
			let last_start_time = this.time_range[0];
 | 
			
		||||
			more = await this.fetch_messages(null, last_start_time);
 | 
			
		||||
			this.update_time_range_from_messages(
 | 
			
		||||
				more.filter((x) => x.timestamp < last_start_time)
 | 
			
		||||
			);
 | 
			
		||||
			this.messages = await this.decrypt([...more, ...this.messages]);
 | 
			
		||||
		} finally {
 | 
			
		||||
			this.loading--;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cancel_load() {
 | 
			
		||||
		this.loading_canceled = true;
 | 
			
		||||
		let last_start_time = this.start_time;
 | 
			
		||||
		this.start_time = last_start_time - 24 * 60 * 60 * 1000;
 | 
			
		||||
		let more = await tfrpc.rpc.query(
 | 
			
		||||
			`
 | 
			
		||||
				WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
				FROM messages
 | 
			
		||||
				JOIN json_each(?) AS following ON messages.author = following.value
 | 
			
		||||
				WHERE messages.timestamp > ?
 | 
			
		||||
				AND messages.timestamp <= ?
 | 
			
		||||
				ORDER BY messages.timestamp DESC)
 | 
			
		||||
				SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
					FROM news
 | 
			
		||||
					JOIN messages_refs ON news.id = messages_refs.ref
 | 
			
		||||
					JOIN messages ON messages_refs.message = messages.id
 | 
			
		||||
				UNION
 | 
			
		||||
				SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
 | 
			
		||||
					FROM news
 | 
			
		||||
					JOIN messages_refs ON news.id = messages_refs.message
 | 
			
		||||
					JOIN messages ON messages_refs.ref = messages.id
 | 
			
		||||
				UNION
 | 
			
		||||
				SELECT news.* FROM news
 | 
			
		||||
			`,
 | 
			
		||||
			[JSON.stringify(this.following), this.start_time, last_start_time]
 | 
			
		||||
		);
 | 
			
		||||
		this.messages = await this.decrypt([...more, ...this.messages]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async decrypt(messages) {
 | 
			
		||||
		console.log('decrypt');
 | 
			
		||||
		let result = [];
 | 
			
		||||
		for (let message of messages) {
 | 
			
		||||
			let content;
 | 
			
		||||
@@ -250,143 +156,44 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	merge_messages(old_messages, new_messages) {
 | 
			
		||||
		let old_by_id = Object.fromEntries(old_messages.map((x) => [x.id, x]));
 | 
			
		||||
		return new_messages.map((x) => (old_by_id[x.id] ? old_by_id[x.id] : x));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load_latest() {
 | 
			
		||||
		this.loading++;
 | 
			
		||||
		let now = new Date().valueOf();
 | 
			
		||||
		let end_time = now + 24 * 60 * 60 * 1000;
 | 
			
		||||
		let messages = [];
 | 
			
		||||
		try {
 | 
			
		||||
			messages = await this.fetch_messages(this.time_range[0], end_time);
 | 
			
		||||
			messages = await this.decrypt(messages);
 | 
			
		||||
			this.update_time_range_from_messages(
 | 
			
		||||
				messages.filter(
 | 
			
		||||
					(x) => x.timestamp >= this.time_range[0] && x.timestamp < end_time
 | 
			
		||||
				)
 | 
			
		||||
			);
 | 
			
		||||
		} finally {
 | 
			
		||||
			this.loading--;
 | 
			
		||||
		}
 | 
			
		||||
		this.messages = this.merge_messages(
 | 
			
		||||
			this.messages,
 | 
			
		||||
			Object.values(
 | 
			
		||||
				Object.fromEntries(
 | 
			
		||||
					[...this.messages, ...messages]
 | 
			
		||||
						.sort((x, y) => x.timestamp - y.timestamp)
 | 
			
		||||
						.slice(-1024)
 | 
			
		||||
						.map((x) => [x.id, x])
 | 
			
		||||
				)
 | 
			
		||||
			)
 | 
			
		||||
		);
 | 
			
		||||
		console.log('done loading latest messages.');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async load_messages() {
 | 
			
		||||
		let start_time = new Date();
 | 
			
		||||
		let self = this;
 | 
			
		||||
		this.loading++;
 | 
			
		||||
		let messages = [];
 | 
			
		||||
		try {
 | 
			
		||||
			if (this._messages_hash !== this.hash) {
 | 
			
		||||
				this.messages = [];
 | 
			
		||||
				this._messages_hash = this.hash;
 | 
			
		||||
			}
 | 
			
		||||
			this._messages_following = this.following;
 | 
			
		||||
			let now = new Date().valueOf();
 | 
			
		||||
			let start_time = now - 24 * 60 * 60 * 1000;
 | 
			
		||||
			this.start_time = start_time;
 | 
			
		||||
			this.time_range = [now + 24 * 60 * 60 * 1000, now + 24 * 60 * 60 * 1000];
 | 
			
		||||
			messages = await this.fetch_messages(null, this.time_range[1]);
 | 
			
		||||
			this.update_time_range_from_messages(
 | 
			
		||||
				messages.filter((x) => x.timestamp < this.time_range[1])
 | 
			
		||||
			);
 | 
			
		||||
			messages = await this.decrypt(messages);
 | 
			
		||||
		} finally {
 | 
			
		||||
			this.loading--;
 | 
			
		||||
		}
 | 
			
		||||
		this.messages = this.merge_messages(this.messages, messages);
 | 
			
		||||
		this.time_loading = undefined;
 | 
			
		||||
		console.log(
 | 
			
		||||
			`loading messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s`
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mark_all_read() {
 | 
			
		||||
		let newest = this.messages.reduce(
 | 
			
		||||
			(accumulator, current) => Math.max(accumulator, current.rowid),
 | 
			
		||||
			this.channels_latest[this.channel()] ?? -1
 | 
			
		||||
		);
 | 
			
		||||
		if (newest >= 0) {
 | 
			
		||||
			this.dispatchEvent(
 | 
			
		||||
				new CustomEvent('channelsetunread', {
 | 
			
		||||
					bubbles: true,
 | 
			
		||||
					composed: true,
 | 
			
		||||
					detail: {
 | 
			
		||||
						channel: this.channel(),
 | 
			
		||||
						unread: newest + 1,
 | 
			
		||||
					},
 | 
			
		||||
				})
 | 
			
		||||
			);
 | 
			
		||||
		}
 | 
			
		||||
	async add_messages(messages) {
 | 
			
		||||
		this.messages = await this.decrypt([...messages, ...this.messages]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		if (
 | 
			
		||||
			!this.messages ||
 | 
			
		||||
			this._messages_hash !== this.hash ||
 | 
			
		||||
			JSON.stringify(this._messages_following) !==
 | 
			
		||||
				JSON.stringify(this.following)
 | 
			
		||||
			this._messages_following !== this.following
 | 
			
		||||
		) {
 | 
			
		||||
			console.log(
 | 
			
		||||
				`loading messages for ${this.whoami} (following ${this.following.length})`
 | 
			
		||||
			);
 | 
			
		||||
			this.load_messages();
 | 
			
		||||
			let self = this;
 | 
			
		||||
			this.messages = [];
 | 
			
		||||
			this._messages_hash = this.hash;
 | 
			
		||||
			this._messages_following = this.following;
 | 
			
		||||
			this.fetch_messages()
 | 
			
		||||
				.then(this.decrypt.bind(this))
 | 
			
		||||
				.then(function (messages) {
 | 
			
		||||
					self.messages = messages;
 | 
			
		||||
					console.log(`loading mesages done for ${self.whoami}`);
 | 
			
		||||
				})
 | 
			
		||||
				.catch(function (error) {
 | 
			
		||||
					alert(JSON.stringify(error, null, 2));
 | 
			
		||||
				});
 | 
			
		||||
		}
 | 
			
		||||
		let more;
 | 
			
		||||
		if (!this.hash.startsWith('#%')) {
 | 
			
		||||
		if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
 | 
			
		||||
			more = html`
 | 
			
		||||
				<p>
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
 | 
			
		||||
						Mark All Read
 | 
			
		||||
					</button>
 | 
			
		||||
					<button
 | 
			
		||||
						?disabled=${this.loading}
 | 
			
		||||
						class="w3-button w3-theme-d1"
 | 
			
		||||
						@click=${this.load_more}
 | 
			
		||||
					>
 | 
			
		||||
					<button class="w3-button w3-theme-d1" @click=${this.load_more}>
 | 
			
		||||
						Load More
 | 
			
		||||
					</button>
 | 
			
		||||
					<button
 | 
			
		||||
						class=${'w3-button w3-theme-d1' + (this.loading ? '' : ' w3-hide')}
 | 
			
		||||
						@click=${this.cancel_load}
 | 
			
		||||
					>
 | 
			
		||||
						Cancel
 | 
			
		||||
					</button>
 | 
			
		||||
					<span
 | 
			
		||||
						>Showing
 | 
			
		||||
						${new Date(
 | 
			
		||||
							this.time_loading
 | 
			
		||||
								? Math.min(this.time_loading[0], this.time_range[0])
 | 
			
		||||
								: this.time_range[0]
 | 
			
		||||
						).toLocaleDateString()}
 | 
			
		||||
						-
 | 
			
		||||
						${new Date(
 | 
			
		||||
							this.time_loading
 | 
			
		||||
								? Math.max(this.time_loading[1], this.time_range[1])
 | 
			
		||||
								: this.time_range[1]
 | 
			
		||||
						).toLocaleDateString()}.</span
 | 
			
		||||
					>
 | 
			
		||||
				</p>
 | 
			
		||||
			`;
 | 
			
		||||
		}
 | 
			
		||||
		return cache(html`
 | 
			
		||||
			<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
 | 
			
		||||
				Mark All Read
 | 
			
		||||
			</button>
 | 
			
		||||
		return html`
 | 
			
		||||
			<tf-news
 | 
			
		||||
				id="news"
 | 
			
		||||
				whoami=${this.whoami}
 | 
			
		||||
@@ -395,11 +202,9 @@ class TfTabNewsFeedElement extends LitElement {
 | 
			
		||||
				.following=${this.following}
 | 
			
		||||
				.drafts=${this.drafts}
 | 
			
		||||
				.expanded=${this.expanded}
 | 
			
		||||
				channel=${this.channel()}
 | 
			
		||||
				channel_unread=${this.channels_unread?.[this.channel()]}
 | 
			
		||||
			></tf-news>
 | 
			
		||||
			${more}
 | 
			
		||||
		`);
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,4 @@
 | 
			
		||||
import {
 | 
			
		||||
	LitElement,
 | 
			
		||||
	cache,
 | 
			
		||||
	keyed,
 | 
			
		||||
	html,
 | 
			
		||||
	unsafeHTML,
 | 
			
		||||
	until,
 | 
			
		||||
} from './lit-all.min.js';
 | 
			
		||||
import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
 | 
			
		||||
import * as tfrpc from '/static/tfrpc.js';
 | 
			
		||||
import {styles} from './tf-styles.js';
 | 
			
		||||
 | 
			
		||||
@@ -15,15 +8,10 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
			whoami: {type: String},
 | 
			
		||||
			users: {type: Object},
 | 
			
		||||
			hash: {type: String},
 | 
			
		||||
			unread: {type: Array},
 | 
			
		||||
			following: {type: Array},
 | 
			
		||||
			drafts: {type: Object},
 | 
			
		||||
			expanded: {type: Object},
 | 
			
		||||
			loading: {type: Boolean},
 | 
			
		||||
			channels: {type: Array},
 | 
			
		||||
			channels_unread: {type: Object},
 | 
			
		||||
			channels_latest: {type: Object},
 | 
			
		||||
			connections: {type: Array},
 | 
			
		||||
			private_messages: {type: Array},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -35,14 +23,11 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
		this.whoami = null;
 | 
			
		||||
		this.users = {};
 | 
			
		||||
		this.hash = '#';
 | 
			
		||||
		this.unread = [];
 | 
			
		||||
		this.following = [];
 | 
			
		||||
		this.cache = {};
 | 
			
		||||
		this.drafts = {};
 | 
			
		||||
		this.expanded = {};
 | 
			
		||||
		this.channels_unread = {};
 | 
			
		||||
		this.channels_latest = {};
 | 
			
		||||
		this.channels = [];
 | 
			
		||||
		this.connections = [];
 | 
			
		||||
		tfrpc.rpc.localStorageGet('drafts').then(function (d) {
 | 
			
		||||
			self.drafts = JSON.parse(d || '{}');
 | 
			
		||||
		});
 | 
			
		||||
@@ -58,13 +43,39 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
		document.body.removeEventListener('keypress', this.on_keypress.bind(this));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	load_latest() {
 | 
			
		||||
	show_more() {
 | 
			
		||||
		let unread = this.unread;
 | 
			
		||||
		let news = this.shadowRoot?.getElementById('news');
 | 
			
		||||
		if (news) {
 | 
			
		||||
			news.load_latest();
 | 
			
		||||
			console.log('injecting messages', news.messages);
 | 
			
		||||
			news.add_messages(
 | 
			
		||||
				Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x])))
 | 
			
		||||
			);
 | 
			
		||||
			this.dispatchEvent(new CustomEvent('refresh'));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	new_messages_text() {
 | 
			
		||||
		if (!this.unread?.length) {
 | 
			
		||||
			return 'No new messages.';
 | 
			
		||||
		}
 | 
			
		||||
		let counts = {};
 | 
			
		||||
		for (let message of this.unread) {
 | 
			
		||||
			let type = 'private';
 | 
			
		||||
			try {
 | 
			
		||||
				type = JSON.parse(message.content).type || type;
 | 
			
		||||
			} catch {}
 | 
			
		||||
			counts[type] = (counts[type] || 0) + 1;
 | 
			
		||||
		}
 | 
			
		||||
		return (
 | 
			
		||||
			'↻ Show New: ' +
 | 
			
		||||
			Object.keys(counts)
 | 
			
		||||
				.sort()
 | 
			
		||||
				.map((x) => counts[x].toString() + ' ' + x + 's')
 | 
			
		||||
				.join(', ')
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	draft(event) {
 | 
			
		||||
		let id = event.detail.id || '';
 | 
			
		||||
		let previous = this.drafts[id];
 | 
			
		||||
@@ -94,255 +105,48 @@ class TfTabNewsElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	unread_status(channel) {
 | 
			
		||||
		if (
 | 
			
		||||
			this.channels_latest[channel] &&
 | 
			
		||||
			this.channels_latest[channel] > 0 &&
 | 
			
		||||
			(this.channels_unread[channel] === undefined ||
 | 
			
		||||
				this.channels_unread[channel] <= this.channels_latest[channel])
 | 
			
		||||
		) {
 | 
			
		||||
			return '✉️ ';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	show_sidebar() {
 | 
			
		||||
		this.renderRoot.getElementById('sidebar').style.display = 'block';
 | 
			
		||||
		this.renderRoot.getElementById('sidebar_overlay').style.display = 'block';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hide_sidebar() {
 | 
			
		||||
		this.renderRoot.getElementById('sidebar').style.display = 'none';
 | 
			
		||||
		this.renderRoot.getElementById('sidebar_overlay').style.display = 'none';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async channel_toggle_subscribed() {
 | 
			
		||||
		let channel = this.hash.substring(2);
 | 
			
		||||
		let subscribed = this.channels.indexOf(channel) != -1;
 | 
			
		||||
		subscribed = !subscribed;
 | 
			
		||||
 | 
			
		||||
		await tfrpc.rpc.appendMessage(this.whoami, {
 | 
			
		||||
			type: 'channel',
 | 
			
		||||
			channel: channel,
 | 
			
		||||
			subscribed: subscribed,
 | 
			
		||||
		});
 | 
			
		||||
		if (subscribed) {
 | 
			
		||||
			this.channels = [].concat([channel], this.channels).sort();
 | 
			
		||||
		} else {
 | 
			
		||||
			this.channels = this.channels.filter((x) => x != channel);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	channel() {
 | 
			
		||||
		return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	compare_follows() {
 | 
			
		||||
		const now = new Date().valueOf();
 | 
			
		||||
		return function (a, b) {
 | 
			
		||||
			return (b[1].ts > now ? -1 : b[1].ts) - (a[1].ts > now ? -1 : a[1].ts);
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	suggested_follows() {
 | 
			
		||||
		/*
 | 
			
		||||
		 ** Filter out people who have used future timestamps so that they aren't
 | 
			
		||||
		 ** pinned at the top.
 | 
			
		||||
		 */
 | 
			
		||||
		let self = this;
 | 
			
		||||
		return Object.entries(this.users)
 | 
			
		||||
			.filter((x) => x[1].follow_depth > 1)
 | 
			
		||||
			.sort(self.compare_follows())
 | 
			
		||||
			.slice(0, 8)
 | 
			
		||||
			.map((x) => x[0]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_sidebar() {
 | 
			
		||||
		return html`
 | 
			
		||||
			<div
 | 
			
		||||
				class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left"
 | 
			
		||||
				style="width: 2in; left: 0; z-index: 5; box-sizing: border-box; top: 0"
 | 
			
		||||
				id="sidebar"
 | 
			
		||||
			>
 | 
			
		||||
				<div
 | 
			
		||||
					class="w3-right w3-button w3-hide-large"
 | 
			
		||||
					@click=${this.hide_sidebar}
 | 
			
		||||
				>
 | 
			
		||||
					×
 | 
			
		||||
				</div>
 | 
			
		||||
				${this.hash.startsWith('##') &&
 | 
			
		||||
				this.channels.indexOf(this.hash.substring(2)) == -1
 | 
			
		||||
					? html`
 | 
			
		||||
							<div class="w3-bar-item w3-theme-d2">Viewing</div>
 | 
			
		||||
							<a
 | 
			
		||||
								href="#"
 | 
			
		||||
								class="w3-bar-item w3-button"
 | 
			
		||||
								style="font-weight: bold"
 | 
			
		||||
								>${this.hash.substring(2)}</a
 | 
			
		||||
							>
 | 
			
		||||
						`
 | 
			
		||||
					: undefined}
 | 
			
		||||
				<h4 class="w3-bar-item w3-theme-d2">Channels</h4>
 | 
			
		||||
				<a
 | 
			
		||||
					href="#"
 | 
			
		||||
					class="w3-bar-item w3-button"
 | 
			
		||||
					style=${this.hash == '#' ? 'font-weight: bold' : undefined}
 | 
			
		||||
					>${this.unread_status('')}general</a
 | 
			
		||||
				>
 | 
			
		||||
				<a
 | 
			
		||||
					href="#@"
 | 
			
		||||
					class="w3-bar-item w3-button"
 | 
			
		||||
					style=${this.hash == '#@' ? 'font-weight: bold' : undefined}
 | 
			
		||||
					>${this.unread_status('@')}@mentions</a
 | 
			
		||||
				>
 | 
			
		||||
				<a
 | 
			
		||||
					href="#🔐"
 | 
			
		||||
					class="w3-bar-item w3-button"
 | 
			
		||||
					style=${this.hash == '#🔐' ? 'font-weight: bold' : undefined}
 | 
			
		||||
					>${this.unread_status('🔐')}🔐private</a
 | 
			
		||||
				>
 | 
			
		||||
				${Object.keys(this.drafts)
 | 
			
		||||
					.sort()
 | 
			
		||||
					.map(
 | 
			
		||||
						(x) => html`
 | 
			
		||||
							<a
 | 
			
		||||
								href=${'#' + encodeURIComponent(x)}
 | 
			
		||||
								class="w3-bar-item w3-button"
 | 
			
		||||
								style="text-wrap: nowrap; text-overflow: ellipsis"
 | 
			
		||||
								>📝 ${this.drafts[x]?.text ?? x}</a
 | 
			
		||||
							>
 | 
			
		||||
						`
 | 
			
		||||
					)}
 | 
			
		||||
				${this.channels.map(
 | 
			
		||||
					(x) => html`
 | 
			
		||||
						<a
 | 
			
		||||
							href=${'#' + encodeURIComponent('#' + x)}
 | 
			
		||||
							class="w3-bar-item w3-button"
 | 
			
		||||
							style=${this.hash == '##' + x ? 'font-weight: bold' : undefined}
 | 
			
		||||
							>${this.unread_status(x)}#${x}</a
 | 
			
		||||
						>
 | 
			
		||||
					`
 | 
			
		||||
				)}
 | 
			
		||||
 | 
			
		||||
				<h4 class="w3-bar-item w3-theme-d2">Connections</h4>
 | 
			
		||||
				${this.connections
 | 
			
		||||
					.filter((x) => x.id && !x.destroy_reason)
 | 
			
		||||
					.map(
 | 
			
		||||
						(x) => html`
 | 
			
		||||
							<tf-user
 | 
			
		||||
								class="w3-bar-item"
 | 
			
		||||
								style="max-width: 100%"
 | 
			
		||||
								id=${x.id}
 | 
			
		||||
								.users=${this.users}
 | 
			
		||||
							></tf-user>
 | 
			
		||||
						`
 | 
			
		||||
					)}
 | 
			
		||||
				<h4 class="w3-bar-item w3-theme-d2">Suggested Follows</h4>
 | 
			
		||||
				${this.suggested_follows().map(
 | 
			
		||||
					(x) => html`
 | 
			
		||||
						<tf-user
 | 
			
		||||
							class="w3-bar-item"
 | 
			
		||||
							style="max-width: 100%"
 | 
			
		||||
							id=${x}
 | 
			
		||||
							.users=${this.users}
 | 
			
		||||
						></tf-user>
 | 
			
		||||
					`
 | 
			
		||||
				)}
 | 
			
		||||
			</div>
 | 
			
		||||
			<div
 | 
			
		||||
				class="w3-overlay"
 | 
			
		||||
				id="sidebar_overlay"
 | 
			
		||||
				@click=${this.hide_sidebar}
 | 
			
		||||
			></div>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let profile =
 | 
			
		||||
			this.hash.startsWith('#@') && this.hash != '#@'
 | 
			
		||||
				? keyed(
 | 
			
		||||
						this.hash.substring(1),
 | 
			
		||||
						html`<tf-profile
 | 
			
		||||
							class="tf-profile"
 | 
			
		||||
							id=${this.hash.substring(1)}
 | 
			
		||||
							whoami=${this.whoami}
 | 
			
		||||
							.users=${this.users}
 | 
			
		||||
						></tf-profile>`
 | 
			
		||||
					)
 | 
			
		||||
				: undefined;
 | 
			
		||||
		let edit_profile;
 | 
			
		||||
		if (
 | 
			
		||||
			!this.loading &&
 | 
			
		||||
			this.users[this.whoami]?.name === undefined &&
 | 
			
		||||
			this.hash.substring(1) != this.whoami
 | 
			
		||||
		) {
 | 
			
		||||
			edit_profile = html` <div
 | 
			
		||||
				class="w3-panel w3-padding w3-round w3-card-4 w3-theme-l3"
 | 
			
		||||
			>
 | 
			
		||||
				ℹ️ Follow your identity link ☝️ above to edit your profile and set your
 | 
			
		||||
				name.
 | 
			
		||||
			</div>`;
 | 
			
		||||
		}
 | 
			
		||||
		return cache(html`
 | 
			
		||||
			${this.render_sidebar()}
 | 
			
		||||
			<div
 | 
			
		||||
				style="margin-left: 2in; padding: 0px; top: 0; max-height: 100%; overflow: auto"
 | 
			
		||||
				id="main"
 | 
			
		||||
				class="w3-main"
 | 
			
		||||
			>
 | 
			
		||||
				<div style="padding: 8px">
 | 
			
		||||
					<p>
 | 
			
		||||
						${this.hash.startsWith('##')
 | 
			
		||||
							? html`
 | 
			
		||||
									<button
 | 
			
		||||
										class="w3-button w3-theme-d1"
 | 
			
		||||
										@click=${this.channel_toggle_subscribed}
 | 
			
		||||
									>
 | 
			
		||||
										${this.channels.indexOf(this.hash.substring(2)) != -1
 | 
			
		||||
											? 'Unsubscribe from #'
 | 
			
		||||
											: 'Subscribe to #'}${this.hash.substring(2)}
 | 
			
		||||
									</button>
 | 
			
		||||
								`
 | 
			
		||||
							: undefined}
 | 
			
		||||
					</p>
 | 
			
		||||
					<div>
 | 
			
		||||
						<div
 | 
			
		||||
							id="show_sidebar"
 | 
			
		||||
							class="w3-button w3-hide-large"
 | 
			
		||||
							@click=${this.show_sidebar}
 | 
			
		||||
						>
 | 
			
		||||
							☰
 | 
			
		||||
						</div>
 | 
			
		||||
						Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
 | 
			
		||||
						${edit_profile}
 | 
			
		||||
					</div>
 | 
			
		||||
					<div>
 | 
			
		||||
						<tf-compose
 | 
			
		||||
							id="tf-compose"
 | 
			
		||||
							whoami=${this.whoami}
 | 
			
		||||
							.users=${this.users}
 | 
			
		||||
							.drafts=${this.drafts}
 | 
			
		||||
							@tf-draft=${this.draft}
 | 
			
		||||
							.channel=${this.channel()}
 | 
			
		||||
						></tf-compose>
 | 
			
		||||
					</div>
 | 
			
		||||
					${profile}
 | 
			
		||||
					<tf-tab-news-feed
 | 
			
		||||
						id="news"
 | 
			
		||||
						whoami=${this.whoami}
 | 
			
		||||
						.users=${this.users}
 | 
			
		||||
						.following=${this.following}
 | 
			
		||||
						hash=${this.hash}
 | 
			
		||||
						.drafts=${this.drafts}
 | 
			
		||||
						.expanded=${this.expanded}
 | 
			
		||||
						@tf-draft=${this.draft}
 | 
			
		||||
						@tf-expand=${this.on_expand}
 | 
			
		||||
						.channels_unread=${this.channels_unread}
 | 
			
		||||
						.channels_latest=${this.channels_latest}
 | 
			
		||||
						.private_messages=${this.private_messages}
 | 
			
		||||
					></tf-tab-news-feed>
 | 
			
		||||
				</div>
 | 
			
		||||
		let profile = this.hash.startsWith('#@')
 | 
			
		||||
			? html`<tf-profile
 | 
			
		||||
					id=${this.hash.substring(1)}
 | 
			
		||||
					whoami=${this.whoami}
 | 
			
		||||
					.users=${this.users}
 | 
			
		||||
				></tf-profile>`
 | 
			
		||||
			: undefined;
 | 
			
		||||
		return html`
 | 
			
		||||
			<p class="w3-bar">
 | 
			
		||||
				<button
 | 
			
		||||
					class="w3-bar-item w3-button w3-theme-d1"
 | 
			
		||||
					@click=${this.show_more}
 | 
			
		||||
				>
 | 
			
		||||
					${this.new_messages_text()}
 | 
			
		||||
				</button>
 | 
			
		||||
			</p>
 | 
			
		||||
			<div>
 | 
			
		||||
				Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
 | 
			
		||||
			</div>
 | 
			
		||||
		`);
 | 
			
		||||
			<div>
 | 
			
		||||
				<tf-compose
 | 
			
		||||
					id="tf-compose"
 | 
			
		||||
					whoami=${this.whoami}
 | 
			
		||||
					.users=${this.users}
 | 
			
		||||
					.drafts=${this.drafts}
 | 
			
		||||
					@tf-draft=${this.draft}
 | 
			
		||||
				></tf-compose>
 | 
			
		||||
			</div>
 | 
			
		||||
			${profile}
 | 
			
		||||
			<tf-tab-news-feed
 | 
			
		||||
				id="news"
 | 
			
		||||
				whoami=${this.whoami}
 | 
			
		||||
				.users=${this.users}
 | 
			
		||||
				.following=${this.following}
 | 
			
		||||
				hash=${this.hash}
 | 
			
		||||
				.drafts=${this.drafts}
 | 
			
		||||
				.expanded=${this.expanded}
 | 
			
		||||
				@tf-draft=${this.draft}
 | 
			
		||||
				@tf-expand=${this.on_expand}
 | 
			
		||||
			></tf-tab-news-feed>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@ import {styles} from './tf-styles.js';
 | 
			
		||||
class TfTabSearchElement extends LitElement {
 | 
			
		||||
	static get properties() {
 | 
			
		||||
		return {
 | 
			
		||||
			drafts: {type: Object},
 | 
			
		||||
			whoami: {type: String},
 | 
			
		||||
			users: {type: Object},
 | 
			
		||||
			following: {type: Array},
 | 
			
		||||
@@ -23,10 +22,6 @@ class TfTabSearchElement extends LitElement {
 | 
			
		||||
		this.users = {};
 | 
			
		||||
		this.following = [];
 | 
			
		||||
		this.expanded = {};
 | 
			
		||||
		this.drafts = {};
 | 
			
		||||
		tfrpc.rpc.localStorageGet('drafts').then(function (d) {
 | 
			
		||||
			self.drafts = JSON.parse(d || '{}');
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async search(query) {
 | 
			
		||||
@@ -75,18 +70,6 @@ class TfTabSearchElement extends LitElement {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	draft(event) {
 | 
			
		||||
		let id = event.detail.id || '';
 | 
			
		||||
		let previous = this.drafts[id];
 | 
			
		||||
		if (event.detail.draft !== undefined) {
 | 
			
		||||
			this.drafts[id] = event.detail.draft;
 | 
			
		||||
		} else {
 | 
			
		||||
			delete this.drafts[id];
 | 
			
		||||
		}
 | 
			
		||||
		this.drafts = Object.assign({}, this.drafts);
 | 
			
		||||
		tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		if (this.query !== this.last_query) {
 | 
			
		||||
			this.last_query = this.query;
 | 
			
		||||
@@ -98,7 +81,7 @@ class TfTabSearchElement extends LitElement {
 | 
			
		||||
				<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input>
 | 
			
		||||
				<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
 | 
			
		||||
			</div>
 | 
			
		||||
			<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} .drafts=${this.drafts} @tf-expand=${this.on_expand} @tf-draft=${this.draft}></tf-news>
 | 
			
		||||
			<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
 | 
			
		||||
		`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,10 +18,10 @@ class TfTagElement extends LitElement {
 | 
			
		||||
	render() {
 | 
			
		||||
		let number = this.count ? html` (${this.count})` : undefined;
 | 
			
		||||
		return html`<a
 | 
			
		||||
			href=${'#' + encodeURIComponent(this.tag)}
 | 
			
		||||
			class="w3-tag w3-theme-d1 w3-round-4 w3-button"
 | 
			
		||||
			href="#q=${this.tag}"
 | 
			
		||||
			style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px"
 | 
			
		||||
			>${this.tag}${number}</a
 | 
			
		||||
		> `;
 | 
			
		||||
		>`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,38 +19,28 @@ class TfUserElement extends LitElement {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render() {
 | 
			
		||||
		let user = this.users[this.id];
 | 
			
		||||
		let shape =
 | 
			
		||||
			user?.follow_depth === undefined || user.follow_depth >= 2
 | 
			
		||||
				? 'w3-circle'
 | 
			
		||||
				: 'w3-round';
 | 
			
		||||
		let image = html`<span
 | 
			
		||||
			class=${'w3-theme-l4 ' + shape}
 | 
			
		||||
			style="display: inline-block; width: 2em; height: 2em; text-align: center; line-height: 2em"
 | 
			
		||||
			>😎</span
 | 
			
		||||
		>`;
 | 
			
		||||
		let name = this.users?.[this.id]?.name;
 | 
			
		||||
		name = html`<a target="_top" href=${'#' + this.id}
 | 
			
		||||
			>${name !== undefined ? name : this.id}</a
 | 
			
		||||
		>`;
 | 
			
		||||
		name =
 | 
			
		||||
			name !== undefined
 | 
			
		||||
				? html`<a target="_top" href=${'#' + this.id}>${name}</a>`
 | 
			
		||||
				: html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
 | 
			
		||||
 | 
			
		||||
		if (user) {
 | 
			
		||||
			let image_link = user.image;
 | 
			
		||||
			image_link =
 | 
			
		||||
				typeof image_link == 'string' ? image_link : image_link?.link;
 | 
			
		||||
			if (image_link !== undefined) {
 | 
			
		||||
				image = html`<img
 | 
			
		||||
					class=${'w3-theme-l4 ' + shape}
 | 
			
		||||
					style="width: 2em; height: 2em; vertical-align: middle; object-fit: cover"
 | 
			
		||||
					src="/${image_link}/view"
 | 
			
		||||
				/>`;
 | 
			
		||||
			}
 | 
			
		||||
		if (this.users[this.id]) {
 | 
			
		||||
			let image = this.users[this.id].image;
 | 
			
		||||
			image = typeof image == 'string' ? image : image?.link;
 | 
			
		||||
			return html` <div style="display: inline-block; font-weight: bold">
 | 
			
		||||
				<img
 | 
			
		||||
					style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%"
 | 
			
		||||
					?hidden=${image === undefined}
 | 
			
		||||
					src="${image ? '/' + image + '/view' : undefined}"
 | 
			
		||||
				/>
 | 
			
		||||
				${name}
 | 
			
		||||
			</div>`;
 | 
			
		||||
		} else {
 | 
			
		||||
			return html` <div style="display: inline-block; font-weight: bold">
 | 
			
		||||
				${name}
 | 
			
		||||
			</div>`;
 | 
			
		||||
		}
 | 
			
		||||
		return html` <div
 | 
			
		||||
			style="display: inline-block; vertical-align: middle; font-weight: bold; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis"
 | 
			
		||||
		>
 | 
			
		||||
			${image} ${name}
 | 
			
		||||
		</div>`;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,6 @@ import * as hashtagify from './commonmark-hashtag.js';
 | 
			
		||||
 | 
			
		||||
const k_code_classes = 'w3-theme-l4 w3-theme-border w3-round';
 | 
			
		||||
 | 
			
		||||
var reUnsafeProtocol = /^javascript:|vbscript:|file:|data:/i;
 | 
			
		||||
var reSafeDataProtocol = /^data:image\/(?:png|gif|jpeg|webp)/i;
 | 
			
		||||
var potentiallyUnsafe = function (url) {
 | 
			
		||||
	return reUnsafeProtocol.test(url) && !reSafeDataProtocol.test(url);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function image(node, entering) {
 | 
			
		||||
	if (
 | 
			
		||||
		node.firstChild?.type === 'text' &&
 | 
			
		||||
@@ -87,8 +81,8 @@ function attrs(node) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function markdown(md) {
 | 
			
		||||
	let reader = new commonmark.Parser();
 | 
			
		||||
	let writer = new commonmark.HtmlRenderer({safe: true});
 | 
			
		||||
	let reader = new commonmark.Parser({safe: true});
 | 
			
		||||
	let writer = new commonmark.HtmlRenderer();
 | 
			
		||||
	writer.image = image;
 | 
			
		||||
	writer.code = code;
 | 
			
		||||
	writer.attrs = attrs;
 | 
			
		||||
 
 | 
			
		||||
@@ -482,7 +482,16 @@ class TributeRange {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getDocument() {
 | 
			
		||||
        return document;
 | 
			
		||||
        let iframe;
 | 
			
		||||
        if (this.tribute.current.collection) {
 | 
			
		||||
            iframe = this.tribute.current.collection.iframe;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!iframe) {
 | 
			
		||||
            return document
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return iframe.contentWindow.document
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    positionMenuAtCaret(scrollTo) {
 | 
			
		||||
@@ -644,8 +653,8 @@ class TributeRange {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getWindowSelection() {
 | 
			
		||||
        if (this.tribute.collection[0].iframe?.getSelection) {
 | 
			
		||||
            return this.tribute.collection[0].iframe.getSelection()
 | 
			
		||||
        if (this.tribute.collection.iframe) {
 | 
			
		||||
            return this.tribute.collection.iframe.contentWindow.getSelection()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return window.getSelection()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "💾",
 | 
			
		||||
	"previous": "&mvGTlWKFR5QM/3nb4fJ2WQq0n/gNKvBmhGDkAvb8ki8=.sha256"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,127 +0,0 @@
 | 
			
		||||
async function query(sql, args) {
 | 
			
		||||
	let rows = [];
 | 
			
		||||
	await ssb.sqlAsync(sql, args ?? [], function (row) {
 | 
			
		||||
		rows.push(row);
 | 
			
		||||
	});
 | 
			
		||||
	return rows;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function get_biggest() {
 | 
			
		||||
	return query(`
 | 
			
		||||
		select author, sum(length(content)) as size from messages group by author order by size desc limit 10;
 | 
			
		||||
	`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function get_total() {
 | 
			
		||||
	return (
 | 
			
		||||
		await query(`
 | 
			
		||||
		select sum(length(content)) as size, count(distinct author) as count from messages;
 | 
			
		||||
	`)
 | 
			
		||||
	)[0];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function get_names(identities) {
 | 
			
		||||
	return query(
 | 
			
		||||
		`
 | 
			
		||||
		SELECT author, name FROM (
 | 
			
		||||
			SELECT
 | 
			
		||||
				messages.author,
 | 
			
		||||
				RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
 | 
			
		||||
				messages.content ->> 'name' AS name
 | 
			
		||||
			FROM messages
 | 
			
		||||
			JOIN json_each(?) AS identities ON identities.value = messages.author
 | 
			
		||||
			WHERE
 | 
			
		||||
				json_extract(messages.content, '$.type') = 'about' AND
 | 
			
		||||
				content ->> 'about' = messages.author AND name IS NOT NULL)
 | 
			
		||||
		WHERE author_rank = 1
 | 
			
		||||
	`,
 | 
			
		||||
		[JSON.stringify(identities)]
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function get_most_follows() {
 | 
			
		||||
	return query(`
 | 
			
		||||
		select author, count(*) as count
 | 
			
		||||
		from messages
 | 
			
		||||
		where content ->> 'type' = 'contact' and content ->> 'following' = true
 | 
			
		||||
		group by author
 | 
			
		||||
		order by count desc
 | 
			
		||||
		limit 10;
 | 
			
		||||
	`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function nice_size(bytes) {
 | 
			
		||||
	let value = bytes;
 | 
			
		||||
	let index = 0;
 | 
			
		||||
	let units = ['B', 'kB', 'MB', 'GB'];
 | 
			
		||||
	while (value > 1024 && index < units.length - 1) {
 | 
			
		||||
		value /= 1024;
 | 
			
		||||
		index++;
 | 
			
		||||
	}
 | 
			
		||||
	return `${Math.round(value * 10) / 10} ${units[index]}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	await app.setDocument(
 | 
			
		||||
		'<p style="color: #fff">Finding the top 10 largest feeds...</p>'
 | 
			
		||||
	);
 | 
			
		||||
	let most_follows = await get_most_follows();
 | 
			
		||||
	let total = await get_total();
 | 
			
		||||
	let identities = await ssb.getAllIdentities();
 | 
			
		||||
	let following1 = await ssb.following(identities, 1);
 | 
			
		||||
	let following2 = await ssb.following(identities, 2);
 | 
			
		||||
	let biggest = await get_biggest();
 | 
			
		||||
	let names = await get_names(
 | 
			
		||||
		[].concat(
 | 
			
		||||
			biggest.map((x) => x.author),
 | 
			
		||||
			most_follows.map((x) => x.author)
 | 
			
		||||
		)
 | 
			
		||||
	);
 | 
			
		||||
	names = Object.fromEntries(names.map((x) => [x.author, x.name]));
 | 
			
		||||
	for (let item of biggest) {
 | 
			
		||||
		item.name = names[item.author];
 | 
			
		||||
		item.following =
 | 
			
		||||
			identities.indexOf(item.author) != -1
 | 
			
		||||
				? 0
 | 
			
		||||
				: following1[item.author] !== undefined
 | 
			
		||||
					? 1
 | 
			
		||||
					: following2[item.author] !== undefined
 | 
			
		||||
						? 2
 | 
			
		||||
						: undefined;
 | 
			
		||||
	}
 | 
			
		||||
	for (let item of most_follows) {
 | 
			
		||||
		item.name = names[item.author];
 | 
			
		||||
	}
 | 
			
		||||
	let html = `<body style="color: #000; background-color: #ddd">\n
 | 
			
		||||
		<h1>Storage Summary</h1>
 | 
			
		||||
		<h2>Top 10 Accounts by Size</h2>
 | 
			
		||||
		<ol>`;
 | 
			
		||||
	for (let item of biggest) {
 | 
			
		||||
		html += `<li>
 | 
			
		||||
			<span style="color: #888">${nice_size(item.size)}</span>
 | 
			
		||||
			<a target="_top" href="/~core/ssb/#${encodeURI(item.author)}">${item.name ?? item.author}</a>
 | 
			
		||||
		</li>
 | 
			
		||||
		\n`;
 | 
			
		||||
	}
 | 
			
		||||
	html += `
 | 
			
		||||
		</ol>
 | 
			
		||||
		<h2>Top 10 Accounts by Follows</h2>
 | 
			
		||||
		<ol>`;
 | 
			
		||||
	for (let item of most_follows) {
 | 
			
		||||
		html += `<li>
 | 
			
		||||
			<span style="color: #888">${item.count}</span>
 | 
			
		||||
			${following2[item.author] ? '✅' : '🚫'}
 | 
			
		||||
			<a target="_top" href="/~core/ssb/#${encodeURI(item.author)}">${item.name ?? item.author}</a>
 | 
			
		||||
		</li>
 | 
			
		||||
		\n`;
 | 
			
		||||
	}
 | 
			
		||||
	html += `
 | 
			
		||||
		</ol>
 | 
			
		||||
		<p>Total <span style="color: #888">${nice_size(total.size)}</span> in ${total.count} accounts.</p>
 | 
			
		||||
	`;
 | 
			
		||||
	await app.setDocument(html);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main().catch(function (e) {
 | 
			
		||||
	print(e);
 | 
			
		||||
});
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "📦"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
app.setDocument(
 | 
			
		||||
	'<p style="color: #fff">Maybe one day this app will run tests, but for now there is nothing to see here.</p>'
 | 
			
		||||
);
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
Hello, world!
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "👋",
 | 
			
		||||
	"previous": "&wAb7J6E35xEXpiXsQ6t1RaWTGIvlatUnyH8ipF6pVic=.sha256"
 | 
			
		||||
	"previous": "&W5aJp2DgOW5rQ0AOIC9Ut3DpsahPrO6PjkJ1PQbNRdM=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,78 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
 | 
			
		||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="48px" height="48px" id="svg3832" version="1.1" inkscape:version="0.47 r22583" sodipodi:docname="appimage-assistant_alt3.svg">
 | 
			
		||||
  <defs id="defs3834">
 | 
			
		||||
    <linearGradient inkscape:collect="always" xlink:href="#linearGradient3308-4-6-931-761-0" id="linearGradient2975" gradientUnits="userSpaceOnUse" x1="24.3125" y1="22.96875" x2="24.3125" y2="41.03125"/>
 | 
			
		||||
    <linearGradient id="linearGradient3308-4-6-931-761-0">
 | 
			
		||||
      <stop id="stop2919-2" style="stop-color:#ffffff;stop-opacity:1" offset="0"/>
 | 
			
		||||
      <stop id="stop2921-76" style="stop-color:#ffffff;stop-opacity:0" offset="1"/>
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
    <linearGradient inkscape:collect="always" xlink:href="#linearGradient4222" id="linearGradient2979" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0,0.3704967,-0.3617496,0,33.508315,6.1670925)" x1="7.6485429" y1="26.437023" x2="41.861729" y2="26.437023"/>
 | 
			
		||||
    <linearGradient id="linearGradient4222">
 | 
			
		||||
      <stop id="stop4224" style="stop-color:#ffffff;stop-opacity:1" offset="0"/>
 | 
			
		||||
      <stop id="stop4226" style="stop-color:#ffffff;stop-opacity:0" offset="1"/>
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
    <linearGradient inkscape:collect="always" xlink:href="#linearGradient3308-4-6-931-761" id="linearGradient2982" gradientUnits="userSpaceOnUse" gradientTransform="translate(0,0.9999987)" x1="23.99999" y1="4.999989" x2="23.99999" y2="43"/>
 | 
			
		||||
    <linearGradient id="linearGradient3308-4-6-931-761">
 | 
			
		||||
      <stop id="stop2919" style="stop-color:#ffffff;stop-opacity:1" offset="0"/>
 | 
			
		||||
      <stop id="stop2921" style="stop-color:#ffffff;stop-opacity:0" offset="1"/>
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
    <radialGradient inkscape:collect="always" xlink:href="#linearGradient3575" id="radialGradient2985" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0,1.0262008,-1.6561124,9.4072203e-4,-56.097482,-45.332325)" cx="48.42384" cy="-48.027504" fx="48.42384" fy="-48.027504" r="38.212933"/>
 | 
			
		||||
    <linearGradient id="linearGradient3575">
 | 
			
		||||
      <stop id="stop3577" style="stop-color:#fafafa;stop-opacity:1" offset="0"/>
 | 
			
		||||
      <stop id="stop3579" style="stop-color:#e6e6e6;stop-opacity:1" offset="1"/>
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
    <radialGradient inkscape:collect="always" xlink:href="#linearGradient3993" id="radialGradient2990" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0,2.0478765,-2.7410544,-8.6412258e-8,47.161382,-8.837436)" cx="9.3330879" cy="8.4497671" fx="9.3330879" fy="8.4497671" r="19.99999"/>
 | 
			
		||||
    <linearGradient id="linearGradient3993">
 | 
			
		||||
      <stop offset="0" style="stop-color:#a3c0d0;stop-opacity:1" id="stop3995"/>
 | 
			
		||||
      <stop offset="1" style="stop-color:#427da1;stop-opacity:1" id="stop4001"/>
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
    <linearGradient inkscape:collect="always" xlink:href="#linearGradient2508" id="linearGradient2992" gradientUnits="userSpaceOnUse" gradientTransform="translate(0,0.9674382)" x1="14.048676" y1="44.137306" x2="14.048676" y2="4.0000005"/>
 | 
			
		||||
    <linearGradient id="linearGradient2508">
 | 
			
		||||
      <stop offset="0" style="stop-color:#2e4a5a;stop-opacity:1" id="stop2510"/>
 | 
			
		||||
      <stop offset="1" style="stop-color:#6e8796;stop-opacity:1" id="stop2512"/>
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
    <radialGradient cx="4.9929786" cy="43.5" r="2.5" fx="4.9929786" fy="43.5" id="radialGradient2873-966-168" xlink:href="#linearGradient3688-166-749" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.003784,0,0,1.4,27.98813,-17.4)"/>
 | 
			
		||||
    <linearGradient id="linearGradient3688-166-749">
 | 
			
		||||
      <stop id="stop2883" style="stop-color:#181818;stop-opacity:1" offset="0"/>
 | 
			
		||||
      <stop id="stop2885" style="stop-color:#181818;stop-opacity:0" offset="1"/>
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
    <radialGradient cx="4.9929786" cy="43.5" r="2.5" fx="4.9929786" fy="43.5" id="radialGradient2875-742-326" xlink:href="#linearGradient3688-464-309" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.003784,0,0,1.4,-20.01187,-104.4)"/>
 | 
			
		||||
    <linearGradient id="linearGradient3688-464-309">
 | 
			
		||||
      <stop id="stop2889" style="stop-color:#181818;stop-opacity:1" offset="0"/>
 | 
			
		||||
      <stop id="stop2891" style="stop-color:#181818;stop-opacity:0" offset="1"/>
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
    <linearGradient x1="25.058096" y1="47.027729" x2="25.058096" y2="39.999443" id="linearGradient2877-634-617" xlink:href="#linearGradient3702-501-757" gradientUnits="userSpaceOnUse"/>
 | 
			
		||||
    <linearGradient id="linearGradient3702-501-757">
 | 
			
		||||
      <stop id="stop2895" style="stop-color:#181818;stop-opacity:0" offset="0"/>
 | 
			
		||||
      <stop id="stop2897" style="stop-color:#181818;stop-opacity:1" offset="0.5"/>
 | 
			
		||||
      <stop id="stop2899" style="stop-color:#181818;stop-opacity:0" offset="1"/>
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
  </defs>
 | 
			
		||||
  <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="7" inkscape:cx="24" inkscape:cy="24" inkscape:current-layer="layer1" showgrid="true" inkscape:grid-bbox="true" inkscape:document-units="px" inkscape:window-width="603" inkscape:window-height="484" inkscape:window-x="417" inkscape:window-y="162" inkscape:window-maximized="0"/>
 | 
			
		||||
  <metadata id="metadata3837">
 | 
			
		||||
    <rdf:RDF>
 | 
			
		||||
      <cc:Work rdf:about="">
 | 
			
		||||
        <dc:format>image/svg+xml</dc:format>
 | 
			
		||||
        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
 | 
			
		||||
        <dc:title/>
 | 
			
		||||
      </cc:Work>
 | 
			
		||||
    </rdf:RDF>
 | 
			
		||||
  </metadata>
 | 
			
		||||
  <g id="layer1" inkscape:label="Layer 1" inkscape:groupmode="layer">
 | 
			
		||||
    <g style="display:inline" id="g2036" transform="matrix(1.1,0,0,0.4444449,-2.4000022,25.11107)">
 | 
			
		||||
      <g style="opacity:0.4" id="g3712" transform="matrix(1.052632,0,0,1.285713,-1.263158,-13.42854)">
 | 
			
		||||
        <rect style="fill:url(#radialGradient2873-966-168);fill-opacity:1;stroke:none" id="rect2801" y="40" x="38" height="7" width="5"/>
 | 
			
		||||
        <rect style="fill:url(#radialGradient2875-742-326);fill-opacity:1;stroke:none" id="rect3696" transform="scale(-1,-1)" y="-47" x="-10" height="7" width="5"/>
 | 
			
		||||
        <rect style="fill:url(#linearGradient2877-634-617);fill-opacity:1;stroke:none" id="rect3700" y="40" x="10" height="7.0000005" width="28"/>
 | 
			
		||||
      </g>
 | 
			
		||||
    </g>
 | 
			
		||||
    <rect style="fill:url(#radialGradient2990);fill-opacity:1;stroke:url(#linearGradient2992);stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" id="rect5505" y="5.4674392" x="4.5" ry="2.2322156" rx="2.2322156" height="39" width="39"/>
 | 
			
		||||
    <path style="opacity:0.05;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.00178742;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path4294-1" d="m 21,6.9687498 a 2.0165107,2.0165107 0 0 0 -2.03125,2.03125 l 0,3.9687502 -1.15625,0 a 2.0165107,2.0165107 0 0 0 -1.5,3.375 l 5.0625,5.75 c -0.06312,0.110777 -0.178724,0.246032 -0.21875,0.34375 -0.195898,0.478256 -0.25,0.83653 -0.25,1.21875 l 0,0.125 L 20.8125,23.6875 C 20.534322,23.409323 20.213169,23.162739 19.71875,22.96875 19.47154,22.87176 19.185456,22.791748 18.75,22.8125 c -0.435456,0.02075 -1.054055,0.210302 -1.46875,0.625 L 15.75,24.96875 c -0.414689,0.414689 -0.604245,1.033294 -0.625,1.46875 -0.02075,0.435456 0.05925,0.721537 0.15625,0.96875 C 15.475241,27.900677 15.721817,28.221821 16,28.5 l 0.09375,0.09375 -0.125,0 c -0.382218,0 -0.740493,0.0541 -1.21875,0.25 -0.239128,0.09795 -0.538285,0.214988 -0.84375,0.53125 -0.305465,0.316262 -0.625,0.914788 -0.625,1.53125 l 0,2.1875 c 0,0.616465 0.319536,1.214989 0.625,1.53125 0.305464,0.316261 0.604622,0.433301 0.84375,0.53125 0.478256,0.195898 0.83653,0.25 1.21875,0.25 l 0.125,0 L 16,35.5 c -0.278175,0.278176 -0.52476,0.599329 -0.71875,1.09375 -0.09699,0.24721 -0.177003,0.533292 -0.15625,0.96875 0.02075,0.435458 0.210304,1.054058 0.625,1.46875 l 1.53125,1.53125 c 0.414691,0.414697 1.033292,0.604245 1.46875,0.625 0.435458,0.02076 0.721537,-0.05926 0.96875,-0.15625 0.494425,-0.19399 0.81557,-0.440568 1.09375,-0.71875 l 0.09375,-0.09375 0,0.125 c 0,0.38222 0.0541,0.740495 0.25,1.21875 0.09795,0.239127 0.214989,0.538285 0.53125,0.84375 0.316261,0.305465 0.914783,0.625 1.53125,0.625 l 2.1875,0 c 0.616466,0 1.214989,-0.319534 1.53125,-0.625 0.316261,-0.305466 0.433302,-0.604622 0.53125,-0.84375 0.195896,-0.478255 0.25,-0.836532 0.25,-1.21875 l 0,-0.125 0.09375,0.09375 c 0.278176,0.278175 0.599329,0.52476 1.09375,0.71875 0.24721,0.09699 0.533292,0.177003 0.96875,0.15625 0.435458,-0.02075 1.054058,-0.210304 1.46875,-0.625 L 32.875,39.03125 C 33.289697,38.616559 33.479245,37.997958 33.5,37.5625 33.52076,37.127042 33.44074,36.840963 33.34375,36.59375 33.14976,36.099325 32.903182,35.77818 32.625,35.5 l -0.09375,-0.09375 0.125,0 c 0.38222,0 0.740494,-0.0541 1.21875,-0.25 0.239128,-0.09795 0.538286,-0.214988 0.84375,-0.53125 0.305464,-0.316262 0.625,-0.914787 0.625,-1.53125 l 0,-2.1875 c 0,-0.61646 -0.319535,-1.214987 -0.625,-1.53125 -0.305465,-0.316263 -0.604621,-0.433301 -0.84375,-0.53125 -0.478257,-0.195898 -0.836532,-0.25 -1.21875,-0.25 l -0.125,0 L 32.625,28.5 c 0.278177,-0.278177 0.52476,-0.599329 0.71875,-1.09375 C 33.44074,27.15904 33.520753,26.872957 33.5,26.4375 33.47925,26.002043 33.289697,25.383443 32.875,24.96875 L 31.34375,23.4375 c -0.414688,-0.414694 -1.03329,-0.604245 -1.46875,-0.625 -0.43546,-0.02076 -0.721537,0.05925 -0.96875,0.15625 -0.494426,0.193991 -0.815572,0.44057 -1.09375,0.71875 l -0.09375,0.09375 0,-0.125 c 0,-0.382218 -0.0541,-0.740493 -0.25,-1.21875 -0.09112,-0.22245 -0.228127,-0.500183 -0.5,-0.78125 l 4.71875,-5.3125 a 2.0165107,2.0165107 0 0 0 -1.5,-3.375 l -1.15625,0 0,-3.9687502 A 2.0165107,2.0165107 0 0 0 27,6.9687498 l -6,0 z M 24.3125,31.25 c 0.427097,0 0.75,0.322904 0.75,0.75 0,0.427096 -0.322903,0.75 -0.75,0.75 -0.427094,0 -0.75,-0.322906 -0.75,-0.75 0,-0.427094 0.322906,-0.75 0.75,-0.75 z"/>
 | 
			
		||||
    <path style="opacity:0.05;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.00178742;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path4294" d="m 20.90625,8.0312498 a 0.96385067,0.96385067 0 0 0 -0.875,0.96875 l 0,5.0312502 -2.21875,0 A 0.96385067,0.96385067 0 0 0 17.09375,15.625 l 5.78125,6.53125 c -0.158814,0.0616 -0.341836,0.0951 -0.4375,0.1875 -0.169161,0.163386 -0.252971,0.323419 -0.3125,0.46875 -0.119058,0.290663 -0.15625,0.566746 -0.15625,0.84375 l 0,1.65625 C 21.718163,25.40233 21.485871,25.509772 21.25,25.625 l -1.1875,-1.1875 c -0.199651,-0.19965 -0.421433,-0.352095 -0.71875,-0.46875 -0.148659,-0.05833 -0.329673,-0.104846 -0.5625,-0.09375 -0.232827,0.0111 -0.53583,0.09833 -0.75,0.3125 L 16.5,25.71875 c -0.214168,0.214168 -0.301403,0.517173 -0.3125,0.75 -0.0111,0.232827 0.03542,0.41384 0.09375,0.5625 0.116655,0.297321 0.269096,0.519099 0.46875,0.71875 l 1.1875,1.1875 c -0.115228,0.235871 -0.222668,0.468163 -0.3125,0.71875 l -1.65625,0 c -0.277003,0 -0.553087,0.03719 -0.84375,0.15625 -0.145332,0.05953 -0.305363,0.143338 -0.46875,0.3125 -0.163387,0.169162 -0.3125,0.46403 -0.3125,0.78125 l 0,2.1875 c 0,0.317221 0.149114,0.612089 0.3125,0.78125 0.163386,0.169161 0.323419,0.252971 0.46875,0.3125 0.290663,0.119058 0.566746,0.15625 0.84375,0.15625 l 1.65625,0 c 0.08983,0.250587 0.197272,0.482879 0.3125,0.71875 L 16.75,36.25 c -0.199649,0.19965 -0.352095,0.421432 -0.46875,0.71875 -0.05833,0.148659 -0.104846,0.329672 -0.09375,0.5625 0.0111,0.232828 0.09833,0.535831 0.3125,0.75 l 1.53125,1.53125 c 0.214168,0.214172 0.517172,0.301403 0.75,0.3125 0.232828,0.0111 0.41384,-0.03542 0.5625,-0.09375 0.29732,-0.116655 0.519098,-0.269096 0.71875,-0.46875 L 21.25,38.375 c 0.235871,0.115228 0.468164,0.222668 0.71875,0.3125 l 0,1.65625 c 0,0.277003 0.03719,0.553087 0.15625,0.84375 0.05953,0.145331 0.143339,0.305364 0.3125,0.46875 0.169161,0.163386 0.464028,0.3125 0.78125,0.3125 l 2.1875,0 c 0.317221,0 0.612089,-0.149113 0.78125,-0.3125 0.169161,-0.163387 0.252971,-0.323419 0.3125,-0.46875 0.119057,-0.290663 0.15625,-0.566748 0.15625,-0.84375 l 0,-1.65625 c 0.250586,-0.08983 0.482879,-0.197272 0.71875,-0.3125 l 1.1875,1.1875 c 0.19965,0.199649 0.421432,0.352095 0.71875,0.46875 0.148659,0.05833 0.329672,0.104846 0.5625,0.09375 0.232828,-0.0111 0.535831,-0.09833 0.75,-0.3125 L 32.125,38.28125 c 0.214172,-0.214168 0.301403,-0.517172 0.3125,-0.75 0.0111,-0.232828 -0.03542,-0.41384 -0.09375,-0.5625 C 32.227095,36.67143 32.074654,36.449652 31.875,36.25 L 30.6875,35.0625 C 30.802728,34.82663 30.910168,34.594337 31,34.34375 l 1.65625,0 c 0.277004,0 0.553087,-0.03719 0.84375,-0.15625 0.145332,-0.05953 0.305364,-0.143339 0.46875,-0.3125 0.163386,-0.169161 0.3125,-0.46403 0.3125,-0.78125 l 0,-2.1875 c 0,-0.317219 -0.149114,-0.612088 -0.3125,-0.78125 C 33.805364,29.955838 33.645332,29.872029 33.5,29.8125 33.209336,29.693442 32.933253,29.65625 32.65625,29.65625 l -1.65625,0 C 30.91017,29.405663 30.802728,29.17337 30.6875,28.9375 L 31.875,27.75 c 0.19965,-0.19965 0.352095,-0.421432 0.46875,-0.71875 0.05833,-0.148659 0.104846,-0.329672 0.09375,-0.5625 -0.0111,-0.232828 -0.09833,-0.535831 -0.3125,-0.75 L 30.59375,24.1875 c -0.214167,-0.21417 -0.517171,-0.301403 -0.75,-0.3125 -0.232829,-0.0111 -0.41384,0.03542 -0.5625,0.09375 -0.29732,0.116656 -0.519099,0.269097 -0.71875,0.46875 L 27.375,25.625 c -0.235871,-0.115228 -0.468163,-0.222668 -0.71875,-0.3125 l 0,-1.65625 c 0,-0.277003 -0.03719,-0.553087 -0.15625,-0.84375 -0.05953,-0.145332 -0.143338,-0.305363 -0.3125,-0.46875 -0.169162,-0.163387 -0.46403,-0.3125 -0.78125,-0.3125 l -0.15625,0 5.65625,-6.40625 A 0.96385067,0.96385067 0 0 0 30.1875,14.03125 l -2.21875,0 0,-5.0312502 A 0.96385067,0.96385067 0 0 0 27,8.0312498 l -6,0 a 0.96385067,0.96385067 0 0 0 -0.09375,0 z M 24.3125,30.1875 c 1.002113,0 1.8125,0.810388 1.8125,1.8125 0,1.002112 -0.810387,1.8125 -1.8125,1.8125 C 23.31039,33.8125 22.5,33.002111 22.5,32 c 0,-1.002111 0.81039,-1.8125 1.8125,-1.8125 z"/>
 | 
			
		||||
    <path style="fill:url(#radialGradient2985);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.00178742;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path2317" d="M 21,8.9999996 21,15 17.8125,15 24,22 30.1875,15 27,15 l 0,-6.0000004 -6,0 z M 23.21875,23 c -0.172892,0 -0.28125,0.294922 -0.28125,0.65625 l 0,2.28125 C 22.24145,26.095996 21.585954,26.379869 21,26.75 l -1.625,-1.625 c -0.255498,-0.255497 -0.533998,-0.372253 -0.65625,-0.25 l -1.53125,1.53125 c -0.122254,0.122254 -0.0055,0.400753 0.25,0.65625 l 1.625,1.625 c -0.37013,0.585953 -0.654003,1.24145 -0.8125,1.9375 l -2.28125,0 c -0.361328,0 -0.65625,0.108357 -0.65625,0.28125 l 0,2.1875 c 0,0.172892 0.294922,0.28125 0.65625,0.28125 l 2.28125,0 c 0.158497,0.69605 0.44237,1.351546 0.8125,1.9375 l -1.625,1.625 c -0.255497,0.255498 -0.372254,0.533997 -0.25,0.65625 l 1.53125,1.53125 c 0.122252,0.122254 0.400752,0.0055 0.65625,-0.25 L 21,37.25 c 0.585954,0.37013 1.24145,0.654002 1.9375,0.8125 l 0,2.28125 C 22.9375,40.705077 23.045858,41 23.21875,41 l 2.1875,0 c 0.172893,0 0.28125,-0.294924 0.28125,-0.65625 l 0,-2.28125 c 0.69605,-0.158498 1.351546,-0.44237 1.9375,-0.8125 l 1.625,1.625 c 0.255498,0.255497 0.533997,0.372254 0.65625,0.25 l 1.53125,-1.53125 c 0.122254,-0.122252 0.0055,-0.400752 -0.25,-0.65625 l -1.625,-1.625 c 0.370129,-0.585954 0.654003,-1.24145 0.8125,-1.9375 l 2.28125,0 c 0.361329,0 0.65625,-0.108358 0.65625,-0.28125 l 0,-2.1875 c 0,-0.172893 -0.294921,-0.28125 -0.65625,-0.28125 l -2.28125,0 c -0.158497,-0.69605 -0.442371,-1.351547 -0.8125,-1.9375 l 1.625,-1.625 c 0.255497,-0.255497 0.372254,-0.533997 0.25,-0.65625 L 29.90625,24.875 C 29.783997,24.752745 29.505498,24.8695 29.25,25.125 l -1.625,1.625 c -0.585954,-0.370131 -1.24145,-0.654004 -1.9375,-0.8125 l 0,-2.28125 C 25.6875,23.294922 25.579143,23 25.40625,23 l -2.1875,0 z m 1.09375,6.21875 c 1.528616,0 2.78125,1.252635 2.78125,2.78125 0,1.528615 -1.252634,2.78125 -2.78125,2.78125 -1.528614,0 -2.78125,-1.252635 -2.78125,-2.78125 0,-1.528615 1.252636,-2.78125 2.78125,-2.78125 z"/>
 | 
			
		||||
    <rect style="opacity:0.4;fill:none;stroke:url(#linearGradient2982);stroke-width:0.99999976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" id="rect6741" y="6.4999886" x="5.4999981" ry="1.365193" rx="1.365193" height="37.000011" width="36.999985"/>
 | 
			
		||||
    <path style="fill:none;stroke:url(#linearGradient2979);stroke-width:0.99829447;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible" id="path2777" d="M 28.926376,15.466668 24,21.177578 18.963089,15.5 21.5,15.5 l 0,-6.0000004 5,0 0,6.0000004 2.426376,-0.03333 z"/>
 | 
			
		||||
    <path style="fill:none;stroke:url(#linearGradient2975);stroke-width:1;stroke-opacity:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" id="path4243" d="m 23.4375,23.46875 c -0.01166,0.05381 -0.03125,0.100205 -0.03125,0.1875 l 0,2.28125 a 0.48185467,0.48185467 0 0 1 -0.375,0.46875 c -0.638467,0.145384 -1.238423,0.407111 -1.78125,0.75 a 0.48185467,0.48185467 0 0 1 -0.59375,-0.0625 l -1.625,-1.625 C 18.9779,25.4154 18.9477,25.40242 18.90625,25.375 l -1.21875,1.21875 c 0.02742,0.04145 0.0404,0.07165 0.09375,0.125 l 1.625,1.625 a 0.48185467,0.48185467 0 0 1 0.0625,0.59375 c -0.342888,0.542826 -0.604615,1.142782 -0.75,1.78125 a 0.48185467,0.48185467 0 0 1 -0.46875,0.375 l -2.28125,0 c -0.08729,0 -0.133695,0.01959 -0.1875,0.03125 l 0,1.75 c 0.05381,0.01166 0.100205,0.03125 0.1875,0.03125 l 2.28125,0 a 0.48185467,0.48185467 0 0 1 0.46875,0.375 c 0.145385,0.638468 0.407112,1.238423 0.75,1.78125 a 0.48185467,0.48185467 0 0 1 -0.0625,0.59375 l -1.625,1.625 c -0.05335,0.05335 -0.06633,0.08355 -0.09375,0.125 l 1.21875,1.21875 c 0.04145,-0.02742 0.07165,-0.0404 0.125,-0.09375 l 1.625,-1.625 A 0.48185467,0.48185467 0 0 1 21.25,36.84375 c 0.542827,0.342888 1.142781,0.604614 1.78125,0.75 a 0.48185467,0.48185467 0 0 1 0.375,0.46875 l 0,2.28125 c 0,0.08729 0.01959,0.133695 0.03125,0.1875 l 1.75,0 c 0.01166,-0.0538 0.03125,-0.100206 0.03125,-0.1875 l 0,-2.28125 a 0.48185467,0.48185467 0 0 1 0.375,-0.46875 c 0.638469,-0.145386 1.238423,-0.407112 1.78125,-0.75 a 0.48185467,0.48185467 0 0 1 0.59375,0.0625 l 1.625,1.625 c 0.05335,0.05335 0.08355,0.06633 0.125,0.09375 l 1.21875,-1.21875 c -0.02742,-0.04145 -0.0404,-0.07165 -0.09375,-0.125 l -1.625,-1.625 a 0.48185467,0.48185467 0 0 1 -0.0625,-0.59375 c 0.342888,-0.542828 0.604615,-1.142783 0.75,-1.78125 a 0.48185467,0.48185467 0 0 1 0.46875,-0.375 l 2.28125,0 c 0.08729,0 0.133695,-0.01959 0.1875,-0.03125 l 0,-1.75 c -0.0538,-0.01166 -0.100204,-0.03125 -0.1875,-0.03125 l -2.28125,0 a 0.48185467,0.48185467 0 0 1 -0.46875,-0.375 c -0.145385,-0.638467 -0.407113,-1.238424 -0.75,-1.78125 a 0.48185467,0.48185467 0 0 1 0.0625,-0.59375 l 1.625,-1.625 c 0.05335,-0.05335 0.06633,-0.08355 0.09375,-0.125 L 29.71875,25.375 c -0.04145,0.02742 -0.07165,0.0404 -0.125,0.09375 l -1.625,1.625 a 0.48185467,0.48185467 0 0 1 -0.59375,0.0625 c -0.542827,-0.342889 -1.142783,-0.604616 -1.78125,-0.75 a 0.48185467,0.48185467 0 0 1 -0.375,-0.46875 l 0,-2.28125 c 0,-0.0873 -0.01959,-0.133695 -0.03125,-0.1875 l -1.75,0 z m 0.875,5.28125 c 1.791829,0 3.25,1.458172 3.25,3.25 0,1.791828 -1.458171,3.25 -3.25,3.25 -1.791827,0 -3.25,-1.458172 -3.25,-3.25 0,-1.791828 1.458173,-3.25 3.25,-3.25 z"/>
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 19 KiB  | 
@@ -1,75 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
 | 
			
		||||
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="48" height="48" viewBox="0 0 48.000001 48.000001" id="svg4230" version="1.1" inkscape:version="0.91 r13725" sodipodi:docname="fdroid-logo.svg">
 | 
			
		||||
  <defs id="defs4232">
 | 
			
		||||
    <linearGradient inkscape:collect="always" id="linearGradient5212">
 | 
			
		||||
      <stop style="stop-color:#ffffff;stop-opacity:0.09803922" offset="0" id="stop5214"/>
 | 
			
		||||
      <stop style="stop-color:#ffffff;stop-opacity:0" offset="1" id="stop5216"/>
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
    <radialGradient inkscape:collect="always" xlink:href="#linearGradient5212" id="radialGradient5220" cx="-98.23381" cy="3.4695871" fx="-98.23381" fy="3.4695871" r="22.671185" gradientTransform="matrix(0,1.9747624,-2.117225,3.9784049e-8,8.677247,1199.588)" gradientUnits="userSpaceOnUse"/>
 | 
			
		||||
    <filter inkscape:collect="always" style="color-interpolation-filters:sRGB" id="filter4175" x="-0.023846937" width="1.0476939" y="-0.02415504" height="1.0483101">
 | 
			
		||||
      <feGaussianBlur inkscape:collect="always" stdDeviation="0.45053152" id="feGaussianBlur4177"/>
 | 
			
		||||
    </filter>
 | 
			
		||||
  </defs>
 | 
			
		||||
  <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="11.313708" inkscape:cx="6.4184057" inkscape:cy="25.737489" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" units="px" inkscape:window-width="1920" inkscape:window-height="1009" inkscape:window-x="0" inkscape:window-y="34" inkscape:window-maximized="1" gridtolerance="10000"/>
 | 
			
		||||
  <metadata id="metadata4235">
 | 
			
		||||
    <rdf:RDF>
 | 
			
		||||
      <cc:Work rdf:about="">
 | 
			
		||||
        <dc:format>image/svg+xml</dc:format>
 | 
			
		||||
        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
 | 
			
		||||
        <dc:title/>
 | 
			
		||||
        <cc:license rdf:resource="http://creativecommons.org/licenses/by-sa/3.0/"/>
 | 
			
		||||
      </cc:Work>
 | 
			
		||||
      <cc:License rdf:about="http://creativecommons.org/licenses/by-sa/3.0/">
 | 
			
		||||
        <cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
 | 
			
		||||
        <cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
 | 
			
		||||
        <cc:requires rdf:resource="http://creativecommons.org/ns#Notice"/>
 | 
			
		||||
        <cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"/>
 | 
			
		||||
        <cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
 | 
			
		||||
        <cc:requires rdf:resource="http://creativecommons.org/ns#ShareAlike"/>
 | 
			
		||||
      </cc:License>
 | 
			
		||||
    </rdf:RDF>
 | 
			
		||||
  </metadata>
 | 
			
		||||
  <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(0,-1004.3622)">
 | 
			
		||||
    <path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#263238;fill-opacity:0.4;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter4175);color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 2.613462,1006.3488 a 1.250125,1.250125 0 0 0 -1.01172,2.0293 l 3.60351,4.6641 c -0.12699,0.3331 -0.20312,0.6915 -0.20312,1.0703 l 0,4 0,2.8652 0,0.1348 c 0,1.662 1.338,3 3,3 l 32,0 c 1.662,0 3,-1.338 3,-3 l 0,-4 0,-2.8652 0,-0.1348 c 0,-0.3803 -0.0771,-0.74 -0.20508,-1.0742 l 3.60156,-4.6602 a 1.250125,1.250125 0 0 0 -1.04882,-2.0273 1.250125,1.250125 0 0 0 -0.92969,0.498 l -3.43164,4.4414 c -0.31022,-0.1079 -0.63841,-0.1777 -0.98633,-0.1777 l -32,0 c -0.34857,0 -0.67757,0.069 -0.98828,0.1777 l -3.4336,-4.4414 a 1.250125,1.250125 0 0 0 -0.96679,-0.5 z m 5.38867,18.7637 c -0.20775,0 -0.40983,0.021 -0.60547,0.061 -1.36951,0.2761 -2.39453,1.4698 -2.39453,2.9101 l 0,0.029 0,19.7793 0,0.029 0,0.1914 c 0,1.662 1.338,3 3,3 l 32,0 c 1.662,0 3,-1.338 3,-3 l 0,-20 0,-0.029 c 0,-1.4403 -1.02502,-2.634 -2.39453,-2.9101 -0.19565,-0.039 -0.39772,-0.061 -0.60547,-0.061 l -32,0 z" id="path4192" inkscape:connector-curvature="0"/>
 | 
			
		||||
    <g id="g5012">
 | 
			
		||||
      <g id="g4179" transform="matrix(-1,0,0,1,47.999779,0)">
 | 
			
		||||
        <path style="fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:#769616;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 2.5889342,1006.8622 4.25,5.5" id="path4181" inkscape:connector-curvature="0" sodipodi:nodetypes="cc"/>
 | 
			
		||||
        <path sodipodi:nodetypes="cccccc" inkscape:connector-curvature="0" id="path4183" d="m 2.6113281,1005.6094 c -0.4534623,0.012 -0.7616975,0.189 -0.9807462,0.4486 2.0269314,2.4089 2.368401,2.7916 5.1354735,6.2214 1.0195329,1.319 2.0816026,0.6373 1.0620696,-0.6817 l -4.25,-5.5 c -0.2289894,-0.3056 -0.5850813,-0.478 -0.9667969,-0.4883 z" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:0.29803923;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"/>
 | 
			
		||||
        <path sodipodi:nodetypes="ccccc" inkscape:connector-curvature="0" id="path4185" d="m 1.6220992,1006.0705 c -0.1238933,0.1479 -0.561176,0.8046 -0.02249,1.5562 l 4.25,5.5 c 1.0195329,1.319 1.1498748,-0.6123 1.1498748,-0.6123 0,0 -3.7344514,-4.51 -5.3773848,-6.4439 z" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#263238;fill-opacity:0.2;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"/>
 | 
			
		||||
        <path sodipodi:nodetypes="cscccc" inkscape:connector-curvature="0" id="path4187" d="m 2.3378905,1005.8443 c -0.438175,0 -0.959862,0.1416 -0.8242183,0.7986 0.103561,0.5016 4.6608262,6.0744 4.6608262,6.0744 1.0195329,1.319 2.4934721,0.6763 1.4739391,-0.6425 l -4.234375,-5.4727 c -0.2602394,-0.29 -0.6085188,-0.7436 -1.076172,-0.7578 z" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"/>
 | 
			
		||||
      </g>
 | 
			
		||||
      <g id="g4955">
 | 
			
		||||
        <path sodipodi:nodetypes="cc" inkscape:connector-curvature="0" id="path4945" d="m 2.5889342,1006.8622 4.25,5.5" style="fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:#769616;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
 | 
			
		||||
        <path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:0.29803923;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 2.6113281,1005.6094 c -0.4534623,0.012 -0.7616975,0.189 -0.9807462,0.4486 2.0269314,2.4089 2.368401,2.7916 5.1354735,6.2214 1.0195329,1.319 2.0816026,0.6373 1.0620696,-0.6817 l -4.25,-5.5 c -0.2289894,-0.3056 -0.5850813,-0.478 -0.9667969,-0.4883 z" id="path4947" inkscape:connector-curvature="0" sodipodi:nodetypes="cccccc"/>
 | 
			
		||||
        <path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#263238;fill-opacity:0.2;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 1.6220992,1006.0705 c -0.1238933,0.1479 -0.561176,0.8046 -0.02249,1.5562 l 4.25,5.5 c 1.0195329,1.319 1.1498748,-0.6123 1.1498748,-0.6123 0,0 -3.7344514,-4.51 -5.3773848,-6.4439 z" id="path4951" inkscape:connector-curvature="0" sodipodi:nodetypes="ccccc"/>
 | 
			
		||||
        <path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#8ab000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 2.3378905,1005.8443 c -0.438175,0 -0.959862,0.1416 -0.8242183,0.7986 0.103561,0.5016 4.6608262,6.0744 4.6608262,6.0744 1.0195329,1.319 2.4934721,0.6763 1.4739391,-0.6425 l -4.234375,-5.4727 c -0.2602394,-0.29 -0.6085188,-0.7436 -1.076172,-0.7578 z" id="path4925" inkscape:connector-curvature="0" sodipodi:nodetypes="cscccc"/>
 | 
			
		||||
      </g>
 | 
			
		||||
      <g transform="translate(42,0)" id="g4967">
 | 
			
		||||
        <rect style="opacity:1;fill:#aeea00;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" id="rect4144" width="38" height="13" x="-37" y="1010.3622" rx="3" ry="3"/>
 | 
			
		||||
        <rect ry="3" rx="3" y="1013.3622" x="-37" height="10" width="38" id="rect4961" style="opacity:1;fill:#263238;fill-opacity:0.2;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
 | 
			
		||||
        <rect ry="3" rx="3" y="1010.3622" x="-37" height="10" width="38" id="rect4963" style="opacity:1;fill:#ffffff;fill-opacity:0.29803923;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
 | 
			
		||||
        <rect ry="2.5384617" rx="3" y="1011.3622" x="-37" height="11" width="38" id="rect4965" style="opacity:1;fill:#aeea00;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
 | 
			
		||||
      </g>
 | 
			
		||||
      <g id="g4979">
 | 
			
		||||
        <rect style="opacity:1;fill:#1976d2;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" id="rect4146" width="38" height="26" x="5" y="1024.3622" rx="3" ry="3"/>
 | 
			
		||||
        <rect ry="3" rx="3" y="1037.3622" x="5" height="13" width="38" id="rect4973" style="opacity:1;fill:#263238;fill-opacity:0.2;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
 | 
			
		||||
        <rect ry="3" rx="3" y="1024.3622" x="5" height="13" width="38" id="rect4975" style="opacity:1;fill:#ffffff;fill-opacity:0.2;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
 | 
			
		||||
        <rect ry="2.7692308" rx="3" y="1025.3622" x="5" height="24" width="38" id="rect4977" style="opacity:1;fill:#1976d2;fill-opacity:1;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"/>
 | 
			
		||||
      </g>
 | 
			
		||||
      <g transform="translate(0,1013.3622)" id="g4211">
 | 
			
		||||
        <path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#0d47a1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 24,17.75 c -2.880662,0 -5.319789,1.984685 -6.033203,4.650391 l 3.212891,0 C 21.734004,21.415044 22.774798,20.75 24,20.75 c 1.812692,0 3.25,1.437308 3.25,3.25 0,1.812693 -1.437308,3.25 -3.25,3.25 -1.307381,0 -2.411251,-0.75269 -2.929688,-1.849609 l -3.154296,0 C 18.558263,28.166146 21.04791,30.25 24,30.25 c 3.434013,0 6.25,-2.815987 6.25,-6.25 0,-3.434012 -2.815987,-6.25 -6.25,-6.25 z" id="path4161" inkscape:connector-curvature="0"/>
 | 
			
		||||
        <circle style="opacity:1;fill:none;fill-opacity:0.40392157;stroke:#0d47a1;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" id="path4209" cx="24" cy="24" r="9.5500002"/>
 | 
			
		||||
      </g>
 | 
			
		||||
      <g id="g4989" transform="translate(0,0.50001738)">
 | 
			
		||||
        <ellipse cy="1016.4872" cx="14.375" id="circle4985" style="opacity:1;fill:#263238;fill-opacity:0.2;stroke:none;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.69721117" rx="3.375" ry="3.875"/>
 | 
			
		||||
        <circle style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.69721117" id="path4859" cx="14.375" cy="1016.9872" r="3.375"/>
 | 
			
		||||
      </g>
 | 
			
		||||
      <g transform="translate(19.5,0.50001738)" id="g4171">
 | 
			
		||||
        <ellipse ry="3.875" rx="3.375" style="opacity:1;fill:#263238;fill-opacity:0.2;stroke:none;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.69721117" id="ellipse4175" cx="14.375" cy="1016.4872"/>
 | 
			
		||||
        <circle r="3.375" cy="1016.9872" cx="14.375" id="circle4177" style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.89999998;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.69721117"/>
 | 
			
		||||
      </g>
 | 
			
		||||
    </g>
 | 
			
		||||
    <path inkscape:connector-curvature="0" id="path5128" d="m 2.613462,1005.5987 a 1.250125,1.250125 0 0 0 -1.01172,2.0293 l 3.60351,4.6641 c -0.12699,0.3331 -0.20312,0.6915 -0.20312,1.0703 l 0,4 0,2.8652 0,0.1348 c 0,1.662 1.338,3 3,3 l 32,0 c 1.662,0 3,-1.338 3,-3 l 0,-4 0,-2.8652 0,-0.1348 c 0,-0.3803 -0.0771,-0.74 -0.20508,-1.0742 l 3.60156,-4.6602 a 1.250125,1.250125 0 0 0 -1.04882,-2.0273 1.250125,1.250125 0 0 0 -0.92969,0.498 l -3.43164,4.4414 c -0.31022,-0.1079 -0.63841,-0.1777 -0.98633,-0.1777 l -32,0 c -0.34857,0 -0.67757,0.069 -0.98828,0.1777 l -3.4336,-4.4414 a 1.250125,1.250125 0 0 0 -0.96679,-0.5 z m 5.38867,18.7637 c -0.20775,0 -0.40983,0.021 -0.60547,0.061 -1.36951,0.2761 -2.39453,1.4698 -2.39453,2.9101 l 0,0.029 0,19.7793 0,0.029 0,0.1914 c 0,1.662 1.338,3 3,3 l 32,0 c 1.662,0 3,-1.338 3,-3 l 0,-20 0,-0.029 c 0,-1.4403 -1.02502,-2.634 -2.39453,-2.9101 -0.19565,-0.039 -0.39772,-0.061 -0.60547,-0.061 l -32,0 z" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#radialGradient5220);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"/>
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 21 KiB  | 
@@ -1,23 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="iso-8859-1"?>
 | 
			
		||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
 | 
			
		||||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 
 | 
			
		||||
	 viewBox="0 0 511.999 511.999" xml:space="preserve">
 | 
			
		||||
<g>
 | 
			
		||||
	<path style="fill:#32BBFF;" d="M382.369,175.623C322.891,142.356,227.427,88.937,79.355,6.028
 | 
			
		||||
		C69.372-0.565,57.886-1.429,47.962,1.93l254.05,254.05L382.369,175.623z"/>
 | 
			
		||||
	<path style="fill:#32BBFF;" d="M47.962,1.93c-1.86,0.63-3.67,1.39-5.401,2.308C31.602,10.166,23.549,21.573,23.549,36v439.96
 | 
			
		||||
		c0,14.427,8.052,25.834,19.012,31.761c1.728,0.917,3.537,1.68,5.395,2.314L302.012,255.98L47.962,1.93z"/>
 | 
			
		||||
	<path style="fill:#32BBFF;" d="M302.012,255.98L47.956,510.035c9.927,3.384,21.413,2.586,31.399-4.103
 | 
			
		||||
		c143.598-80.41,237.986-133.196,298.152-166.746c1.675-0.941,3.316-1.861,4.938-2.772L302.012,255.98z"/>
 | 
			
		||||
</g>
 | 
			
		||||
<path style="fill:#2C9FD9;" d="M23.549,255.98v219.98c0,14.427,8.052,25.834,19.012,31.761c1.728,0.917,3.537,1.68,5.395,2.314
 | 
			
		||||
	L302.012,255.98H23.549z"/>
 | 
			
		||||
<path style="fill:#29CC5E;" d="M79.355,6.028C67.5-1.8,53.52-1.577,42.561,4.239l255.595,255.596l84.212-84.212
 | 
			
		||||
	C322.891,142.356,227.427,88.937,79.355,6.028z"/>
 | 
			
		||||
<path style="fill:#D93F21;" d="M298.158,252.126L42.561,507.721c10.96,5.815,24.939,6.151,36.794-1.789
 | 
			
		||||
	c143.598-80.41,237.986-133.196,298.152-166.746c1.675-0.941,3.316-1.861,4.938-2.772L298.158,252.126z"/>
 | 
			
		||||
<path style="fill:#FFD500;" d="M488.45,255.98c0-12.19-6.151-24.492-18.342-31.314c0,0-22.799-12.721-92.682-51.809l-83.123,83.123
 | 
			
		||||
	l83.204,83.205c69.116-38.807,92.6-51.892,92.6-51.892C482.299,280.472,488.45,268.17,488.45,255.98z"/>
 | 
			
		||||
<path style="fill:#FFAA00;" d="M470.108,287.294c12.191-6.822,18.342-19.124,18.342-31.314H294.303l83.204,83.205
 | 
			
		||||
	C446.624,300.379,470.108,287.294,470.108,287.294z"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 1.8 KiB  | 
@@ -10,6 +10,17 @@
 | 
			
		||||
		<link rel="stylesheet" href="brands.min.css" />
 | 
			
		||||
 | 
			
		||||
		<style>
 | 
			
		||||
			body,
 | 
			
		||||
			h1,
 | 
			
		||||
			h2,
 | 
			
		||||
			h3,
 | 
			
		||||
			h4,
 | 
			
		||||
			h5 {
 | 
			
		||||
				font-family: 'Poppins', sans-serif;
 | 
			
		||||
			}
 | 
			
		||||
			body {
 | 
			
		||||
				font-size: 16px;
 | 
			
		||||
			}
 | 
			
		||||
			img {
 | 
			
		||||
				margin-bottom: -8px;
 | 
			
		||||
			}
 | 
			
		||||
@@ -28,14 +39,11 @@
 | 
			
		||||
						<b>😎 Tilde Friends</b>
 | 
			
		||||
					</h1>
 | 
			
		||||
					<h1 class="w3-xxlarge w3-text-green">
 | 
			
		||||
						<b
 | 
			
		||||
							>the Secure Scuttlebutt decentralized social network client that's
 | 
			
		||||
							<i>fancy🎩</i></b
 | 
			
		||||
						>
 | 
			
		||||
						<b>Make apps and friends from the comfort of your web browser.</b>
 | 
			
		||||
					</h1>
 | 
			
		||||
					<p>
 | 
			
		||||
						In addition to participating in Secure Scuttlebutt, Tilde Friends is
 | 
			
		||||
						a platform for building, running, and sharing applications.
 | 
			
		||||
						Tilde Friends is a platform for building, running, and sharing web
 | 
			
		||||
						applications.
 | 
			
		||||
					</p>
 | 
			
		||||
					<p>
 | 
			
		||||
						Available for lots of devices:
 | 
			
		||||
@@ -52,41 +60,14 @@
 | 
			
		||||
					>
 | 
			
		||||
					<a
 | 
			
		||||
						class="w3-button w3-black w3-padding-large"
 | 
			
		||||
						href="https://www.tildefriends.net/~core/ssb/"
 | 
			
		||||
						href="https://www.tildefriends.net/~cory/apps/"
 | 
			
		||||
						><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
 | 
			
		||||
						><i class="fa fa-mug-hot"></i> Code</a
 | 
			
		||||
					>
 | 
			
		||||
					<a
 | 
			
		||||
						class="w3-button w3-black w3-padding-large"
 | 
			
		||||
						href="https://docs.tildefriends.net/"
 | 
			
		||||
						><i class="fa fa-book"></i> Documentation</a
 | 
			
		||||
					>
 | 
			
		||||
					<p>
 | 
			
		||||
						<a
 | 
			
		||||
							class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
							href="https://f-droid.org/en/packages/com.unprompted.tildefriends.fdroid/"
 | 
			
		||||
							><img src="f-droid.svg" style="height: 2em; margin: 0" /> Get it
 | 
			
		||||
							on F-Droid</a
 | 
			
		||||
						>
 | 
			
		||||
						<a
 | 
			
		||||
							class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
							href="https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage"
 | 
			
		||||
						>
 | 
			
		||||
							<img src="appimage.svg" style="height: 2em; margin: 0" />
 | 
			
		||||
							Get Linux 64-bit AppImage
 | 
			
		||||
						</a>
 | 
			
		||||
						<a
 | 
			
		||||
							class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
 | 
			
		||||
							href="https://play.google.com/store/apps/details?id=com.unprompted.tildefriends"
 | 
			
		||||
						>
 | 
			
		||||
							<img src="googleplay.svg" style="height: 2em; margin: 0" />
 | 
			
		||||
							Get it on Google Play (Open Testing)
 | 
			
		||||
						</a>
 | 
			
		||||
					</p>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="w3-col l4 m6">
 | 
			
		||||
					<img src="tildefriends.png" class="w3-image w3-right w3-hide-small" />
 | 
			
		||||
@@ -107,31 +88,36 @@
 | 
			
		||||
							<a href="https://dev.tildefriends.net/cory/tildefriends/releases"
 | 
			
		||||
								>Download</a
 | 
			
		||||
							>
 | 
			
		||||
							Tilde Friends or use
 | 
			
		||||
							Tilde Friends and run your own instance, or use
 | 
			
		||||
							<a href="https://www.tildefriends.net/"
 | 
			
		||||
								>https://www.tildefriends.net/</a
 | 
			
		||||
							>.
 | 
			
		||||
						</li>
 | 
			
		||||
						<li>Create an account to identify yourself with that instance.</li>
 | 
			
		||||
						<li>
 | 
			
		||||
							Create an account to identify yourself with that instance by
 | 
			
		||||
							username and password.
 | 
			
		||||
						</li>
 | 
			
		||||
						<li>
 | 
			
		||||
							Create an SSB identity in the <b>ssb</b> app. This will generate a
 | 
			
		||||
							keypair used to identify yourself to other users and sign your
 | 
			
		||||
							messages so that they can be verified as from you.
 | 
			
		||||
						</li>
 | 
			
		||||
						<li>
 | 
			
		||||
							Describe yourself in your profile in the <b>ssb</b> app. Give
 | 
			
		||||
							yourself a name and an avatar if you like.
 | 
			
		||||
						</li>
 | 
			
		||||
						<li>
 | 
			
		||||
							Connect to others.
 | 
			
		||||
							<ul>
 | 
			
		||||
								<li>Automatically discover peers on the same network.</li>
 | 
			
		||||
								<li>
 | 
			
		||||
									Manually connect to rooms and pubs, including
 | 
			
		||||
									<a href="https://www.tildefriends.net/~cory/room/"
 | 
			
		||||
										>tildefriends.net itself</a
 | 
			
		||||
									>.
 | 
			
		||||
								</li>
 | 
			
		||||
								<li>
 | 
			
		||||
									Enable <b>Peer Exchange</b> in the <b>admin</b> to discover
 | 
			
		||||
									internet peers.
 | 
			
		||||
								</li>
 | 
			
		||||
							</ul>
 | 
			
		||||
							Connect to others. You will automatically discover peers on the
 | 
			
		||||
							same instance and same network if there are any. Or use
 | 
			
		||||
							<a href="https://github.com/staltz/ssb-room/blob/master/FAQ.md"
 | 
			
		||||
								>rooms</a
 | 
			
		||||
							>
 | 
			
		||||
							and pubs to reach more distant users.
 | 
			
		||||
							<a href="https://www.tildefriends.net/~cory/room/"
 | 
			
		||||
								>tildefriends.net itself</a
 | 
			
		||||
							>
 | 
			
		||||
							operates as a room, so you can connect and see who else is online
 | 
			
		||||
							and establish a connection.
 | 
			
		||||
						</li>
 | 
			
		||||
						<li>Follow people to grow your network.</li>
 | 
			
		||||
						<li>
 | 
			
		||||
@@ -220,15 +206,11 @@
 | 
			
		||||
 | 
			
		||||
		<!-- Technlology Section -->
 | 
			
		||||
		<div class="w3-container w3-padding-64 w3-light-grey w3-center">
 | 
			
		||||
			<h1 class="w3-jumbo"><b>Built the Old Fashioned Way</b></h1>
 | 
			
		||||
			<p>
 | 
			
		||||
				Tilde Friends strives to use only simple and widely adopted dependencies
 | 
			
		||||
				in order to keep it easy to build for all sorts of platforms and
 | 
			
		||||
				maintainable for a very long time.
 | 
			
		||||
			</p>
 | 
			
		||||
			<h1 class="w3-jumbo"><b>Trusted Technology</b></h1>
 | 
			
		||||
			<p>Tilde Friends is built using boring, trusted tech.</p>
 | 
			
		||||
			<p>
 | 
			
		||||
				Though of course for building Tilde Friends apps, you are free to use
 | 
			
		||||
				whatever fits on top.
 | 
			
		||||
				whatever fits.
 | 
			
		||||
			</p>
 | 
			
		||||
 | 
			
		||||
			<div class="w3-row" style="margin-top: 64px">
 | 
			
		||||
@@ -262,7 +244,7 @@
 | 
			
		||||
					<i class="fa fa-lock w3-text-purple w3-jumbo"></i>
 | 
			
		||||
					<p>libsodium</p>
 | 
			
		||||
				</a>
 | 
			
		||||
				<a href="https://github.com/openssl/openssl/releases" class="w3-col s3">
 | 
			
		||||
				<a href="https://www.openssl.org/" class="w3-col s3">
 | 
			
		||||
					<i class="fa fa-shield-halved w3-text-green w3-jumbo"></i>
 | 
			
		||||
					<p>OpenSSL</p>
 | 
			
		||||
				</a>
 | 
			
		||||
@@ -288,13 +270,6 @@
 | 
			
		||||
					<i class="fa fa-fire w3-text-cyan w3-jumbo"></i>
 | 
			
		||||
					<p>Lit</p>
 | 
			
		||||
				</a>
 | 
			
		||||
				<a href="https://github.com/c-ares/c-ares" class="w3-col s3">
 | 
			
		||||
					<i class="fa fa-book-atlas w3-text-purple w3-jumbo"></i>
 | 
			
		||||
					<p>c-ares</p>
 | 
			
		||||
				</a>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="w3-row" style="margin-top: 64px">
 | 
			
		||||
				<a href="https://www.gnu.org/software/make/" class="w3-col s3">
 | 
			
		||||
					<i class="fa fa-hammer w3-text-teal w3-jumbo"></i>
 | 
			
		||||
					<p>GNU Make</p>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
/* W3.CSS 5.01 March 14 2025 by Jan Egil and Borge Refsnes */
 | 
			
		||||
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
 | 
			
		||||
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
 | 
			
		||||
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
 | 
			
		||||
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
 | 
			
		||||
@@ -108,10 +108,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
 | 
			
		||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
 | 
			
		||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
 | 
			
		||||
 | 
			
		||||
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
 | 
			
		||||
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
 | 
			
		||||
 | 
			
		||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
 | 
			
		||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
 | 
			
		||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
 | 
			
		||||
@@ -153,9 +149,9 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
 | 
			
		||||
.w3-hover-none:hover{box-shadow:none!important}
 | 
			
		||||
/* Colors */
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover,.w3-warning{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
 | 
			
		||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
 | 
			
		||||
.w3-blue,.w3-hover-blue:hover,.w3-info,.w3-primary{color:#fff!important;background-color:#2196F3!important}
 | 
			
		||||
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
 | 
			
		||||
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
 | 
			
		||||
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
 | 
			
		||||
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
 | 
			
		||||
@@ -170,24 +166,15 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
 | 
			
		||||
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
 | 
			
		||||
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
 | 
			
		||||
.w3-red,.w3-hover-red:hover,.w3-danger{color:#fff!important;background-color:#f44336!important}
 | 
			
		||||
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
 | 
			
		||||
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
 | 
			
		||||
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
 | 
			
		||||
.w3-yellow,.w3-hover-yellow:hover,.w3-note{color:#000!important;background-color:#ffeb3b!important}
 | 
			
		||||
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
 | 
			
		||||
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
 | 
			
		||||
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
 | 
			
		||||
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover,.w3-secondary{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
 | 
			
		||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
 | 
			
		||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
 | 
			
		||||
 | 
			
		||||
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
 | 
			
		||||
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
 | 
			
		||||
.w3-emerald,.w3-hover-emerald:hover,.w3-success{color:#fff!important;background-color:#008a00!important}
 | 
			
		||||
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
 | 
			
		||||
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
 | 
			
		||||
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
 | 
			
		||||
 | 
			
		||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
 | 
			
		||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
 | 
			
		||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
 | 
			
		||||
@@ -245,4 +232,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
 | 
			
		||||
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
 | 
			
		||||
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
 | 
			
		||||
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
 | 
			
		||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
 | 
			
		||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "📝",
 | 
			
		||||
	"previous": "&4UHlsfQJvSh7L3D86uFtr7KUKCMRVBBTFxRIMqIc5as=.sha256"
 | 
			
		||||
	"previous": "&DaYqKHRBKhjFGaOzbKZ1+/pLspJeEkDJYTF2B50tH6k=.sha256"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								apps/wiki/commonmark.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								apps/wiki/commonmark.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -2,8 +2,8 @@ import * as utils from './utils.js';
 | 
			
		||||
import * as commonmark from './commonmark.min.js';
 | 
			
		||||
 | 
			
		||||
function markdown(md) {
 | 
			
		||||
	let reader = new commonmark.Parser();
 | 
			
		||||
	let writer = new commonmark.HtmlRenderer({safe: true});
 | 
			
		||||
	let reader = new commonmark.Parser({safe: true});
 | 
			
		||||
	let writer = new commonmark.HtmlRenderer();
 | 
			
		||||
	let parsed = reader.parse(md || '');
 | 
			
		||||
	let walker = parsed.walker();
 | 
			
		||||
	let event;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								apps/wiki/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										42
									
								
								apps/wiki/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -20,8 +20,8 @@ class TfWikiDocElement extends LitElement {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	markdown(md) {
 | 
			
		||||
		let reader = new commonmark.Parser();
 | 
			
		||||
		let writer = new commonmark.HtmlRenderer({safe: true});
 | 
			
		||||
		let reader = new commonmark.Parser({safe: true});
 | 
			
		||||
		let writer = new commonmark.HtmlRenderer();
 | 
			
		||||
		let parsed = reader.parse(md || '');
 | 
			
		||||
		let walker = parsed.walker();
 | 
			
		||||
		let event;
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ function new_message() {
 | 
			
		||||
	return g_new_message_promise;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
core.register('onMessage', function (id) {
 | 
			
		||||
ssb.addEventListener('message', function (id) {
 | 
			
		||||
	let resolve = g_new_message_resolve;
 | 
			
		||||
	g_new_message_promise = new Promise(function (resolve, reject) {
 | 
			
		||||
		g_new_message_resolve = resolve;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										125
									
								
								core/app.js
									
									
									
									
									
								
							
							
						
						
									
										125
									
								
								core/app.js
									
									
									
									
									
								
							@@ -1,26 +1,53 @@
 | 
			
		||||
import * as core from './core.js';
 | 
			
		||||
 | 
			
		||||
let g_next_id = 1;
 | 
			
		||||
let g_calls = {};
 | 
			
		||||
 | 
			
		||||
let gSessionIndex = 0;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function makeSessionId() {
 | 
			
		||||
	return (gSessionIndex++).toString();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function App() {
 | 
			
		||||
	this._on_output = null;
 | 
			
		||||
	this._send_queue = [];
 | 
			
		||||
	this.calls = {};
 | 
			
		||||
	this._next_call_id = 1;
 | 
			
		||||
	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 = self._next_call_id++;
 | 
			
		||||
		while (!id || self.calls[id]) {
 | 
			
		||||
			id = self._next_call_id++;
 | 
			
		||||
		let id = g_next_id++;
 | 
			
		||||
		while (!id || g_calls[id]) {
 | 
			
		||||
			id = g_next_id++;
 | 
			
		||||
		}
 | 
			
		||||
		let promise = new Promise(function (resolve, reject) {
 | 
			
		||||
			self.calls[id] = {resolve: resolve, reject: reject};
 | 
			
		||||
			g_calls[id] = {resolve: resolve, reject: reject};
 | 
			
		||||
		});
 | 
			
		||||
		let message = {
 | 
			
		||||
			action: 'tfrpc',
 | 
			
		||||
			message: 'tfrpc',
 | 
			
		||||
			method: api[0],
 | 
			
		||||
			params: [...arguments],
 | 
			
		||||
			id: id,
 | 
			
		||||
@@ -32,6 +59,10 @@ App.prototype.makeFunction = function (api) {
 | 
			
		||||
	return result;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} message
 | 
			
		||||
 */
 | 
			
		||||
App.prototype.send = function (message) {
 | 
			
		||||
	if (this._send_queue) {
 | 
			
		||||
		if (this._on_output) {
 | 
			
		||||
@@ -46,10 +77,16 @@ App.prototype.send = function (message) {
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
exports.app_socket = async function socket(request, response) {
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} request
 | 
			
		||||
 * @param {*} response
 | 
			
		||||
 * @param {*} client
 | 
			
		||||
 */
 | 
			
		||||
function socket(request, response, client) {
 | 
			
		||||
	let process;
 | 
			
		||||
	let options = {};
 | 
			
		||||
	let credentials = await httpd.auth_query(request.headers);
 | 
			
		||||
	let credentials = httpd.auth_query(request.headers);
 | 
			
		||||
 | 
			
		||||
	response.onClose = async function () {
 | 
			
		||||
		if (process && process.task) {
 | 
			
		||||
@@ -66,16 +103,10 @@ exports.app_socket = async function socket(request, response) {
 | 
			
		||||
			try {
 | 
			
		||||
				message = JSON.parse(event.data);
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				print(
 | 
			
		||||
					'WebSocket error:',
 | 
			
		||||
					error,
 | 
			
		||||
					event.data,
 | 
			
		||||
					event.data.length,
 | 
			
		||||
					event.opCode
 | 
			
		||||
				);
 | 
			
		||||
				print('ERROR', error, event.data, event.data.length, event.opCode);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			if (!process && message.action == 'hello') {
 | 
			
		||||
			if (message.action == 'hello') {
 | 
			
		||||
				let packageOwner;
 | 
			
		||||
				let packageName;
 | 
			
		||||
				let blobId;
 | 
			
		||||
@@ -92,7 +123,7 @@ exports.app_socket = async function socket(request, response) {
 | 
			
		||||
					if (!blobId) {
 | 
			
		||||
						response.send(
 | 
			
		||||
							JSON.stringify({
 | 
			
		||||
								action: 'tfrpc',
 | 
			
		||||
								message: 'tfrpc',
 | 
			
		||||
								method: 'error',
 | 
			
		||||
								params: [message.path + ' not found'],
 | 
			
		||||
								id: -1,
 | 
			
		||||
@@ -133,7 +164,7 @@ exports.app_socket = async function socket(request, response) {
 | 
			
		||||
				options.packageOwner = packageOwner;
 | 
			
		||||
				options.packageName = packageName;
 | 
			
		||||
				options.url = message.url;
 | 
			
		||||
				let sessionId = 'session_' + (gSessionIndex++).toString();
 | 
			
		||||
				let sessionId = makeSessionId();
 | 
			
		||||
				if (blobId) {
 | 
			
		||||
					if (message.edit_only) {
 | 
			
		||||
						response.send(
 | 
			
		||||
@@ -141,28 +172,17 @@ exports.app_socket = async function socket(request, response) {
 | 
			
		||||
							0x1
 | 
			
		||||
						);
 | 
			
		||||
					} else {
 | 
			
		||||
						process = await core.getProcessBlob(blobId, sessionId, options);
 | 
			
		||||
						process = await core.getSessionProcessBlob(
 | 
			
		||||
							blobId,
 | 
			
		||||
							sessionId,
 | 
			
		||||
							options
 | 
			
		||||
						);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				if (process) {
 | 
			
		||||
					process.client_api.tfrpc = function (message) {
 | 
			
		||||
						if (message.id) {
 | 
			
		||||
							let calls = process?.app?.calls;
 | 
			
		||||
							if (calls) {
 | 
			
		||||
								let call = calls[message.id];
 | 
			
		||||
								if (call) {
 | 
			
		||||
									if (message.error !== undefined) {
 | 
			
		||||
										call.reject(message.error);
 | 
			
		||||
									} else {
 | 
			
		||||
										call.resolve(message.result);
 | 
			
		||||
									}
 | 
			
		||||
									delete calls[message.id];
 | 
			
		||||
								}
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					};
 | 
			
		||||
					process.app._on_output = (message) =>
 | 
			
		||||
					process.app.readOutput(function (message) {
 | 
			
		||||
						response.send(JSON.stringify(message), 0x1);
 | 
			
		||||
					});
 | 
			
		||||
					process.app.send();
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
@@ -191,13 +211,30 @@ exports.app_socket = async function socket(request, response) {
 | 
			
		||||
				if (process && process.timeout > 0) {
 | 
			
		||||
					setTimeout(ping, process.timeout);
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
			} else if (message.action == 'enableStats') {
 | 
			
		||||
				if (process) {
 | 
			
		||||
					if (process.client_api[message.action]) {
 | 
			
		||||
						process.client_api[message.action](message);
 | 
			
		||||
					} else if (process.eventHandlers['message']) {
 | 
			
		||||
						await core.invoke(process.eventHandlers['message'], [message]);
 | 
			
		||||
					core.enableStats(process, message.enabled);
 | 
			
		||||
				}
 | 
			
		||||
			} 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') {
 | 
			
		||||
				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) {
 | 
			
		||||
@@ -216,6 +253,6 @@ exports.app_socket = async function socket(request, response) {
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	response.upgrade(100, {});
 | 
			
		||||
};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {App};
 | 
			
		||||
export {socket, App};
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
	<head>
 | 
			
		||||
		<title>Tilde Friends Sign-in</title>
 | 
			
		||||
		<link type="text/css" rel="stylesheet" href="/static/style.css" />
 | 
			
		||||
		<link type="image/svg+xml" rel="icon" href="/static/tildefriends.svg" />
 | 
			
		||||
		<link type="image/png" rel="shortcut icon" href="/static/favicon.png" />
 | 
			
		||||
		<meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
			
		||||
	</head>
 | 
			
		||||
	<body>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										214
									
								
								core/client.js
									
									
									
									
									
								
							
							
						
						
									
										214
									
								
								core/client.js
									
									
									
									
									
								
							@@ -10,7 +10,6 @@ let gEditor;
 | 
			
		||||
let gOriginalInput;
 | 
			
		||||
 | 
			
		||||
let kErrorColor = '#dc322f';
 | 
			
		||||
let kDisconnectColor = '#f00';
 | 
			
		||||
let kStatusColor = '#fff';
 | 
			
		||||
 | 
			
		||||
// Functions that server-side app code can call through the app object.
 | 
			
		||||
@@ -56,7 +55,7 @@ class TfNavigationElement extends LitElement {
 | 
			
		||||
			status: {type: Object},
 | 
			
		||||
			spark_lines: {type: Object},
 | 
			
		||||
			version: {type: Object},
 | 
			
		||||
			show_expanded: {type: Boolean},
 | 
			
		||||
			show_version: {type: Boolean},
 | 
			
		||||
			identity: {type: String},
 | 
			
		||||
			identities: {type: Array},
 | 
			
		||||
			names: {type: Object},
 | 
			
		||||
@@ -105,6 +104,7 @@ class TfNavigationElement extends LitElement {
 | 
			
		||||
			let spark_line = document.createElement('tf-sparkline');
 | 
			
		||||
			spark_line.title = key;
 | 
			
		||||
			spark_line.classList.add('w3-bar-item');
 | 
			
		||||
			spark_line.classList.add('w3-hide-small');
 | 
			
		||||
			spark_line.style.paddingRight = '0';
 | 
			
		||||
			if (options) {
 | 
			
		||||
				if (options.max) {
 | 
			
		||||
@@ -117,6 +117,28 @@ class TfNavigationElement extends LitElement {
 | 
			
		||||
		return this.spark_lines[key];
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * TODOC
 | 
			
		||||
	 * @returns
 | 
			
		||||
	 */
 | 
			
		||||
	render_login() {
 | 
			
		||||
		if (this?.credentials?.session?.name) {
 | 
			
		||||
			return html`<a
 | 
			
		||||
				class="w3-bar-item w3-right"
 | 
			
		||||
				id="login"
 | 
			
		||||
				href="/login/logout?return=${url() + hash()}"
 | 
			
		||||
				>logout ${this.credentials.session.name}</a
 | 
			
		||||
			>`;
 | 
			
		||||
		} else {
 | 
			
		||||
			return html`<a
 | 
			
		||||
				class="w3-bar-item w3-right"
 | 
			
		||||
				id="login"
 | 
			
		||||
				href="/login?return=${url() + hash()}"
 | 
			
		||||
				>login</a
 | 
			
		||||
			>`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	set_active_identity(id) {
 | 
			
		||||
		send({action: 'setActiveIdentity', identity: id});
 | 
			
		||||
		this.renderRoot.getElementById('id_dropdown').classList.remove('w3-show');
 | 
			
		||||
@@ -136,110 +158,67 @@ class TfNavigationElement extends LitElement {
 | 
			
		||||
		window.location.href = '/~core/ssb/#' + this.identity;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logout() {
 | 
			
		||||
		window.location.href = `/login/logout?return=${encodeURIComponent(url() + hash())}`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	render_identity() {
 | 
			
		||||
		let self = this;
 | 
			
		||||
 | 
			
		||||
		if (this?.credentials?.session?.name) {
 | 
			
		||||
			if (this.identities?.length) {
 | 
			
		||||
				return html`
 | 
			
		||||
					<link type="text/css" rel="stylesheet" href="/static/w3.css" />
 | 
			
		||||
					<div class="w3-dropdown-click w3-right" style="max-width: 100%">
 | 
			
		||||
		if (this.identities?.length) {
 | 
			
		||||
			return html`
 | 
			
		||||
				<link type="text/css" rel="stylesheet" href="/static/w3.css" />
 | 
			
		||||
				<div class="w3-dropdown-click w3-right" style="max-width: 100%">
 | 
			
		||||
					<button
 | 
			
		||||
						class="w3-button w3-rest w3-cyan"
 | 
			
		||||
						style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%"
 | 
			
		||||
						@click=${self.toggle_id_dropdown}
 | 
			
		||||
					>
 | 
			
		||||
						${self.names[this.identity]}${self.names[this.identity] ===
 | 
			
		||||
						this.identity
 | 
			
		||||
							? ''
 | 
			
		||||
							: html` - ${this.identity}`}
 | 
			
		||||
						▾
 | 
			
		||||
					</button>
 | 
			
		||||
					<div
 | 
			
		||||
						id="id_dropdown"
 | 
			
		||||
						class="w3-dropdown-content w3-bar-block w3-card-4"
 | 
			
		||||
						style="max-width: 100%"
 | 
			
		||||
					>
 | 
			
		||||
						<button
 | 
			
		||||
							class="w3-button w3-rest"
 | 
			
		||||
							style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 100%"
 | 
			
		||||
							id="identity"
 | 
			
		||||
							@click=${self.toggle_id_dropdown}
 | 
			
		||||
							class="w3-bar-item w3-button w3-border"
 | 
			
		||||
							@click=${() => (window.location.href = '/~core/identity')}
 | 
			
		||||
						>
 | 
			
		||||
							${self.names[this.identity]}▾
 | 
			
		||||
							Manage Identities...
 | 
			
		||||
						</button>
 | 
			
		||||
						<div
 | 
			
		||||
							id="id_dropdown"
 | 
			
		||||
							class="w3-dropdown-content w3-bar-block w3-card-4"
 | 
			
		||||
							style="max-width: 100%; right: 0"
 | 
			
		||||
						<button
 | 
			
		||||
							class="w3-bar-item w3-button w3-border"
 | 
			
		||||
							@click=${self.edit_profile}
 | 
			
		||||
						>
 | 
			
		||||
							<div
 | 
			
		||||
								style="position: fixed; left: 0; right: 0; top: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.25); z-index: -100"
 | 
			
		||||
								@click=${self.toggle_id_dropdown}
 | 
			
		||||
							></div>
 | 
			
		||||
							<button
 | 
			
		||||
								class="w3-bar-item w3-button w3-border"
 | 
			
		||||
								@click=${() => (window.location.href = '/~core/identity')}
 | 
			
		||||
							>
 | 
			
		||||
								Manage Identities...
 | 
			
		||||
							</button>
 | 
			
		||||
							<button
 | 
			
		||||
								id="edit_profile"
 | 
			
		||||
								class="w3-bar-item w3-button w3-border"
 | 
			
		||||
								@click=${self.edit_profile}
 | 
			
		||||
							>
 | 
			
		||||
								Edit Profile...
 | 
			
		||||
							</button>
 | 
			
		||||
							${this.identities.map(
 | 
			
		||||
								(x) => html`
 | 
			
		||||
									<button
 | 
			
		||||
										class="w3-bar-item w3-button ${x === self.identity
 | 
			
		||||
											? 'w3-cyan'
 | 
			
		||||
											: ''}"
 | 
			
		||||
										@click=${() => self.set_active_identity(x)}
 | 
			
		||||
										style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap"
 | 
			
		||||
									>
 | 
			
		||||
										${self.names[x]}${self.names[x] === x ? '' : html` - ${x}`}
 | 
			
		||||
									</button>
 | 
			
		||||
								`
 | 
			
		||||
							)}
 | 
			
		||||
							<button
 | 
			
		||||
								class="w3-bar-item w3-button w3-border"
 | 
			
		||||
								id="logout"
 | 
			
		||||
								@click=${self.logout}
 | 
			
		||||
							>
 | 
			
		||||
								Logout ${this.credentials.session.name}
 | 
			
		||||
							</button>
 | 
			
		||||
						</div>
 | 
			
		||||
							Edit Profile...
 | 
			
		||||
						</button>
 | 
			
		||||
						${this.identities.map(
 | 
			
		||||
							(x) => html`
 | 
			
		||||
								<button
 | 
			
		||||
									class="w3-bar-item w3-button ${x === self.identity
 | 
			
		||||
										? 'w3-cyan'
 | 
			
		||||
										: ''}"
 | 
			
		||||
									@click=${() => self.set_active_identity(x)}
 | 
			
		||||
									style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap"
 | 
			
		||||
								>
 | 
			
		||||
									${self.names[x]}${self.names[x] === x ? '' : html` - ${x}`}
 | 
			
		||||
								</button>
 | 
			
		||||
							`
 | 
			
		||||
						)}
 | 
			
		||||
					</div>
 | 
			
		||||
				`;
 | 
			
		||||
			} else if (
 | 
			
		||||
				this.credentials?.session?.name &&
 | 
			
		||||
				this.credentials.session.name !== 'guest'
 | 
			
		||||
			) {
 | 
			
		||||
				return html`
 | 
			
		||||
					<link type="text/css" rel="stylesheet" href="/static/w3.css" />
 | 
			
		||||
					<button
 | 
			
		||||
						class="w3-bar-item w3-button w3-right w3-cyan"
 | 
			
		||||
						id="logout"
 | 
			
		||||
						@click=${self.logout}
 | 
			
		||||
					>
 | 
			
		||||
						Logout ${this.credentials.session.name}
 | 
			
		||||
					</button>
 | 
			
		||||
					<button
 | 
			
		||||
						id="create_identity"
 | 
			
		||||
						@click=${this.create_identity}
 | 
			
		||||
						class="w3-button w3-mobile w3-red w3-right"
 | 
			
		||||
					>
 | 
			
		||||
						Create an Identity
 | 
			
		||||
					</button>
 | 
			
		||||
				`;
 | 
			
		||||
			} else {
 | 
			
		||||
				return html`
 | 
			
		||||
					<button
 | 
			
		||||
						class="w3-bar-item w3-button w3-right w3-cyan"
 | 
			
		||||
						id="logout"
 | 
			
		||||
						@click=${self.logout}
 | 
			
		||||
					>
 | 
			
		||||
						Logout ${this.credentials.session.name}
 | 
			
		||||
					</button>
 | 
			
		||||
				`;
 | 
			
		||||
			}
 | 
			
		||||
				</div>
 | 
			
		||||
			`;
 | 
			
		||||
		} else {
 | 
			
		||||
			return html`<a
 | 
			
		||||
				class="w3-bar-item w3-cyan w3-right"
 | 
			
		||||
				id="login"
 | 
			
		||||
				href="/login?return=${url() + hash()}"
 | 
			
		||||
				>login</a
 | 
			
		||||
			>`;
 | 
			
		||||
			return html`
 | 
			
		||||
				<link type="text/css" rel="stylesheet" href="/static/w3.css" />
 | 
			
		||||
				<button
 | 
			
		||||
					id="create_identity"
 | 
			
		||||
					@click=${this.create_identity}
 | 
			
		||||
					class="w3-button w3-mobile w3-blue w3-right"
 | 
			
		||||
				>
 | 
			
		||||
					Create an Identity
 | 
			
		||||
				</button>
 | 
			
		||||
			`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -315,13 +294,13 @@ class TfNavigationElement extends LitElement {
 | 
			
		||||
				<span
 | 
			
		||||
					class="w3-bar-item"
 | 
			
		||||
					style="cursor: pointer"
 | 
			
		||||
					@click=${() => (this.show_expanded = !this.show_expanded)}
 | 
			
		||||
					@click=${() => (this.show_version = !this.show_version)}
 | 
			
		||||
					>😎</span
 | 
			
		||||
				>
 | 
			
		||||
				<span
 | 
			
		||||
					class="w3-bar-item"
 | 
			
		||||
					style=${'white-space: nowrap' +
 | 
			
		||||
					(this.show_expanded ? '' : '; display: none')}
 | 
			
		||||
					(this.show_version ? '' : '; display: none')}
 | 
			
		||||
					title=${this.version?.name +
 | 
			
		||||
					' ' +
 | 
			
		||||
					Object.entries(this.version || {})
 | 
			
		||||
@@ -376,20 +355,18 @@ class TfNavigationElement extends LitElement {
 | 
			
		||||
							</div>
 | 
			
		||||
						`
 | 
			
		||||
					: undefined}
 | 
			
		||||
				<span class=${this.show_expanded ? '' : 'w3-hide-small'}>
 | 
			
		||||
					${Object.keys(this.spark_lines)
 | 
			
		||||
						.sort()
 | 
			
		||||
						.map((x) => this.spark_lines[x])}
 | 
			
		||||
				</span>
 | 
			
		||||
				${this.render_identity()}
 | 
			
		||||
				${Object.keys(this.spark_lines)
 | 
			
		||||
					.sort()
 | 
			
		||||
					.map((x) => this.spark_lines[x])}
 | 
			
		||||
				${this.render_login()} ${this.render_identity()}
 | 
			
		||||
			</div>
 | 
			
		||||
			${this.status?.is_error
 | 
			
		||||
				? html`
 | 
			
		||||
					<link type="text/css" rel="stylesheet" href="/static/w3.css" />
 | 
			
		||||
					<div class="w3-model w3-animate-top" style="position: absolute; left: 50%; transform: translate(-50%); z-index: 1">
 | 
			
		||||
						<dijv class="w3-modal-content w3-card-4" style="display: block; padding: 1em">
 | 
			
		||||
							<span id="close_error" @click=${self.clear_error} class="w3-button w3-display-topright">×</span>
 | 
			
		||||
							<div style="color: ${this.status.color ?? kErrorColor}"><b>ERROR:</b><p id="error" style="white-space: pre">${this.status.message}</p></div>
 | 
			
		||||
							<span @click=${self.clear_error} class="w3-button w3-display-topright">×</span>
 | 
			
		||||
							<div style="color: ${this.status.color ?? kErrorColor}"><b>ERROR:</b><p style="white-space: pre">${this.status.message}</p></div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
					`
 | 
			
		||||
@@ -1180,7 +1157,7 @@ function api_requestPermission(permission, id) {
 | 
			
		||||
 | 
			
		||||
	let div = document.createElement('div');
 | 
			
		||||
	div.appendChild(
 | 
			
		||||
		document.createTextNode('This app is requesting the following permission: ')
 | 
			
		||||
		document.createTextNode('This app is requesting the following permission:')
 | 
			
		||||
	);
 | 
			
		||||
	let span = document.createElement('span');
 | 
			
		||||
	span.style = 'font-weight: bold';
 | 
			
		||||
@@ -1196,7 +1173,6 @@ function api_requestPermission(permission, id) {
 | 
			
		||||
	check.classList.add('w3-check');
 | 
			
		||||
	check.classList.add('w3-blue');
 | 
			
		||||
	div.appendChild(check);
 | 
			
		||||
	div.appendChild(document.createTextNode(' '));
 | 
			
		||||
	let label = document.createElement('label');
 | 
			
		||||
	label.htmlFor = check.id;
 | 
			
		||||
	label.appendChild(document.createTextNode('Remember this decision.'));
 | 
			
		||||
@@ -1281,6 +1257,7 @@ function _receive_websocket_message(message) {
 | 
			
		||||
		document.getElementById('viewPane').style.display = message.edit_only
 | 
			
		||||
			? 'none'
 | 
			
		||||
			: 'flex';
 | 
			
		||||
		send({action: 'enableStats', enabled: true});
 | 
			
		||||
	} else if (message && message.action == 'ping') {
 | 
			
		||||
		send({action: 'pong'});
 | 
			
		||||
	} else if (message && message.action == 'stats') {
 | 
			
		||||
@@ -1325,7 +1302,7 @@ function _receive_websocket_message(message) {
 | 
			
		||||
				line.append(key, message.stats[key]);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else if (message && message.action === 'tfrpc' && message.method) {
 | 
			
		||||
	} else if (message && message.message === 'tfrpc' && message.method) {
 | 
			
		||||
		let api = k_api[message.method];
 | 
			
		||||
		let id = message.id;
 | 
			
		||||
		let params = message.params;
 | 
			
		||||
@@ -1333,14 +1310,14 @@ function _receive_websocket_message(message) {
 | 
			
		||||
			Promise.resolve(api.func(...params))
 | 
			
		||||
				.then(function (result) {
 | 
			
		||||
					send({
 | 
			
		||||
						action: 'tfrpc',
 | 
			
		||||
						message: 'tfrpc',
 | 
			
		||||
						id: id,
 | 
			
		||||
						result: result,
 | 
			
		||||
					});
 | 
			
		||||
				})
 | 
			
		||||
				.catch(function (error) {
 | 
			
		||||
					send({
 | 
			
		||||
						action: 'tfrpc',
 | 
			
		||||
						message: 'tfrpc',
 | 
			
		||||
						id: id,
 | 
			
		||||
						error: error,
 | 
			
		||||
					});
 | 
			
		||||
@@ -1568,7 +1545,7 @@ function connectSocket(path) {
 | 
			
		||||
			};
 | 
			
		||||
			setStatusMessage(
 | 
			
		||||
				'🔴 Closed: ' + (k_codes[event.code] || event.code),
 | 
			
		||||
				kDisconnectColor
 | 
			
		||||
				kErrorColor
 | 
			
		||||
			);
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
@@ -1789,11 +1766,10 @@ async function sourcePretty() {
 | 
			
		||||
	let prettier = (await import('/prettier/standalone.mjs')).default;
 | 
			
		||||
	let babel = (await import('/prettier/babel.mjs')).default;
 | 
			
		||||
	let estree = (await import('/prettier/estree.mjs')).default;
 | 
			
		||||
	let prettier_html = (await import('/prettier/html.mjs')).default;
 | 
			
		||||
	let source = gEditor.state.doc.toString();
 | 
			
		||||
	let formatted = await prettier.format(source, {
 | 
			
		||||
		parser: gCurrentFile?.toLowerCase()?.endsWith('.html') ? 'html' : 'babel',
 | 
			
		||||
		plugins: [babel, estree, prettier_html],
 | 
			
		||||
		parser: 'babel',
 | 
			
		||||
		plugins: [babel, estree],
 | 
			
		||||
		trailingComma: 'es5',
 | 
			
		||||
		useTabs: true,
 | 
			
		||||
		semi: true,
 | 
			
		||||
@@ -1828,8 +1804,8 @@ function toggleVisibleWhitespace() {
 | 
			
		||||
			.cm-highlightTab {
 | 
			
		||||
				background-image: unset !important;
 | 
			
		||||
			}
 | 
			
		||||
			.cm-highlightSpace {
 | 
			
		||||
				background-image: unset !important;
 | 
			
		||||
			.cm-highlightSpace:before {
 | 
			
		||||
				content: unset !important;
 | 
			
		||||
			}
 | 
			
		||||
		`;
 | 
			
		||||
		window.localStorage.setItem('visible_whitespace', '1');
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1138
									
								
								core/core.js
									
									
									
									
									
								
							
							
						
						
									
										1138
									
								
								core/core.js
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								core/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								core/favicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 320 B  | 
							
								
								
									
										44
									
								
								core/form.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								core/form.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} encoded
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function decode(encoded) {
 | 
			
		||||
	let result = '';
 | 
			
		||||
	for (let i = 0; i < encoded.length; i++) {
 | 
			
		||||
		let c = encoded[i];
 | 
			
		||||
		if (c == '+') {
 | 
			
		||||
			result += ' ';
 | 
			
		||||
		} else if (c == '%') {
 | 
			
		||||
			result += String.fromCharCode(parseInt(encoded.slice(i + 1, i + 3), 16));
 | 
			
		||||
			i += 2;
 | 
			
		||||
		} else {
 | 
			
		||||
			result += c;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODOC
 | 
			
		||||
 * @param {*} encoded
 | 
			
		||||
 * @param {*} initial
 | 
			
		||||
 * @returns
 | 
			
		||||
 */
 | 
			
		||||
function decodeForm(encoded, initial) {
 | 
			
		||||
	let result = initial || {};
 | 
			
		||||
	if (encoded) {
 | 
			
		||||
		encoded = encoded.trim();
 | 
			
		||||
		let items = encoded.split('&');
 | 
			
		||||
		for (let i = 0; i < items.length; i++) {
 | 
			
		||||
			let item = items[i];
 | 
			
		||||
			let equals = item.indexOf('=');
 | 
			
		||||
			let key = decode(item.slice(0, equals));
 | 
			
		||||
			let value = decode(item.slice(equals + 1));
 | 
			
		||||
			result[key] = value;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export {decodeForm};
 | 
			
		||||
@@ -4,28 +4,8 @@
 | 
			
		||||
		<title>Tilde Friends</title>
 | 
			
		||||
		<link type="text/css" rel="stylesheet" href="/static/style.css" />
 | 
			
		||||
		<link type="text/css" rel="stylesheet" href="/static/w3.css" />
 | 
			
		||||
		<link type="image/svg+xml" rel="icon" href="/static/tildefriends.svg" />
 | 
			
		||||
		<link type="image/png" rel="shortcut icon" href="/static/favicon.png" />
 | 
			
		||||
		<meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
			
		||||
		<meta
 | 
			
		||||
			name="title"
 | 
			
		||||
			content="Tilde Friends - Make friends and apps from your web browser."
 | 
			
		||||
		/>
 | 
			
		||||
		<meta
 | 
			
		||||
			name="description"
 | 
			
		||||
			content="Tilde Friends is a Secure Scuttlebutt client and a platform for building, running, and sharing web applications. "
 | 
			
		||||
		/>
 | 
			
		||||
		<meta property="og:type" content="website" />
 | 
			
		||||
		<meta property="og:url" content="https://metatags.io/" />
 | 
			
		||||
		<meta
 | 
			
		||||
			property="og:title"
 | 
			
		||||
			content="Tilde Friends - Make friends and apps from your web browser."
 | 
			
		||||
		/>
 | 
			
		||||
		<meta
 | 
			
		||||
			property="og:description"
 | 
			
		||||
			content="Tilde Friends is a Secure Scuttlebutt client and a platform for building, running, and sharing web applications. "
 | 
			
		||||
		/>
 | 
			
		||||
		<meta property="og:image" content="/static/tildefriends.svg" />
 | 
			
		||||
 | 
			
		||||
		<script>
 | 
			
		||||
			function set_access_key_title(event) {
 | 
			
		||||
				if (!event.srcElement.title) {
 | 
			
		||||
@@ -38,24 +18,13 @@
 | 
			
		||||
		style="
 | 
			
		||||
			display: flex;
 | 
			
		||||
			flex-flow: column;
 | 
			
		||||
			width: 100%;
 | 
			
		||||
			height: 100%;
 | 
			
		||||
			width: 100vw;
 | 
			
		||||
			height: 100vh;
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			max-width: 100%;
 | 
			
		||||
			max-height: 100%;
 | 
			
		||||
		"
 | 
			
		||||
	>
 | 
			
		||||
		<noscript>
 | 
			
		||||
			<div class="w3-container">
 | 
			
		||||
				<div class="w3-panel w3-red w3-padding w3-card-4">
 | 
			
		||||
					<h1>TildeFriends requires JavaScript.</h1>
 | 
			
		||||
					<p>
 | 
			
		||||
						It looks like JavaScript is disabled or unsupported. This isn't
 | 
			
		||||
						going to work.
 | 
			
		||||
					</p>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</noscript>
 | 
			
		||||
		<tf-navigation></tf-navigation>
 | 
			
		||||
		<div id="content" class="hbox" style="flex: 1 0; overflow: auto">
 | 
			
		||||
			<div
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,8 @@ html {
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
	font-family: monospace;
 | 
			
		||||
	background-color: #444;
 | 
			
		||||
	color: #fff;
 | 
			
		||||
	background-color: #002b36;
 | 
			
		||||
	color: #eee8d5;
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	height: 100%;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,88 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg
 | 
			
		||||
   width="65"
 | 
			
		||||
   height="65"
 | 
			
		||||
   viewBox="0 0 61 65"
 | 
			
		||||
   fill="none"
 | 
			
		||||
   version="1.1"
 | 
			
		||||
   id="svg910"
 | 
			
		||||
   sodipodi:docname="tildefriends.svg"
 | 
			
		||||
   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
 | 
			
		||||
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
 | 
			
		||||
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg">
 | 
			
		||||
  <defs
 | 
			
		||||
     id="defs914" />
 | 
			
		||||
  <sodipodi:namedview
 | 
			
		||||
     id="namedview912"
 | 
			
		||||
     pagecolor="#ffffff"
 | 
			
		||||
     bordercolor="#666666"
 | 
			
		||||
     borderopacity="1.0"
 | 
			
		||||
     inkscape:pageshadow="2"
 | 
			
		||||
     inkscape:pageopacity="0.0"
 | 
			
		||||
     inkscape:pagecheckerboard="0"
 | 
			
		||||
     showgrid="false"
 | 
			
		||||
     inkscape:zoom="18.369231"
 | 
			
		||||
     inkscape:cx="32.472781"
 | 
			
		||||
     inkscape:cy="32.5"
 | 
			
		||||
     inkscape:window-width="2256"
 | 
			
		||||
     inkscape:window-height="1447"
 | 
			
		||||
     inkscape:window-x="0"
 | 
			
		||||
     inkscape:window-y="0"
 | 
			
		||||
     inkscape:window-maximized="1"
 | 
			
		||||
     inkscape:current-layer="svg910" />
 | 
			
		||||
  <path
 | 
			
		||||
     style="fill:#0af;stroke-width:.712717;fill-opacity:1"
 | 
			
		||||
     d="M6 0h49a8 8 45 0 1 8 8v49a8 8 135 0 1-8 8H6a8 8 45 0 1-8-8V8a8 8 135 0 1 8-8Z"
 | 
			
		||||
     id="path886" />
 | 
			
		||||
  <g
 | 
			
		||||
     aria-label="~"
 | 
			
		||||
     id="text890"
 | 
			
		||||
     style="font-size:40px;line-height:1.25;fill:#000000">
 | 
			
		||||
    <path
 | 
			
		||||
       d="m 1.6762187,36.689095 v -4.003907 q 2.0703125,-2.34375 5.4296875,-2.34375 1.171875,0 2.4609375,0.351563 1.2890623,0.332031 3.6718753,1.347656 1.347656,0.566406 2.011718,0.742188 0.683594,0.175781 1.367188,0.175781 1.269531,0 2.617187,-0.761719 1.367188,-0.761719 2.421875,-1.914062 v 4.140625 q -1.25,1.171875 -2.539062,1.699218 -1.269531,0.527344 -2.871094,0.527344 -1.171875,0 -2.246094,-0.273437 -1.054687,-0.273438 -3.378906,-1.308594 -2.3046873,-1.035156 -3.847656,-1.035156 -1.25,0 -2.3632813,0.546875 -1.09375,0.527343 -2.734375,2.109375 z"
 | 
			
		||||
       style="font-family:Arial;-inkscape-font-specification:'Arial, Normal'"
 | 
			
		||||
       id="path1704" />
 | 
			
		||||
  </g>
 | 
			
		||||
  <g
 | 
			
		||||
     transform="translate(16.213 5.975) scale(.72923)"
 | 
			
		||||
     id="g896">
 | 
			
		||||
    <circle
 | 
			
		||||
       cx="36"
 | 
			
		||||
       cy="36"
 | 
			
		||||
       r="23"
 | 
			
		||||
       fill="#fcea2b"
 | 
			
		||||
       id="circle892" />
 | 
			
		||||
    <path
 | 
			
		||||
       fill="#3f3f3f"
 | 
			
		||||
       d="M45.331 38.564c3.963 0 7.178-2.862 7.178-6.389 0-1.765.448-3.53-.852-4.685-1.299-1.156-4.345-1.704-6.326-1.704-2.357 0-5.143.143-6.451 1.704-.894 1.065-.727 3.253-.727 4.685 0 3.527 3.213 6.389 7.178 6.389zM25.738 38.564c3.963 0 7.179-2.862 7.179-6.389 0-1.765.447-3.53-.852-4.685-1.3-1.156-4.345-1.704-6.327-1.704-2.356 0-5.142.143-6.451 1.704-.893 1.065-.727 3.253-.727 4.685 0 3.527 3.213 6.389 7.178 6.389z"
 | 
			
		||||
       id="path894" />
 | 
			
		||||
  </g>
 | 
			
		||||
  <g
 | 
			
		||||
     stroke="#000"
 | 
			
		||||
     stroke-linecap="round"
 | 
			
		||||
     stroke-linejoin="round"
 | 
			
		||||
     stroke-miterlimit="10"
 | 
			
		||||
     stroke-width="2"
 | 
			
		||||
     transform="translate(16.213 5.975) scale(.72923)"
 | 
			
		||||
     id="g908">
 | 
			
		||||
    <circle
 | 
			
		||||
       cx="35.887"
 | 
			
		||||
       cy="36.056"
 | 
			
		||||
       r="23"
 | 
			
		||||
       id="circle898" />
 | 
			
		||||
    <path
 | 
			
		||||
       d="M45.702 44.862c-6.574 3.525-14.045 3.658-19.63 0M18.883 30.464s-.953 8.55 6.86 7.918c2.62-.212 7.817-.65 7.867-8.342.005-.698-.007-1.6-.81-2.63-1.065-1.367-3.572-1.971-9.945-1.422 0 0-3.446-.1-3.972 4.476z"
 | 
			
		||||
       id="path900" />
 | 
			
		||||
    <path
 | 
			
		||||
       d="m18.953 29.931-.433-3.372 3.833-.527M52.741 30.464s.953 8.55-6.86 7.918c-2.62-.212-7.817-.65-7.868-8.342-.004-.698.008-1.6.811-2.63 1.065-1.367 3.572-1.971 9.945-1.422 0 0 3.446-.1 3.972 4.476z"
 | 
			
		||||
       id="path902" />
 | 
			
		||||
    <path
 | 
			
		||||
       d="M31.505 26.416s4.124 2.534 8.657 0M33.536 31.318s2.202-3.751 4.536 0M52.664 29.933l.433-3.371-3.833-.528"
 | 
			
		||||
       id="path904" />
 | 
			
		||||
    <path
 | 
			
		||||
       d="M33.955 30.027s1.795-3.75 3.699 0"
 | 
			
		||||
       id="path906" />
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 3.6 KiB  | 
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user