Compare commits
	
		
			21 Commits
		
	
	
		
			v0.2025.8
			...
			submodules
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e00f73e1d5 | |||
| 4c11667ebd | |||
| 658e7089be | |||
| 0965e90d7b | |||
| d1f87a8fb4 | |||
| 2b4265f9ee | |||
| 3bd827a9f7 | |||
| 474e39c9c3 | |||
| 0272382e0e | |||
| b1c8b51377 | |||
| 1a5acca5cf | |||
| 2d5417f7dc | |||
| 2a10d26215 | |||
| b8e5caba0d | |||
| a4b324127a | |||
| acae3e9562 | |||
| 4aa7424977 | |||
| 758f177617 | |||
| 9291de41d8 | |||
| 3603ce5ba6 | |||
| bff231751e | 
| @@ -14,7 +14,7 @@ IndentWidth: 4 | ||||
| MaxEmptyLinesToKeep: 1 | ||||
| ObjCBlockIndentWidth: 4 | ||||
| ObjCBreakBeforeNestedBlockParam: false | ||||
| SortIncludes: true | ||||
| SortIncludes: false | ||||
| TabWidth: 4 | ||||
| UseTab: Always | ||||
| ... | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| .git | ||||
| db.sqlite* | ||||
| out/ | ||||
| .svn | ||||
| db.sqlite | ||||
| out/**/*.o | ||||
| out/**/*.d | ||||
|   | ||||
| @@ -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 -j`nproc` docs | ||||
|           echo 'pildefriends ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3Kde5vDO0TrMBDK0IGGeNGe/XinWAZkSQ/rXxwUjt' >> ~/.ssh/known_hosts | ||||
|           rsync -avP --delete -e "ssh -i /opt/keys/ssh.ed25519" out/html/ tfdocs@pildefriends:docs/html/ | ||||
|       - name: Setup JDK | ||||
|         uses: actions/setup-java@v3 | ||||
|         with: | ||||
|           java-version: '17' | ||||
|           distribution: 'temurin' | ||||
|       - name: Setup Android SDK | ||||
|         uses: android-actions/setup-android@v3 | ||||
|         with: | ||||
|           packages: 'tools platform-tools build-tools;35.0.0 platforms;android-35 ndk;27.2.12479018' | ||||
|       - name: Docker build | ||||
|         run: DOCKER_BUILDKIT=1 docker build . | ||||
|       - name: Build | ||||
|         run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist | ||||
|       - name: Upload artifacts | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: dist | ||||
|           path: dist/* | ||||
							
								
								
									
										16
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,20 +1,14 @@ | ||||
| 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/ | ||||
|  | ||||
| deps/codemirror/cm6.js | ||||
| deps/prettier/standalone.mjs | ||||
| deps/lit | ||||
|   | ||||
							
								
								
									
										11
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,6 +1,7 @@ | ||||
| [submodule "deps/zlib"] | ||||
| 	path = deps/zlib | ||||
| 	url = https://github.com/madler/zlib.git | ||||
| 	branch = master | ||||
| [submodule "deps/libsodium"] | ||||
| 	path = deps/libsodium | ||||
| 	url = https://github.com/jedisct1/libsodium.git | ||||
| @@ -19,13 +20,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 | ||||
|   | ||||
| @@ -2,7 +2,6 @@ node_modules | ||||
| src | ||||
| deps | ||||
| .clang-format | ||||
| flake.lock | ||||
|  | ||||
| # Minified files | ||||
| **/*.min.css | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										918
									
								
								GNUmakefile
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										71
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,70 +1,47 @@ | ||||
| # 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. | ||||
| 1. Make it easy and fun to run all sorts of web applications. | ||||
| 2. Provide security that is easy to understand and protects your data. | ||||
| 3. Make creating and sharing web applications accessible to anyone with a | ||||
|    browser. | ||||
|  | ||||
| ## Building | ||||
|  | ||||
| Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. It's possible | ||||
| to build for Android, iOS, and Windows on Linux, if you have the right | ||||
| dependencies in the right places. | ||||
| Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for | ||||
| all of those host platforms plus mingw64, iOS, and android. | ||||
|  | ||||
| ### Requirements | ||||
|  | ||||
| System OpenSSL libraries are assumed to be available on Haiku and OpenBSD. | ||||
|  | ||||
| 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. | ||||
| 1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies | ||||
|    are kept up to date in the tree. | ||||
| 2. To build, run `make debug` or `make release`. An executable will be | ||||
|    generated in a subdirectory of `out/`. | ||||
| 3. It's possible to build for Android, iOS, and Windows on Linux, if you have | ||||
|    the right dependencies in the right places. `make windebug winrelease | ||||
| iosdebug-ipa iosrelease-ipa release-apk`. | ||||
| 4. To build in docker, `docker build .`. | ||||
| 5. `make format` 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/>. `tildefriends -h` lists further | ||||
| options. | ||||
| By default, running the built `tildefriends` executable will start a web server | ||||
| at <http://localhost:12345/>. `tildefriends -h` lists further options. | ||||
|  | ||||
| The first user to create an account and log in will be granted administrative | ||||
| privileges. Further administration can be done in the `admin` app at | ||||
| privileges. Further administration can be done at | ||||
| <http://localhost:12345/~core/admin/>. | ||||
|  | ||||
| ## Documentation | ||||
|  | ||||
| Docs live here: <https://docs.tildefriends.net/>. | ||||
| Docs are a work in progress: | ||||
| <https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>. | ||||
|  | ||||
| ## License | ||||
|  | ||||
|   | ||||
							
								
								
									
										27
									
								
								android-sdk.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,27 @@ | ||||
| with import <nixpkgs> {}; | ||||
| let | ||||
|   androidComposition = androidenv.composeAndroidPackages { | ||||
|     cmdLineToolsVersion = "9.0"; | ||||
|     toolsVersion = "26.1.1"; | ||||
|     platformToolsVersion = "34.0.5"; | ||||
|     buildToolsVersions = [ "34.0.0" ]; | ||||
|     includeEmulator = false; | ||||
|     #emulatorVersion = "30.3.4"; | ||||
|     platformVersions = [ "34" ]; | ||||
|     includeSources = false; | ||||
|     includeSystemImages = false; | ||||
|     #systemImageTypes = [ "google_apis_playstore" ]; | ||||
|     #abiVersions = [ "armeabi-v7a" "arm64-v8a" ]; | ||||
|     #cmakeVersions = [ "3.10.2" ]; | ||||
|     includeNDK = true; | ||||
|     ndkVersions = ["26.0.10792818"]; | ||||
|     useGoogleAPIs = false; | ||||
|     useGoogleTVAddOns = false; | ||||
|     #includeExtras = [ | ||||
|     #  "extras;google;gcm" | ||||
|     #]; | ||||
|   }; | ||||
| in | ||||
| androidComposition.androidsdk | ||||
|  | ||||
| # $ NIXPKGS_ACCEPT_ANDROID_SDK_LICENSE=1 NIXPKGS_ALLOW_UNFREE=1 nix-build android-sdk.nix --impure | ||||
| @@ -1,5 +1,4 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "🎛", | ||||
| 	"previous": "&kmKNyb/uaXNb24gCinJtfS8iWx4cLUWdtl0y2DwEUas=.sha256" | ||||
| 	"emoji": "🎛" | ||||
| } | ||||
|   | ||||
| @@ -4,38 +4,9 @@ | ||||
| 		<script> | ||||
| 			const g_data = $data; | ||||
| 		</script> | ||||
| 		<link rel="stylesheet" href="w3.css" /> | ||||
| 		<!-- prettier-ignore --> | ||||
| 		<style> | ||||
| 			/* 2018 Valiant Poppy */ | ||||
| 			.w3-theme-l5 {color:#000 !important; background-color:#fbf3f3 !important} | ||||
| 			.w3-theme-l4 {color:#000 !important; background-color:#f3d7d6 !important} | ||||
| 			.w3-theme-l3 {color:#000 !important; background-color:#e6afae !important} | ||||
| 			.w3-theme-l2 {color:#fff !important; background-color:#da8785 !important} | ||||
| 			.w3-theme-l1 {color:#fff !important; background-color:#cd5f5d !important} | ||||
| 			.w3-theme-d1 {color:#fff !important; background-color:#a93634 !important} | ||||
| 			.w3-theme-d2 {color:#fff !important; background-color:#96302e !important} | ||||
| 			.w3-theme-d3 {color:#fff !important; background-color:#832a28 !important} | ||||
| 			.w3-theme-d4 {color:#fff !important; background-color:#702423 !important} | ||||
| 			.w3-theme-d5 {color:#fff !important; background-color:#5e1e1d !important} | ||||
|  | ||||
| 			.w3-theme-light {color:#000 !important; background-color:#fbf3f3 !important} | ||||
| 			.w3-theme-dark {color:#fff !important; background-color:#5e1e1d !important} | ||||
| 			.w3-theme-action {color:#fff !important; background-color:#5e1e1d !important} | ||||
|  | ||||
| 			.w3-theme {color:#fff !important; background-color:#bd3d3a !important} | ||||
| 			.w3-text-theme {color:#bd3d3a !important} | ||||
| 			.w3-border-theme {border-color:#bd3d3a !important} | ||||
|  | ||||
| 			.w3-hover-theme:hover {color:#fff !important; background-color:#bd3d3a !important} | ||||
| 			.w3-hover-text-theme:hover {color:#bd3d3a !important} | ||||
| 			.w3-hover-border-theme:hover {border-color:#bd3d3a !important} | ||||
| 		</style> | ||||
| 	</head> | ||||
| 	<body class="w3-theme-l4"> | ||||
| 		<header class="w3-row w3-padding w3-header w3-theme-l1"> | ||||
| 			<h1>Tilde Friends Administration</h1> | ||||
| 		</header> | ||||
| 	<body style="color: #fff; width: 100%"> | ||||
| 		<h1>Tilde Friends Administration</h1> | ||||
| 	</body> | ||||
| 	<script type="module" src="script.js"></script> | ||||
| </html> | ||||
|   | ||||
| @@ -27,87 +27,64 @@ 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> | ||||
| 					<div class="w3-quarter w3-padding">${description.description}</div> | ||||
| 					<div class="w3-quarter w3-padding w3-center"><input class="w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input></div> | ||||
| 					<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstChild.checked)}>Set</button> | ||||
| 				</li> | ||||
| 				<div style="margin-top: 1em"> | ||||
| 					<label for=${'gs_' + key} style="font-weight: bold">${key}: </label> | ||||
| 					<div> | ||||
| 						<input type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input> | ||||
| 						<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button> | ||||
| 						<div>${description.description}</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			`; | ||||
| 		} else if (description.type === 'textarea') { | ||||
| 			return html` | ||||
| 				<li class="w3-row"> | ||||
| 					<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold" | ||||
| 						>${title_case(key)}</label | ||||
| 					> | ||||
| 					<div class="w3-rest w3-padding">${description.description}</div> | ||||
| 					<textarea | ||||
| 						class="w3-input" | ||||
| 						style="vertical-align: top; resize: vertical" | ||||
| 						id=${'gs_' + key} | ||||
| 					> | ||||
| ${description.value}</textarea | ||||
| 					> | ||||
| 					<button | ||||
| 						class="w3-button w3-right w3-quarter w3-theme-action" | ||||
| 						@click=${(e) => | ||||
| 							global_settings_set( | ||||
| 								key, | ||||
| 								e.srcElement.previousElementSibling.value | ||||
| 							)} | ||||
| 					> | ||||
| 						Set | ||||
| 					</button> | ||||
| 				</li> | ||||
| 				<div style="margin-top: 1em""> | ||||
| 					<label for=${'gs_' + key} style="font-weight: bold">${key}: </label> | ||||
| 					<div style="width: 100%; padding: 0; margin: 0"> | ||||
| 						<div style="width: 90%; padding: 0 margin: 0"> | ||||
| 							<textarea style="vertical-align: top; width: 100%" rows=20 cols=80 id=${'gs_' + key}>${description.value}</textarea> | ||||
| 						</div> | ||||
| 						<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstElementChild.value)}>Set</button> | ||||
| 						<div>${description.description}</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			`; | ||||
| 		} 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> | ||||
| 					<div class="w3-quarter w3-padding">${description.description}</div> | ||||
| 					<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input> | ||||
| 					<button class="w3-button w3-quarter w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button> | ||||
| 				</li> | ||||
| 				<div style="margin-top: 1em"> | ||||
| 					<label for=${'gs_' + key} style="font-weight: bold">${key}: </label> | ||||
| 					<div> | ||||
| 						<input type="text" value="${description.value}" id=${'gs_' + key}></input> | ||||
| 						<button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button> | ||||
| 						<div>${description.description}</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			`; | ||||
| 		} | ||||
| 	} | ||||
| 	const user_template = (user, permissions) => html` | ||||
| 		<li class="w3-card w3-margin"> | ||||
| 			<button | ||||
| 				class="w3-button w3-theme-action" | ||||
| 				@click=${(e) => delete_user(user)} | ||||
| 			> | ||||
| 				Delete | ||||
| 			</button> | ||||
| 		<li> | ||||
| 			<button @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> | ||||
| 			<ul class="w3-ul"> | ||||
| 		html`<h2>Users</h2> | ||||
| 			<ul> | ||||
| 				${Object.entries(users).map((u) => user_template(u[0], u[1]))} | ||||
| 			</ul>`; | ||||
| 	const page_template = (data) => | ||||
| 		html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%"> | ||||
| 			<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header> | ||||
| 			<div class="w3-container"> | ||||
| 				<ul class="w3-ul"> | ||||
| 					${Object.keys(data.settings) | ||||
| 						.sort() | ||||
| 						.map((x) => html`${input_template(x, data.settings[x])}`)} | ||||
| 				</ul> | ||||
| 			<h2>Global Settings</h2> | ||||
| 			<div> | ||||
| 				${Object.keys(data.settings) | ||||
| 					.sort() | ||||
| 					.map((x) => html`${input_template(x, data.settings[x])}`)} | ||||
| 			</div> | ||||
| 			${users_template(data.users)} | ||||
| 		</div> `; | ||||
|   | ||||
| @@ -1,251 +0,0 @@ | ||||
| /* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */ | ||||
| html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit} | ||||
| /* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ | ||||
| html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} | ||||
| article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item} | ||||
| audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline} | ||||
| audio:not([controls]){display:none;height:0}[hidden],template{display:none} | ||||
| a{background-color:transparent}a:active,a:hover{outline-width:0} | ||||
| abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted} | ||||
| b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000} | ||||
| small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} | ||||
| sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none} | ||||
| code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible} | ||||
| button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold} | ||||
| button,input{overflow:visible}button,select{text-transform:none} | ||||
| button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button} | ||||
| button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0} | ||||
| button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText} | ||||
| fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em} | ||||
| legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto} | ||||
| [type=checkbox],[type=radio]{padding:0} | ||||
| [type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto} | ||||
| [type=search]{-webkit-appearance:textfield;outline-offset:-2px} | ||||
| [type=search]::-webkit-search-decoration{-webkit-appearance:none} | ||||
| ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} | ||||
| /* End extract */ | ||||
| html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden} | ||||
| h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px} | ||||
| .w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace} | ||||
| h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px} | ||||
| hr{border:0;border-top:1px solid #eee;margin:20px 0} | ||||
| .w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit} | ||||
| .w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc} | ||||
| .w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center} | ||||
| .w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top} | ||||
| .w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px} | ||||
| .w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap} | ||||
| .w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}    | ||||
| .w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none} | ||||
| .w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none} | ||||
| .w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%} | ||||
| .w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none} | ||||
| .w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block} | ||||
| .w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s} | ||||
| .w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%} | ||||
| .w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc} | ||||
| .w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer} | ||||
| .w3-dropdown-hover:hover .w3-dropdown-content{display:block} | ||||
| .w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1} | ||||
| .w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px} | ||||
| .w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto} | ||||
| .w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px} | ||||
| .w3-main,#main{transition:margin-left .4s} | ||||
| .w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)} | ||||
| .w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px} | ||||
| .w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto} | ||||
| .w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0} | ||||
| .w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left} | ||||
| .w3-bar .w3-button{white-space:normal} | ||||
| .w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0} | ||||
| .w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%} | ||||
| .w3-responsive{display:block;overflow-x:auto} | ||||
| .w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before, | ||||
| .w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both} | ||||
| .w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%} | ||||
| .w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%} | ||||
| .w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%} | ||||
| .w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%} | ||||
| @media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%} | ||||
| .w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%} | ||||
| .w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}} | ||||
| @media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%} | ||||
| .w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%} | ||||
| .w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}} | ||||
| .w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px} | ||||
| .w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px} | ||||
| .w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell} | ||||
| .w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom} | ||||
| .w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important} | ||||
| @media (max-width:1205px){.w3-auto{max-width:95%}} | ||||
| @media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px} | ||||
| .w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}	 | ||||
| .w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center} | ||||
| .w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}} | ||||
| @media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}} | ||||
| @media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}} | ||||
| @media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}} | ||||
| @media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}} | ||||
| .w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0} | ||||
| .w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2} | ||||
| .w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0} | ||||
| .w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0} | ||||
| .w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)} | ||||
| .w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)} | ||||
| .w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)} | ||||
| .w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none} | ||||
| .w3-display-position{position:absolute} | ||||
| .w3-circle{border-radius:50%} | ||||
| .w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px} | ||||
| .w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px} | ||||
| .w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px} | ||||
| .w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex} | ||||
| .w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic} | ||||
| .w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px} | ||||
| .w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word} | ||||
| .w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%} | ||||
| .w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)} | ||||
| .w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}} | ||||
| .w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} | ||||
| .w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}} | ||||
| .w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}} | ||||
| .w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}} | ||||
| .w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}} | ||||
| .w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}} | ||||
| .w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}} | ||||
| .w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important} | ||||
| .w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1} | ||||
| .w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75} | ||||
| .w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)} | ||||
| .w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)} | ||||
| .w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)} | ||||
| .w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important} | ||||
| .w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important} | ||||
| .w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important} | ||||
| .w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important} | ||||
| .w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important} | ||||
| .w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important} | ||||
| .w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important} | ||||
| .w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important} | ||||
| .w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important} | ||||
| .w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important} | ||||
| .w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important} | ||||
| .w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important} | ||||
| .w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important} | ||||
| .w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important} | ||||
| .w3-padding-64{padding-top:64px!important;padding-bottom:64px!important} | ||||
| .w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important} | ||||
| .w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important} | ||||
| .w3-left{float:left!important}.w3-right{float:right!important} | ||||
| .w3-button:hover{color:#000!important;background-color:#ccc!important} | ||||
| .w3-transparent,.w3-hover-none:hover{background-color:transparent!important} | ||||
| .w3-hover-none:hover{box-shadow:none!important} | ||||
| .w3-rtl{direction:rtl}.w3-ltr{direction:ltr} | ||||
| /* Colors */ | ||||
| .w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important} | ||||
| .w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important} | ||||
| .w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important} | ||||
| .w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important} | ||||
| .w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important} | ||||
| .w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important} | ||||
| .w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important} | ||||
| .w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important} | ||||
| .w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important} | ||||
| .w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important} | ||||
| .w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important} | ||||
| .w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important} | ||||
| .w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important} | ||||
| .w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important} | ||||
| .w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important} | ||||
| .w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important} | ||||
| .w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important} | ||||
| .w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important} | ||||
| .w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important} | ||||
| .w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important} | ||||
| .w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important} | ||||
| .w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important} | ||||
| .w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important} | ||||
| .w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important} | ||||
| .w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important} | ||||
| .w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important} | ||||
| .w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important} | ||||
| .w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important} | ||||
| .w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important} | ||||
| .w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important} | ||||
| .w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important} | ||||
| .w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important} | ||||
| .w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important} | ||||
| .w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important} | ||||
| .w3-danger{color:#fff!important;background-color:#dd0000!important} | ||||
| .w3-note{color:#000!important;background-color:#fff599!important} | ||||
| .w3-info{color:#fff!important;background-color:#0a6fc2!important} | ||||
| .w3-warning{color:#000!important;background-color:#ffb305!important} | ||||
| .w3-success{color:#fff!important;background-color:#008a00!important} | ||||
| .w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important} | ||||
| .w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important} | ||||
| .w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important} | ||||
| .w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important} | ||||
| .w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important} | ||||
| .w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important} | ||||
| .w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important} | ||||
| .w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important} | ||||
| .w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important} | ||||
| .w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important} | ||||
| .w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important} | ||||
| .w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important} | ||||
| .w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important} | ||||
| .w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important} | ||||
| .w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important} | ||||
| .w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important} | ||||
| .w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important} | ||||
| .w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important} | ||||
| .w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important} | ||||
| .w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important} | ||||
| .w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important} | ||||
| .w3-text-red,.w3-hover-text-red:hover{color:#f44336!important} | ||||
| .w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important} | ||||
| .w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important} | ||||
| .w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important} | ||||
| .w3-text-white,.w3-hover-text-white:hover{color:#fff!important} | ||||
| .w3-text-black,.w3-hover-text-black:hover{color:#000!important} | ||||
| .w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important} | ||||
| .w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important} | ||||
| .w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important} | ||||
| .w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important} | ||||
| .w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important} | ||||
| .w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important} | ||||
| .w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important} | ||||
| .w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important} | ||||
| .w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important} | ||||
| .w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important} | ||||
| .w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important} | ||||
| .w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important} | ||||
| .w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important} | ||||
| .w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important} | ||||
| .w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important} | ||||
| .w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important} | ||||
| .w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important} | ||||
| .w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important} | ||||
| .w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important} | ||||
| .w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important} | ||||
| .w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important} | ||||
| .w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important} | ||||
| .w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important} | ||||
| .w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important} | ||||
| .w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important} | ||||
| .w3-border-black,.w3-hover-border-black:hover{border-color:#000!important} | ||||
| .w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important} | ||||
| .w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important} | ||||
| .w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important} | ||||
| .w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important} | ||||
| .w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} | ||||
| @@ -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> | ||||
|  | ||||
|   | ||||
							
								
								
									
										251
									
								
								apps/apps/w3.css
									
									
									
									
									
								
							
							
						
						| @@ -1,251 +0,0 @@ | ||||
| /* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */ | ||||
| html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit} | ||||
| /* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ | ||||
| html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} | ||||
| article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item} | ||||
| audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline} | ||||
| audio:not([controls]){display:none;height:0}[hidden],template{display:none} | ||||
| a{background-color:transparent}a:active,a:hover{outline-width:0} | ||||
| abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted} | ||||
| b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000} | ||||
| small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} | ||||
| sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none} | ||||
| code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible} | ||||
| button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold} | ||||
| button,input{overflow:visible}button,select{text-transform:none} | ||||
| button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button} | ||||
| button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0} | ||||
| button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText} | ||||
| fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em} | ||||
| legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto} | ||||
| [type=checkbox],[type=radio]{padding:0} | ||||
| [type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto} | ||||
| [type=search]{-webkit-appearance:textfield;outline-offset:-2px} | ||||
| [type=search]::-webkit-search-decoration{-webkit-appearance:none} | ||||
| ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} | ||||
| /* End extract */ | ||||
| html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden} | ||||
| h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px} | ||||
| .w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace} | ||||
| h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px} | ||||
| hr{border:0;border-top:1px solid #eee;margin:20px 0} | ||||
| .w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit} | ||||
| .w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc} | ||||
| .w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center} | ||||
| .w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top} | ||||
| .w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px} | ||||
| .w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap} | ||||
| .w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}    | ||||
| .w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none} | ||||
| .w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none} | ||||
| .w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%} | ||||
| .w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none} | ||||
| .w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block} | ||||
| .w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s} | ||||
| .w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%} | ||||
| .w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc} | ||||
| .w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer} | ||||
| .w3-dropdown-hover:hover .w3-dropdown-content{display:block} | ||||
| .w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1} | ||||
| .w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px} | ||||
| .w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto} | ||||
| .w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px} | ||||
| .w3-main,#main{transition:margin-left .4s} | ||||
| .w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)} | ||||
| .w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px} | ||||
| .w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto} | ||||
| .w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0} | ||||
| .w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left} | ||||
| .w3-bar .w3-button{white-space:normal} | ||||
| .w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0} | ||||
| .w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%} | ||||
| .w3-responsive{display:block;overflow-x:auto} | ||||
| .w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before, | ||||
| .w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both} | ||||
| .w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%} | ||||
| .w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%} | ||||
| .w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%} | ||||
| .w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%} | ||||
| @media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%} | ||||
| .w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%} | ||||
| .w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}} | ||||
| @media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%} | ||||
| .w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%} | ||||
| .w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}} | ||||
| .w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px} | ||||
| .w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px} | ||||
| .w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell} | ||||
| .w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom} | ||||
| .w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important} | ||||
| @media (max-width:1205px){.w3-auto{max-width:95%}} | ||||
| @media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px} | ||||
| .w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}	 | ||||
| .w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center} | ||||
| .w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}} | ||||
| @media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}} | ||||
| @media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}} | ||||
| @media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}} | ||||
| @media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}} | ||||
| .w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0} | ||||
| .w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2} | ||||
| .w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0} | ||||
| .w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0} | ||||
| .w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)} | ||||
| .w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)} | ||||
| .w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)} | ||||
| .w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none} | ||||
| .w3-display-position{position:absolute} | ||||
| .w3-circle{border-radius:50%} | ||||
| .w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px} | ||||
| .w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px} | ||||
| .w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px} | ||||
| .w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex} | ||||
| .w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic} | ||||
| .w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px} | ||||
| .w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word} | ||||
| .w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%} | ||||
| .w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)} | ||||
| .w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}} | ||||
| .w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} | ||||
| .w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}} | ||||
| .w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}} | ||||
| .w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}} | ||||
| .w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}} | ||||
| .w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}} | ||||
| .w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}} | ||||
| .w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important} | ||||
| .w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1} | ||||
| .w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75} | ||||
| .w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)} | ||||
| .w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)} | ||||
| .w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)} | ||||
| .w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important} | ||||
| .w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important} | ||||
| .w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important} | ||||
| .w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important} | ||||
| .w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important} | ||||
| .w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important} | ||||
| .w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important} | ||||
| .w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important} | ||||
| .w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important} | ||||
| .w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important} | ||||
| .w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important} | ||||
| .w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important} | ||||
| .w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important} | ||||
| .w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important} | ||||
| .w3-padding-64{padding-top:64px!important;padding-bottom:64px!important} | ||||
| .w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important} | ||||
| .w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important} | ||||
| .w3-left{float:left!important}.w3-right{float:right!important} | ||||
| .w3-button:hover{color:#000!important;background-color:#ccc!important} | ||||
| .w3-transparent,.w3-hover-none:hover{background-color:transparent!important} | ||||
| .w3-hover-none:hover{box-shadow:none!important} | ||||
| .w3-rtl{direction:rtl}.w3-ltr{direction:ltr} | ||||
| /* Colors */ | ||||
| .w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important} | ||||
| .w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important} | ||||
| .w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important} | ||||
| .w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important} | ||||
| .w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important} | ||||
| .w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important} | ||||
| .w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important} | ||||
| .w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important} | ||||
| .w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important} | ||||
| .w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important} | ||||
| .w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important} | ||||
| .w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important} | ||||
| .w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important} | ||||
| .w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important} | ||||
| .w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important} | ||||
| .w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important} | ||||
| .w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important} | ||||
| .w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important} | ||||
| .w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important} | ||||
| .w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important} | ||||
| .w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important} | ||||
| .w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important} | ||||
| .w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important} | ||||
| .w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important} | ||||
| .w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important} | ||||
| .w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important} | ||||
| .w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important} | ||||
| .w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important} | ||||
| .w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important} | ||||
| .w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important} | ||||
| .w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important} | ||||
| .w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important} | ||||
| .w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important} | ||||
| .w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important} | ||||
| .w3-danger{color:#fff!important;background-color:#dd0000!important} | ||||
| .w3-note{color:#000!important;background-color:#fff599!important} | ||||
| .w3-info{color:#fff!important;background-color:#0a6fc2!important} | ||||
| .w3-warning{color:#000!important;background-color:#ffb305!important} | ||||
| .w3-success{color:#fff!important;background-color:#008a00!important} | ||||
| .w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important} | ||||
| .w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important} | ||||
| .w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important} | ||||
| .w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important} | ||||
| .w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important} | ||||
| .w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important} | ||||
| .w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important} | ||||
| .w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important} | ||||
| .w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important} | ||||
| .w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important} | ||||
| .w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important} | ||||
| .w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important} | ||||
| .w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important} | ||||
| .w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important} | ||||
| .w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important} | ||||
| .w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important} | ||||
| .w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important} | ||||
| .w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important} | ||||
| .w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important} | ||||
| .w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important} | ||||
| .w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important} | ||||
| .w3-text-red,.w3-hover-text-red:hover{color:#f44336!important} | ||||
| .w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important} | ||||
| .w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important} | ||||
| .w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important} | ||||
| .w3-text-white,.w3-hover-text-white:hover{color:#fff!important} | ||||
| .w3-text-black,.w3-hover-text-black:hover{color:#000!important} | ||||
| .w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important} | ||||
| .w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important} | ||||
| .w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important} | ||||
| .w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important} | ||||
| .w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important} | ||||
| .w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important} | ||||
| .w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important} | ||||
| .w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important} | ||||
| .w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important} | ||||
| .w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important} | ||||
| .w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important} | ||||
| .w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important} | ||||
| .w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important} | ||||
| .w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important} | ||||
| .w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important} | ||||
| .w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important} | ||||
| .w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important} | ||||
| .w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important} | ||||
| .w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important} | ||||
| .w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important} | ||||
| .w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important} | ||||
| .w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important} | ||||
| .w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important} | ||||
| .w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important} | ||||
| .w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important} | ||||
| .w3-border-black,.w3-hover-border-black:hover{border-color:#000!important} | ||||
| .w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important} | ||||
| .w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important} | ||||
| .w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important} | ||||
| .w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important} | ||||
| .w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} | ||||
| @@ -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
									
									
								
							
							
						
						
							
								
								
									
										42
									
								
								apps/blog/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -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": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.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,44 +15,11 @@ 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> | ||||
| 			<link rel="stylesheet" href="w3.css"></link> | ||||
| 			<style> | ||||
| 				/* "2018 Sargasso Sea" */ | ||||
| 				.w3-theme-l5 {color:#000 !important; background-color:#f3f4f7 !important} | ||||
| 				.w3-theme-l4 {color:#000 !important; background-color:#d7dbe3 !important} | ||||
| 				.w3-theme-l3 {color:#000 !important; background-color:#b0b6c8 !important} | ||||
| 				.w3-theme-l2 {color:#fff !important; background-color:#8892ac !important} | ||||
| 				.w3-theme-l1 {color:#fff !important; background-color:#636f8e !important} | ||||
| 				.w3-theme-d1 {color:#fff !important; background-color:#40485c !important} | ||||
| 				.w3-theme-d2 {color:#fff !important; background-color:#394052 !important} | ||||
| 				.w3-theme-d3 {color:#fff !important; background-color:#323848 !important} | ||||
| 				.w3-theme-d4 {color:#fff !important; background-color:#2b303d !important} | ||||
| 				.w3-theme-d5 {color:#fff !important; background-color:#242833 !important} | ||||
|  | ||||
| 				.w3-theme-light {color:#000 !important; background-color:#f3f4f7 !important} | ||||
| 				.w3-theme-dark {color:#fff !important; background-color:#242833 !important} | ||||
| 				.w3-theme-action {color:#fff !important; background-color:#242833 !important} | ||||
|  | ||||
| 				.w3-theme {color:#fff !important; background-color:#485167 !important} | ||||
| 				.w3-text-theme {color:#485167 !important} | ||||
| 				.w3-border-theme {border-color:#485167 !important} | ||||
|  | ||||
| 				.w3-hover-theme:hover {color:#fff !important; background-color:#485167 !important} | ||||
| 				.w3-hover-text-theme:hover {color:#485167 !important} | ||||
| 				.w3-hover-border-theme:hover {border-color:#485167 !important} | ||||
| 			</style> | ||||
| 		</head> | ||||
| 		<body class="w3-theme-l3"> | ||||
| 		`<body style="color: #fff"> | ||||
| 		<script>const handler = {};</script> | ||||
| 		<script type="module"> | ||||
| 			import * as tfrpc from '/static/tfrpc.js'; | ||||
| @@ -62,8 +27,7 @@ async function main() { | ||||
| 				let id = event.srcElement.dataset.id; | ||||
| 				let element = document.createElement('textarea'); | ||||
| 				element.value = await tfrpc.rpc.get_private_key(id); | ||||
| 				element.style = 'width: 100%; height: auto; read-only: true; resize: none'; | ||||
| 				element.classList.add('w3-input'); | ||||
| 				element.style = 'width: 100%; read-only: true'; | ||||
| 				element.readOnly = true; | ||||
| 				event.srcElement.parentElement.appendChild(element); | ||||
| 				event.srcElement.onclick = event => handler.hide_id(event, element); | ||||
| @@ -84,7 +48,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,48 +68,24 @@ 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"> | ||||
| 			<header class="w3-container w3-theme-l2"><h2>Create a new identity</h2></header> | ||||
| 			<footer class="w3-padding"> | ||||
| 				<button id="create_id" onclick="handler.create_id()" class="w3-button w3-theme">Create Identity</button> | ||||
| 			</footer> | ||||
| 		</div> | ||||
| 		<div class="w3-card-4 w3-margin"> | ||||
| 			<header class="w3-container w3-theme-l2"><h2>Import an SSB Identity from 12 BIP39 English Words</h2></header> | ||||
| 			<textarea id="add_id" style="width: 100%" rows="4" class="w3-input"></textarea> | ||||
| 			<footer class="w3-padding"> | ||||
| 				<button id="add" onclick="handler.add_id(event)" class="w3-button w3-theme">Import Identity</button> | ||||
| 			</footer> | ||||
| 		</div> | ||||
| 		<div class="w3-card-4 w3-margin"> | ||||
| 			<header class="w3-container w3-theme-l2"><h2>Identities</h2></header> | ||||
| 			<ul class="w3-ul">` + | ||||
| 			(ids ?? []) | ||||
| 		<h1>SSB Identity Management</h1> | ||||
| 		<h2>Create a new identity</h2> | ||||
| 		<button id="create_id" onclick="handler.create_id()">Create Identity</button> | ||||
| 		<h2>Import an SSB Identity from 12 BIP39 English Words</h2> | ||||
| 		<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button> | ||||
| 		<h2>Identities</h2> | ||||
| 		<ul>` + | ||||
| 			ids | ||||
| 				.map( | ||||
| 					( | ||||
| 						id | ||||
| 					) => `<li style="overflow: hidden; text-wrap: nowrap; text-overflow: ellipsis"> | ||||
| 				<button onclick="handler.export_id(event)" data-id="${id}" class="w3-button w3-theme">Export Identity</button> | ||||
| 				<button onclick="handler.delete_id(event)" data-id="${id}" class="w3-button w3-theme">Delete Identity</button> | ||||
| 				${is_admin && id != server_id ? `<button onclick="handler.make_server(event)" data-id="${id}" class="w3-button w3-theme">Make Server Identity</button>` : ''} | ||||
| 				${id}${id == server_id ? ' <div class="w3-tag w3-theme-l4 w3-round">🖥 local server</div>' : ''} | ||||
| 			</li>` | ||||
| 					(id) => `<li> | ||||
| 			<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button> | ||||
| 			<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button> | ||||
| 			${id} | ||||
| 		</li>` | ||||
| 				) | ||||
| 				.join('\n') + | ||||
| 			`	</ul> | ||||
| 		</div> | ||||
| 	</body>` | ||||
| 	); | ||||
| } | ||||
|   | ||||
| @@ -1,251 +0,0 @@ | ||||
| /* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */ | ||||
| html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit} | ||||
| /* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ | ||||
| html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} | ||||
| article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item} | ||||
| audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline} | ||||
| audio:not([controls]){display:none;height:0}[hidden],template{display:none} | ||||
| a{background-color:transparent}a:active,a:hover{outline-width:0} | ||||
| abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted} | ||||
| b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000} | ||||
| small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} | ||||
| sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none} | ||||
| code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible} | ||||
| button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold} | ||||
| button,input{overflow:visible}button,select{text-transform:none} | ||||
| button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button} | ||||
| button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0} | ||||
| button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText} | ||||
| fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em} | ||||
| legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto} | ||||
| [type=checkbox],[type=radio]{padding:0} | ||||
| [type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto} | ||||
| [type=search]{-webkit-appearance:textfield;outline-offset:-2px} | ||||
| [type=search]::-webkit-search-decoration{-webkit-appearance:none} | ||||
| ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} | ||||
| /* End extract */ | ||||
| html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden} | ||||
| h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px} | ||||
| .w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace} | ||||
| h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px} | ||||
| hr{border:0;border-top:1px solid #eee;margin:20px 0} | ||||
| .w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit} | ||||
| .w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc} | ||||
| .w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center} | ||||
| .w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top} | ||||
| .w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px} | ||||
| .w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap} | ||||
| .w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}    | ||||
| .w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none} | ||||
| .w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none} | ||||
| .w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%} | ||||
| .w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none} | ||||
| .w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block} | ||||
| .w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s} | ||||
| .w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%} | ||||
| .w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc} | ||||
| .w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer} | ||||
| .w3-dropdown-hover:hover .w3-dropdown-content{display:block} | ||||
| .w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1} | ||||
| .w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px} | ||||
| .w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto} | ||||
| .w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px} | ||||
| .w3-main,#main{transition:margin-left .4s} | ||||
| .w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)} | ||||
| .w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px} | ||||
| .w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto} | ||||
| .w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0} | ||||
| .w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left} | ||||
| .w3-bar .w3-button{white-space:normal} | ||||
| .w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0} | ||||
| .w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%} | ||||
| .w3-responsive{display:block;overflow-x:auto} | ||||
| .w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before, | ||||
| .w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both} | ||||
| .w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%} | ||||
| .w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%} | ||||
| .w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%} | ||||
| .w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%} | ||||
| @media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%} | ||||
| .w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%} | ||||
| .w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}} | ||||
| @media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%} | ||||
| .w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%} | ||||
| .w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}} | ||||
| .w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px} | ||||
| .w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px} | ||||
| .w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell} | ||||
| .w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom} | ||||
| .w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important} | ||||
| @media (max-width:1205px){.w3-auto{max-width:95%}} | ||||
| @media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px} | ||||
| .w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}	 | ||||
| .w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center} | ||||
| .w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}} | ||||
| @media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}} | ||||
| @media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}} | ||||
| @media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}} | ||||
| @media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}} | ||||
| .w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0} | ||||
| .w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2} | ||||
| .w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0} | ||||
| .w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0} | ||||
| .w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)} | ||||
| .w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)} | ||||
| .w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)} | ||||
| .w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none} | ||||
| .w3-display-position{position:absolute} | ||||
| .w3-circle{border-radius:50%} | ||||
| .w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px} | ||||
| .w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px} | ||||
| .w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px} | ||||
| .w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex} | ||||
| .w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic} | ||||
| .w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px} | ||||
| .w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word} | ||||
| .w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%} | ||||
| .w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)} | ||||
| .w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}} | ||||
| .w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} | ||||
| .w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}} | ||||
| .w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}} | ||||
| .w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}} | ||||
| .w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}} | ||||
| .w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}} | ||||
| .w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}} | ||||
| .w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important} | ||||
| .w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1} | ||||
| .w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75} | ||||
| .w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)} | ||||
| .w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)} | ||||
| .w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)} | ||||
| .w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important} | ||||
| .w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important} | ||||
| .w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important} | ||||
| .w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important} | ||||
| .w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important} | ||||
| .w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important} | ||||
| .w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important} | ||||
| .w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important} | ||||
| .w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important} | ||||
| .w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important} | ||||
| .w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important} | ||||
| .w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important} | ||||
| .w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important} | ||||
| .w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important} | ||||
| .w3-padding-64{padding-top:64px!important;padding-bottom:64px!important} | ||||
| .w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important} | ||||
| .w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important} | ||||
| .w3-left{float:left!important}.w3-right{float:right!important} | ||||
| .w3-button:hover{color:#000!important;background-color:#ccc!important} | ||||
| .w3-transparent,.w3-hover-none:hover{background-color:transparent!important} | ||||
| .w3-hover-none:hover{box-shadow:none!important} | ||||
| .w3-rtl{direction:rtl}.w3-ltr{direction:ltr} | ||||
| /* Colors */ | ||||
| .w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important} | ||||
| .w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important} | ||||
| .w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important} | ||||
| .w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important} | ||||
| .w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important} | ||||
| .w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important} | ||||
| .w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important} | ||||
| .w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important} | ||||
| .w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important} | ||||
| .w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important} | ||||
| .w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important} | ||||
| .w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important} | ||||
| .w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important} | ||||
| .w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important} | ||||
| .w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important} | ||||
| .w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important} | ||||
| .w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important} | ||||
| .w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important} | ||||
| .w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important} | ||||
| .w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important} | ||||
| .w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important} | ||||
| .w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important} | ||||
| .w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important} | ||||
| .w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important} | ||||
| .w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important} | ||||
| .w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important} | ||||
| .w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important} | ||||
| .w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important} | ||||
| .w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important} | ||||
| .w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important} | ||||
| .w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important} | ||||
| .w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important} | ||||
| .w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important} | ||||
| .w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important} | ||||
| .w3-danger{color:#fff!important;background-color:#dd0000!important} | ||||
| .w3-note{color:#000!important;background-color:#fff599!important} | ||||
| .w3-info{color:#fff!important;background-color:#0a6fc2!important} | ||||
| .w3-warning{color:#000!important;background-color:#ffb305!important} | ||||
| .w3-success{color:#fff!important;background-color:#008a00!important} | ||||
| .w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important} | ||||
| .w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important} | ||||
| .w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important} | ||||
| .w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important} | ||||
| .w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important} | ||||
| .w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important} | ||||
| .w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important} | ||||
| .w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important} | ||||
| .w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important} | ||||
| .w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important} | ||||
| .w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important} | ||||
| .w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important} | ||||
| .w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important} | ||||
| .w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important} | ||||
| .w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important} | ||||
| .w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important} | ||||
| .w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important} | ||||
| .w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important} | ||||
| .w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important} | ||||
| .w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important} | ||||
| .w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important} | ||||
| .w3-text-red,.w3-hover-text-red:hover{color:#f44336!important} | ||||
| .w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important} | ||||
| .w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important} | ||||
| .w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important} | ||||
| .w3-text-white,.w3-hover-text-white:hover{color:#fff!important} | ||||
| .w3-text-black,.w3-hover-text-black:hover{color:#000!important} | ||||
| .w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important} | ||||
| .w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important} | ||||
| .w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important} | ||||
| .w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important} | ||||
| .w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important} | ||||
| .w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important} | ||||
| .w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important} | ||||
| .w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important} | ||||
| .w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important} | ||||
| .w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important} | ||||
| .w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important} | ||||
| .w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important} | ||||
| .w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important} | ||||
| .w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important} | ||||
| .w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important} | ||||
| .w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important} | ||||
| .w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important} | ||||
| .w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important} | ||||
| .w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important} | ||||
| .w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important} | ||||
| .w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important} | ||||
| .w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important} | ||||
| .w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important} | ||||
| .w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important} | ||||
| .w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important} | ||||
| .w3-border-black,.w3-hover-border-black:hover{border-color:#000!important} | ||||
| .w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important} | ||||
| .w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important} | ||||
| .w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important} | ||||
| .w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important} | ||||
| .w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} | ||||
| @@ -1,5 +0,0 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "💡", | ||||
| 	"previous": "&eN6DNPpQUNhGvxneLuLPgsOXR6qyFZ7u+MAz0b4fa7k=.sha256" | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| import * as tfrpc from '/tfrpc.js'; | ||||
|  | ||||
| async function main() { | ||||
| 	await app.setDocument(utf8Decode(getFile('index.html'))); | ||||
| } | ||||
|  | ||||
| tfrpc.register(async function complete() { | ||||
| 	if ( | ||||
| 		core.user?.credentials?.permissions?.administration && | ||||
| 		(await core.globalSettingsGet('index')) == '/~core/intro/' | ||||
| 	) { | ||||
| 		return await core.globalSettingsSet('index', '/~core/ssb/'); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| main(); | ||||
| @@ -1,286 +0,0 @@ | ||||
| <!doctype html> | ||||
| <html style="height: 100%; margin: 0; padding: 0; box-sizing: border-box"> | ||||
| 	<head> | ||||
| 		<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||
| 		<link rel="stylesheet" type="text/css" href="w3.css" /> | ||||
| 		<style> | ||||
| 			.slide { | ||||
| 				display: none; | ||||
| 				margin-left: auto; | ||||
| 				margin-right: auto; | ||||
| 			} | ||||
| 			.dot { | ||||
| 				width: 1em; | ||||
| 				height: 1em; | ||||
| 				cursor: pointer; | ||||
| 			} | ||||
| 			.w3-left, | ||||
| 			.w3-right { | ||||
| 				cursor: pointer; | ||||
| 			} | ||||
| 		</style> | ||||
| 	</head> | ||||
| 	<body | ||||
| 		style=" | ||||
| 			width: 100%; | ||||
| 			height: 100%; | ||||
| 			max-width: 100%; | ||||
| 			max-height: 100%; | ||||
| 			margin: 0; | ||||
| 			padding: 0; | ||||
| 			flex-direction: column; | ||||
| 			align-items: center; | ||||
| 		" | ||||
| 		class="w3-flex w3-dark-gray w3-center" | ||||
| 	> | ||||
| 		<div | ||||
| 			style=" | ||||
| 				flex: 1 1 auto; | ||||
| 				overflow: auto; | ||||
| 				contain: content; | ||||
| 				padding-top: 16px; | ||||
| 				padding-bottom: 16px; | ||||
| 			" | ||||
| 		> | ||||
| 			<div class="slide"> | ||||
| 				<div | ||||
| 					class="w3-content w3-xlarge w3-card-4 w3-blue w3-panel w3-padding-32 w3-round-xlarge" | ||||
| 					style="margin: 32px" | ||||
| 				> | ||||
| 					<div> | ||||
| 						<div>Welcome to</div> | ||||
| 						<div>~😎 Tilde Friends.</div> | ||||
| 					</div> | ||||
| 					<footer> | ||||
| 						<button class="w3-button w3-yellow proceed">Next</button> | ||||
| 					</footer> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="slide w3-card-4 w3-gray" style="width: 90%"> | ||||
| 				<header class="w3-container w3-blue w3-xlarge"> | ||||
| 					<h1>This brief tutorial will introduce:</h1> | ||||
| 				</header> | ||||
| 				<ul class="w3-large w3-left-align"> | ||||
| 					<li><b>Secure Scuttlebutt</b>, a decentralized social network.</li> | ||||
| 					<li> | ||||
| 						<b>Tilde Friends</b>, the application platform that you are using | ||||
| 						right now. | ||||
| 					</li> | ||||
| 					<li> | ||||
| 						<b>How to get started</b> if you want to get gossiping right away. | ||||
| 					</li> | ||||
| 				</ul> | ||||
| 				<footer class="w3-center w3-xlarge w3-padding"> | ||||
| 					<button class="w3-button w3-yellow proceed">Onward</button> | ||||
| 				</footer> | ||||
| 			</div> | ||||
| 			<div class="slide w3-gray" style="width: 90%"> | ||||
| 				<div class="w3-card-4 w3-xlarge"> | ||||
| 					<header class="w3-container w3-blue"> | ||||
| 						<h1>💻Secure Scuttlebutt in a Nutshell🦀</h1> | ||||
| 					</header> | ||||
| 					<div class="w3-container w3-large w3-left-align"> | ||||
| 						<p> | ||||
| 							Secure Scuttlebutt is a social network whose technical operation | ||||
| 							attempts to mirror human social interaction. | ||||
| 						</p> | ||||
| 						<ul> | ||||
| 							<li> | ||||
| 								You can create your own account and post to your own feed on | ||||
| 								your own device. This is all <b>local</b> with no external | ||||
| 								communication. This puts you fully in control of your own words | ||||
| 								and actions. | ||||
| 							</li> | ||||
| 							<li> | ||||
| 								Before you can interact with others, you need to | ||||
| 								<b>connect over the network</b>, either directly to your friends | ||||
| 								(i.e., peer-to-peer between your phones on coffee shop Wi-Fi) or | ||||
| 								to 🚪<i>rooms</i> and 🍻<i>pubs</i> (hint: search the web for | ||||
| 								<i>#ssbroom</i>). | ||||
| 							</li> | ||||
| 							<li> | ||||
| 								Who you choose to <b>follow</b> determines what you see, with | ||||
| 								most people choosing to see messages from friends and friends of | ||||
| 								those friends. If you encounter content you'd rather not see, | ||||
| 								<b>block</b> the offending account to improve the experience for | ||||
| 								you and your followers. | ||||
| 							</li> | ||||
| 							<li> | ||||
| 								Your feed is an <b>immutable</b> log of your activity. Post with | ||||
| 								care, because like your words in real life, posts can't be taken | ||||
| 								back. | ||||
| 							</li> | ||||
| 						</ul> | ||||
| 					</div> | ||||
| 					<footer class="w3-center w3-xlarge w3-padding"> | ||||
| 						<a | ||||
| 							class="w3-button w3-light-gray" | ||||
| 							href="https://scuttlebutt.nz/" | ||||
| 							target="_blank" | ||||
| 							>See scuttlebutt.nz</a | ||||
| 						> | ||||
| 						<button class="w3-button w3-yellow proceed">Got It</button> | ||||
| 					</footer> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="slide w3-gray" style="width: 90%"> | ||||
| 				<div class="w3-card-4 w3-xlarge"> | ||||
| 					<header class="w3-container w3-blue w3-center"> | ||||
| 						<h1>~😎 Let's Talk Tilde Friends ~😎</h1> | ||||
| 					</header> | ||||
| 					<div class="w3-container w3-large w3-left-align"> | ||||
| 						<p> | ||||
| 							Tilde Friends is an application platform that is an application of | ||||
| 							its own. | ||||
| 						</p> | ||||
| 						<ul> | ||||
| 							<li> | ||||
| 								This intro is a Tilde Friends app. You can click <b>edit</b> at | ||||
| 								the top to look under the hood and make changes. | ||||
| 							</li> | ||||
| 							<li> | ||||
| 								It is already possible to make and share new applications using | ||||
| 								only Tilde Friends and Secure Scuttlebutt without having to set | ||||
| 								up development environments, configure web servers, register | ||||
| 								domain names, or pay for hosting services. | ||||
| 							</li> | ||||
| 							<li> | ||||
| 								But it's also set up so that you can't just break an app that | ||||
| 								everybody is using or do malicious things with personal content. | ||||
| 								There are <b>protections</b> in place like an operating system. | ||||
| 								The intent is also for it to be <b>safe</b> to run strange apps | ||||
| 								without worrying about adverse effects. | ||||
| 							</li> | ||||
| 							<li> | ||||
| 								But this is all a big 🚧work in progress🚧 and | ||||
| 								<b>experiment</b>. Let's see where it takes us. | ||||
| 							</li> | ||||
| 						</ul> | ||||
| 					</div> | ||||
| 					<footer class="w3-center w3-xlarge w3-padding"> | ||||
| 						<button class="w3-button w3-yellow proceed">Okay</button> | ||||
| 					</footer> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="slide w3-gray" style="width: 90%"> | ||||
| 				<div class="w3-card-4 w3-xlarge"> | ||||
| 					<header class="w3-container w3-blue w3-center"> | ||||
| 						<h1>🦀Let's Get this Tilde Friends Party Started🎉</h1> | ||||
| 					</header> | ||||
| 					<div class="w3-container w3-large w3-left-align"> | ||||
| 						<p>The button below will take you to the Secure Scuttlebutt app.</p> | ||||
| 						<ul> | ||||
| 							<li> | ||||
| 								Remember: | ||||
| 								<ol> | ||||
| 									<li>You are in charge. This is all on your device.</li> | ||||
| 									<li> | ||||
| 										Make network connections to exchange messages with others. | ||||
| 									</li> | ||||
| 									<li> | ||||
| 										Follow more accounts to see more content, and block those | ||||
| 										posting content you'd rather not see. | ||||
| 									</li> | ||||
| 									<li> | ||||
| 										Be respectful, and consider the consequences of what you | ||||
| 										post. | ||||
| 									</li> | ||||
| 									<li> | ||||
| 										This is all under active development. Exercise patience, and | ||||
| 										report issues encountered. | ||||
| 									</li> | ||||
| 								</ol> | ||||
| 							</li> | ||||
| 							<li> | ||||
| 								To see this tutorial again later, select <b>apps</b> -> | ||||
| 								<b>Core Apps</b> -> <b>intro</b>. | ||||
| 							</li> | ||||
| 						</ul> | ||||
| 					</div> | ||||
| 					<footer class="w3-center w3-xlarge w3-padding"> | ||||
| 						<button class="w3-button w3-yellow" id="complete">Let's Go!</button> | ||||
| 					</footer> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div | ||||
| 			class="w3-text-white w3-xlarge w3-center w3-flex" | ||||
| 			style=" | ||||
| 				width: 100%; | ||||
| 				flex: 0 1; | ||||
| 				flex-direction: row; | ||||
| 				align-items: center; | ||||
| 				gap: 8px; | ||||
| 			" | ||||
| 		> | ||||
| 			<div class="w3-jumbo" id="left" style="flex: 1 0; cursor: pointer"> | ||||
| 				❮ | ||||
| 			</div> | ||||
| 			<span class="w3-badge dot w3-border w3-hover-yellow"></span> | ||||
| 			<span class="w3-badge dot w3-border w3-hover-yellow"></span> | ||||
| 			<span class="w3-badge dot w3-border w3-hover-yellow"></span> | ||||
| 			<span class="w3-badge dot w3-border w3-hover-yellow"></span> | ||||
| 			<span class="w3-badge dot w3-border w3-hover-yellow"></span> | ||||
| 			<div class="w3-jumbo" style="flex: 1 0; cursor: pointer" id="right"> | ||||
| 				❯ | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<script type="module"> | ||||
| 			import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| 			let index = 0; | ||||
| 			function set(i) { | ||||
| 				show(i - index); | ||||
| 			} | ||||
| 			function show(delta) { | ||||
| 				let slides = [...document.getElementsByClassName('slide')]; | ||||
| 				let dots = [...document.getElementsByClassName('dot')]; | ||||
| 				index = (index + delta + slides.length) % slides.length; | ||||
| 				for (let slide of slides) { | ||||
| 					slide.style.display = | ||||
| 						slides.indexOf(slide) == index ? 'block' : 'none'; | ||||
| 				} | ||||
| 				for (let dot of dots) { | ||||
| 					if (dots.indexOf(dot) == index) { | ||||
| 						dot.classList.add('w3-white'); | ||||
| 					} else { | ||||
| 						dot.classList.remove('w3-white'); | ||||
| 					} | ||||
| 				} | ||||
| 				document.getElementById('left').style.visibility = | ||||
| 					index == 0 ? 'hidden' : 'visible'; | ||||
| 				document.getElementById('right').style.visibility = | ||||
| 					index == slides.length - 1 ? 'hidden' : 'visible'; | ||||
| 			} | ||||
|  | ||||
| 			let dots = [...document.getElementsByClassName('dot')]; | ||||
| 			for (let dot of dots) { | ||||
| 				dot.onclick = () => set(dots.indexOf(dot)); | ||||
| 			} | ||||
| 			for (let button of document.getElementsByClassName('proceed')) { | ||||
| 				button.onclick = () => show(1); | ||||
| 			} | ||||
| 			document.getElementById('left').onclick = () => show(-1); | ||||
| 			document.getElementById('right').onclick = () => show(1); | ||||
| 			document.getElementById('complete').onclick = function () { | ||||
| 				console.log('completing'); | ||||
| 				tfrpc.rpc.complete().finally(function () { | ||||
| 					console.log('completed'); | ||||
| 					let a = document.createElement('a'); | ||||
| 					a.href = '/~core/ssb/'; | ||||
| 					a.target = '_top'; | ||||
| 					document.body.appendChild(a); | ||||
| 					a.click(); | ||||
| 				}); | ||||
| 			}; | ||||
| 			window.addEventListener('keyup', function (event) { | ||||
| 				if (event.key == 'ArrowLeft') { | ||||
| 					show(-1); | ||||
| 				} else if (event.key == 'ArrowRight') { | ||||
| 					show(1); | ||||
| 				} | ||||
| 			}); | ||||
| 			show(0); | ||||
| 		</script> | ||||
| 	</body> | ||||
| </html> | ||||
| @@ -1,251 +0,0 @@ | ||||
| /* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */ | ||||
| html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit} | ||||
| /* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ | ||||
| html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} | ||||
| article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item} | ||||
| audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline} | ||||
| audio:not([controls]){display:none;height:0}[hidden],template{display:none} | ||||
| a{background-color:transparent}a:active,a:hover{outline-width:0} | ||||
| abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted} | ||||
| b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000} | ||||
| small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} | ||||
| sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none} | ||||
| code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible} | ||||
| button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold} | ||||
| button,input{overflow:visible}button,select{text-transform:none} | ||||
| button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button} | ||||
| button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0} | ||||
| button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText} | ||||
| fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em} | ||||
| legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto} | ||||
| [type=checkbox],[type=radio]{padding:0} | ||||
| [type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto} | ||||
| [type=search]{-webkit-appearance:textfield;outline-offset:-2px} | ||||
| [type=search]::-webkit-search-decoration{-webkit-appearance:none} | ||||
| ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} | ||||
| /* End extract */ | ||||
| html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden} | ||||
| h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px} | ||||
| .w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace} | ||||
| h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px} | ||||
| hr{border:0;border-top:1px solid #eee;margin:20px 0} | ||||
| .w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit} | ||||
| .w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc} | ||||
| .w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1} | ||||
| .w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center} | ||||
| .w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top} | ||||
| .w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px} | ||||
| .w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap} | ||||
| .w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}    | ||||
| .w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none} | ||||
| .w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none} | ||||
| .w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%} | ||||
| .w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none} | ||||
| .w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block} | ||||
| .w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s} | ||||
| .w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%} | ||||
| .w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc} | ||||
| .w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer} | ||||
| .w3-dropdown-hover:hover .w3-dropdown-content{display:block} | ||||
| .w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000} | ||||
| .w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1} | ||||
| .w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px} | ||||
| .w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto} | ||||
| .w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%} | ||||
| .w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px} | ||||
| .w3-main,#main{transition:margin-left .4s} | ||||
| .w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)} | ||||
| .w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px} | ||||
| .w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto} | ||||
| .w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0} | ||||
| .w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left} | ||||
| .w3-bar .w3-button{white-space:normal} | ||||
| .w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0} | ||||
| .w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%} | ||||
| .w3-responsive{display:block;overflow-x:auto} | ||||
| .w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before, | ||||
| .w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both} | ||||
| .w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%} | ||||
| .w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%} | ||||
| .w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%} | ||||
| .w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%} | ||||
| @media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%} | ||||
| .w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%} | ||||
| .w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}} | ||||
| @media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%} | ||||
| .w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%} | ||||
| .w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}} | ||||
| .w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px} | ||||
| .w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px} | ||||
| .w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell} | ||||
| .w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom} | ||||
| .w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important} | ||||
| @media (max-width:1205px){.w3-auto{max-width:95%}} | ||||
| @media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px} | ||||
| .w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}	 | ||||
| .w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center} | ||||
| .w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}} | ||||
| @media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}} | ||||
| @media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}} | ||||
| @media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}} | ||||
| @media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}} | ||||
| .w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0} | ||||
| .w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2} | ||||
| .w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0} | ||||
| .w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0} | ||||
| .w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)} | ||||
| .w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)} | ||||
| .w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)} | ||||
| .w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} | ||||
| .w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none} | ||||
| .w3-display-position{position:absolute} | ||||
| .w3-circle{border-radius:50%} | ||||
| .w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px} | ||||
| .w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px} | ||||
| .w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px} | ||||
| .w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex} | ||||
| .w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic} | ||||
| .w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px} | ||||
| .w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word} | ||||
| .w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%} | ||||
| .w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)} | ||||
| .w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)} | ||||
| .w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}} | ||||
| .w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} | ||||
| .w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}} | ||||
| .w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}} | ||||
| .w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}} | ||||
| .w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}} | ||||
| .w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}} | ||||
| .w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}} | ||||
| .w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important} | ||||
| .w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1} | ||||
| .w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75} | ||||
| .w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)} | ||||
| .w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)} | ||||
| .w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)} | ||||
| .w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important} | ||||
| .w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important} | ||||
| .w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important} | ||||
| .w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important} | ||||
| .w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important} | ||||
| .w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important} | ||||
| .w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important} | ||||
| .w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important} | ||||
| .w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important} | ||||
| .w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important} | ||||
| .w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important} | ||||
| .w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important} | ||||
| .w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important} | ||||
| .w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important} | ||||
| .w3-padding-64{padding-top:64px!important;padding-bottom:64px!important} | ||||
| .w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important} | ||||
| .w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important} | ||||
| .w3-left{float:left!important}.w3-right{float:right!important} | ||||
| .w3-button:hover{color:#000!important;background-color:#ccc!important} | ||||
| .w3-transparent,.w3-hover-none:hover{background-color:transparent!important} | ||||
| .w3-hover-none:hover{box-shadow:none!important} | ||||
| .w3-rtl{direction:rtl}.w3-ltr{direction:ltr} | ||||
| /* Colors */ | ||||
| .w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important} | ||||
| .w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important} | ||||
| .w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important} | ||||
| .w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important} | ||||
| .w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important} | ||||
| .w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important} | ||||
| .w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important} | ||||
| .w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important} | ||||
| .w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important} | ||||
| .w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important} | ||||
| .w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important} | ||||
| .w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important} | ||||
| .w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important} | ||||
| .w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important} | ||||
| .w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important} | ||||
| .w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important} | ||||
| .w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important} | ||||
| .w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important} | ||||
| .w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important} | ||||
| .w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important} | ||||
| .w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important} | ||||
| .w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important} | ||||
| .w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important} | ||||
| .w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important} | ||||
| .w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important} | ||||
| .w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important} | ||||
| .w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important} | ||||
| .w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important} | ||||
| .w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important} | ||||
| .w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important} | ||||
| .w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important} | ||||
| .w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important} | ||||
| .w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important} | ||||
| .w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important} | ||||
| .w3-danger{color:#fff!important;background-color:#dd0000!important} | ||||
| .w3-note{color:#000!important;background-color:#fff599!important} | ||||
| .w3-info{color:#fff!important;background-color:#0a6fc2!important} | ||||
| .w3-warning{color:#000!important;background-color:#ffb305!important} | ||||
| .w3-success{color:#fff!important;background-color:#008a00!important} | ||||
| .w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important} | ||||
| .w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important} | ||||
| .w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important} | ||||
| .w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important} | ||||
| .w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important} | ||||
| .w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important} | ||||
| .w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important} | ||||
| .w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important} | ||||
| .w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important} | ||||
| .w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important} | ||||
| .w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important} | ||||
| .w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important} | ||||
| .w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important} | ||||
| .w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important} | ||||
| .w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important} | ||||
| .w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important} | ||||
| .w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important} | ||||
| .w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important} | ||||
| .w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important} | ||||
| .w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important} | ||||
| .w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important} | ||||
| .w3-text-red,.w3-hover-text-red:hover{color:#f44336!important} | ||||
| .w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important} | ||||
| .w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important} | ||||
| .w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important} | ||||
| .w3-text-white,.w3-hover-text-white:hover{color:#fff!important} | ||||
| .w3-text-black,.w3-hover-text-black:hover{color:#000!important} | ||||
| .w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important} | ||||
| .w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important} | ||||
| .w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important} | ||||
| .w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important} | ||||
| .w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important} | ||||
| .w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important} | ||||
| .w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important} | ||||
| .w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important} | ||||
| .w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important} | ||||
| .w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important} | ||||
| .w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important} | ||||
| .w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important} | ||||
| .w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important} | ||||
| .w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important} | ||||
| .w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important} | ||||
| .w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important} | ||||
| .w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important} | ||||
| .w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important} | ||||
| .w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important} | ||||
| .w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important} | ||||
| .w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important} | ||||
| .w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important} | ||||
| .w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important} | ||||
| .w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important} | ||||
| .w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important} | ||||
| .w3-border-black,.w3-hover-border-black:hover{border-color:#000!important} | ||||
| .w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important} | ||||
| .w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important} | ||||
| .w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important} | ||||
| .w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important} | ||||
| .w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} | ||||
| @@ -1,5 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "🦟", | ||||
| 	"previous": "&O0huuEgL/UQC9bkUfhPOyZFo9eRiz+koOkba6QwCGNA=.sha256" | ||||
| 	"previous": "&TegdzvFE+im94shygaHkgDYSaSrwY2h0OKUXSRPBQDM=.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); | ||||
| @@ -82,18 +85,13 @@ tfrpc.register(async function store_message(message) { | ||||
| tfrpc.register(function apps() { | ||||
| 	return core.apps(); | ||||
| }); | ||||
| tfrpc.register(function getActiveIdentity() { | ||||
| 	return ssb.getActiveIdentity(); | ||||
| }); | ||||
| tfrpc.register(async function try_decrypt(id, content) { | ||||
| 	return await ssb.privateMessageDecrypt(id, content); | ||||
| }); | ||||
| core.register('onMessage', async function (id) { | ||||
| 	await tfrpc.rpc.notifyNewMessage(id); | ||||
| }); | ||||
| core.register('onBroadcastsChanged', async function () { | ||||
| 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
									
									
								
							
							
						
						
							
								
								
									
										42
									
								
								apps/issues/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -4,6 +4,48 @@ import * as tfutils from './tf-utils.js'; | ||||
|  | ||||
| const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256'; | ||||
|  | ||||
| class TfIdPickerElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			ids: {type: Array}, | ||||
| 			selected: {type: String}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.load(); | ||||
| 	} | ||||
|  | ||||
| 	async load() { | ||||
| 		this.selected = await tfrpc.rpc.localStorageGet('whoami'); | ||||
| 		this.ids = (await tfrpc.rpc.getIdentities()) || []; | ||||
| 	} | ||||
|  | ||||
| 	changed(event) { | ||||
| 		this.selected = event.srcElement.value; | ||||
| 		tfrpc.rpc.localStorageSet('whoami', this.selected); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		if (this.ids) { | ||||
| 			return html` | ||||
| 				<select @change=${this.changed} style="max-width: 100%"> | ||||
| 					${this.ids.map( | ||||
| 						(id) => | ||||
| 							html`<option ?selected=${id == this.selected} value=${id}> | ||||
| 								${id} | ||||
| 							</option>` | ||||
| 					)} | ||||
| 				</select> | ||||
| 			`; | ||||
| 		} else { | ||||
| 			return html`<div>Loading...</div>`; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| customElements.define('tf-id-picker', TfIdPickerElement); | ||||
|  | ||||
| class TfComposeElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| @@ -63,10 +105,10 @@ class TfIssuesAppElement extends LitElement { | ||||
| 		let issues = {}; | ||||
| 		let messages = await tfrpc.rpc.query( | ||||
| 			` | ||||
| 			WITH issues AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM messages_refs JOIN messages ON | ||||
| 			WITH issues AS (SELECT messages.* FROM messages_refs JOIN messages ON | ||||
| 				messages.id = messages_refs.message | ||||
| 				WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'), | ||||
| 			edits AS (SELECT messages.id, json(messages.content) AS content, messages.author, messages.timestamp FROM issues JOIN messages_refs ON | ||||
| 			edits AS (SELECT messages.* FROM issues JOIN messages_refs ON | ||||
| 				issues.id = messages_refs.ref JOIN messages ON | ||||
| 				messages.id = messages_refs.message | ||||
| 				WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post')) | ||||
| @@ -164,7 +206,7 @@ class TfIssuesAppElement extends LitElement { | ||||
| 		if ( | ||||
| 			confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`) | ||||
| 		) { | ||||
| 			let whoami = await tfrpc.rpc.getActiveIdentity(); | ||||
| 			let whoami = this.shadowRoot.getElementById('picker').selected; | ||||
| 			await tfrpc.rpc.appendMessage(whoami, { | ||||
| 				type: 'issue-edit', | ||||
| 				issues: [ | ||||
| @@ -179,7 +221,7 @@ class TfIssuesAppElement extends LitElement { | ||||
| 	} | ||||
|  | ||||
| 	async create_issue(event) { | ||||
| 		let whoami = await tfrpc.rpc.getActiveIdentity(); | ||||
| 		let whoami = this.shadowRoot.getElementById('picker').selected; | ||||
| 		await tfrpc.rpc.appendMessage(whoami, { | ||||
| 			type: 'issue', | ||||
| 			project: k_project, | ||||
| @@ -189,7 +231,7 @@ class TfIssuesAppElement extends LitElement { | ||||
| 	} | ||||
|  | ||||
| 	async reply_to_issue(event) { | ||||
| 		let whoami = await tfrpc.rpc.getActiveIdentity(); | ||||
| 		let whoami = this.shadowRoot.getElementById('picker').selected; | ||||
| 		await tfrpc.rpc.appendMessage(whoami, { | ||||
| 			type: 'post', | ||||
| 			text: event.detail.value, | ||||
| @@ -207,7 +249,10 @@ class TfIssuesAppElement extends LitElement { | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let header = html` <h1>Tilde Friends Issues</h1> `; | ||||
| 		let header = html` | ||||
| 			<h1>Tilde Friends Issues</h1> | ||||
| 			<tf-id-picker id="picker"></tf-id-picker> | ||||
| 		`; | ||||
| 		if (this.selected) { | ||||
| 			return html` | ||||
| 				${header} | ||||
|   | ||||
| @@ -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
									
									
								
							
							
						
						
							
								
								
									
										42
									
								
								apps/journal/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -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,5 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "🚪", | ||||
| 	"previous": "&DJwkqNfYWtW9yBtJQMseEXm7l04Enpi+yAxZulLq9Vk=.sha256" | ||||
| 	"emoji": "📦", | ||||
| 	"previous": "&IU+TwyM7TznD8NBfnw7tgW2zxVlMqTVxSqWFjuosLwo=.sha256" | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| async function main() { | ||||
| 	print(core.url); | ||||
| 	let host = core.url.match(/.*?\/\/([^:/]*)/)[1]; | ||||
| 	let port = await ssb.port(); | ||||
| 	let id = (await ssb.getServerIdentity()).substring(1).split('.')[0]; | ||||
| 	let room = `net:${host}:${port}~shs:${id}:SSB+Room+PSK3TLYC2T86EHQCUHBUHASCASE18JBV24=`; | ||||
| 	let host = core.url.match(/.*\/\/(.*?)\//)[1]; | ||||
| 	let id = (await ssb.getServerIdentity()).substring(1); | ||||
| 	let room = `net:${host}:${ssb.port}~shs:${id}:SSB+Room+SK3TLYC2T86EHQCUHBUHASCASE18JBV24=`; | ||||
| 	await app.setDocument(` | ||||
| 		<body style="color: #fff"> | ||||
| 			<h1>Server</h1> | ||||
|   | ||||
							
								
								
									
										42
									
								
								apps/sneaker/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,5 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "🦀", | ||||
| 	"previous": "&Hd6CuhhnZIf13PdFJYZBUYLYZO84WdaKvWXLC29M7Ac=.sha256" | ||||
| 	"emoji": "🐌", | ||||
| 	"previous": "&Xs1X5TzLCk6KVr+5IDc80JAHYxJyoD10cXKBUYpFqWQ=.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) { | ||||
| @@ -97,35 +100,13 @@ tfrpc.register(async function try_decrypt(id, content) { | ||||
| tfrpc.register(async function encrypt(id, recipients, content) { | ||||
| 	return await ssb.privateMessageEncrypt(id, recipients, content); | ||||
| }); | ||||
| tfrpc.register(async function getActiveIdentity() { | ||||
| 	return await ssb.getActiveIdentity(); | ||||
| }); | ||||
| tfrpc.register(async function sync() { | ||||
| 	return await ssb.sync(); | ||||
| }); | ||||
| tfrpc.register(async function url() { | ||||
| 	return core.url; | ||||
| }); | ||||
| tfrpc.register(async function globalSettingsGet(key) { | ||||
| 	return core.globalSettingsGet(key); | ||||
| }); | ||||
| tfrpc.register(async function globalSettingsSet(key, value) { | ||||
| 	return core.globalSettingsSet(key, value); | ||||
| }); | ||||
| tfrpc.register(function isAdministrator() { | ||||
| 	return core.user?.credentials?.permissions?.administration; | ||||
| }); | ||||
|  | ||||
| core.register('onBroadcastsChanged', async function () { | ||||
| 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()); | ||||
| }); | ||||
| core.register('setActiveIdentity', async function (id) { | ||||
| 	await tfrpc.rpc.set('identity', id); | ||||
| }); | ||||
|  | ||||
| async function main() { | ||||
| 	if (typeof database !== 'undefined') { | ||||
|   | ||||
| @@ -1,94 +1,90 @@ | ||||
| function textNode(text) { | ||||
| 	const node = new commonmark.Node('text', undefined); | ||||
| 	node.literal = text; | ||||
| 	return node; | ||||
|   const node = new commonmark.Node("text", undefined); | ||||
|   node.literal = text; | ||||
|   return node; | ||||
| } | ||||
|  | ||||
| function linkNode(text, link) { | ||||
| 	const linkNode = new commonmark.Node('link', undefined); | ||||
| 	if (link.startsWith('#')) { | ||||
| 		linkNode.destination = `#${encodeURIComponent(link)}`; | ||||
| 	} else { | ||||
| 		linkNode.destination = link; | ||||
| 	} | ||||
| 	linkNode.appendChild(textNode(text)); | ||||
| 	return linkNode; | ||||
|   const linkNode = new commonmark.Node("link", undefined); | ||||
|   linkNode.destination = `#q=${encodeURIComponent(link)}`; | ||||
|   linkNode.appendChild(textNode(text)); | ||||
|   return linkNode; | ||||
| } | ||||
|  | ||||
| function splitMatches(text, regexp) { | ||||
| 	// Regexp must be sticky. | ||||
| 	regexp = new RegExp(regexp, 'gm'); | ||||
|   // Regexp must be sticky. | ||||
|   regexp = new RegExp(regexp, "gm"); | ||||
|  | ||||
| 	let i = 0; | ||||
| 	const result = []; | ||||
|   let i = 0; | ||||
|   const result = []; | ||||
|  | ||||
| 	let match = regexp.exec(text); | ||||
| 	while (match) { | ||||
| 		const matchText = match[0]; | ||||
|   let match = regexp.exec(text); | ||||
|   while (match) { | ||||
|     const matchText = match[0]; | ||||
|  | ||||
| 		if (match.index > i) { | ||||
| 			result.push([text.substring(i, match.index), false]); | ||||
| 		} | ||||
|     if (match.index > i) { | ||||
|       result.push([text.substring(i, match.index), false]); | ||||
|     } | ||||
|  | ||||
| 		result.push([matchText, true]); | ||||
| 		i = match.index + matchText.length; | ||||
|     result.push([matchText, true]); | ||||
|     i = match.index + matchText.length; | ||||
|  | ||||
| 		match = regexp.exec(text); | ||||
| 	} | ||||
|     match = regexp.exec(text); | ||||
|   } | ||||
|  | ||||
| 	if (i < text.length) { | ||||
| 		result.push([text.substring(i, text.length), false]); | ||||
| 	} | ||||
|   if (i < text.length) { | ||||
|     result.push([text.substring(i, text.length), false]); | ||||
|   } | ||||
|  | ||||
| 	return result; | ||||
|   return result; | ||||
| } | ||||
|  | ||||
| const regex = new RegExp('(?:https?://[^ ]+[^ .,])|(?:(?<!\\w)#[\\w-]+)|(?:@[A-Za-z0-9+/]+=.ed25519)|(?:[%&][A-Za-z0-9+/]+=.sha256)'); | ||||
| const regex = new RegExp("(?<!\\w)#[\\w-]+"); | ||||
|  | ||||
| function split(textNodes) { | ||||
| 	const text = textNodes.map((n) => n.literal).join(''); | ||||
| 	const parts = splitMatches(text, regex); | ||||
|   const text = textNodes.map(n => n.literal).join(""); | ||||
|   const parts = splitMatches(text, regex); | ||||
|  | ||||
| 	return parts.map((part) => { | ||||
| 		if (part[1]) { | ||||
| 			return linkNode(part[0], part[0]); | ||||
| 		} else { | ||||
| 			return textNode(part[0]); | ||||
| 		} | ||||
| 	}); | ||||
|   return parts.map(part => { | ||||
|     if (part[1]) { | ||||
|       return linkNode(part[0], part[0]); | ||||
|     } else { | ||||
|       return textNode(part[0]); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| export function transform(parsed) { | ||||
| 	const walker = parsed.walker(); | ||||
| 	let event; | ||||
|   const walker = parsed.walker(); | ||||
|   let event; | ||||
|  | ||||
| 	let nodes = []; | ||||
| 	while ((event = walker.next())) { | ||||
| 		const node = event.node; | ||||
| 		if (event.entering && node.type === 'text') { | ||||
| 			nodes.push(node); | ||||
| 		} else { | ||||
| 			if (nodes.length > 0) { | ||||
| 				split(nodes) | ||||
| 					.reverse() | ||||
| 					.forEach((newNode) => { | ||||
| 						nodes[0].insertAfter(newNode); | ||||
| 					}); | ||||
|   let nodes = []; | ||||
|   while ((event = walker.next())) { | ||||
|     const node = event.node; | ||||
|     if (event.entering && node.type === "text") { | ||||
|       nodes.push(node); | ||||
|     } else { | ||||
|       if (nodes.length > 0) { | ||||
|         split(nodes) | ||||
|           .reverse() | ||||
|           .forEach(newNode => { | ||||
|             nodes[0].insertAfter(newNode); | ||||
|           }); | ||||
|  | ||||
| 				nodes.forEach((n) => n.unlink()); | ||||
| 				nodes = []; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|         nodes.forEach(n => n.unlink()); | ||||
|         nodes = []; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| 	if (nodes.length > 0) { | ||||
| 		split(nodes) | ||||
| 			.reverse() | ||||
| 			.forEach((newNode) => { | ||||
| 				nodes[0].insertAfter(newNode); | ||||
| 			}); | ||||
| 		nodes.forEach((n) => n.unlink()); | ||||
| 	} | ||||
|   if (nodes.length > 0) { | ||||
|     split(nodes) | ||||
|       .reverse() | ||||
|       .forEach(newNode => { | ||||
|         nodes[0].insertAfter(newNode); | ||||
|       }); | ||||
|     nodes.forEach(n => n.unlink()); | ||||
|   } | ||||
|  | ||||
| 	return parsed; | ||||
|   return parsed; | ||||
| } | ||||
|   | ||||
							
								
								
									
										91
									
								
								apps/ssb/commonmark-linkify.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,91 @@ | ||||
| function textNode(text) { | ||||
|   const node = new commonmark.Node("text", undefined); | ||||
|   node.literal = text; | ||||
|   return node; | ||||
| } | ||||
|  | ||||
| function linkNode(text, url) { | ||||
|   const urlNode = new commonmark.Node("link", undefined); | ||||
|   urlNode.destination = url; | ||||
|   urlNode.appendChild(textNode(text)); | ||||
|  | ||||
|   return urlNode; | ||||
| } | ||||
|  | ||||
| function splitMatches(text, regexp) { | ||||
|   // Regexp must be sticky. | ||||
|   regexp = new RegExp(regexp, "gm"); | ||||
|  | ||||
|   let i = 0; | ||||
|   const result = []; | ||||
|  | ||||
|   let match = regexp.exec(text); | ||||
|   while (match) { | ||||
|     const matchText = match[0]; | ||||
|  | ||||
|     if (match.index > i) { | ||||
|       result.push([text.substring(i, match.index), false]); | ||||
|     } | ||||
|  | ||||
|     result.push([matchText, true]); | ||||
|     i = match.index + matchText.length; | ||||
|  | ||||
|     match = regexp.exec(text); | ||||
|   } | ||||
|  | ||||
|   if (i < text.length) { | ||||
|     result.push([text.substring(i, text.length), false]); | ||||
|   } | ||||
|  | ||||
|   return result; | ||||
| } | ||||
|  | ||||
| const urlRegexp = new RegExp("https?://[^ ]+[^ .,]"); | ||||
|  | ||||
| function splitURLs(textNodes) { | ||||
|   const text = textNodes.map(n => n.literal).join(""); | ||||
|   const parts = splitMatches(text, urlRegexp); | ||||
|  | ||||
|   return parts.map(part => { | ||||
|     if (part[1]) { | ||||
|       return linkNode(part[0], part[0]); | ||||
|     } else { | ||||
|       return textNode(part[0]); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| export function transform(parsed) { | ||||
|   const walker = parsed.walker(); | ||||
|   let event; | ||||
|  | ||||
|   let nodes = []; | ||||
|   while ((event = walker.next())) { | ||||
|     const node = event.node; | ||||
|     if (event.entering && node.type === "text") { | ||||
|       nodes.push(node); | ||||
|     } else { | ||||
|       if (nodes.length > 0) { | ||||
|         splitURLs(nodes) | ||||
|           .reverse() | ||||
|           .forEach(newNode => { | ||||
|             nodes[0].insertAfter(newNode); | ||||
|           }); | ||||
|  | ||||
|         nodes.forEach(n => n.unlink()); | ||||
|         nodes = []; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (nodes.length > 0) { | ||||
|     splitURLs(nodes) | ||||
|       .reverse() | ||||
|       .forEach(newNode => { | ||||
|         nodes[0].insertAfter(newNode); | ||||
|       }); | ||||
|     nodes.forEach(n => n.unlink()); | ||||
|   } | ||||
|  | ||||
|   return parsed; | ||||
| } | ||||
							
								
								
									
										2
									
								
								apps/ssb/commonmark.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,7 +1,3 @@ | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| import {html, render} from './lit-all.min.js'; | ||||
| import {styles} from './tf-styles.js'; | ||||
|  | ||||
| let g_emojis; | ||||
|  | ||||
| function get_emojis() { | ||||
| @@ -14,158 +10,105 @@ function get_emojis() { | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| export async function picker(callback, anchor, author, recent) { | ||||
| 	let json = await get_emojis(); | ||||
| export function picker(callback, anchor) { | ||||
| 	get_emojis().then(function (json) { | ||||
| 		let div = document.createElement('div'); | ||||
| 		div.id = 'emoji_picker'; | ||||
| 		div.style.color = '#000'; | ||||
| 		div.style.background = '#fff'; | ||||
| 		div.style.border = '1px solid #000'; | ||||
| 		div.style.display = 'block'; | ||||
| 		div.style.position = 'absolute'; | ||||
| 		div.style.minWidth = 'min(16em, 90vw)'; | ||||
| 		div.style.width = 'min(16em, 90vw)'; | ||||
| 		div.style.maxWidth = 'min(16em, 90vw)'; | ||||
| 		div.style.maxHeight = '16em'; | ||||
| 		div.style.overflow = 'scroll'; | ||||
| 		div.style.fontWeight = 'bold'; | ||||
| 		div.style.fontSize = 'xx-large'; | ||||
| 		let input = document.createElement('input'); | ||||
| 		input.type = 'text'; | ||||
| 		input.style.display = 'block'; | ||||
| 		input.style.boxSizing = 'border-box'; | ||||
| 		input.style.width = '100%'; | ||||
| 		input.style.margin = '0'; | ||||
| 		input.style.position = 'relative'; | ||||
| 		div.appendChild(input); | ||||
| 		let list = document.createElement('div'); | ||||
| 		div.appendChild(list); | ||||
| 		div.addEventListener('mousedown', function (event) { | ||||
| 			event.stopPropagation(); | ||||
| 		}); | ||||
|  | ||||
| 	let div = document.createElement('div'); | ||||
| 	div.id = 'emoji_picker'; | ||||
| 	div.style.color = '#000'; | ||||
| 	div.style.background = '#fff'; | ||||
| 	div.style.border = '1px solid #000'; | ||||
| 	div.style.display = 'flex'; | ||||
| 	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'; | ||||
| 	input.style.boxSizing = 'border-box'; | ||||
| 	input.style.width = '100%'; | ||||
| 	input.style.margin = '0'; | ||||
| 	input.style.position = 'relative'; | ||||
| 	div.appendChild(input); | ||||
| 	let list = document.createElement('div'); | ||||
| 	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') { | ||||
| 		function key_down(event) { | ||||
| 			if (event.key == 'Escape') { | ||||
| 				cleanup(); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		function chosen(event) { | ||||
| 			console.log(event.srcElement.innerText); | ||||
| 			callback(event.srcElement.innerText); | ||||
| 			cleanup(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	function chosen(event) { | ||||
| 		console.log(event.srcElement.innerText); | ||||
| 		callback(event.srcElement.innerText); | ||||
| 		cleanup(); | ||||
| 	} | ||||
|  | ||||
| 	function refresh() { | ||||
| 		while (list.firstChild) { | ||||
| 			list.removeChild(list.firstChild); | ||||
| 		} | ||||
| 		let search = input.value.toLowerCase(); | ||||
| 		let any_at_all = false; | ||||
| 		if (recent) { | ||||
| 			let emoji_to_name = {}; | ||||
| 			for (let row of Object.values(json)) { | ||||
| 				for (let entry of Object.entries(row)) { | ||||
| 					emoji_to_name[entry[1]] = entry[0]; | ||||
| 		function refresh() { | ||||
| 			while (list.firstChild) { | ||||
| 				list.removeChild(list.firstChild); | ||||
| 			} | ||||
| 			let search = input.value.toLowerCase(); | ||||
| 			let any_at_all = false; | ||||
| 			for (let row of Object.entries(json)) { | ||||
| 				let header = document.createElement('div'); | ||||
| 				header.appendChild(document.createTextNode(row[0])); | ||||
| 				list.appendChild(header); | ||||
| 				let any = false; | ||||
| 				for (let entry of Object.entries(row[1])) { | ||||
| 					if ( | ||||
| 						search && | ||||
| 						search.length && | ||||
| 						entry[0].toLowerCase().indexOf(search) == -1 | ||||
| 					) { | ||||
| 						continue; | ||||
| 					} | ||||
| 					let emoji = document.createElement('span'); | ||||
| 					const k_size = '1.25em'; | ||||
| 					emoji.style.display = 'inline-block'; | ||||
| 					emoji.style.overflow = 'hidden'; | ||||
| 					emoji.style.cursor = 'pointer'; | ||||
| 					emoji.onclick = chosen; | ||||
| 					emoji.title = entry[0]; | ||||
| 					emoji.appendChild(document.createTextNode(entry[1])); | ||||
| 					list.appendChild(emoji); | ||||
| 					any = true; | ||||
| 					any_at_all = true; | ||||
| 				} | ||||
| 				if (!any) { | ||||
| 					list.removeChild(header); | ||||
| 				} | ||||
| 			} | ||||
| 			let header = document.createElement('div'); | ||||
| 			header.appendChild(document.createTextNode('Recent')); | ||||
| 			list.appendChild(header); | ||||
| 			let any = false; | ||||
| 			for (let entry of recent) { | ||||
| 				if ( | ||||
| 					search && | ||||
| 					search.length && | ||||
| 					(emoji_to_name[entry] || '').toLowerCase().indexOf(search) == -1 | ||||
| 				) { | ||||
| 					continue; | ||||
| 				} | ||||
| 				let emoji = document.createElement('span'); | ||||
| 				const k_size = '1.25em'; | ||||
| 				emoji.style.display = 'inline-block'; | ||||
| 				emoji.style.overflow = 'hidden'; | ||||
| 				emoji.style.cursor = 'pointer'; | ||||
| 				emoji.onclick = chosen; | ||||
| 				emoji.title = emoji_to_name[entry] || entry; | ||||
| 				emoji.appendChild(document.createTextNode(entry)); | ||||
| 				list.appendChild(emoji); | ||||
| 				any = true; | ||||
| 			} | ||||
| 			if (!any) { | ||||
| 				list.removeChild(header); | ||||
| 			if (!any_at_all) { | ||||
| 				list.appendChild(document.createTextNode('No matches found.')); | ||||
| 			} | ||||
| 		} | ||||
| 		for (let row of Object.entries(json)) { | ||||
| 			let header = document.createElement('div'); | ||||
| 			header.appendChild(document.createTextNode(row[0])); | ||||
| 			list.appendChild(header); | ||||
| 			let any = false; | ||||
| 			for (let entry of Object.entries(row[1])) { | ||||
| 				if ( | ||||
| 					search && | ||||
| 					search.length && | ||||
| 					entry[0].toLowerCase().indexOf(search) == -1 | ||||
| 				) { | ||||
| 					continue; | ||||
| 				} | ||||
| 				let emoji = document.createElement('span'); | ||||
| 				const k_size = '1.25em'; | ||||
| 				emoji.style.display = 'inline-block'; | ||||
| 				emoji.style.overflow = 'hidden'; | ||||
| 				emoji.style.cursor = 'pointer'; | ||||
| 				emoji.onclick = chosen; | ||||
| 				emoji.title = entry[0]; | ||||
| 				emoji.appendChild(document.createTextNode(entry[1])); | ||||
| 				list.appendChild(emoji); | ||||
| 				any = true; | ||||
| 				any_at_all = true; | ||||
| 			} | ||||
| 			if (!any) { | ||||
| 				list.removeChild(header); | ||||
| 			} | ||||
| 		} | ||||
| 		if (!any_at_all) { | ||||
| 			list.appendChild(document.createTextNode('No matches found.')); | ||||
| 		} | ||||
| 	} | ||||
| 	refresh(); | ||||
| 	input.oninput = refresh; | ||||
| 	let 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); | ||||
| 	input.focus(); | ||||
| 	document.body.addEventListener('mousedown', cleanup); | ||||
| 	window.addEventListener('keydown', key_down); | ||||
| 		refresh(); | ||||
| 		input.oninput = refresh; | ||||
| 		document.body.appendChild(div); | ||||
| 		div.style.position = 'fixed'; | ||||
| 		div.style.top = '50%'; | ||||
| 		div.style.left = '50%'; | ||||
| 		div.style.transform = 'translate(-50%, -50%)'; | ||||
| 		input.focus(); | ||||
| 		console.log('adding click'); | ||||
| 		document.body.addEventListener('mousedown', cleanup); | ||||
| 		window.addEventListener('keydown', key_down); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <!doctype html> | ||||
| <html> | ||||
| <html style="color: #fff"> | ||||
| 	<head> | ||||
| 		<title>Tilde Friends</title> | ||||
| 		<base target="_top" /> | ||||
| @@ -10,14 +10,14 @@ | ||||
| 			} | ||||
| 		</style> | ||||
| 	</head> | ||||
| 	<body style="margin: 0; padding: 0"> | ||||
| 		<tf-app></tf-app> | ||||
| 		<tf-reactions-modal id="reactions_modal"></tf-reactions-modal> | ||||
| 	<body style="background-color: #223a5e"> | ||||
| 		<tf-app class="w3-deep-purple" /> | ||||
| 		<script> | ||||
| 			window.litDisableBundleWarning = true; | ||||
| 		</script> | ||||
| 		<script src="filesaver.min.js"></script> | ||||
| 		<script src="commonmark.min.js"></script> | ||||
| 		<script src="commonmark-linkify.js" type="module"></script> | ||||
| 		<script src="commonmark-hashtag.js" type="module"></script> | ||||
| 		<script src="script.js" type="module"></script> | ||||
| 	</body> | ||||
|   | ||||
							
								
								
									
										42
									
								
								apps/ssb/lit-all.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,23 +1,17 @@ | ||||
| import {LitElement, html} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| import * as tf_id_picker from './tf-id-picker.js'; | ||||
| import * as tf_app from './tf-app.js'; | ||||
| import * as tf_message from './tf-message.js'; | ||||
| import * as tf_user from './tf-user.js'; | ||||
| import * as tf_compose from './tf-compose.js'; | ||||
| import * as tf_news from './tf-news.js'; | ||||
| import * as tf_profile from './tf-profile.js'; | ||||
| import * as tf_reactions_modal from './tf-reactions-modal.js'; | ||||
| import * as tf_tab_mentions from './tf-tab-mentions.js'; | ||||
| import * as tf_tab_news from './tf-tab-news.js'; | ||||
| import * as tf_tab_news_feed from './tf-tab-news-feed.js'; | ||||
| import * as tf_tab_search from './tf-tab-search.js'; | ||||
| import * as tf_tab_connections from './tf-tab-connections.js'; | ||||
| import * as tf_tab_query from './tf-tab-query.js'; | ||||
| import * as tf_tag from './tf-tag.js'; | ||||
| import * as tf_styles from './tf-styles.js'; | ||||
|  | ||||
| window.addEventListener('load', function () { | ||||
| 	let style = document.createElement('style'); | ||||
| 	style.innerText = tf_styles.styles; | ||||
| 	document.body.appendChild(style); | ||||
| }); | ||||
|   | ||||
| @@ -7,27 +7,16 @@ class TfElement extends LitElement { | ||||
| 		return { | ||||
| 			whoami: {type: String}, | ||||
| 			hash: {type: String}, | ||||
| 			unread: {type: Array}, | ||||
| 			tab: {type: String}, | ||||
| 			broadcasts: {type: Array}, | ||||
| 			connections: {type: Array}, | ||||
| 			loading: {type: Boolean}, | ||||
| 			loading_about: {type: Number}, | ||||
| 			loaded: {type: Boolean}, | ||||
| 			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_closed: {type: Object}, | ||||
| 			private_messages: {type: Array}, | ||||
| 			grouped_private_messages: {type: Object}, | ||||
| 			recent_reactions: {type: Array}, | ||||
| 			is_administrator: {type: Boolean}, | ||||
| 			stay_connected: {type: Boolean}, | ||||
| 			progress: {type: Number}, | ||||
| 			tags: {type: Array}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| @@ -37,20 +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.loading_about = 0; | ||||
| 		this.channels = []; | ||||
| 		this.channels_unread = {}; | ||||
| 		this.channels_latest = {}; | ||||
| 		this.loading_latest = 0; | ||||
| 		this.loading_latest_scheduled = 0; | ||||
| 		this.recent_reactions = []; | ||||
| 		this.private_closed = {}; | ||||
| 		this.tags = []; | ||||
| 		tfrpc.rpc.getBroadcasts().then((b) => { | ||||
| 			self.broadcasts = b || []; | ||||
| 		}); | ||||
| @@ -60,7 +43,6 @@ class TfElement extends LitElement { | ||||
| 		tfrpc.rpc.getHash().then((hash) => self.set_hash(hash)); | ||||
| 		tfrpc.register(function hashChanged(hash) { | ||||
| 			self.set_hash(hash); | ||||
| 			self.reset_progress(); | ||||
| 		}); | ||||
| 		tfrpc.register(async function notifyNewMessage(id) { | ||||
| 			await self.fetch_new_message(id); | ||||
| @@ -70,131 +52,26 @@ class TfElement extends LitElement { | ||||
| 				self.broadcasts = value; | ||||
| 			} else if (name === 'connections') { | ||||
| 				self.connections = value; | ||||
| 			} else if (name === 'identity') { | ||||
| 				self.whoami = value; | ||||
| 			} | ||||
| 		}); | ||||
| 		this.initial_load(); | ||||
| 	} | ||||
|  | ||||
| 	async initial_load() { | ||||
| 		let whoami = await tfrpc.rpc.getActiveIdentity(); | ||||
| 		let whoami = await tfrpc.rpc.localStorageGet('whoami'); | ||||
| 		let ids = (await tfrpc.rpc.getIdentities()) || []; | ||||
| 		this.is_administrator = await tfrpc.rpc.isAdministrator(); | ||||
| 		this.stay_connected = | ||||
| 			this.is_administrator && | ||||
| 			(await tfrpc.rpc.globalSettingsGet('stay_connected')); | ||||
| 		this.url = await tfrpc.rpc.url(); | ||||
| 		this.whoami = whoami ?? (ids.length ? ids[0] : undefined); | ||||
| 		this.guest = !this.whoami?.length; | ||||
| 		this.ids = ids; | ||||
| 		let private_closed = | ||||
| 			(await tfrpc.rpc.databaseGet('private_closed')) ?? '{}'; | ||||
| 		this.private_closed = JSON.parse(private_closed); | ||||
| 		await this.load_channels(); | ||||
| 	} | ||||
|  | ||||
| 	async close_private_chat(event) { | ||||
| 		let update = {}; | ||||
| 		update[event.detail.key] = true; | ||||
| 		this.private_closed = Object.assign(update, this.private_closed); | ||||
| 		await tfrpc.rpc.databaseSet( | ||||
| 			'private_closed', | ||||
| 			JSON.stringify(this.private_closed) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	async load_channels() { | ||||
| 		let channels = await tfrpc.rpc.query( | ||||
| 			` | ||||
| 			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(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	visible_private() { | ||||
| 		if (!this.grouped_private_messages || !this.private_closed) { | ||||
| 			return []; | ||||
| 		} | ||||
| 		let self = this; | ||||
| 		return Object.fromEntries( | ||||
| 			Object.entries(this.grouped_private_messages).filter(([key, value]) => { | ||||
| 				let channel = '🔐' + [...new Set(JSON.parse(key))].sort().join(','); | ||||
| 				let grouped_latest = Math.max(...value.map((x) => x.rowid)); | ||||
| 				return ( | ||||
| 					!self.private_closed[key] || | ||||
| 					self.channels_unread[channel] === undefined || | ||||
| 					grouped_latest > self.channels_unread[channel] | ||||
| 				); | ||||
| 			}) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	next_channel(delta) { | ||||
| 		let channel_names = [ | ||||
| 			'', | ||||
| 			'@', | ||||
| 			'👍', | ||||
| 			...Object.keys(this.visible_private()) | ||||
| 				.sort() | ||||
| 				.map((x) => '🔐' + JSON.parse(x).join(',')), | ||||
| 			...this.channels.map((x) => '#' + x), | ||||
| 		]; | ||||
| 		let 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 { | ||||
| @@ -202,97 +79,79 @@ class TfElement extends LitElement { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async fetch_about(following, users) { | ||||
| 		this.loading_about++; | ||||
| 		let ids = Object.keys(following).sort(); | ||||
| 		const k_cache_version = 3; | ||||
| 	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 = { | ||||
| 				version: k_cache_version, | ||||
| 				about: {}, | ||||
| 				last_row_id: 0, | ||||
| 			}; | ||||
| 		} | ||||
|  | ||||
| 		let ids_out_of_date = ids.filter( | ||||
| 			(x) => | ||||
| 				(users[x]?.seq && !cache.about[x]?.seq) || | ||||
| 				(users[x]?.seq && users[x]?.seq > cache.about[x].seq) | ||||
| 		); | ||||
|  | ||||
| 		let max_row_id = ( | ||||
| 			await tfrpc.rpc.query( | ||||
| 				` | ||||
| 			SELECT MAX(rowid) AS max_row_id FROM messages | ||||
| 		`, | ||||
| 				[] | ||||
| 			) | ||||
| 		)[0].max_row_id; | ||||
| 		for (let id of Object.keys(cache.about)) { | ||||
| 			if (ids.indexOf(id) == -1) { | ||||
| 				delete cache.about[id]; | ||||
| 			} else { | ||||
| 				users[id] = Object.assign(cache.about[id], users[id] || {}); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		console.log( | ||||
| 			'loading about for', | ||||
| 			ids.length, | ||||
| 			'accounts', | ||||
| 			ids_out_of_date.length, | ||||
| 			'out of date' | ||||
| 		let abouts = await tfrpc.rpc.query( | ||||
| 			` | ||||
| 				SELECT | ||||
| 					messages.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 | ||||
| 				WHERE | ||||
| 					messages.author = following.value AND | ||||
| 					messages.rowid > ?3 AND | ||||
| 					messages.rowid <= ?4 AND | ||||
| 					json_extract(messages.content, '$.type') = 'about' | ||||
| 				UNION | ||||
| 				SELECT | ||||
| 					messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature | ||||
| 				FROM | ||||
| 					messages, | ||||
| 					json_each(?2) AS following | ||||
| 				WHERE | ||||
| 					messages.author = following.value AND | ||||
| 					messages.rowid <= ?4 AND | ||||
| 					json_extract(messages.content, '$.type') = 'about' | ||||
| 				ORDER BY messages.author, messages.sequence | ||||
| 			`, | ||||
| 			[ | ||||
| 				JSON.stringify(ids.filter((id) => cache.about[id])), | ||||
| 				JSON.stringify(ids.filter((id) => !cache.about[id])), | ||||
| 				cache.last_row_id, | ||||
| 				max_row_id, | ||||
| 			] | ||||
| 		); | ||||
| 		if (ids_out_of_date.length) { | ||||
| 			try { | ||||
| 				let rows = await tfrpc.rpc.query( | ||||
| 					` | ||||
| 						SELECT all_abouts.author, json(json_group_object(all_abouts.key, all_abouts.value)) AS about | ||||
| 						FROM ( | ||||
| 							SELECT | ||||
| 								messages.author, | ||||
| 								fields.key, | ||||
| 								RANK() OVER (PARTITION BY messages.author, fields.key ORDER BY messages.sequence DESC) AS rank, | ||||
| 								fields.value | ||||
| 							FROM messages JOIN json_each(messages.content) AS fields | ||||
| 							WHERE | ||||
| 								messages.content ->> '$.type' = 'about' AND | ||||
| 								messages.content ->> '$.about' = messages.author AND | ||||
| 								NOT fields.key IN ('about', 'type')) all_abouts | ||||
| 						JOIN json_each(?) AS following ON all_abouts.author = following.value | ||||
| 						WHERE rank = 1 | ||||
| 						GROUP BY all_abouts.author | ||||
| 					`, | ||||
| 					[JSON.stringify(ids_out_of_date)] | ||||
| 		for (let about of abouts) { | ||||
| 			let content = JSON.parse(about.content); | ||||
| 			if (content.about === about.author) { | ||||
| 				delete content.type; | ||||
| 				delete content.about; | ||||
| 				cache.about[about.author] = Object.assign( | ||||
| 					cache.about[about.author] || {}, | ||||
| 					content | ||||
| 				); | ||||
| 				users = users || {}; | ||||
| 				for (let row of rows) { | ||||
| 					users[row.author] = Object.assign( | ||||
| 						users[row.author] || {}, | ||||
| 						JSON.parse(row.about) | ||||
| 					); | ||||
| 					cache.about[row.author] = Object.assign( | ||||
| 						{seq: users[row.author].seq}, | ||||
| 						JSON.parse(row.about) | ||||
| 					); | ||||
| 				} | ||||
| 			} catch (e) { | ||||
| 				console.log(e); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		for (let id of ids_out_of_date) { | ||||
| 			if (!cache.about[id]?.seq) { | ||||
| 				cache.about[id] = Object.assign(cache.about[id] ?? {}, { | ||||
| 					seq: users[id]?.seq ?? 0, | ||||
| 				}); | ||||
| 			} | ||||
| 		cache.last_row_id = max_row_id; | ||||
| 		await tfrpc.rpc.databaseSet('about', JSON.stringify(cache)); | ||||
| 		users = users || {}; | ||||
| 		for (let id of Object.keys(cache.about)) { | ||||
| 			users[id] = Object.assign(users[id] || {}, cache.about[id]); | ||||
| 		} | ||||
|  | ||||
| 		this.loading_about--; | ||||
|  | ||||
| 		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); | ||||
| 			}); | ||||
| 		} | ||||
|  | ||||
| 		return Object.assign({}, users); | ||||
| 	} | ||||
|  | ||||
| @@ -306,15 +165,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) { | ||||
| @@ -329,353 +183,85 @@ 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), i, 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 messages.rowid 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 query_timed(sql, args) { | ||||
| 	render_id_picker() { | ||||
| 		return html` | ||||
| 			<div style="display: flex; gap: 8px"> | ||||
| 				<tf-id-picker | ||||
| 					id="picker" | ||||
| 					style="flex: 1 1 auto" | ||||
| 					selected=${this.whoami} | ||||
| 					.ids=${this.ids} | ||||
| 					.users=${this.users} | ||||
| 					@change=${this._handle_whoami_changed} | ||||
| 				></tf-id-picker> | ||||
| 				<button | ||||
| 					class="w3-button w3-dark-grey w3-border" | ||||
| 					style="flex: 0 0 auto" | ||||
| 					@click=${this.create_identity} | ||||
| 					id="create_identity" | ||||
| 				> | ||||
| 					Create Identity | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		`; | ||||
| 	} | ||||
|  | ||||
| 	async load_recent_tags() { | ||||
| 		let start = new Date(); | ||||
| 		let result = await tfrpc.rpc.query(sql, args); | ||||
| 		let end = new Date(); | ||||
| 		console.log((end - start) / 1000, sql.replaceAll(/\s+/g, ' ').trim()); | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	async group_private_messages(messages) { | ||||
| 		let groups = {}; | ||||
| 		let result = await this.decrypt( | ||||
| 			await tfrpc.rpc.query( | ||||
| 				` | ||||
| 				SELECT messages.rowid, messages.id, author, timestamp, json(content) AS content | ||||
| 				FROM messages | ||||
| 				JOIN json_each(?) AS ids | ||||
| 				WHERE messages.id = ids.value | ||||
| 				ORDER BY timestamp DESC | ||||
| 			`, | ||||
| 				[JSON.stringify(messages)] | ||||
| 			) | ||||
| 		); | ||||
| 		for (let message of result) { | ||||
| 			let key = JSON.stringify( | ||||
| 				[ | ||||
| 					...new Set( | ||||
| 						message?.decrypted?.recps?.filter((x) => x != this.whoami) | ||||
| 					), | ||||
| 				].sort() ?? [] | ||||
| 			); | ||||
| 			if (!groups[key]) { | ||||
| 				groups[key] = []; | ||||
| 			} | ||||
| 			groups[key].push(message); | ||||
| 		} | ||||
| 		return groups; | ||||
| 	} | ||||
|  | ||||
| 	async load_channels_latest(following) { | ||||
| 		let start_time = new Date(); | ||||
| 		let latest_private = this.get_latest_private(following); | ||||
| 		const k_args = [ | ||||
| 			JSON.stringify(this.channels), | ||||
| 			JSON.stringify(following), | ||||
| 			'"' + this.whoami.replace('"', '""') + '"', | ||||
| 			this.whoami, | ||||
| 		]; | ||||
| 		let channels = ( | ||||
| 			await Promise.all([ | ||||
| 				this.query_timed( | ||||
| 					` | ||||
| 					SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages | ||||
| 					JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value | ||||
| 					JOIN json_each(?2) AS following ON messages.author = following.value | ||||
| 					WHERE | ||||
| 						messages.content ->> 'type' = 'post' AND | ||||
| 						messages.content ->> 'root' IS NULL AND | ||||
| 						messages.author != ?4 | ||||
| 					GROUP by channel | ||||
| 				`, | ||||
| 					k_args | ||||
| 				), | ||||
| 				this.query_timed( | ||||
| 					` | ||||
| 					SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages | ||||
| 					JOIN messages_refs ON messages.id = messages_refs.message | ||||
| 					JOIN json_each(?1) AS channels ON messages_refs.ref = '#' || channels.value | ||||
| 					JOIN json_each(?2) AS following ON messages.author = following.value | ||||
| 					WHERE | ||||
| 						messages.content ->> 'type' = 'post' AND | ||||
| 						messages.content ->> 'root' IS NULL AND | ||||
| 						messages.author != ?4 | ||||
| 					GROUP by channel | ||||
| 				`, | ||||
| 					k_args | ||||
| 				), | ||||
| 				this.query_timed( | ||||
| 					` | ||||
| 					SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages | ||||
| 					JOIN json_each(?2) AS following ON messages.author = following.value | ||||
| 					WHERE | ||||
| 						messages.content ->> 'type' = 'post' AND | ||||
| 						messages.content ->> 'root' IS NULL AND | ||||
| 						messages.author != ?4 | ||||
| 				`, | ||||
| 					k_args | ||||
| 				), | ||||
| 				this.query_timed( | ||||
| 					` | ||||
| 					SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3) | ||||
| 					JOIN messages ON messages.rowid = messages_fts.rowid | ||||
| 					JOIN json_each(?2) AS following ON messages.author = following.value | ||||
| 					WHERE messages.author != ?4 | ||||
| 				`, | ||||
| 					k_args | ||||
| 				), | ||||
| 			]) | ||||
| 		).flat(); | ||||
| 		let latest = {}; | ||||
| 		for (let row of channels) { | ||||
| 			if (!latest[row.channel]) { | ||||
| 				latest[row.channel] = row.rowid; | ||||
| 			} else { | ||||
| 				latest[row.channel] = Math.max(row.rowid, latest[row.channel]); | ||||
| 			} | ||||
| 		} | ||||
| 		this.channels_latest = latest; | ||||
| 		console.log('channels took', (new Date() - start_time) / 1000.0); | ||||
| 		let self = this; | ||||
| 		start_time = new Date(); | ||||
| 		latest_private.then(async function (latest) { | ||||
| 			self.channels_latest = Object.assign({}, self.channels_latest, { | ||||
| 				'🔐': latest[0], | ||||
| 			}); | ||||
| 			console.log('private took', (new Date() - start_time) / 1000.0); | ||||
| 			self.private_messages = latest[1]; | ||||
| 			self.grouped_private_messages = await self.group_private_messages( | ||||
| 				latest[1] | ||||
| 			); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	_schedule_load_latest_timer() { | ||||
| 		--this.loading_latest_scheduled; | ||||
| 		this.schedule_load_latest(); | ||||
| 	} | ||||
|  | ||||
| 	reset_progress() { | ||||
| 		if (this.progress === undefined) { | ||||
| 			this._progress_start = new Date(); | ||||
| 			requestAnimationFrame(this.update_progress.bind(this)); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	update_progress() { | ||||
| 		if ( | ||||
| 			!this.loading_latest && | ||||
| 			!this.loading_latest_scheduled && | ||||
| 			!this.shadowRoot.getElementById('tf-tab-news')?.is_loading() | ||||
| 		) { | ||||
| 			this.progress = undefined; | ||||
| 			return; | ||||
| 		} | ||||
| 		this.progress = (new Date() - this._progress_start).valueOf(); | ||||
| 		requestAnimationFrame(this.update_progress.bind(this)); | ||||
| 	} | ||||
|  | ||||
| 	schedule_load_latest() { | ||||
| 		this.reset_progress(); | ||||
| 		if (!this.loading_latest) { | ||||
| 			this.shadowRoot.getElementById('tf-tab-news')?.load_latest(); | ||||
| 			this.load(); | ||||
| 		} 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( | ||||
| 		this.tags = 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] = Object.assign(users[row.author], { | ||||
| 				seq: row.max_sequence, | ||||
| 				ts: row.max_ts, | ||||
| 			}); | ||||
| 		} | ||||
| 		return users; | ||||
| 	} | ||||
|  | ||||
| 	async load_recent_reactions() { | ||||
| 		this.recent_reactions = ( | ||||
| 			await tfrpc.rpc.query( | ||||
| 				` | ||||
| 			SELECT DISTINCT content ->> '$.vote.expression' AS value | ||||
| 			FROM messages | ||||
| 			WHERE author = ? AND | ||||
| 			content ->> '$.type' = 'vote' | ||||
| 			ORDER BY timestamp DESC LIMIT 10 | ||||
| 			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 | ||||
| 		`, | ||||
| 				[this.whoami] | ||||
| 			) | ||||
| 		).map((x) => x.value); | ||||
| 			[new Date() - 7 * 24 * 60 * 60 * 1000] | ||||
| 		); | ||||
| 		console.log('tags took', (new Date() - start) / 1000.0, 'seconds'); | ||||
| 	} | ||||
|  | ||||
| 	async load() { | ||||
| 		this.loading_latest = true; | ||||
| 		this.reset_progress(); | ||||
| 		try { | ||||
| 			let start_time = new Date(); | ||||
| 			let whoami = this.whoami; | ||||
| 			let following = await tfrpc.rpc.following([whoami], 2); | ||||
| 			let old_users = this.users ?? {}; | ||||
| 			let users = {}; | ||||
| 			let by_count = []; | ||||
| 			for (let [id, v] of Object.entries(following)) { | ||||
| 				users[id] = Object.assign( | ||||
| 					{ | ||||
| 						following: v.of, | ||||
| 						blocking: v.ob, | ||||
| 						followed: v.if, | ||||
| 						blocked: v.ib, | ||||
| 						follow_depth: following[id]?.d, | ||||
| 					}, | ||||
| 					old_users[id] | ||||
| 				); | ||||
| 				by_count.push({count: v.of, id: id}); | ||||
| 			} | ||||
| 			let reactions = this.load_recent_reactions(); | ||||
| 			this.load_channels_latest(Object.keys(following)); | ||||
| 			this.channels_unread = JSON.parse( | ||||
| 				(await tfrpc.rpc.databaseGet('unread')) ?? '{}' | ||||
| 			); | ||||
| 			this.following = Object.keys(following); | ||||
| 			let about_start_time = new Date(); | ||||
| 			start_time = new Date(); | ||||
| 			users = await this.fetch_user_info(users); | ||||
| 			console.log( | ||||
| 				'user info took', | ||||
| 				(new Date() - start_time) / 1000.0, | ||||
| 				'seconds' | ||||
| 			); | ||||
| 			this.users = users; | ||||
|  | ||||
| 			let self = this; | ||||
| 			this.fetch_about(following, users).then(function (result) { | ||||
| 				self.users = result; | ||||
| 				console.log( | ||||
| 					'about took', | ||||
| 					(new Date() - about_start_time) / 1000.0, | ||||
| 					'seconds for', | ||||
| 					Object.keys(users).length, | ||||
| 					'users' | ||||
| 				); | ||||
| 			}); | ||||
| 			console.log( | ||||
| 				`load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}` | ||||
| 			); | ||||
| 			await reactions; | ||||
| 			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)); | ||||
| 		users = await this.fetch_about(Object.keys(following).sort(), 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() { | ||||
| @@ -689,21 +275,8 @@ class TfElement extends LitElement { | ||||
| 					whoami=${this.whoami} | ||||
| 					.users=${this.users} | ||||
| 					hash=${this.hash} | ||||
| 					?loading=${this.loading || this.loading_about != 0} | ||||
| 					.channels=${this.channels} | ||||
| 					.channels_latest=${this.channels_latest} | ||||
| 					.channels_unread=${this.channels_unread} | ||||
| 					@channelsetunread=${this.channel_set_unread} | ||||
| 					@refresh=${this.refresh} | ||||
| 					@toggle_stay_connected=${this.toggle_stay_connected} | ||||
| 					@loadmessages=${this.reset_progress} | ||||
| 					@closeprivatechat=${this.close_private_chat} | ||||
| 					.connections=${this.connections} | ||||
| 					.private_messages=${this.private_messages} | ||||
| 					.grouped_private_messages=${this.visible_private()} | ||||
| 					.recent_reactions=${this.recent_reactions} | ||||
| 					?is_administrator=${this.is_administrator} | ||||
| 					?stay_connected=${this.stay_connected} | ||||
| 					.unread=${this.unread} | ||||
| 					@refresh=${() => (this.unread = [])} | ||||
| 				></tf-tab-news> | ||||
| 			`; | ||||
| 		} else if (this.tab === 'connections') { | ||||
| @@ -714,6 +287,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 | ||||
| @@ -742,31 +323,16 @@ class TfElement extends LitElement { | ||||
| 	async set_tab(tab) { | ||||
| 		this.tab = tab; | ||||
| 		if (tab === 'news') { | ||||
| 			this.schedule_load_latest(); | ||||
| 			await tfrpc.rpc.setHash('#'); | ||||
| 		} else if (tab === 'connections') { | ||||
| 			await tfrpc.rpc.setHash('#connections'); | ||||
| 		} else if (tab === 'mentions') { | ||||
| 			await tfrpc.rpc.setHash('#mentions'); | ||||
| 		} else if (tab === 'query') { | ||||
| 			await tfrpc.rpc.setHash('#sql='); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	refresh() { | ||||
| 		tfrpc.rpc.sync(); | ||||
| 	} | ||||
|  | ||||
| 	async toggle_stay_connected() { | ||||
| 		let stay_connected = await tfrpc.rpc.globalSettingsGet('stay_connected'); | ||||
| 		let new_stay_connected = !this.stay_connected; | ||||
| 		try { | ||||
| 			if (new_stay_connected != stay_connected) { | ||||
| 				await tfrpc.rpc.globalSettingsSet('stay_connected', new_stay_connected); | ||||
| 			} | ||||
| 		} finally { | ||||
| 			this.stay_connected = await tfrpc.rpc.globalSettingsGet('stay_connected'); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let self = this; | ||||
|  | ||||
| @@ -780,96 +346,40 @@ 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" | ||||
| 			> | ||||
| 				${this.is_administrator && self.tab != 'news' | ||||
| 					? html` | ||||
| 							<button | ||||
| 								class=${'w3-bar-item w3-button w3-circle w3-ripple' + | ||||
| 								(this.connections?.some((x) => x.flags.one_shot) | ||||
| 									? ' w3-spin' | ||||
| 									: '')} | ||||
| 								style="width: 1.5em; height: 1.5em; padding: 8px" | ||||
| 								@click=${this.refresh} | ||||
| 							> | ||||
| 								↻ | ||||
| 							</button> | ||||
| 							<button | ||||
| 								class="w3-bar-item w3-button w3-ripple" | ||||
| 								@click=${this.toggle_stay_connected} | ||||
| 							> | ||||
| 								${this.stay_connected ? '🔗' : '⛓️💥'} | ||||
| 							</button> | ||||
| 						` | ||||
| 					: undefined} | ||||
| 			<div class="w3-bar w3-black"> | ||||
| 				${Object.entries(k_tabs).map( | ||||
| 					([k, v]) => html` | ||||
| 						<button | ||||
| 							title=${v} | ||||
| 							class="w3-bar-item w3-padding w3-hover-theme tab ${self.tab == v | ||||
| 								? 'w3-theme-l2' | ||||
| 								: 'w3-theme-l1'}" | ||||
| 							class="w3-bar-item w3-padding-large w3-hover-gray tab ${self.tab == | ||||
| 							v | ||||
| 								? 'w3-red' | ||||
| 								: 'w3-black'}" | ||||
| 							@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 progress = | ||||
| 			this.progress !== undefined | ||||
| 				? html` | ||||
| 						<div style="position: absolute; width: 100%" id="progress"> | ||||
| 							<div | ||||
| 								class="w3-theme-l3" | ||||
| 								style=${`height: 4px; position: absolute; right: ${Math.cos(this.progress / 250) > 0 ? 'auto' : '0'}; width: ${50 * Math.sin(this.progress / 250) + 50}%`} | ||||
| 							></div> | ||||
| 						</div> | ||||
| 					` | ||||
| 				: undefined; | ||||
| 		let contents = !this.loaded | ||||
| 			? this.loading | ||||
| 				? html`<div>Loading...</div>` | ||||
| 				: html`<div>Select or create an identity.</div>` | ||||
| 			: this.render_tab(); | ||||
| 		return html` | ||||
| 			<div | ||||
| 				style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column" | ||||
| 				class="w3-theme-dark" | ||||
| 			> | ||||
| 				${progress} | ||||
| 				<div style="flex: 0 0">${tabs}</div> | ||||
| 				<div style="flex: 1 1; overflow: auto; contain: layout"> | ||||
| 					${contents} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			${this.render_id_picker()} ${tabs} | ||||
| 			${this.tags.map( | ||||
| 				(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>` | ||||
| 			)} | ||||
| 			${contents} | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import {LitElement, html, unsafeHTML, live} from './lit-all.min.js'; | ||||
| import {LitElement, html, unsafeHTML} from './lit-all.min.js'; | ||||
| import * as tfutils from './tf-utils.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| import {styles} from './tf-styles.js'; | ||||
| @@ -13,10 +13,6 @@ class TfComposeElement extends LitElement { | ||||
| 			branch: {type: String}, | ||||
| 			apps: {type: Object}, | ||||
| 			drafts: {type: Object}, | ||||
| 			author: {type: String}, | ||||
| 			channel: {type: String}, | ||||
| 			new_thread: {type: Boolean}, | ||||
| 			recipients: {type: Array}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| @@ -29,8 +25,6 @@ class TfComposeElement extends LitElement { | ||||
| 		this.branch = undefined; | ||||
| 		this.apps = undefined; | ||||
| 		this.drafts = {}; | ||||
| 		this.author = undefined; | ||||
| 		this.new_thread = false; | ||||
| 	} | ||||
|  | ||||
| 	process_text(text) { | ||||
| @@ -70,7 +64,7 @@ class TfComposeElement extends LitElement { | ||||
| 			updated = true; | ||||
| 		} | ||||
| 		if (updated) { | ||||
| 			setTimeout(() => this.notify(draft), 0); | ||||
| 			this.requestUpdate(); | ||||
| 		} | ||||
| 		return tfutils.markdown(text); | ||||
| 	} | ||||
| @@ -78,12 +72,14 @@ class TfComposeElement extends LitElement { | ||||
| 	input(event) { | ||||
| 		let edit = this.renderRoot.getElementById('edit'); | ||||
| 		let preview = this.renderRoot.getElementById('preview'); | ||||
| 		preview.innerHTML = this.process_text(edit.innerText); | ||||
| 		preview.innerHTML = this.process_text(edit.value); | ||||
| 		let content_warning = this.renderRoot.getElementById('content_warning'); | ||||
| 		let draft = this.get_draft(); | ||||
| 		draft.text = edit.innerText; | ||||
| 		draft.content_warning = content_warning?.value; | ||||
| 		setTimeout(() => this.notify(draft), 0); | ||||
| 		let content_warning_preview = this.renderRoot.getElementById( | ||||
| 			'content_warning_preview' | ||||
| 		); | ||||
| 		if (content_warning && content_warning_preview) { | ||||
| 			content_warning_preview.innerText = content_warning.value; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	notify(draft) { | ||||
| @@ -92,15 +88,21 @@ class TfComposeElement extends LitElement { | ||||
| 				bubbles: true, | ||||
| 				composed: true, | ||||
| 				detail: { | ||||
| 					id: | ||||
| 						this.branch ?? | ||||
| 						(this.recipients ? this.recipients.join(',') : undefined), | ||||
| 					id: this.branch, | ||||
| 					draft: draft, | ||||
| 				}, | ||||
| 			}) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	change() { | ||||
| 		let draft = this.get_draft(); | ||||
| 		draft.text = this.renderRoot.getElementById('edit')?.value; | ||||
| 		draft.content_warning = | ||||
| 			this.renderRoot.getElementById('content_warning')?.value; | ||||
| 		this.notify(draft); | ||||
| 	} | ||||
|  | ||||
| 	convert_to_format(buffer, type, mime_type) { | ||||
| 		return new Promise(function (resolve, reject) { | ||||
| 			let img = new Image(); | ||||
| @@ -167,7 +169,8 @@ class TfComposeElement extends LitElement { | ||||
| 				size: buffer.length ?? buffer.byteLength, | ||||
| 			}; | ||||
| 			let edit = self.renderRoot.getElementById('edit'); | ||||
| 			edit.innerText += `\n`; | ||||
| 			edit.value += `\n`; | ||||
| 			self.change(); | ||||
| 			self.input(); | ||||
| 		} catch (e) { | ||||
| 			alert(e?.message); | ||||
| @@ -186,13 +189,6 @@ class TfComposeElement extends LitElement { | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		event.preventDefault(); | ||||
| 		document.execCommand( | ||||
| 			'insertText', | ||||
| 			false, | ||||
| 			event.clipboardData.getData('text/plain') | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	async submit() { | ||||
| @@ -201,27 +197,12 @@ class TfComposeElement extends LitElement { | ||||
| 		let edit = this.renderRoot.getElementById('edit'); | ||||
| 		let message = { | ||||
| 			type: 'post', | ||||
| 			text: edit.innerText, | ||||
| 			channel: this.channel, | ||||
| 			text: edit.value, | ||||
| 		}; | ||||
| 		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); | ||||
| 		} | ||||
| @@ -243,27 +224,35 @@ 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.value = ''; | ||||
| 				self.change(); | ||||
| 				self.notify(undefined); | ||||
| 				self.requestUpdate(); | ||||
| 			}); | ||||
| 		} catch (error) { | ||||
| 			alert(error.message); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	discard() { | ||||
| 		let edit = this.renderRoot.getElementById('edit'); | ||||
| 		edit.value = ''; | ||||
| 		this.change(); | ||||
| 		let preview = this.renderRoot.getElementById('preview'); | ||||
| 		preview.innerHTML = ''; | ||||
| 		this.notify(undefined); | ||||
| 	} | ||||
|  | ||||
| 	attach() { | ||||
| 		let self = this; | ||||
| 		let edit = this.renderRoot.getElementById('edit'); | ||||
| 		let input = document.createElement('input'); | ||||
| 		input.type = 'file'; | ||||
| 		input.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(); | ||||
| 	} | ||||
|  | ||||
| @@ -273,9 +262,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('"', '""') + '"', `%%`] | ||||
| @@ -294,65 +283,41 @@ class TfComposeElement extends LitElement { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	get_values() { | ||||
| 		let values = Object.entries(this.users).map((x) => ({ | ||||
| 			key: x[1].name ?? x[0], | ||||
| 			value: x[0], | ||||
| 		})); | ||||
| 		if (this.author) { | ||||
| 			values = [].concat( | ||||
| 				[ | ||||
| 					{ | ||||
| 						key: this.users[this.author]?.name, | ||||
| 						value: this.author, | ||||
| 					}, | ||||
| 				], | ||||
| 				values | ||||
| 			); | ||||
| 		} | ||||
| 		return values; | ||||
| 	} | ||||
|  | ||||
| 	firstUpdated() { | ||||
| 		let tribute = new Tribute({ | ||||
| 			iframe: this.shadowRoot, | ||||
| 			collection: [ | ||||
| 				{ | ||||
| 					values: this.get_values(), | ||||
| 					values: Object.entries(this.users).map((x) => ({ | ||||
| 						key: x[1].name, | ||||
| 						value: x[0], | ||||
| 					})), | ||||
| 					selectTemplate: function (item) { | ||||
| 						return item | ||||
| 							? `[@${item.original.key}](${item.original.value})` | ||||
| 							: undefined; | ||||
| 						return `[@${item.original.key}](${item.original.value})`; | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					trigger: '&', | ||||
| 					values: this.autocomplete, | ||||
| 					selectTemplate: function (item) { | ||||
| 						return item | ||||
| 							? `` | ||||
| 							: undefined; | ||||
| 						return ``; | ||||
| 					}, | ||||
| 				}, | ||||
| 			], | ||||
| 		}); | ||||
| 		tribute.attach(this.renderRoot.getElementById('edit')); | ||||
| 		this._tribute = tribute; | ||||
| 	} | ||||
|  | ||||
| 	updated() { | ||||
| 		super.updated(); | ||||
| 		let edit = this.renderRoot.getElementById('edit'); | ||||
| 		if (this.last_updated_text !== edit.innerText) { | ||||
| 		if (this.last_updated_text !== edit.value) { | ||||
| 			let preview = this.renderRoot.getElementById('preview'); | ||||
| 			preview.innerHTML = this.process_text(edit.innerText); | ||||
| 			this.last_updated_text = edit.innerText; | ||||
| 			preview.innerHTML = this.process_text(edit.value); | ||||
| 			this.last_updated_text = edit.value; | ||||
| 		} | ||||
| 		this._tribute.collection[0].values = this.get_values(); | ||||
| 		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], | ||||
| @@ -368,7 +333,8 @@ class TfComposeElement extends LitElement { | ||||
| 	remove_mention(id) { | ||||
| 		let draft = this.get_draft(); | ||||
| 		delete draft.mentions[id]; | ||||
| 		setTimeout(() => this.notify(draft), 0); | ||||
| 		this.notify(draft); | ||||
| 		this.requestUpdate(); | ||||
| 	} | ||||
|  | ||||
| 	render_mention(mention) { | ||||
| @@ -376,7 +342,7 @@ class TfComposeElement extends LitElement { | ||||
| 		return html` <div style="display: flex; flex-direction: row"> | ||||
| 			<div style="align-self: center; margin: 0.5em"> | ||||
| 				<button | ||||
| 					class="w3-button w3-theme-d1" | ||||
| 					class="w3-button w3-dark-grey" | ||||
| 					title="Remove ${mention.name} mention" | ||||
| 					@click=${() => self.remove_mention(mention.link)} | ||||
| 				> | ||||
| @@ -430,16 +396,16 @@ class TfComposeElement extends LitElement { | ||||
| 		if (this.apps) { | ||||
| 			return html` | ||||
| 				<div class="w3-card-4 w3-margin w3-padding"> | ||||
| 					<select id="select" class="w3-select w3-theme-d1"> | ||||
| 					<select id="select" class="w3-select w3-dark-grey"> | ||||
| 						${Object.keys(self.apps).map( | ||||
| 							(app) => html`<option value=${app}>${app}</option>` | ||||
| 						)} | ||||
| 					</select> | ||||
| 					<button class="w3-button w3-theme-d1" @click=${attach_selected_app}> | ||||
| 					<button class="w3-button w3-dark-grey" @click=${attach_selected_app}> | ||||
| 						Attach | ||||
| 					</button> | ||||
| 					<button | ||||
| 						class="w3-button w3-theme-d1" | ||||
| 						class="w3-button w3-dark-grey" | ||||
| 						@click=${() => (this.apps = null)} | ||||
| 					> | ||||
| 						Cancel | ||||
| @@ -455,15 +421,12 @@ class TfComposeElement extends LitElement { | ||||
| 			self.apps = await tfrpc.rpc.apps(); | ||||
| 		} | ||||
| 		if (!this.apps) { | ||||
| 			return html`<button | ||||
| 				class="w3-button w3-bar-item w3-theme-d1" | ||||
| 				@click=${attach_app} | ||||
| 			> | ||||
| 			return html`<button class="w3-button w3-dark-grey" @click=${attach_app}> | ||||
| 				Attach App | ||||
| 			</button>`; | ||||
| 		} else { | ||||
| 			return html`<button | ||||
| 				class="w3-button w3-bar-item w3-theme-d1" | ||||
| 				class="w3-button w3-dark-grey" | ||||
| 				@click=${() => (this.apps = null)} | ||||
| 			> | ||||
| 				Discard App | ||||
| @@ -484,38 +447,23 @@ class TfComposeElement extends LitElement { | ||||
| 		if (draft.content_warning !== undefined) { | ||||
| 			return html` | ||||
| 				<div class="w3-container w3-padding"> | ||||
| 					<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> | ||||
| 					<p> | ||||
| 						<input type="checkbox" class="w3-check w3-dark-grey" 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-dark-grey" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input> | ||||
| 				</div> | ||||
| 			`; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render_new_thread() { | ||||
| 		let self = this; | ||||
| 		if ( | ||||
| 			this.root !== undefined && | ||||
| 			this.branch !== undefined && | ||||
| 			this.root != this.branch | ||||
| 		) { | ||||
| 		} else { | ||||
| 			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> | ||||
| 				<input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning('')}></input> | ||||
| 				<label for="cw">CW</label> | ||||
| 			`; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	get_draft() { | ||||
| 		let key = | ||||
| 			this.branch || | ||||
| 			(this.recipients ? this.recipients.join(',') : undefined) || | ||||
| 			''; | ||||
| 		let draft = this.drafts[key] || {}; | ||||
| 		if (this.recipients && !draft.encrypt_to?.length) { | ||||
| 			draft.encrypt_to = [ | ||||
| 				...new Set(this.recipients).union(new Set(draft.encrypt_to ?? [])), | ||||
| 			]; | ||||
| 		} | ||||
| 		return draft; | ||||
| 		return this.drafts[this.branch || ''] || {}; | ||||
| 	} | ||||
|  | ||||
| 	update_encrypt(event) { | ||||
| @@ -537,15 +485,15 @@ class TfComposeElement extends LitElement { | ||||
| 		return html` | ||||
| 			<div style="display: flex; flex-direction: row; width: 100%"> | ||||
| 				<label for="encrypt_to">🔐 To:</label> | ||||
| 				<input type="text" id="encrypt_to" class="w3-input w3-theme-d1 w3-border" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input> | ||||
| 				<button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button> | ||||
| 				<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input> | ||||
| 				<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt(undefined)}>🚮</button> | ||||
| 			</div> | ||||
| 			<ul> | ||||
| 				${draft.encrypt_to.map( | ||||
| 					(x) => html` | ||||
| 					<li> | ||||
| 						<tf-user id=${x} .users=${this.users}></tf-user> | ||||
| 						<input type="button" class="w3-button w3-theme-d1" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input> | ||||
| 						<input type="button" class="w3-button w3-dark-grey" value="🚮" @click=${() => this.set_encrypt(draft.encrypt_to.filter((id) => id != x))}></input> | ||||
| 					</li>` | ||||
| 				)} | ||||
| 			</ul> | ||||
| @@ -559,37 +507,12 @@ class TfComposeElement extends LitElement { | ||||
| 		this.requestUpdate(); | ||||
| 	} | ||||
|  | ||||
| 	toggle_menu(event) { | ||||
| 		event.srcElement.parentNode | ||||
| 			.querySelector('.w3-dropdown-content') | ||||
| 			.classList.toggle('w3-show'); | ||||
| 	} | ||||
|  | ||||
| 	connectedCallback() { | ||||
| 		super.connectedCallback(); | ||||
| 		this._click_callback = this.document_click.bind(this); | ||||
| 		document.body.addEventListener('mouseup', this._click_callback); | ||||
| 	} | ||||
|  | ||||
| 	disconnectedCallback() { | ||||
| 		super.disconnectedCallback(); | ||||
| 		document.body.removeEventListener('mouseup', this._click_callback); | ||||
| 	} | ||||
|  | ||||
| 	document_click(event) { | ||||
| 		let content = this.renderRoot.querySelector('.w3-dropdown-content'); | ||||
| 		let target = event.target; | ||||
| 		if (content && !content.contains(target)) { | ||||
| 			content.classList.remove('w3-show'); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let self = this; | ||||
| 		let draft = self.get_draft(); | ||||
| 		let content_warning = | ||||
| 			draft.content_warning !== undefined | ||||
| 				? html`<div class="w3-panel w3-round-xlarge w3-theme-d2"> | ||||
| 				? html`<div class="w3-panel w3-round-xlarge w3-blue"> | ||||
| 						<p id="content_warning_preview">${draft.content_warning}</p> | ||||
| 					</div>` | ||||
| 				: undefined; | ||||
| @@ -597,99 +520,56 @@ class TfComposeElement extends LitElement { | ||||
| 			draft.encrypt_to !== undefined | ||||
| 				? undefined | ||||
| 				: html`<button | ||||
| 						class="w3-button w3-bar-item w3-theme-d1" | ||||
| 						class="w3-button w3-dark-grey" | ||||
| 						@click=${() => this.set_encrypt([])} | ||||
| 					> | ||||
| 						🔐 Encrypt | ||||
| 						🔐 | ||||
| 					</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-blue-grey w3-padding" | ||||
| 				style="box-sizing: border-box" | ||||
| 			> | ||||
| 				<header class="w3-container"> | ||||
| 					${this.channel !== undefined | ||||
| 						? html`<p>To #${this.channel}:</p>` | ||||
| 						: undefined} | ||||
| 					${this.render_encrypt()} | ||||
| 				</header> | ||||
| 				<div class="w3-container" style="padding: 0 0 16px 0"> | ||||
| 					<div class="w3-half"> | ||||
| 						<span | ||||
| 							class="w3-input w3-theme-d1 w3-border" | ||||
| 							style="resize: vertical; width: 100%; overflow: hidden; white-space: pre-wrap" | ||||
| 							placeholder="Write a post here." | ||||
| 							id="edit" | ||||
| 							@input=${this.input} | ||||
| 							@paste=${this.paste} | ||||
| 							contenteditable="plaintext-only" | ||||
| 							.innerText=${live(draft.text ?? '')} | ||||
| 						></span> | ||||
| 				${this.render_encrypt()} | ||||
| 				<div style="display: flex; flex-direction: row; width: 100%; gap: 4px"> | ||||
| 					<div style="flex: 1 0 50%"> | ||||
| 						<p> | ||||
| 							<textarea | ||||
| 								class="w3-input w3-dark-grey w3-border" | ||||
| 								style="resize: vertical" | ||||
| 								placeholder="Write a post here." | ||||
| 								id="edit" | ||||
| 								@input=${this.input} | ||||
| 								@change=${this.change} | ||||
| 								@paste=${this.paste} | ||||
| 							> | ||||
| ${draft.text}</textarea | ||||
| 							> | ||||
| 						</p> | ||||
| 					</div> | ||||
| 					<div class="w3-half w3-container"> | ||||
| 					<div style="flex: 1 0 50%"> | ||||
| 						${content_warning} | ||||
| 						<p id="preview"></p> | ||||
| 						<div id="preview"></div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				${Object.values(draft.mentions || {}).map((x) => | ||||
| 					self.render_mention(x) | ||||
| 				)} | ||||
| 				<footer> | ||||
| 					${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> | ||||
| 					<div class="w3-dropdown-click"> | ||||
| 						<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}> | ||||
| 							⚙️ | ||||
| 						</button> | ||||
| 						<div class="w3-dropdown-content w3-bar-block"> | ||||
| 							${this.get_draft().content_warning === undefined | ||||
| 								? html` | ||||
| 										<button | ||||
| 											class="w3-button w3-bar-item w3-theme-d1" | ||||
| 											@click=${() => self.set_content_warning('')} | ||||
| 										> | ||||
| 											Add Content Warning | ||||
| 										</button> | ||||
| 									` | ||||
| 								: html` | ||||
| 										<button | ||||
| 											class="w3-button w3-bar-item w3-theme-d1" | ||||
| 											@click=${() => self.set_content_warning(undefined)} | ||||
| 										> | ||||
| 											Remove Content Warning | ||||
| 										</button> | ||||
| 									`} | ||||
| 							<button | ||||
| 								class="w3-button w3-bar-item w3-theme-d1" | ||||
| 								@click=${this.attach} | ||||
| 							> | ||||
| 								Attach | ||||
| 							</button> | ||||
| 							${this.render_attach_app_button()} ${encrypt} | ||||
| 							<button | ||||
| 								class="w3-button w3-bar-item w3-theme-d1" | ||||
| 								@click=${this.discard} | ||||
| 							> | ||||
| 								Discard | ||||
| 							</button> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</footer> | ||||
| 				${this.render_attach_app()} ${this.render_content_warning()} | ||||
| 				<button | ||||
| 					class="w3-button w3-dark-grey" | ||||
| 					id="submit" | ||||
| 					@click=${this.submit} | ||||
| 				> | ||||
| 					Submit | ||||
| 				</button> | ||||
| 				<button class="w3-button w3-dark-grey" @click=${this.attach}> | ||||
| 					Attach | ||||
| 				</button> | ||||
| 				${this.render_attach_app_button()} ${encrypt} | ||||
| 				<button class="w3-button w3-dark-grey" @click=${this.discard}> | ||||
| 					Discard | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		`; | ||||
| 		return result; | ||||
|   | ||||
							
								
								
									
										54
									
								
								apps/ssb/tf-id-picker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,54 @@ | ||||
| import {LitElement, html} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| import {styles} from './tf-styles.js'; | ||||
|  | ||||
| /* | ||||
|  ** Provide a list of IDs, and this lets the user pick one. | ||||
|  */ | ||||
| class TfIdentityPickerElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			ids: {type: Array}, | ||||
| 			selected: {type: String}, | ||||
| 			users: {type: Object}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	static styles = styles; | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.ids = []; | ||||
| 		this.users = {}; | ||||
| 	} | ||||
|  | ||||
| 	changed(event) { | ||||
| 		this.selected = event.srcElement.value; | ||||
| 		this.dispatchEvent( | ||||
| 			new Event('change', { | ||||
| 				srcElement: this, | ||||
| 			}) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		return html` | ||||
| 			<select | ||||
| 				class="w3-select w3-dark-grey w3-padding w3-border" | ||||
| 				@change=${this.changed} | ||||
| 				style="max-width: 100%; overflow: hidden" | ||||
| 			> | ||||
| 				${(this.ids ?? []).map( | ||||
| 					(id) => | ||||
| 						html`<option ?selected=${id == this.selected} value=${id}> | ||||
| 							${this.users[id]?.name | ||||
| 								? this.users[id]?.name + ' - ' | ||||
| 								: undefined}<small>${id}</small> | ||||
| 						</option>` | ||||
| 				)} | ||||
| 			</select> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-id-picker', TfIdentityPickerElement); | ||||
| @@ -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,10 +11,6 @@ class TfNewsElement extends LitElement { | ||||
| 			following: {type: Array}, | ||||
| 			drafts: {type: Object}, | ||||
| 			expanded: {type: Object}, | ||||
| 			channel: {type: String}, | ||||
| 			channel_unread: {type: Number}, | ||||
| 			recent_reactions: {type: Array}, | ||||
| 			hash: {type: String}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| @@ -29,8 +25,6 @@ class TfNewsElement extends LitElement { | ||||
| 		this.following = []; | ||||
| 		this.drafts = {}; | ||||
| 		this.expanded = {}; | ||||
| 		this.channel_unread = -1; | ||||
| 		this.recent_reactions = []; | ||||
| 	} | ||||
|  | ||||
| 	process_messages(messages) { | ||||
| @@ -39,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"', | ||||
| @@ -60,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 = []; | ||||
| 				} | ||||
| @@ -69,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 = []; | ||||
| 						} | ||||
| @@ -167,12 +160,8 @@ class TfNewsElement extends LitElement { | ||||
| 			if (message?.content?.type === 'contact') { | ||||
| 				group.push(message); | ||||
| 			} else { | ||||
| 				if (group.length == 1) { | ||||
| 					result.push(group[0]); | ||||
| 					group = []; | ||||
| 				} else if (group.length > 1) { | ||||
| 				if (group.length > 0) { | ||||
| 					result.push({ | ||||
| 						rowid: Math.max(...group.map((x) => x.rowid)), | ||||
| 						type: 'contact_group', | ||||
| 						messages: group, | ||||
| 					}); | ||||
| @@ -181,77 +170,26 @@ class TfNewsElement extends LitElement { | ||||
| 				result.push(message); | ||||
| 			} | ||||
| 		} | ||||
| 		if (group.length == 1) { | ||||
| 			result.push(group[0]); | ||||
| 			group = []; | ||||
| 		} else if (group.length > 1) { | ||||
| 			result.push({ | ||||
| 				rowid: Math.max(...group.map((x) => x.rowid)), | ||||
| 				type: 'contact_group', | ||||
| 				messages: group, | ||||
| 			}); | ||||
| 		} | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	unread_allowed() { | ||||
| 		return !this.hash?.startsWith('#%') && !this.hash?.startsWith('#@'); | ||||
| 	} | ||||
|  | ||||
| 	load_and_render(messages) { | ||||
| 		let messages_by_id = this.process_messages(messages); | ||||
| 		let final_messages = this.group_following( | ||||
| 			this.finalize_messages(messages_by_id) | ||||
| 		); | ||||
| 		let unread_rowid = -1; | ||||
| 		if (this.unread_allowed()) { | ||||
| 			for (let message of final_messages) { | ||||
| 				if (message.rowid >= this.channel_unread) { | ||||
| 					unread_rowid = message.rowid; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		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} | ||||
| 							.recent_reactions=${this.recent_reactions} | ||||
| 						></tf-message> | ||||
| 						${x.rowid == unread_rowid | ||||
| 							? html`<div style="display: flex; flex-direction: row"> | ||||
| 									<div | ||||
| 										style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px" | ||||
| 									></div> | ||||
| 									<button | ||||
| 										style="color: #f00; padding: 8px" | ||||
| 										class="w3-button" | ||||
| 										@click=${() => | ||||
| 											this.dispatchEvent( | ||||
| 												new Event('mark_all_read', { | ||||
| 													bubbles: true, | ||||
| 													composed: true, | ||||
| 												}) | ||||
| 											)} | ||||
| 									> | ||||
| 										unread | ||||
| 									</button> | ||||
| 									<div | ||||
| 										style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px" | ||||
| 									></div> | ||||
| 								</div>` | ||||
| 							: undefined} | ||||
| 					` | ||||
| 						></tf-message>` | ||||
| 				)} | ||||
| 			</div> | ||||
| 		`; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import {LitElement, html, until, unsafeHTML} from './lit-all.min.js'; | ||||
| import {LitElement, html, unsafeHTML} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
| import * as tfutils from './tf-utils.js'; | ||||
| import {styles} from './tf-styles.js'; | ||||
| @@ -11,10 +11,9 @@ class TfProfileElement extends LitElement { | ||||
| 			id: {type: String}, | ||||
| 			users: {type: Object}, | ||||
| 			size: {type: Number}, | ||||
| 			sequence: {type: Number}, | ||||
| 			server_follows_me: {type: Boolean}, | ||||
| 			following: {type: Boolean}, | ||||
| 			blocking: {type: Boolean}, | ||||
| 			show_followed: {type: Boolean}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| @@ -28,7 +27,7 @@ class TfProfileElement extends LitElement { | ||||
| 		this.id = null; | ||||
| 		this.users = {}; | ||||
| 		this.size = 0; | ||||
| 		this.sequence = 0; | ||||
| 		this.server_follows_me = undefined; | ||||
| 	} | ||||
|  | ||||
| 	async load() { | ||||
| @@ -64,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, | ||||
| @@ -77,10 +95,6 @@ class TfProfileElement extends LitElement { | ||||
| 					change | ||||
| 				) | ||||
| 			) | ||||
| 			.then(function () { | ||||
| 				self._follow_whoami = undefined; | ||||
| 				self.load(); | ||||
| 			}) | ||||
| 			.catch(function (error) { | ||||
| 				alert(error?.message); | ||||
| 			}); | ||||
| @@ -142,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() | ||||
| @@ -158,205 +171,140 @@ class TfProfileElement extends LitElement { | ||||
| 				.catch(function (e) { | ||||
| 					alert(e.message); | ||||
| 				}); | ||||
| 		}); | ||||
| 		document.body.appendChild(input); | ||||
| 		}; | ||||
| 		input.click(); | ||||
| 	} | ||||
|  | ||||
| 	copy_id() { | ||||
| 		navigator.clipboard.writeText(this.id); | ||||
| 	} | ||||
|  | ||||
| 	show_image(link) { | ||||
| 		let div = document.createElement('div'); | ||||
| 		div.style.left = 0; | ||||
| 		div.style.top = 0; | ||||
| 		div.style.width = '100%'; | ||||
| 		div.style.height = '100%'; | ||||
| 		div.style.position = 'fixed'; | ||||
| 		div.style.background = '#000'; | ||||
| 		div.style.zIndex = 100; | ||||
| 		div.style.display = 'grid'; | ||||
| 		let img = document.createElement('img'); | ||||
| 		img.src = link; | ||||
| 		img.style.maxWidth = '100vw'; | ||||
| 		img.style.maxHeight = '100vh'; | ||||
| 		img.style.display = 'block'; | ||||
| 		img.style.margin = 'auto'; | ||||
| 		img.style.objectFit = 'contain'; | ||||
| 		img.style.width = '100vw'; | ||||
| 		div.appendChild(img); | ||||
| 		function image_close(event) { | ||||
| 			document.body.removeChild(div); | ||||
| 			window.removeEventListener('keydown', image_close); | ||||
| 	async server_follow_me(follow) { | ||||
| 		try { | ||||
| 			await tfrpc.rpc.setServerFollowingMe(this.whoami, follow); | ||||
| 		} catch (e) { | ||||
| 			console.log(e); | ||||
| 		} | ||||
| 		div.onclick = image_close; | ||||
| 		window.addEventListener('keydown', image_close); | ||||
| 		document.body.appendChild(div); | ||||
| 	} | ||||
|  | ||||
| 	body_click(event) { | ||||
| 		if (event.srcElement.tagName == 'IMG') { | ||||
| 			this.show_image(event.srcElement.src); | ||||
| 		try { | ||||
| 			await this.initial_load(); | ||||
| 		} catch (e) { | ||||
| 			console.log(e); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	toggle_account_list(event) { | ||||
| 		let content = event.srcElement.nextElementSibling; | ||||
| 		this.show_followed = !this.show_followed; | ||||
| 	} | ||||
|  | ||||
| 	async load_follows() { | ||||
| 		let accounts = await tfrpc.rpc.following([this.id], 1); | ||||
| 		return html` | ||||
| 			<div class="w3-container"> | ||||
| 				<button | ||||
| 					class="w3-button w3-block w3-theme-d1 followed_accounts" | ||||
| 					@click=${this.toggle_account_list} | ||||
| 				> | ||||
| 					${this.show_followed ? 'Hide' : 'Show'} Followed Accounts | ||||
| 					(${Object.keys(accounts).length}) | ||||
| 				</button> | ||||
| 				<div class=${'w3-card' + (this.show_followed ? '' : ' w3-hide')}> | ||||
| 					<ul class="w3-ul w3-theme-d4 w3-border-theme"> | ||||
| 						${Object.keys(accounts).map( | ||||
| 							(x) => html` | ||||
| 								<li class="w3-border-theme"> | ||||
| 									<tf-user id=${x} .users=${this.users}></tf-user> | ||||
| 								</li> | ||||
| 							` | ||||
| 						)} | ||||
| 					</ul> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		`; | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		if ( | ||||
| 			this.id == this.whoami && | ||||
| 			this.editing && | ||||
| 			this.server_follows_me === undefined | ||||
| 		) { | ||||
| 			this.initial_load(); | ||||
| 		} | ||||
| 		this.load(); | ||||
| 		let self = this; | ||||
| 		let profile = this.users[this.id] || {}; | ||||
| 		tfrpc.rpc | ||||
| 			.query( | ||||
| 				`SELECT SUM(LENGTH(content)) AS size, MAX(sequence) AS sequence FROM messages WHERE author = ?`, | ||||
| 				`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`, | ||||
| 				[this.id] | ||||
| 			) | ||||
| 			.then(function (result) { | ||||
| 				self.size = result[0].size; | ||||
| 				self.sequence = result[0].sequence; | ||||
| 			}); | ||||
| 		let edit; | ||||
| 		let follow; | ||||
| 		let block; | ||||
| 		if (this.id === this.whoami) { | ||||
| 			if (this.editing) { | ||||
| 				edit = html` | ||||
| 					<button | ||||
| 						id="save_profile" | ||||
| 						class="w3-button w3-theme-d1" | ||||
| 						@click=${this.save_edits} | ||||
| 				let server_follow; | ||||
| 				if (this.server_follows_me === true) { | ||||
| 					server_follow = html`<button | ||||
| 						class="w3-button w3-dark-grey" | ||||
| 						@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-dark-grey" | ||||
| 						@click=${() => this.server_follow_me(true)} | ||||
| 					> | ||||
| 						Server, Follow Me | ||||
| 					</button>`; | ||||
| 				} | ||||
| 				edit = html` | ||||
| 					<button class="w3-button w3-dark-grey" @click=${this.save_edits}> | ||||
| 						Save Profile | ||||
| 					</button> | ||||
| 					<button class="w3-button w3-theme-d1" @click=${this.discard_edits}> | ||||
| 					<button class="w3-button w3-dark-grey" @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-dark-grey" @click=${this.edit}> | ||||
| 					Edit Profile | ||||
| 				</button>`; | ||||
| 			} | ||||
| 		} | ||||
| 		if (this.id !== this.whoami && this.following !== undefined) { | ||||
| 			follow = this.following | ||||
| 				? html`<button class="w3-button w3-theme-d1" @click=${this.unfollow}> | ||||
| 				? html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}> | ||||
| 						Unfollow | ||||
| 					</button>` | ||||
| 				: html`<button class="w3-button w3-theme-d1" @click=${this.follow}> | ||||
| 				: html`<button class="w3-button w3-dark-grey" @click=${this.follow}> | ||||
| 						Follow | ||||
| 					</button>`; | ||||
| 		} | ||||
| 		if (this.id !== this.whoami && this.blocking !== undefined) { | ||||
| 			block = this.blocking | ||||
| 				? html`<button class="w3-button w3-theme-d1" @click=${this.unblock}> | ||||
| 				? html`<button class="w3-button w3-dark-grey" @click=${this.unblock}> | ||||
| 						Unblock | ||||
| 					</button>` | ||||
| 				: html`<button class="w3-button w3-theme-d1" @click=${this.block}> | ||||
| 				: html`<button class="w3-button w3-dark-grey" @click=${this.block}> | ||||
| 						Block | ||||
| 					</button>`; | ||||
| 		} | ||||
| 		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-dark-grey" 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-dark-grey" 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-dark-grey" 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-dark-grey" @click=${this.attach_image}>Attach Image</button> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div>` | ||||
| 			: null; | ||||
| 		let image = profile.image; | ||||
| 		if (typeof image == 'string' && !image.startsWith('&')) { | ||||
| 			try { | ||||
| 				image = JSON.parse(image)?.link; | ||||
| 			} catch {} | ||||
| 		} | ||||
| 		let image = | ||||
| 			typeof profile.image == 'string' ? profile.image : profile.image?.link; | ||||
| 		image = this.editing?.image ?? image; | ||||
| 		let description = this.editing?.description ?? profile.description; | ||||
| 		return html`<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box"> | ||||
| 			<header class="w3-container"> | ||||
| 				<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)} in ${this.sequence} messages)</p> | ||||
| 			</header> | ||||
| 			<div class="w3-container" @click=${this.body_click}> | ||||
| 				<div class="w3-margin-bottom" style="display: flex; flex-direction: row"> | ||||
| 					<input type="text" class="w3-input w3-border w3-theme-d1" style="display: flex 1 1" readonly value=${this.id}></input> | ||||
| 					<button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button> | ||||
| 				</div> | ||||
| 				<div style="display: flex; flex-direction: row; gap: 1em"> | ||||
| 					${edit_profile} | ||||
| 					<div style="flex: 1 0 50%; contain: layout; overflow: auto; word-wrap: normal; word-break: normal"> | ||||
| 						${ | ||||
| 							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> | ||||
| 			${until(this.load_follows(), html`<p>Loading accounts followed...</p>`)} | ||||
| 			<footer class="w3-container"> | ||||
| 				<p> | ||||
| 					<a class="w3-button w3-theme-d1" href=${'#🔐' + (this.id != this.whoami ? this.id : '')}> | ||||
| 						Open Private Chat | ||||
| 					</a> | ||||
| 					${edit} | ||||
| 					${follow} | ||||
| 					${block} | ||||
| 				</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>`; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,75 +0,0 @@ | ||||
| import {LitElement, html, unsafeHTML} from './lit-all.min.js'; | ||||
| import {styles} from './tf-styles.js'; | ||||
|  | ||||
| class TfReactionsModalElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			users: {type: Object}, | ||||
| 			votes: {type: Array}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	static styles = styles; | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.votes = []; | ||||
| 		this.users = {}; | ||||
| 	} | ||||
|  | ||||
| 	clear() { | ||||
| 		this.votes = []; | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		let self = this; | ||||
| 		return this.votes?.length | ||||
| 			? html` <div | ||||
| 					class="w3-modal w3-animate-opacity" | ||||
| 					style="display: block; box-sizing: border-box; z-index: 10" | ||||
| 					@click=${this.clear} | ||||
| 				> | ||||
| 					<div | ||||
| 						class="w3-modal-content w3-card-4 w3-theme-d1" | ||||
| 						onclick="event.stopPropagation()" | ||||
| 					> | ||||
| 						<div class="w3-container w3-padding"> | ||||
| 							<header class="w3-container"> | ||||
| 								<h2>Reactions</h2> | ||||
| 								<span class="w3-button w3-display-topright" @click=${this.clear} | ||||
| 									>×</span | ||||
| 								> | ||||
| 							</header> | ||||
| 							<ul class="w3-theme-dark w3-container w3-ul"> | ||||
| 								${this.votes | ||||
| 									.sort((x, y) => y.timestamp - x.timestamp) | ||||
| 									.map( | ||||
| 										(x) => html` | ||||
| 											<li style="display: flex; flex-direction: row; gap: 4px"> | ||||
| 												<span style="flex-basis: 3em" | ||||
| 													>${x?.content?.vote?.expression}</span | ||||
| 												> | ||||
| 												<tf-user | ||||
| 													style="flex: 1 1" | ||||
| 													id=${x.author} | ||||
| 													.users=${this.users} | ||||
| 												></tf-user> | ||||
| 												<span | ||||
| 													style="flex-shrink: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis" | ||||
| 													>${new Date(x?.timestamp).toLocaleString()}</span | ||||
| 												> | ||||
| 											</li> | ||||
| 										` | ||||
| 									)} | ||||
| 							</ul> | ||||
| 							<footer class="w3-container w3-padding"> | ||||
| 								<button class="w3-button" @click=${this.clear}>Close</button> | ||||
| 							</footer> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div>` | ||||
| 			: undefined; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-reactions-modal', TfReactionsModalElement); | ||||
| @@ -7,55 +7,35 @@ 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) { | ||||
| 		if (connection.address && connection.port) { | ||||
| 			return html`<div> | ||||
| 				<small>${connection.address}:${connection.port}</small> | ||||
| 			</div>`; | ||||
| 			return html`(<small>${connection.address}:${connection.port}</small>)`; | ||||
| 		} else if (connection.tunnel) { | ||||
| 			return html`<div>room peer</div>`; | ||||
| 			return html`(room peer)`; | ||||
| 		} else { | ||||
| 			return JSON.stringify(connection); | ||||
| 		} | ||||
| @@ -81,7 +61,7 @@ class TfTabConnectionsElement extends LitElement { | ||||
| 		return html` | ||||
| 			<li> | ||||
| 				<button | ||||
| 					class="w3-button w3-theme-d1" | ||||
| 					class="w3-button w3-dark-grey" | ||||
| 					@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} | ||||
| 				> | ||||
| 					Connect | ||||
| @@ -91,53 +71,17 @@ 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> | ||||
| 				</div> | ||||
| 				${this.render_message(connection)} | ||||
| 				<button | ||||
| 					class="w3-button w3-dark-grey" | ||||
| 					@click=${() => tfrpc.rpc.connect(connection)} | ||||
| 				> | ||||
| 					Connect | ||||
| 				</button> | ||||
| 				<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> | ||||
| 				${this.render_connection_summary(connection)} | ||||
| 			</li> | ||||
| 		`; | ||||
| 	} | ||||
| @@ -148,206 +92,81 @@ 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-dark-grey" | ||||
| 				@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> | ||||
| 			<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"> | ||||
| 			<div class="w3-container"> | ||||
| 				<h2>New Connection</h2> | ||||
| 				<textarea class="w3-input w3-theme-d1" id="code"></textarea> | ||||
| 				${this.render_message(this.renderRoot.getElementById('code')?.value)} | ||||
| 				<textarea class="w3-input w3-dark-grey" id="code"></textarea> | ||||
| 				<button | ||||
| 					class="w3-button w3-theme-d1" | ||||
| 					class="w3-button w3-dark-grey" | ||||
| 					@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> | ||||
| 					${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')} | ||||
| 				> | ||||
| 					Discovery (${this.valid_broadcasts().length}) | ||||
| 				</h2> | ||||
| 				<ul class="w3-ul w3-border w3-hide" id="broadcasts"> | ||||
| 					${this.valid_broadcasts().map((x) => self.render_broadcast(x))} | ||||
| 				<h2>Connections</h2> | ||||
| 				<ul> | ||||
| 					${this.connections | ||||
| 						.filter((x) => x.tunnel === undefined) | ||||
| 						.map((x) => html` <li>${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 (WIP)</h2> | ||||
| 				<ul> | ||||
| 					${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> | ||||
| 											<small | ||||
| 												>Last connection: | ||||
| 												${new Date(x.last_success * 1000)}</small | ||||
| 											> | ||||
| 										</div> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 								${this.render_message(x)} | ||||
| 								<button | ||||
| 									class="w3-button w3-dark-grey" | ||||
| 									@click=${() => self.forget_stored_connection(x)} | ||||
| 								> | ||||
| 									Forget | ||||
| 								</button> | ||||
| 								<button | ||||
| 									class="w3-button w3-dark-grey" | ||||
| 									@click=${() => tfrpc.rpc.connect(x)} | ||||
| 								> | ||||
| 									Connect | ||||
| 								</button> | ||||
| 								${x.address}:${x.port} | ||||
| 								<tf-user id=${x.pubkey} .users=${self.users}></tf-user> | ||||
| 							</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> | ||||
| 					${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} | ||||
| 								<tf-user id=${x} .users=${this.users}></tf-user> | ||||
| 							</div>` | ||||
| 							html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>` | ||||
| 					)} | ||||
| 				</div> | ||||
| 				</ul> | ||||
| 			</div> | ||||
| 		`; | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										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,14 +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}, | ||||
| 			grouped_private_messages: {type: Object}, | ||||
| 			recent_reactions: {type: Array}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| @@ -34,294 +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.recent_reactions = []; | ||||
| 		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_related_messages(messages) { | ||||
| 		let refs = await tfrpc.rpc.query( | ||||
| 			` | ||||
| 				WITH | ||||
| 					news AS ( | ||||
| 						SELECT value AS id FROM json_each(?) | ||||
| 					) | ||||
| 				SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id | ||||
| 				UNION | ||||
| 				SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id | ||||
| 			`, | ||||
| 			[JSON.stringify(messages.map((x) => x.id))] | ||||
| 		); | ||||
| 		let related_messages = await tfrpc.rpc.query( | ||||
| 			` | ||||
| 				SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature | ||||
| 				FROM messages | ||||
| 				JOIN json_each(?2) refs ON messages.id = refs.value | ||||
| 				JOIN json_each(?1) AS following ON messages.author = following.value | ||||
| 			`, | ||||
| 			[JSON.stringify(this.following), JSON.stringify(refs.map((x) => x.ref))] | ||||
| 		); | ||||
| 		let combined = [].concat(messages, related_messages); | ||||
| 		let refs2 = await tfrpc.rpc.query( | ||||
| 			` | ||||
| 				WITH | ||||
| 					news AS ( | ||||
| 						SELECT value AS id FROM json_each(?) | ||||
| 					) | ||||
| 				SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id | ||||
| 				UNION | ||||
| 				SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id | ||||
| 			`, | ||||
| 			[JSON.stringify(combined.map((x) => x.id))] | ||||
| 		); | ||||
| 		let t0 = new Date(); | ||||
| 		let result = [].concat( | ||||
| 			combined, | ||||
| 			await tfrpc.rpc.query( | ||||
| 	async fetch_messages() { | ||||
| 		if (this.hash.startsWith('#@')) { | ||||
| 			let r = await tfrpc.rpc.query( | ||||
| 				` | ||||
| 				SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature | ||||
| 				FROM json_each(?2) refs | ||||
| 				JOIN messages ON messages.id = refs.value | ||||
| 				JOIN json_each(?1) following ON messages.author = following.value | ||||
| 				WHERE messages.content ->> 'type' != 'post' | ||||
| 			`, | ||||
| 				[ | ||||
| 					JSON.stringify(this.following), | ||||
| 					JSON.stringify(refs2.map((x) => x.ref)), | ||||
| 				] | ||||
| 			) | ||||
| 		); | ||||
| 		let t1 = new Date(); | ||||
| 		console.log((t1 - t0) / 1000); | ||||
| 		return result; | ||||
| 	} | ||||
|  | ||||
| 	async fetch_messages(start_time, end_time) { | ||||
| 		this.dispatchEvent( | ||||
| 			new CustomEvent('loadmessages', { | ||||
| 				bubbles: true, | ||||
| 				composed: true, | ||||
| 			}) | ||||
| 		); | ||||
| 		this.time_loading = [start_time, end_time]; | ||||
| 		let result; | ||||
| 		const k_max_results = 64; | ||||
| 		if (this.hash == '#@') { | ||||
| 			result = await tfrpc.rpc.query( | ||||
| 				` | ||||
| 					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 ?5) | ||||
| 					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature | ||||
| 						FROM 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, | ||||
| 					k_max_results, | ||||
| 				] | ||||
| 			); | ||||
| 		} 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 ?4 | ||||
| 						) | ||||
| 					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature | ||||
| 						FROM 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, k_max_results] | ||||
| 				[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('##')) { | ||||
| 			let t0 = new Date(); | ||||
| 			let initial_messages = await tfrpc.rpc.query( | ||||
| 				` | ||||
| 					WITH | ||||
| 						all_news AS ( | ||||
| 							SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature | ||||
| 								FROM messages | ||||
| 								JOIN json_each(?) AS following ON messages.author = following.value | ||||
| 								WHERE messages.content ->> 'channel' = ?4 AND messages.content ->> 'type' != 'vote' | ||||
| 							UNION | ||||
| 							SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature | ||||
| 								FROM messages_refs | ||||
| 								JOIN messages ON messages.id = messages_refs.message | ||||
| 								JOIN json_each(?1) AS following ON messages.author = following.value | ||||
| 								WHERE messages_refs.ref = '#' || ?4 AND messages.content ->> 'type' != 'vote' | ||||
| 							) | ||||
| 					SELECT TRUE AS is_primary, all_news.* FROM all_news | ||||
| 						WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3 | ||||
| 						ORDER BY all_news.timestamp DESC LIMIT ?5 | ||||
| 				`, | ||||
| 				[ | ||||
| 					JSON.stringify(this.following), | ||||
| 					start_time, | ||||
| 					end_time, | ||||
| 					this.hash.substring(2), | ||||
| 					k_max_results, | ||||
| 				] | ||||
| 			); | ||||
| 			let t1 = new Date(); | ||||
| 			result = await this._fetch_related_messages(initial_messages); | ||||
| 			let t2 = new Date(); | ||||
| 			console.log( | ||||
| 				`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}` | ||||
| 			); | ||||
| 		} else if (this.hash.startsWith('#🔐')) { | ||||
| 			let ids = | ||||
| 				this.hash == '#🔐' ? [] : this.hash.substring('#🔐'.length).split(','); | ||||
| 			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.rowid DESC LIMIT ?4 | ||||
| 				`, | ||||
| 				[ | ||||
| 					JSON.stringify( | ||||
| 						this.grouped_private_messages?.[JSON.stringify(ids)]?.map( | ||||
| 							(x) => x.id | ||||
| 						) ?? [] | ||||
| 					), | ||||
| 					start_time, | ||||
| 					end_time, | ||||
| 					k_max_results, | ||||
| 				] | ||||
| 			); | ||||
| 			result = (await this.decrypt(result)).filter((x) => x.decrypted); | ||||
| 		} else if (this.hash == '#👍') { | ||||
| 			result = await tfrpc.rpc.query( | ||||
| 				` | ||||
| 					WITH votes AS (SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature | ||||
| 						FROM messages | ||||
| 						JOIN json_each(?1) AS following ON messages.author = following.value | ||||
| 						WHERE | ||||
| 							messages.content ->> 'type' = 'vote' AND | ||||
| 							(?2 IS NULL OR messages.timestamp >= ?2) AND messages.timestamp < ?3 | ||||
| 						ORDER BY timestamp DESC limit ?4) | ||||
| 					SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature | ||||
| 						FROM votes | ||||
| 						JOIN messages ON messages.id = votes.content ->> '$.vote.link' | ||||
| 					UNION | ||||
| 					SELECT TRUE AS is_primary, * FROM votes | ||||
| 				`, | ||||
| 				[JSON.stringify(this.following), start_time, end_time, k_max_results] | ||||
| 			); | ||||
| 		} else { | ||||
| 			let t0 = new Date(); | ||||
| 			let initial_messages = await tfrpc.rpc.query( | ||||
| 				` | ||||
| 					SELECT TRUE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature | ||||
| 					FROM messages | ||||
| 					JOIN json_each(?) AS following ON messages.author = following.value | ||||
| 					WHERE messages.timestamp < ?3 AND (?2 IS NULL OR messages.timestamp >= ?2) AND | ||||
| 						messages.content ->> 'type' != 'vote' | ||||
| 					ORDER BY timestamp DESC LIMIT ?4 | ||||
| 				`, | ||||
| 				[JSON.stringify(this.following), start_time, end_time, k_max_results] | ||||
| 			); | ||||
| 			let t1 = new Date(); | ||||
| 			result = await this._fetch_related_messages(initial_messages); | ||||
| 			let t2 = new Date(); | ||||
| 			console.log( | ||||
| 				`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}` | ||||
| 			); | ||||
| 			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] | ||||
| 			), | ||||
| 		]; | ||||
| 	} | ||||
|  | ||||
| 	unread_allowed() { | ||||
| 		return ( | ||||
| 			this.hash == '#@' || | ||||
| 			(!this.hash.startsWith('#%') && !this.hash.startsWith('#@')) | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	async load_more() { | ||||
| 		this.loading++; | ||||
| 		this.loading_canceled = false; | ||||
| 		try { | ||||
| 			let more = []; | ||||
| 			let last_start_time = this.time_range[0]; | ||||
| 			try { | ||||
| 				more = await this.fetch_messages(null, last_start_time); | ||||
| 			} catch (e) { | ||||
| 				console.log(e); | ||||
| 			} | ||||
| 			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; | ||||
| @@ -346,192 +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 = []; | ||||
| 		let original_hash = this.hash; | ||||
| 		try { | ||||
| 			if (this._messages_hash !== this.hash) { | ||||
| 				this.messages = []; | ||||
| 				this._messages_hash = this.hash; | ||||
| 			} | ||||
| 			this._messages_following = JSON.stringify(this.following); | ||||
| 			this._private_messages = | ||||
| 				JSON.stringify(this.private_messages) + | ||||
| 				JSON.stringify(this.grouped_private_messages); | ||||
| 			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--; | ||||
| 		} | ||||
| 		if (this.hash == original_hash) { | ||||
| 			this.messages = this.merge_messages(this.messages, messages); | ||||
| 		} | ||||
| 		this.time_loading = undefined; | ||||
| 		console.log( | ||||
| 			`loading ${messages.length} messages done for ${self.whoami} in ${(new Date() - start_time) / 1000}s` | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	mark_all_read() { | ||||
| 		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, | ||||
| 					}, | ||||
| 				}) | ||||
| 			); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	close_private_chat() { | ||||
| 		this.mark_all_read(); | ||||
| 		this.dispatchEvent( | ||||
| 			new CustomEvent('closeprivatechat', { | ||||
| 				bubbles: true, | ||||
| 				composed: true, | ||||
| 				detail: { | ||||
| 					key: JSON.stringify( | ||||
| 						this.hash == '#🔐' | ||||
| 							? [] | ||||
| 							: this.hash.substring('#🔐'.length).split(',') | ||||
| 					), | ||||
| 				}, | ||||
| 			}) | ||||
| 		); | ||||
| 		tfrpc.rpc.setHash('#'); | ||||
| 	} | ||||
|  | ||||
| 	render_close_chat_button() { | ||||
| 		if (this.hash.startsWith('#🔐')) { | ||||
| 			return html` | ||||
| 				<button class="w3-button w3-theme-d1" @click=${this.close_private_chat}> | ||||
| 					Close Chat | ||||
| 				</button> | ||||
| 			`; | ||||
| 		} | ||||
| 	async add_messages(messages) { | ||||
| 		this.messages = await this.decrypt([...messages, ...this.messages]); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		if ( | ||||
| 			!this.messages || | ||||
| 			this._messages_hash !== this.hash || | ||||
| 			this._messages_following !== JSON.stringify(this.following) || | ||||
| 			this._private_messages !== | ||||
| 				JSON.stringify(this.private_messages) + | ||||
| 					JSON.stringify(this.grouped_private_messages) | ||||
| 			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> | ||||
| 					${this.unread_allowed() | ||||
| 						? html` | ||||
| 								<button | ||||
| 									class="w3-button w3-theme-d1" | ||||
| 									@click=${this.mark_all_read} | ||||
| 								> | ||||
| 									Mark All Read | ||||
| 								</button> | ||||
| 							` | ||||
| 						: undefined} | ||||
| 					<button | ||||
| 						?disabled=${this.loading} | ||||
| 						class="w3-button w3-theme-d1" | ||||
| 						@click=${this.load_more} | ||||
| 					> | ||||
| 					<button class="w3-button w3-dark-grey" @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` | ||||
| 			${this.unread_allowed() | ||||
| 				? html`<button | ||||
| 						class="w3-button w3-theme-d1" | ||||
| 						@click=${this.mark_all_read} | ||||
| 					> | ||||
| 						Mark All Read | ||||
| 					</button>` | ||||
| 				: undefined} | ||||
| 			${this.render_close_chat_button()} | ||||
| 		return html` | ||||
| 			<tf-news | ||||
| 				id="news" | ||||
| 				whoami=${this.whoami} | ||||
| @@ -540,14 +202,9 @@ class TfTabNewsFeedElement extends LitElement { | ||||
| 				.following=${this.following} | ||||
| 				.drafts=${this.drafts} | ||||
| 				.expanded=${this.expanded} | ||||
| 				hash=${this.hash} | ||||
| 				channel=${this.channel()} | ||||
| 				channel_unread=${this.channels_unread?.[this.channel()]} | ||||
| 				.recent_reactions=${this.recent_reactions} | ||||
| 				@mark_all_read=${this.mark_all_read} | ||||
| 			></tf-news> | ||||
| 			${more} | ||||
| 		`); | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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,20 +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}, | ||||
| 			grouped_private_messages: {type: Object}, | ||||
| 			recent_reactions: {type: Array}, | ||||
| 			peer_exchange: {type: Boolean}, | ||||
| 			is_administrator: {type: Boolean}, | ||||
| 			stay_connected: {type: Boolean}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| @@ -40,19 +23,14 @@ 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 = []; | ||||
| 		this.recent_reactions = []; | ||||
| 		tfrpc.rpc.localStorageGet('drafts').then(function (d) { | ||||
| 			self.drafts = JSON.parse(d || '{}'); | ||||
| 		}); | ||||
| 		this.check_peer_exchange(); | ||||
| 	} | ||||
|  | ||||
| 	connectedCallback() { | ||||
| @@ -65,19 +43,37 @@ class TfTabNewsElement extends LitElement { | ||||
| 		document.body.removeEventListener('keypress', this.on_keypress.bind(this)); | ||||
| 	} | ||||
|  | ||||
| 	async check_peer_exchange() { | ||||
| 		if (await tfrpc.rpc.isAdministrator()) { | ||||
| 			this.peer_exchange = await tfrpc.rpc.globalSettingsGet('peer_exchange'); | ||||
| 		} else { | ||||
| 			this.peer_exchange = undefined; | ||||
| 	show_more() { | ||||
| 		let unread = this.unread; | ||||
| 		let news = this.shadowRoot?.getElementById('news'); | ||||
| 		if (news) { | ||||
| 			console.log('injecting messages', news.messages); | ||||
| 			news.add_messages( | ||||
| 				Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x]))) | ||||
| 			); | ||||
| 			this.dispatchEvent(new CustomEvent('refresh')); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	load_latest() { | ||||
| 		let news = this.shadowRoot?.getElementById('news'); | ||||
| 		if (news) { | ||||
| 			news.load_latest(); | ||||
| 	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) { | ||||
| @@ -88,7 +84,10 @@ class TfTabNewsElement extends LitElement { | ||||
| 		} else { | ||||
| 			delete this.drafts[id]; | ||||
| 		} | ||||
| 		this.drafts = Object.assign({}, this.drafts); | ||||
| 		/* Only trigger a re-render if we're creating a new draft or discarding an old one. */ | ||||
| 		if ((previous !== undefined) != (event.detail.draft !== undefined)) { | ||||
| 			this.drafts = Object.assign({}, this.drafts); | ||||
| 		} | ||||
| 		tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts)); | ||||
| 	} | ||||
|  | ||||
| @@ -109,371 +108,48 @@ class TfTabNewsElement extends LitElement { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	unread_status(channel) { | ||||
| 		if (channel === undefined) { | ||||
| 			if ( | ||||
| 				Object.keys(this.channels_unread).some((x) => this.unread_status(x)) | ||||
| 			) { | ||||
| 				return '✉️ '; | ||||
| 			} | ||||
| 		} else if (channel?.startsWith('🔐')) { | ||||
| 			let key = JSON.stringify(channel.substring('🔐'.length).split(',')); | ||||
| 			if (this.grouped_private_messages?.[key]) { | ||||
| 				let grouped_latest = Math.max( | ||||
| 					...this.grouped_private_messages?.[key]?.map((x) => x.rowid) | ||||
| 				); | ||||
| 				if ( | ||||
| 					this.channels_unread[channel] === undefined || | ||||
| 					grouped_latest > this.channels_unread[channel] | ||||
| 				) { | ||||
| 					return '✉️ '; | ||||
| 				} | ||||
| 			} | ||||
| 		} else if ( | ||||
| 			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(a, b) { | ||||
| 		return b[1].ts > a[1].ts ? 1 : b[1].ts < a[1].ts ? -1 : 0; | ||||
| 	} | ||||
|  | ||||
| 	suggested_follows() { | ||||
| 		/* | ||||
| 		 ** Filter out people who have used future timestamps so that they aren't | ||||
| 		 ** pinned at the top. | ||||
| 		 */ | ||||
| 		let self = this; | ||||
| 		let now = new Date().valueOf(); | ||||
| 		return Object.entries(this.users) | ||||
| 			.filter((x) => x[1].ts < now) | ||||
| 			.filter((x) => x[1].follow_depth > 1) | ||||
| 			.sort(self.compare_follows) | ||||
| 			.slice(0, 8) | ||||
| 			.map((x) => x[0]); | ||||
| 	} | ||||
|  | ||||
| 	async enable_peer_exchange() { | ||||
| 		await tfrpc.rpc.globalSettingsSet('peer_exchange', true); | ||||
| 		await this.check_peer_exchange(); | ||||
| 	} | ||||
|  | ||||
| 	is_loading() { | ||||
| 		return this.shadowRoot?.getElementById('news')?.loading; | ||||
| 	} | ||||
|  | ||||
| 	render_sidebar() { | ||||
| 		return html` | ||||
| 			<div | ||||
| 				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.is_administrator | ||||
| 					? html` | ||||
| 							<button | ||||
| 								class="w3-bar-item w3-button" | ||||
| 								@click=${() => | ||||
| 									this.dispatchEvent( | ||||
| 										new Event('refresh', {bubbles: true, composed: true}) | ||||
| 									)} | ||||
| 							> | ||||
| 								<span style="display: inline-block; width: 1.8em">↻</span> | ||||
| 								Sync now | ||||
| 							</button> | ||||
| 							<button | ||||
| 								class="w3-bar-item w3-button w3-ripple" | ||||
| 								@click=${() => | ||||
| 									this.dispatchEvent( | ||||
| 										new Event('toggle_stay_connected', { | ||||
| 											bubbles: true, | ||||
| 											composed: true, | ||||
| 										}) | ||||
| 									)} | ||||
| 							> | ||||
| 								<span style="display: inline-block; width: 1.8em" | ||||
| 									>${this.stay_connected ? '🔗' : '⛓️💥'}</span | ||||
| 								> | ||||
| 								${this.stay_connected ? 'Online mode' : 'Passive mode'} | ||||
| 							</button> | ||||
| 						` | ||||
| 					: undefined} | ||||
| 				${this.hash.startsWith('##') && | ||||
| 				this.channels.indexOf(this.hash.substring(2)) == -1 | ||||
| 					? html` | ||||
| 							<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('👍')}👍votes</a | ||||
| 				> | ||||
| 				${Object.keys(this?.grouped_private_messages ?? []) | ||||
| 					?.sort() | ||||
| 					?.map( | ||||
| 						(key) => html` | ||||
| 							<a | ||||
| 								href=${'#🔐' + JSON.parse(key).join(',')} | ||||
| 								class="w3-bar-item w3-button" | ||||
| 								style=${this.hash == '#🔐' + JSON.parse(key).join(',') | ||||
| 									? 'font-weight: bold' | ||||
| 									: undefined} | ||||
| 								>${this.unread_status('🔐' + JSON.parse(key).join(','))} | ||||
| 								${(key != '[]' ? JSON.parse(key) : [this.whoami]).map( | ||||
| 									(id) => html` | ||||
| 										<tf-user | ||||
| 											id=${id} | ||||
| 											nolink="true" | ||||
| 											.users=${this.users} | ||||
| 										></tf-user> | ||||
| 									` | ||||
| 								)}</a | ||||
| 							> | ||||
| 						` | ||||
| 					)} | ||||
| 				${Object.keys(this.drafts) | ||||
| 					.sort() | ||||
| 					.map( | ||||
| 						(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 | ||||
| 						> | ||||
| 					` | ||||
| 				)} | ||||
|  | ||||
| 				<a class="w3-bar-item w3-theme-d2 w3-button" href="#connections"> | ||||
| 					<h4 style="margin: 0">Connections</h4> | ||||
| 				</a> | ||||
| 				${this.connections?.filter((x) => x.id)?.length == 0 | ||||
| 					? html` | ||||
| 							<button | ||||
| 								class=${'w3-bar-item w3-button' + | ||||
| 								(this.connections?.some((x) => x.flags.one_shot) | ||||
| 									? ' w3-spin' | ||||
| 									: '')} | ||||
| 								@click=${() => | ||||
| 									this.dispatchEvent( | ||||
| 										new Event('refresh', {bubbles: true, composed: true}) | ||||
| 									)} | ||||
| 							> | ||||
| 								↻ Sync now | ||||
| 							</button> | ||||
| 							<button | ||||
| 								class=${'w3-bar-item w3-button' + | ||||
| 								(this.peer_exchange !== false ? ' w3-hide' : '')} | ||||
| 								@click=${this.enable_peer_exchange} | ||||
| 							> | ||||
| 								Enable peer exchange | ||||
| 							</button> | ||||
| 						` | ||||
| 					: undefined} | ||||
| 				${this.connections | ||||
| 					.filter((x) => x.id) | ||||
| 					.map( | ||||
| 						(x) => html` | ||||
| 							<tf-user | ||||
| 								class="w3-bar-item" | ||||
| 								style=${x.destroy_reason | ||||
| 									? 'border-left: 4px solid red; border-right: 4px solid red' | ||||
| 									: x.connected | ||||
| 										? x.flags?.one_shot | ||||
| 											? 'border-left: 4px solid blue; border-right: 4px solid blue' | ||||
| 											: 'border-left: 4px solid green; border-right: 4px solid green' | ||||
| 										: ''} | ||||
| 								id=${x.id} | ||||
| 								fallback_name=${x.host} | ||||
| 								.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; contain: layout" | ||||
| 				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} | ||||
| 						> | ||||
| 							${this.unread_status()}☰ | ||||
| 						</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()} | ||||
| 							.recipients=${this.hash.startsWith('#🔐') | ||||
| 								? this.hash.substring('#🔐'.length).split(',') | ||||
| 								: undefined} | ||||
| 						></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} | ||||
| 						.grouped_private_messages=${this.grouped_private_messages} | ||||
| 						.recent_reactions=${this.recent_reactions} | ||||
| 					></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-dark-grey" | ||||
| 					@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> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -110,14 +110,14 @@ class TfTabQueryElement extends LitElement { | ||||
| 				<textarea | ||||
| 					id="search" | ||||
| 					rows="8" | ||||
| 					class="w3-input w3-theme-d1" | ||||
| 					class="w3-input w3-dark-grey" | ||||
| 					style="flex: 1; resize: vertical" | ||||
| 					@keydown=${this.search_keydown} | ||||
| 				> | ||||
| ${this.query}</textarea | ||||
| 				> | ||||
| 				<button | ||||
| 					class="w3-button w3-theme-d1" | ||||
| 					class="w3-button w3-dark-grey" | ||||
| 					@click=${(event) => | ||||
| 						self.search(self.renderRoot.getElementById('search').value)} | ||||
| 				> | ||||
|   | ||||
| @@ -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; | ||||
| @@ -95,10 +78,10 @@ class TfTabSearchElement extends LitElement { | ||||
| 		let self = this; | ||||
| 		return html` | ||||
| 			<div style="display: flex; flex-direction: row; gap: 4px"> | ||||
| 				<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input> | ||||
| 				<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button> | ||||
| 				<input type="text" class="w3-input w3-dark-grey" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input> | ||||
| 				<button class="w3-button w3-dark-grey" @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 | ||||
| 		> `; | ||||
| 		>`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -6,10 +6,7 @@ class TfUserElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			id: {type: String}, | ||||
| 			fallback_name: {type: String}, | ||||
| 			icon_only: {type: Boolean}, | ||||
| 			users: {type: Object}, | ||||
| 			nolink: {type: Boolean}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| @@ -18,52 +15,32 @@ class TfUserElement extends LitElement { | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.id = null; | ||||
| 		this.fallback_name = null; | ||||
| 		this.icon_only = false; | ||||
| 		this.users = {}; | ||||
| 	} | ||||
|  | ||||
| 	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; | ||||
| 		let name_string = name ?? this.fallback_name ?? this.id; | ||||
| 		name = this.icon_only | ||||
| 			? undefined | ||||
| 			: !this.nolink | ||||
| 				? html`<a target="_top" href=${'#' + this.id}>${name_string}</a>` | ||||
| 				: html`<span>${name_string}</span>`; | ||||
| 		name = | ||||
| 			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; | ||||
| 			if (typeof image_link == 'string' && !image_link.startsWith('&')) { | ||||
| 				try { | ||||
| 					image_link = JSON.parse(image_link)?.link; | ||||
| 				} catch {} | ||||
| 			} | ||||
| 			if (image_link !== undefined) { | ||||
| 				image = html`<img | ||||
| 					class=${'w3-theme-l4 ' + shape} | ||||
| 					style="width: 2em; height: 2em; vertical-align: middle; object-fit: cover" | ||||
| 					src="/${image_link}/view" | ||||
| 					title=${name_string + ' (' + this.id + ')'} | ||||
| 				/>`; | ||||
| 			} | ||||
| 		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; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis' + | ||||
| 			(this.nolink ? '' : '; font-weight: bold')} | ||||
| 		> | ||||
| 			${image} ${name} | ||||
| 		</div>`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,13 +1,6 @@ | ||||
| import * as linkify from './commonmark-linkify.js'; | ||||
| import * as hashtagify from './commonmark-hashtag.js'; | ||||
|  | ||||
| const k_code_classes = 'w3-theme-l4 w3-theme-border w3-round'; | ||||
|  | ||||
| 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' && | ||||
| @@ -50,9 +43,9 @@ function image(node, entering) { | ||||
| 						'</div>' | ||||
| 				); | ||||
| 				if (this.options.safe && potentiallyUnsafe(node.destination)) { | ||||
| 					this.lit('<img src="" title="'); | ||||
| 					this.lit('<img src="" alt="'); | ||||
| 				} else { | ||||
| 					this.lit('<img src="' + this.esc(node.destination) + '" title="'); | ||||
| 					this.lit('<img src="' + this.esc(node.destination) + '" alt="'); | ||||
| 				} | ||||
| 			} | ||||
| 			this.disableTags += 1; | ||||
| @@ -68,32 +61,13 @@ function image(node, entering) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| function code(node) { | ||||
| 	let attrs = this.attrs(node); | ||||
| 	attrs.push(['class', k_code_classes]); | ||||
| 	this.tag('code', attrs); | ||||
| 	this.out(node.literal); | ||||
| 	this.tag('/code'); | ||||
| } | ||||
|  | ||||
| function attrs(node) { | ||||
| 	let result = commonmark.HtmlRenderer.prototype.attrs.bind(this)(node); | ||||
| 	if (node.type == 'block_quote') { | ||||
| 		result.push(['class', 'w3-theme-d1']); | ||||
| 	} else if (node.type == 'code_block') { | ||||
| 		result.push(['class', k_code_classes]); | ||||
| 	} | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| export function markdown(md) { | ||||
| 	let reader = new commonmark.Parser(); | ||||
| 	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; | ||||
| 	let parsed = reader.parse(md || ''); | ||||
| 	parsed = hashtagify.transform(parsed); | ||||
| 	parsed = linkify.transform(parsed); | ||||
| 	let walker = parsed.walker(); | ||||
| 	let event, node; | ||||
| 	while ((event = walker.next())) { | ||||
|   | ||||
| @@ -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": "&tzZFIe7Y54O4sx1QtAPdemkXh+p5qHXSG/dlS7NP6OQ=.sha256" | ||||
| } | ||||
| @@ -1,126 +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, size from messages_stats 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">Analyzing feeds...</p>'); | ||||
| 	let most_follows = get_most_follows(); | ||||
| 	let total = await get_total(); | ||||
| 	let identities = await ssb.getAllIdentities(); | ||||
| 	let following1 = await ssb.following(identities, 1); | ||||
| 	let following2 = await ssb.following(identities, 2); | ||||
| 	let biggest = await get_biggest(); | ||||
| 	most_follows = await most_follows; | ||||
| 	let names = await get_names( | ||||
| 		[].concat( | ||||
| 			biggest.map((x) => x.author), | ||||
| 			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 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 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 +0,0 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "🕸", | ||||
| 	"previous": "&n7hu5b8/TsfiG6FDlCRG5nPCrIdCr96+xpIJ/aQT/uM=.sha256" | ||||
| } | ||||
							
								
								
									
										100
									
								
								apps/web/app.js
									
									
									
									
									
								
							
							
						
						| @@ -1,100 +0,0 @@ | ||||
| let g_hash; | ||||
|  | ||||
| async function query(sql, params) { | ||||
| 	let results = []; | ||||
| 	await ssb.sqlAsync(sql, params, function (row) { | ||||
| 		results.push(row); | ||||
| 	}); | ||||
| 	return results; | ||||
| } | ||||
|  | ||||
| async function resolve(id) { | ||||
| 	try { | ||||
| 		let blob = await ssb.blobGet(id); | ||||
| 		if (blob) { | ||||
| 			let json; | ||||
| 			try { | ||||
| 				json = JSON.parse(utf8Decode(blob)); | ||||
| 			} catch { | ||||
| 				return {id: utf8Decode(blob)}; | ||||
| 			} | ||||
| 			if (json?.links) { | ||||
| 				for (let [key, value] of Object.entries(json.links)) { | ||||
| 					json.links[key] = await resolve(value); | ||||
| 				} | ||||
| 				return json; | ||||
| 			} else { | ||||
| 				return 'huh?' + json; | ||||
| 			} | ||||
| 		} else { | ||||
| 			return `missing<${id}>`; | ||||
| 		} | ||||
| 	} catch (e) { | ||||
| 		return id + ': ' + e.message; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| async function get_names(identities) { | ||||
| 	return Object.fromEntries( | ||||
| 		( | ||||
| 			await query( | ||||
| 				` | ||||
| 		SELECT author, name FROM ( | ||||
| 			SELECT | ||||
| 				messages.author, | ||||
| 				RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank, | ||||
| 				messages.content ->> 'name' AS name | ||||
| 			FROM messages | ||||
| 			JOIN json_each(?) AS identities ON identities.value = messages.author | ||||
| 			WHERE | ||||
| 				json_extract(messages.content, '$.type') = 'about' AND | ||||
| 				content ->> 'about' = messages.author AND name IS NOT NULL) | ||||
| 		WHERE author_rank = 1 | ||||
| 	`, | ||||
| 				[JSON.stringify(identities)] | ||||
| 			) | ||||
| 		).map((x) => [x.author, x.name]) | ||||
| 	); | ||||
| } | ||||
|  | ||||
| async function render(hash) { | ||||
| 	g_hash = hash; | ||||
| 	if (!hash) { | ||||
| 		let sites = await query( | ||||
| 			` | ||||
| 			SELECT site.author, site.id | ||||
| 			FROM messages site | ||||
| 			WHERE site.content ->> 'type' = 'web-init' | ||||
| 		`, | ||||
| 			[] | ||||
| 		); | ||||
| 		let names = await get_names(sites.map((x) => x.author)); | ||||
| 		if (hash === g_hash) { | ||||
| 			await app.setDocument( | ||||
| 				`<ul style="background-color: #ddd">${sites.map((x) => `<li><a target="_top" href="#${encodeURIComponent(x.id)}">${names[x.author] ?? x.author} - ${x.id}</a></li>`).join('\n')}</ul>` | ||||
| 			); | ||||
| 		} | ||||
| 	} else { | ||||
| 		let site_id = | ||||
| 			hash.charAt(0) == '#' | ||||
| 				? decodeURIComponent(hash.substring(1)) | ||||
| 				: decodeURIComponent(hash); | ||||
| 		await app.setDocument(`<html style="margin: 0; padding: 0; width: 100vw; height: 100vh; margin: 0; padding: 0"> | ||||
| 			<body style="display: flex; flex-direction: column; width: 100vw; height: 100vh"> | ||||
| 				<iframe src="${encodeURIComponent(site_id)}/index.html" style="flex: 1 1; border: 0; background-color: #fff"></iframe> | ||||
| 			</body> | ||||
| 		</html>`); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| core.register('message', async function message_handler(message) { | ||||
| 	if (message.event == 'hashChange') { | ||||
| 		await render(message.hash); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| async function main() { | ||||
| 	render(null); | ||||
| } | ||||
|  | ||||
| main(); | ||||
| @@ -1,63 +0,0 @@ | ||||
| async function query(sql, params) { | ||||
| 	let results = []; | ||||
| 	await ssb.sqlAsync(sql, params, function (row) { | ||||
| 		results.push(row); | ||||
| 	}); | ||||
| 	return results; | ||||
| } | ||||
|  | ||||
| function guess_content_type(name) { | ||||
| 	if (name.endsWith('.html')) { | ||||
| 		return 'text/html; charset=UTF-8'; | ||||
| 	} else if (name.endsWith('.js') || name.endsWith('.mjs')) { | ||||
| 		return 'text/javascript; charset=UTF-8'; | ||||
| 	} else if (name.endsWith('.css')) { | ||||
| 		return 'text/stylesheet; charset=UTF-8'; | ||||
| 	} else { | ||||
| 		return 'application/binary'; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| async function main() { | ||||
| 	let path = request.path.replaceAll(/(%[0-9a-fA-F]{2})/g, (x) => | ||||
| 		String.fromCharCode(parseInt(x.substring(1), 16)) | ||||
| 	); | ||||
| 	let match = path.match(/^(%.{44}\.sha256)(?:\/)?(.*)$/); | ||||
|  | ||||
| 	let content_type = guess_content_type(request.path); | ||||
| 	let root = await query( | ||||
| 		` | ||||
| 		SELECT root.content ->> 'root' AS root | ||||
| 		FROM messages site | ||||
| 		JOIN messages root | ||||
| 		ON site.id = ? AND root.author = site.author AND root.content ->> 'site' = site.id | ||||
| 		ORDER BY root.sequence DESC LIMIT 1 | ||||
| 	`, | ||||
| 		[match[1]] | ||||
| 	); | ||||
| 	let root_id = root[0]['root']; | ||||
| 	let last_id = root_id; | ||||
| 	let blob = await ssb.blobGet(root_id); | ||||
| 	try { | ||||
| 		for (let part of match[2]?.split('/')) { | ||||
| 			let dir = JSON.parse(utf8Decode(blob)); | ||||
| 			last_id = dir?.links[part]; | ||||
| 			blob = await ssb.blobGet(dir?.links[part]); | ||||
| 			content_type = guess_content_type(part); | ||||
| 		} | ||||
| 	} catch {} | ||||
|  | ||||
| 	respond({ | ||||
| 		status_code: 200, | ||||
| 		data: blob ? utf8Decode(blob) : `${last_id} not found`, | ||||
| 		content_type: content_type, | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| main().catch(function (e) { | ||||
| 	respond({ | ||||
| 		status_code: 200, | ||||
| 		data: `${e.message}\n${e.stack}`, | ||||
| 		content_type: 'text/plain', | ||||
| 	}); | ||||
| }); | ||||
| @@ -1,5 +1,5 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "👋", | ||||
| 	"previous": "&5NkMRSgcMqCYF3xcLOBmaytkoxfV9zx4br7JladKPTs=.sha256" | ||||
| 	"previous": "&zFISmRDAv+SXFonfZ9/sHNhrmMe+poTU22gwZzuSkT4=.sha256" | ||||
| } | ||||
|   | ||||
							
								
								
									
										5
									
								
								apps/welcome/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | ||||
| async function main() { | ||||
| 	await app.setDocument(utf8Decode(getFile('index.html'))); | ||||
| } | ||||
|  | ||||
| main(); | ||||
| @@ -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 +0,0 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640" width="32" height="32"><path d="m395.9 484.2-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12" style="fill:#fff"/><path d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6M125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1m300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1" style="fill:#609926"/><path d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8s2 16.3 9.1 20c7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3s17.4 1.7 22.5-5.3c5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8z" style="fill:#609926"/></svg> | ||||
| Before Width: | Height: | Size: 2.1 KiB | 
| @@ -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 | 
| Before Width: | Height: | Size: 86 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,38 +39,29 @@ | ||||
| 						<b>😎 Tilde Friends</b> | ||||
| 					</h1> | ||||
| 					<h1 class="w3-xxlarge w3-text-green"> | ||||
| 						<b>a Secure Scuttlebutt decentralized social network client</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: | ||||
| 						<i class="fa-brands fa-linux w3-xlarge"></i> | ||||
| 						<i class="fa-brands fa-android w3-xlarge"></i> | ||||
| 						<i class="fa-brands fa-apple w3-xlarge"></i> | ||||
| 						<i class="fa fa-mobile-screen w3-xlarge"></i> | ||||
| 						<i class="fa-brands fa-windows w3-xlarge"></i> | ||||
| 					</p> | ||||
| 					<a | ||||
| 						class="w3-button w3-blue w3-padding-large" | ||||
| 						href="https://www.tildefriends.net/~core/ssb/" | ||||
| 						>🦀 Try It</a | ||||
| 					> | ||||
| 					<a | ||||
| 						class="w3-button w3-black w3-padding-large" | ||||
| 						href="https://dev.tildefriends.net/cory/tildefriends/releases/latest" | ||||
| 						href="https://www.tildefriends.net/~cory/releases/" | ||||
| 						><i class="fa fa-download"></i> Download</a | ||||
| 					> | ||||
| 					<a | ||||
| 						class="w3-button w3-black w3-padding-large" | ||||
| 						href="https://dev.tildefriends.net/cory/tildefriends" | ||||
| 					> | ||||
| 						<img src="gitea.svg" style="height: 1em; margin: 0" /> | ||||
| 						Development | ||||
| 					</a> | ||||
| 					<a | ||||
| 						class="w3-button w3-black w3-padding-large" | ||||
| 						href="https://docs.tildefriends.net/" | ||||
| 						><i class="fa fa-book"></i> Documentation</a | ||||
| 					> | ||||
| 					<a | ||||
| 						class="w3-button w3-black w3-padding-large" | ||||
| 						href="https://www.tildefriends.net/~cory/tildeblog/" | ||||
| 						><i class="fa fa-solid fa-square-rss"></i> Blog</a | ||||
| 						href="https://www.tildefriends.net/~cory/apps/" | ||||
| 						><i class="fa fa-link"></i> Try It</a | ||||
| 					> | ||||
| 				</div> | ||||
| 				<div class="w3-col l4 m6"> | ||||
| @@ -68,167 +70,14 @@ | ||||
| 			</div> | ||||
| 		</div> | ||||
|  | ||||
| 		<!-- Getting Starting Section --> | ||||
| 		<div class="w3-indigo w3-center"> | ||||
| 			<div class="w3-row-padding w3-padding-64"> | ||||
| 				<div class="w3-jumbo"> | ||||
| 					<i class="fa fa-rocket"></i> <b>Getting Started</b> | ||||
| 				</div> | ||||
| 				<div> | ||||
| 					<h2>First-time user checklist:</h2> | ||||
| 					<ol type="1" style="text-align: left"> | ||||
| 						<li> | ||||
| 							<a | ||||
| 								href="https://dev.tildefriends.net/cory/tildefriends/releases/latest" | ||||
| 								>Download</a | ||||
| 							> | ||||
| 							Tilde Friends or use | ||||
| 							<a href="https://www.tildefriends.net/" | ||||
| 								>https://www.tildefriends.net/</a | ||||
| 							>. | ||||
| 							<div class="w3-cell-row"> | ||||
| 								<div class="w3-container w3-cell w3-mobile"> | ||||
| 									<h3>Mobile</h3> | ||||
| 									<p> | ||||
| 										<a | ||||
| 											class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top" | ||||
| 											href="https://f-droid.org/en/packages/com.unprompted.tildefriends.fdroid/" | ||||
| 											><img src="f-droid.svg" style="height: 2em; margin: 0" /> | ||||
| 											Get it on F-Droid</a | ||||
| 										> | ||||
| 										<a | ||||
| 											class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top" | ||||
| 											href="https://play.google.com/store/apps/details?id=com.unprompted.tildefriends" | ||||
| 										> | ||||
| 											<img | ||||
| 												src="googleplay.svg" | ||||
| 												style="height: 2em; margin: 0" | ||||
| 											/> | ||||
| 											Get it on Google Play (Open Testing) | ||||
| 										</a> | ||||
| 										<a | ||||
| 											class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top" | ||||
| 											href="https://testflight.apple.com/join/tXxgtSpE" | ||||
| 										> | ||||
| 											<img src="ios.svg" style="height: 2em; margin: 0" /> | ||||
| 											Get it on iOS (TestFlight) | ||||
| 										</a> | ||||
| 									</p> | ||||
| 									<p>Just launch the app.</p> | ||||
| 								</div> | ||||
| 								<div class="w3-container w3-cell w3-mobile"> | ||||
| 									<h3>Web</h3> | ||||
| 									<p> | ||||
| 										<a | ||||
| 											class="w3-button w3-round-large w3-blue w3-padding-large" | ||||
| 											href="https://www.tildefriends.net/~core/ssb/" | ||||
| 											>🦀 Try It</a | ||||
| 										> | ||||
| 									</p> | ||||
| 									<p> | ||||
| 										<a href="/login?return=/~core/intro" | ||||
| 											>Register an account with tildefriends.net</a | ||||
| 										> | ||||
| 										to take it for a spin right away. | ||||
| 									</p> | ||||
| 									<h3>PeachCloud</h3> | ||||
| 									<p> | ||||
| 										Tilde Friends is also a part of 🍑☁️<a | ||||
| 											href="https://peach-docs.commoninternet.net/" | ||||
| 											>PeachCloud</a | ||||
| 										>, which is available on | ||||
| 										<a href="https://apps.yunohost.org/app/peachpub" | ||||
| 											>YunoHost</a | ||||
| 										> | ||||
| 										for accessible self-hosting. | ||||
| 									</p> | ||||
| 								</div> | ||||
| 								<div class="w3-container w3-cell w3-mobile"> | ||||
| 									<h3>Desktop</h3> | ||||
| 									<p> | ||||
| 										<a | ||||
| 											class="w3-button w3-round-large w3-black w3-padding-large" | ||||
| 											href="https://dev.tildefriends.net/cory/tildefriends/releases" | ||||
| 											><i class="fa fa-download"></i> Download</a | ||||
| 										> | ||||
| 										<a | ||||
| 											class="w3-button w3-round-large w3-padding w3-blue-gray" | ||||
| 											href="https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage" | ||||
| 										> | ||||
| 											<img src="appimage.svg" style="height: 2em; margin: 0" /> | ||||
| 											Get Linux 64-bit AppImage | ||||
| 										</a> | ||||
| 									</p> | ||||
| 									<p> | ||||
| 										Tilde Friends is distributed as a single executable file (or | ||||
| 										source that you can | ||||
| 										<a href="http://dev.tildefriends.net">build yourself</a>) | ||||
| 										and stores all of its data in a single | ||||
| 										file(<code>db.sqlite</code>). You can generally download the | ||||
| 										latest executable from | ||||
| 										<a | ||||
| 											href="https://dev.tildefriends.net/cory/tildefriends/releases" | ||||
| 											>releases</a | ||||
| 										> | ||||
| 										for your platform, mark it as executable (<code | ||||
| 											>chmod +x tildefriends*</code | ||||
| 										> | ||||
| 										on macOS and Linux), and run. Run with <code>-h</code> to | ||||
| 										learn more. | ||||
| 									</p> | ||||
| 									<p> | ||||
| 										Tilde Friends will run in the console and provide a web | ||||
| 										interface at | ||||
| 										<a href="http://localhost:12345/">http://localhost:12345/</a | ||||
| 										>. You will have to register a username and password to sign | ||||
| 										into your instance. | ||||
| 									</p> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 							<p> | ||||
| 								After a <a href="/~core/intro">brief introduction</a>, Tilde | ||||
| 								Friends will take you to the Secure Scuttlebutt social network | ||||
| 								app. | ||||
| 							</p> | ||||
| 						</li> | ||||
| 						<li> | ||||
| 							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> | ||||
| 						</li> | ||||
| 						<li>Follow people to grow your network.</li> | ||||
| 						<li> | ||||
| 							Use the <b>edit</b> link at the top of any page to start modifying | ||||
| 							and making apps. | ||||
| 						</li> | ||||
| 					</ol> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
|  | ||||
| 		<!-- SSB Section --> | ||||
| 		<div class="w3-light-grey"> | ||||
| 			<div class="w3-row-padding w3-padding-64"> | ||||
| 				<div class="w3-col l4 m6 s4 w3-center"> | ||||
| 				<div class="w3-col l4 m6 s4"> | ||||
| 					<a href="https://scuttlebutt.nz/" | ||||
| 						><img | ||||
| 							class="w3-image" | ||||
| 							src="hermietildefriends.svg" | ||||
| 							class="w3-image w3-round-large" | ||||
| 							src="ssb.png" | ||||
| 							alt="Secure Scuttlebutt" | ||||
| 					/></a> | ||||
| 				</div> | ||||
| @@ -298,15 +147,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"> | ||||
| @@ -340,7 +185,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> | ||||
| @@ -354,7 +199,7 @@ | ||||
| 			</div> | ||||
|  | ||||
| 			<div class="w3-row" style="margin-top: 64px"> | ||||
| 				<a href="https://codemirror.net/docs/changelog/" class="w3-col s3"> | ||||
| 				<a href="https://codemirror.net/5/" class="w3-col s3"> | ||||
| 					<i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i> | ||||
| 					<p>CodeMirror</p> | ||||
| 				</a> | ||||
| @@ -366,13 +211,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,3 +0,0 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="814" height="1000"> | ||||
|   <path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/> | ||||
| </svg> | ||||
| Before Width: | Height: | Size: 660 B | 
							
								
								
									
										
											BIN
										
									
								
								apps/welcome/ssb.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 50 KiB |