Compare commits

..

10 Commits

1776 changed files with 566591 additions and 39685 deletions

View File

@ -14,7 +14,7 @@ IndentWidth: 4
MaxEmptyLinesToKeep: 1 MaxEmptyLinesToKeep: 1
ObjCBlockIndentWidth: 4 ObjCBlockIndentWidth: 4
ObjCBreakBeforeNestedBlockParam: false ObjCBreakBeforeNestedBlockParam: false
SortIncludes: true SortIncludes: false
TabWidth: 4 TabWidth: 4
UseTab: Always UseTab: Always
... ...

View File

@ -1,3 +1,4 @@
.git .svn
db.sqlite* db.sqlite
out/ out/**/*.o
out/**/*.d

View File

@ -1,71 +0,0 @@
name: Build Tilde Friends
run-name: ${{ gitea.actor }} running 🚀
on: [push]
jobs:
Build-All:
runs-on: ubuntu-latest
container:
image: node:23-bookworm-slim
valid_volumes:
- '/opt/keys'
- '/opt/deps'
volumes:
- /opt/keys:/opt/keys
- /opt/deps:/opt/deps
steps:
- name: Install build dependencies
run: >
apt update && apt install -y \
build-essential \
clang-19 \
cmake \
curl \
docker.io \
doxygen \
file \
gcc-aarch64-linux-gnu \
git \
graphviz \
libgpgme11 \
libssl-dev \
mingw-w64 \
rsync \
unzip \
zip \
zlib1g-dev
- name: Get code
uses: actions/checkout@v4
with:
submodules: true
- name: Setup environment
run: |
update-alternatives --install /usr/bin/clang clang /usr/bin/clang-19 100
update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-19 100
ln -s /opt/keys .keys
ln -sf /opt/deps/ios_toolchain deps/
ln -sf /opt/deps/macos_toolchain deps/
- name: Build documentation
run: |
mkdir -p out/html/ ~/.ssh/
make docs
echo 'pildefriends ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKD3Kde5vDO0TrMBDK0IGGeNGe/XinWAZkSQ/rXxwUjt' >> ~/.ssh/known_hosts
rsync -avP --delete -e "ssh -i /opt/keys/ssh.ed25519" out/html/ tfdocs@pildefriends:docs/html/
- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
packages: 'tools platform-tools build-tools;34.0.0 platforms;android-34 ndk;26.3.11579264'
- name: Docker build
run: DOCKER_BUILDKIT=1 docker build .
- name: Build
run: ANDROID_SDK=$HOME/.android/sdk make -j`nproc` all dist docs
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/*

12
.gitignore vendored
View File

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

31
.gitmodules vendored
View File

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

View File

@ -2,7 +2,6 @@ node_modules
src src
deps deps
.clang-format .clang-format
flake.lock
# Minified files # Minified files
**/*.min.css **/*.min.css

View File

@ -1,16 +1,19 @@
FROM bitnami/minideb:bookworm AS build FROM bitnami/minideb:bullseye AS build
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
gcc \ gcc \
libc6-dev \ libc6-dev \
perl \ libssl-dev \
make make
COPY . /app COPY . /app
RUN make -C /app -j $(nproc) release 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/out/release/tildefriends /app/out/release/tildefriends
COPY --from=build /app/apps /app/apps COPY --from=build /app/apps /app/apps

667
Doxyfile

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,70 +1,47 @@
# Tilde Friends # Tilde Friends
Tilde Friends participates in the Secure Scuttlebutt decentralized social Tilde Friends is a tool for making and sharing.
network while also functioning as a platform for making, sharing, and running
web applications.
A public instance lives at https://www.tildefriends.net/. 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 ## Goals
1. Be the fanciest, best-maintained Secure Scuttlebutt client in town. 1. Make it easy and fun to run all sorts of web applications.
1. Make it easy to make, share, and run all sorts of applications while 2. Provide security that is easy to understand and protects your data.
respecting the privacy and safety of your data. 3. Make creating and sharing web applications accessible to anyone with a
browser.
## Getting the Source
Tilde Friends uses git submodules, so either:
```
git clone --recurse-submodules https://dev.tildefriends.net/cory/tildefriends.git
```
or:
```
git clone https://dev.tildefriends.net/cory/tildefriends.git
cd tildefriends
git submodule update --init --recursive
```
The `.tar.xz` source releases are all-inclusive.
## Building ## Building
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. It's possible Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
to build for Android, iOS, and Windows on Linux, if you have the right all of those host platforms plus mingw64, iOS, and android.
dependencies in the right places.
### Requirements 1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
are kept up to date in the tree.
System OpenSSL libraries are assumed to be available on Haiku and OpenBSD. 2. To build, run `make debug` or `make release`. An executable will be
generated in a subdirectory of `out/`.
On MacOS, Xcode's command-line tools are expected to be available. 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
### Build Commands iosdebug-ipa iosrelease-ipa release-apk`.
4. To build in docker, `docker build .`.
Run `make` with no arguments to see available build targets and options. `make 5. `make format` will normalize formatting to the coding standard.
debug` is a good place to start.
To build in docker, `docker build .`.
`make format` and `make prettier` will normalize formatting to the coding
standard.
## Running ## Running
By default, running the built `out/debug/tildefriends` executable will start a By default, running the built `tildefriends` executable will start a web server
web server at <http://localhost:12345/>. `tildefriends -h` lists further at <http://localhost:12345/>. `tildefriends -h` lists further options.
options.
The first user to create an account and log in will be granted administrative The first user to create an account and log in will be granted administrative
privileges. Further administration can be done in the `admin` app at privileges. Further administration can be done at
<http://localhost:12345/~core/admin/>. <http://localhost:12345/~core/admin/>.
## Documentation ## 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 ## License

View File

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

View File

@ -4,38 +4,9 @@
<script> <script>
const g_data = $data; const g_data = $data;
</script> </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> </head>
<body class="w3-theme-l4"> <body style="color: #fff; width: 100%">
<header class="w3-row w3-padding w3-header w3-theme-l1"> <h1>Tilde Friends Administration</h1>
<h1>Tilde Friends Administration</h1>
</header>
</body> </body>
<script type="module" src="script.js"></script> <script type="module" src="script.js"></script>
</html> </html>

View File

@ -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 () { window.addEventListener('load', function () {
const permission_template = (permission) => html` <code>${permission}</code>`; const permission_template = (permission) => html` <code>${permission}</code>`;
function input_template(key, description) { function input_template(key, description) {
if (description.type === 'boolean') { if (description.type === 'boolean') {
return html` return html`
<li class="w3-row"> <div style="margin-top: 1em">
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label> <label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
<div class="w3-quarter w3-padding">${description.description}</div> <div>
<div class="w3-quarter w3-padding w3-center"><input class="w3-check" type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input></div> <input type="checkbox" ?checked=${description.value} id=${'gs_' + key}></input>
<button class="w3-quarter w3-button w3-theme-action" @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstChild.checked)}>Set</button> <button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.checked)}>Set</button>
</li> <div>${description.description}</div>
</div>
</div>
`; `;
} else if (description.type === 'textarea') { } else if (description.type === 'textarea') {
return html` return html`
<li class="w3-row"> <div style="margin-top: 1em"">
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold" <label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
>${title_case(key)}</label <div style="width: 100%; padding: 0; margin: 0">
> <div style="width: 90%; padding: 0 margin: 0">
<div class="w3-rest w3-padding">${description.description}</div> <textarea style="vertical-align: top; width: 100%" rows=20 cols=80 id=${'gs_' + key}>${description.value}</textarea>
<textarea </div>
class="w3-input" <button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.firstElementChild.value)}>Set</button>
style="vertical-align: top; resize: vertical" <div>${description.description}</div>
id=${'gs_' + key} </div>
> </div>
${description.value}</textarea
>
<button
class="w3-button w3-right w3-quarter w3-theme-action"
@click=${(e) =>
global_settings_set(
key,
e.srcElement.previousElementSibling.value
)}
>
Set
</button>
</li>
`; `;
} else if (description.type != 'hidden') { } else {
return html` return html`
<li class="w3-row"> <div style="margin-top: 1em">
<label class="w3-quarter" for=${'gs_' + key} style="font-weight: bold">${title_case(key)}</label> <label for=${'gs_' + key} style="font-weight: bold">${key}: </label>
<div class="w3-quarter w3-padding">${description.description}</div> <div>
<input class="w3-input w3-quarter" type="text" value="${description.value}" id=${'gs_' + key}></input> <input 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> <button @click=${(e) => global_settings_set(key, e.srcElement.previousElementSibling.value)}>Set</button>
</li> <div>${description.description}</div>
</div>
</div>
`; `;
} }
} }
const user_template = (user, permissions) => html` const user_template = (user, permissions) => html`
<li class="w3-card w3-margin"> <li>
<button <button @click=${(e) => delete_user(user)}>Delete</button>
class="w3-button w3-theme-action"
@click=${(e) => delete_user(user)}
>
Delete
</button>
${user}: ${permissions.map((x) => permission_template(x))} ${user}: ${permissions.map((x) => permission_template(x))}
</li> </li>
`; `;
const users_template = (users) => const users_template = (users) =>
html` <header class="w3-container w3-theme-l2"><h2>Users</h2></header> html`<h2>Users</h2>
<ul class="w3-ul"> <ul>
${Object.entries(users).map((u) => user_template(u[0], u[1]))} ${Object.entries(users).map((u) => user_template(u[0], u[1]))}
</ul>`; </ul>`;
const page_template = (data) => const page_template = (data) =>
html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%"> html`<div style="padding: 0; margin: 0; width: 100%; max-width: 100%">
<header class="w3-container w3-theme-l2"><h2>Global Settings</h2></header> <h2>Global Settings</h2>
<div class="w3-container"> <div>
<ul class="w3-ul"> ${Object.keys(data.settings)
${Object.keys(data.settings) .sort()
.sort() .map((x) => html`${input_template(x, data.settings[x])}`)}
.map((x) => html`${input_template(x, data.settings[x])}`)}
</ul>
</div> </div>
${users_template(data.users)} ${users_template(data.users)}
</div> `; </div> `;

View File

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

View File

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

View File

@ -21,7 +21,7 @@ function* treeify(prefix, o) {
function markdown(md) { function markdown(md) {
let parsed = new commonmark.Parser().parse(md ?? '*undocumented*'); 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) { function document(api) {

View File

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

View File

@ -19,6 +19,10 @@ async function fetch_info(apps) {
return result; return result;
} }
/**
*
*
*/
async function fetch_shared_apps() { async function fetch_shared_apps() {
let messages = {}; let messages = {};
@ -65,17 +69,17 @@ async function main() {
const stylesheet = ` const stylesheet = `
body { body {
color: whitesmoke; color: whitesmoke;
margin: 8px; font-family: sans-serif;
margin: 16px;
} }
.container {
.iconbox {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); grid-template-columns: repeat(auto-fill, 64px);
} gap: 1em;
justify-content: space-around;
.iconbox::after { background-color: #ffffff10;
content: ""; border: 2px solid #073642;
flex: auto; border-radius: 8px;
} }
.app { .app {
@ -97,28 +101,16 @@ async function main() {
`; `;
const body = ` 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"> <h2>your apps</h2>
<header class="w3-container w3-light-blue"> <div id="apps" class="container"></div>
<h2>Your Apps</h2>
</header>
<div id="apps" class="w3-indigo iconbox"></div>
</div>
<div class="w3-card-4 w3-margin-top"> <h2>shared apps</h2>
<header class="w3-container w3-light-blue"> <div id="shared_apps" class="container"></div>
<h2>Shared Apps</h2>
</header>
<div id="shared_apps" class="w3-indigo iconbox"></div>
</div>
<div class="w3-card-4 w3-margin-top"> <h2>core apps</h2>
<header class="w3-container w3-light-blue"> <div id="core_apps" class="container"></div>
<h2>Core Apps</h2>
</header>
<div id="core_apps" class="w3-indigo iconbox"></div>
</div>
`; `;
const script = ` const script = `
@ -134,13 +126,9 @@ async function main() {
// For each app in the provided list // For each app in the provided list
for (let app of Object.keys(apps).sort()) { for (let app of Object.keys(apps).sort()) {
// Create the item // Create the item
let inline = document.createElement('div'); let div = list.appendChild(document.createElement('div'));
inline.style.display = 'inline-block';
inline.classList.add('w3-button');
list.appendChild(inline);
let div = document.createElement('div');
inline.appendChild(div);
div.classList.add('app'); div.classList.add('app');
// The app's icon // The app's icon
@ -173,13 +161,12 @@ async function main() {
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link type="text/css" rel="stylesheet" href="w3.css"></link>
<style> <style>
${stylesheet} ${stylesheet}
</style> </style>
</head> </head>
<body class="w3-darkgray"> <body>
${body} ${body}
</body> </body>

View File

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

View File

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

View File

@ -52,8 +52,8 @@ export async function get_blog_message(id) {
} }
export function markdown(md) { export function markdown(md) {
let reader = new commonmark.Parser(); let reader = new commonmark.Parser({safe: true});
let writer = new commonmark.HtmlRenderer({safe: true}); let writer = new commonmark.HtmlRenderer();
let parsed = reader.parse(md || ''); let parsed = reader.parse(md || '');
let walker = parsed.walker(); let walker = parsed.walker();
let event, node; let event, node;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,4 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "💽", "emoji": "💽"
"previous": "&uQzkIe/Aj8yNhLKe3hEq+5fEJsGwIUx8NVBjJKwoV2U=.sha256"
} }

View File

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

View File

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

View File

@ -14,7 +14,7 @@ async function contacts_internal(id, last_row_id, following, max_row_id) {
result.blocking = result.blocking || {}; result.blocking = result.blocking || {};
let contacts = await query( let contacts = await query(
` `
SELECT json(content) AS content FROM messages SELECT content FROM messages
WHERE author = ? AND WHERE author = ? AND
rowid > ? AND rowid > ? AND
rowid <= ? AND rowid <= ? AND
@ -189,6 +189,50 @@ async function fetch_about(db, ids, users) {
return Object.assign({}, 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) { async function getSize(db, id) {
let size = 0; let size = 0;
await ssb.sqlAsync( await ssb.sqlAsync(

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🦟", "emoji": "🦟",
"previous": "&O0huuEgL/UQC9bkUfhPOyZFo9eRiz+koOkba6QwCGNA=.sha256" "previous": "&TegdzvFE+im94shygaHkgDYSaSrwY2h0OKUXSRPBQDM=.sha256"
} }

View File

@ -67,6 +67,9 @@ tfrpc.register(function getHash(id, message) {
tfrpc.register(function setHash(hash) { tfrpc.register(function setHash(hash) {
return app.setHash(hash); return app.setHash(hash);
}); });
ssb.addEventListener('message', async function (id) {
await tfrpc.rpc.notifyNewMessage(id);
});
tfrpc.register(async function store_blob(blob) { tfrpc.register(async function store_blob(blob) {
if (Array.isArray(blob)) { if (Array.isArray(blob)) {
blob = Uint8Array.from(blob); blob = Uint8Array.from(blob);
@ -82,18 +85,13 @@ tfrpc.register(async function store_message(message) {
tfrpc.register(function apps() { tfrpc.register(function apps() {
return core.apps(); return core.apps();
}); });
tfrpc.register(function getActiveIdentity() {
return ssb.getActiveIdentity();
});
tfrpc.register(async function try_decrypt(id, content) { tfrpc.register(async function try_decrypt(id, content) {
return await ssb.privateMessageDecrypt(id, content); return await ssb.privateMessageDecrypt(id, content);
}); });
core.register('onMessage', async function (id) { ssb.addEventListener('broadcasts', async function () {
await tfrpc.rpc.notifyNewMessage(id);
});
core.register('onBroadcastsChanged', async function () {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts()); await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
}); });
core.register('onConnectionsChanged', async function () { core.register('onConnectionsChanged', async function () {
await tfrpc.rpc.set('connections', await ssb.connections()); await tfrpc.rpc.set('connections', await ssb.connections());
}); });

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,48 @@ import * as tfutils from './tf-utils.js';
const k_project = '%Hr+4xEVtjplidSKBlRWi4Aw/0Tfw7B+1OR9BzlDKmOI=.sha256'; 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 { class TfComposeElement extends LitElement {
static get properties() { static get properties() {
return { return {
@ -63,10 +105,10 @@ class TfIssuesAppElement extends LitElement {
let issues = {}; let issues = {};
let messages = await tfrpc.rpc.query( 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 messages.id = messages_refs.message
WHERE messages_refs.ref = ? AND json_extract(messages.content, '$.type') = 'issue'), 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 issues.id = messages_refs.ref JOIN messages ON
messages.id = messages_refs.message messages.id = messages_refs.message
WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post')) WHERE json_extract(messages.content, '$.type') IN ('issue-edit', 'post'))
@ -164,7 +206,7 @@ class TfIssuesAppElement extends LitElement {
if ( if (
confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`) 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, { await tfrpc.rpc.appendMessage(whoami, {
type: 'issue-edit', type: 'issue-edit',
issues: [ issues: [
@ -179,7 +221,7 @@ class TfIssuesAppElement extends LitElement {
} }
async create_issue(event) { async create_issue(event) {
let whoami = await tfrpc.rpc.getActiveIdentity(); let whoami = this.shadowRoot.getElementById('picker').selected;
await tfrpc.rpc.appendMessage(whoami, { await tfrpc.rpc.appendMessage(whoami, {
type: 'issue', type: 'issue',
project: k_project, project: k_project,
@ -189,7 +231,7 @@ class TfIssuesAppElement extends LitElement {
} }
async reply_to_issue(event) { async reply_to_issue(event) {
let whoami = await tfrpc.rpc.getActiveIdentity(); let whoami = this.shadowRoot.getElementById('picker').selected;
await tfrpc.rpc.appendMessage(whoami, { await tfrpc.rpc.appendMessage(whoami, {
type: 'post', type: 'post',
text: event.detail.value, text: event.detail.value,
@ -207,7 +249,10 @@ class TfIssuesAppElement extends LitElement {
} }
render() { 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) { if (this.selected) {
return html` return html`
${header} ${header}

View File

@ -1,11 +1,5 @@
import * as linkify from './commonmark-linkify.js'; 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) { function image(node, entering) {
if ( if (
node.firstChild?.type === 'text' && node.firstChild?.type === 'text' &&
@ -67,8 +61,8 @@ function image(node, entering) {
} }
export function markdown(md) { export function markdown(md) {
var reader = new commonmark.Parser(); var reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer({safe: true}); var writer = new commonmark.HtmlRenderer();
writer.image = image; writer.image = image;
var parsed = reader.parse(md || ''); var parsed = reader.parse(md || '');
parsed = linkify.transform(parsed); parsed = linkify.transform(parsed);

View File

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

View File

@ -55,7 +55,7 @@ function new_message() {
return g_new_message_promise; return g_new_message_promise;
} }
core.register('onMessage', function (id) { ssb.addEventListener('message', function (id) {
let resolve = g_new_message_resolve; let resolve = g_new_message_resolve;
g_new_message_promise = new Promise(function (resolve, reject) { g_new_message_promise = new Promise(function (resolve, reject) {
g_new_message_resolve = resolve; g_new_message_resolve = resolve;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -18,8 +18,8 @@ class TfJournalEntryElement extends LitElement {
} }
markdown(md) { markdown(md) {
var reader = new commonmark.Parser(); var reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer({safe: true}); var writer = new commonmark.HtmlRenderer();
var parsed = reader.parse(md || ''); var parsed = reader.parse(md || '');
return writer.render(parsed); return writer.render(parsed);
} }

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🦀", "emoji": "🐌",
"previous": "&jko2iokTaY2t/pD9m4ekMr0wsLjov3LVl9ShysXEjkE=.sha256" "previous": "&Xs1X5TzLCk6KVr+5IDc80JAHYxJyoD10cXKBUYpFqWQ=.sha256"
} }

View File

@ -21,6 +21,9 @@ tfrpc.register(async function createIdentity() {
tfrpc.register(async function getServerIdentity() { tfrpc.register(async function getServerIdentity() {
return ssb.getServerIdentity(); return ssb.getServerIdentity();
}); });
tfrpc.register(async function setServerFollowingMe(id, following) {
return ssb.setServerFollowingMe(id, following);
});
tfrpc.register(async function getIdentities() { tfrpc.register(async function getIdentities() {
return ssb.getIdentities(); return ssb.getIdentities();
}); });
@ -73,7 +76,7 @@ tfrpc.register(function getHash(id, message) {
tfrpc.register(function setHash(hash) { tfrpc.register(function setHash(hash) {
return app.setHash(hash); return app.setHash(hash);
}); });
core.register('onMessage', async function (id) { ssb.addEventListener('message', async function (id) {
await tfrpc.rpc.notifyNewMessage(id); await tfrpc.rpc.notifyNewMessage(id);
}); });
tfrpc.register(async function store_blob(blob) { tfrpc.register(async function store_blob(blob) {
@ -97,26 +100,13 @@ tfrpc.register(async function try_decrypt(id, content) {
tfrpc.register(async function encrypt(id, recipients, content) { tfrpc.register(async function encrypt(id, recipients, content) {
return await ssb.privateMessageEncrypt(id, recipients, content); return await ssb.privateMessageEncrypt(id, recipients, content);
}); });
tfrpc.register(async function getActiveIdentity() { ssb.addEventListener('broadcasts', async function () {
return await ssb.getActiveIdentity();
});
tfrpc.register(async function sync() {
return await ssb.sync();
});
tfrpc.register(async function url() {
return core.url;
});
core.register('onBroadcastsChanged', async function () {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts()); await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
}); });
core.register('onConnectionsChanged', async function () { core.register('onConnectionsChanged', async function () {
await tfrpc.rpc.set('connections', await ssb.connections()); await tfrpc.rpc.set('connections', await ssb.connections());
}); });
core.register('setActiveIdentity', async function (id) {
await tfrpc.rpc.set('identity', id);
});
async function main() { async function main() {
if (typeof database !== 'undefined') { if (typeof database !== 'undefined') {

View File

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

View 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;
}

File diff suppressed because one or more lines are too long

View File

@ -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; let g_emojis;
function get_emojis() { function get_emojis() {
@ -14,173 +10,105 @@ function get_emojis() {
}); });
} }
async function get_recent(author) { export function picker(callback, anchor) {
let recent = await tfrpc.rpc.query( get_emojis().then(function (json) {
` let div = document.createElement('div');
SELECT DISTINCT content ->> '$.vote.expression' AS value div.id = 'emoji_picker';
FROM messages div.style.color = '#000';
WHERE author = ? AND div.style.background = '#fff';
content ->> '$.type' = 'vote' div.style.border = '1px solid #000';
ORDER BY timestamp DESC LIMIT 10 div.style.display = 'block';
`, div.style.position = 'absolute';
[author] div.style.minWidth = 'min(16em, 90vw)';
); div.style.width = 'min(16em, 90vw)';
return recent.map((x) => x.value); div.style.maxWidth = 'min(16em, 90vw)';
} div.style.maxHeight = '16em';
div.style.overflow = 'scroll';
div.style.fontWeight = 'bold';
div.style.fontSize = 'xx-large';
let input = document.createElement('input');
input.type = 'text';
input.style.display = 'block';
input.style.boxSizing = 'border-box';
input.style.width = '100%';
input.style.margin = '0';
input.style.position = 'relative';
div.appendChild(input);
let list = document.createElement('div');
div.appendChild(list);
div.addEventListener('mousedown', function (event) {
event.stopPropagation();
});
export async function picker(callback, anchor, author) { function cleanup() {
let json = await get_emojis(); console.log('emoji cleanup');
let recent = await get_recent(author); div.parentElement.removeChild(div);
window.removeEventListener('keydown', key_down);
console.log('removing click');
document.body.removeEventListener('mousedown', cleanup);
}
let div = document.createElement('div'); function key_down(event) {
div.id = 'emoji_picker'; if (event.key == 'Escape') {
div.style.color = '#000'; cleanup();
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 key_down(event) { function chosen(event) {
if (event.key == 'Escape') { console.log(event.srcElement.innerText);
callback(event.srcElement.innerText);
cleanup(); cleanup();
} }
}
function chosen(event) { function refresh() {
console.log(event.srcElement.innerText); while (list.firstChild) {
callback(event.srcElement.innerText); list.removeChild(list.firstChild);
cleanup(); }
} let search = input.value.toLowerCase();
let any_at_all = false;
function refresh() { for (let row of Object.entries(json)) {
while (list.firstChild) { let header = document.createElement('div');
list.removeChild(list.firstChild); header.appendChild(document.createTextNode(row[0]));
} list.appendChild(header);
let search = input.value.toLowerCase(); let any = false;
let any_at_all = false; for (let entry of Object.entries(row[1])) {
if (recent) { if (
let emoji_to_name = {}; search &&
for (let row of Object.values(json)) { search.length &&
for (let entry of Object.entries(row)) { entry[0].toLowerCase().indexOf(search) == -1
emoji_to_name[entry[1]] = entry[0]; ) {
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'); if (!any_at_all) {
header.appendChild(document.createTextNode('Recent')); list.appendChild(document.createTextNode('No matches found.'));
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);
} }
} }
for (let row of Object.entries(json)) { refresh();
let header = document.createElement('div'); input.oninput = refresh;
header.appendChild(document.createTextNode(row[0])); document.body.appendChild(div);
list.appendChild(header); div.style.position = 'fixed';
let any = false; div.style.top = '50%';
for (let entry of Object.entries(row[1])) { div.style.left = '50%';
if ( div.style.transform = 'translate(-50%, -50%)';
search && input.focus();
search.length && console.log('adding click');
entry[0].toLowerCase().indexOf(search) == -1 document.body.addEventListener('mousedown', cleanup);
) { window.addEventListener('keydown', key_down);
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}
>&times;</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);
} }

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html> <html style="color: #fff">
<head> <head>
<title>Tilde Friends</title> <title>Tilde Friends</title>
<base target="_top" /> <base target="_top" />
@ -10,14 +10,14 @@
} }
</style> </style>
</head> </head>
<body style="margin: 0; padding: 0"> <body style="background-color: #223a5e">
<tf-app></tf-app> <tf-app class="w3-deep-purple" />
<tf-reactions-modal id="reactions_modal"></tf-reactions-modal>
<script> <script>
window.litDisableBundleWarning = true; window.litDisableBundleWarning = true;
</script> </script>
<script src="filesaver.min.js"></script> <script src="filesaver.min.js"></script>
<script src="commonmark.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="commonmark-hashtag.js" type="module"></script>
<script src="script.js" type="module"></script> <script src="script.js" type="module"></script>
</body> </body>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,23 +1,17 @@
import {LitElement, html} from './lit-all.min.js'; import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import * as tf_id_picker from './tf-id-picker.js';
import * as tf_app from './tf-app.js'; import * as tf_app from './tf-app.js';
import * as tf_message from './tf-message.js'; import * as tf_message from './tf-message.js';
import * as tf_user from './tf-user.js'; import * as tf_user from './tf-user.js';
import * as tf_compose from './tf-compose.js'; import * as tf_compose from './tf-compose.js';
import * as tf_news from './tf-news.js'; import * as tf_news from './tf-news.js';
import * as tf_profile from './tf-profile.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 from './tf-tab-news.js';
import * as tf_tab_news_feed from './tf-tab-news-feed.js'; import * as tf_tab_news_feed from './tf-tab-news-feed.js';
import * as tf_tab_search from './tf-tab-search.js'; import * as tf_tab_search from './tf-tab-search.js';
import * as tf_tab_connections from './tf-tab-connections.js'; import * as tf_tab_connections from './tf-tab-connections.js';
import * as tf_tab_query from './tf-tab-query.js'; import * as tf_tab_query from './tf-tab-query.js';
import * as tf_tag from './tf-tag.js'; import * as tf_tag from './tf-tag.js';
import * as tf_styles from './tf-styles.js';
window.addEventListener('load', function () {
let style = document.createElement('style');
style.innerText = tf_styles.styles;
document.body.appendChild(style);
});

View File

@ -7,6 +7,7 @@ class TfElement extends LitElement {
return { return {
whoami: {type: String}, whoami: {type: String},
hash: {type: String}, hash: {type: String},
unread: {type: Array},
tab: {type: String}, tab: {type: String},
broadcasts: {type: Array}, broadcasts: {type: Array},
connections: {type: Array}, connections: {type: Array},
@ -15,12 +16,7 @@ class TfElement extends LitElement {
following: {type: Array}, following: {type: Array},
users: {type: Object}, users: {type: Object},
ids: {type: Array}, ids: {type: Array},
channels: {type: Array}, tags: {type: Array},
channels_unread: {type: Object},
channels_latest: {type: Object},
guest: {type: Boolean},
url: {type: String},
private_messages: {type: Array},
}; };
} }
@ -30,17 +26,14 @@ class TfElement extends LitElement {
super(); super();
let self = this; let self = this;
this.hash = '#'; this.hash = '#';
this.unread = [];
this.tab = 'news'; this.tab = 'news';
this.broadcasts = []; this.broadcasts = [];
this.connections = []; this.connections = [];
this.following = []; this.following = [];
this.users = {}; this.users = {};
this.loaded = false; this.loaded = false;
this.channels = []; this.tags = [];
this.channels_unread = {};
this.channels_latest = {};
this.loading_latest = 0;
this.loading_latest_scheduled = 0;
tfrpc.rpc.getBroadcasts().then((b) => { tfrpc.rpc.getBroadcasts().then((b) => {
self.broadcasts = b || []; self.broadcasts = b || [];
}); });
@ -59,88 +52,26 @@ class TfElement extends LitElement {
self.broadcasts = value; self.broadcasts = value;
} else if (name === 'connections') { } else if (name === 'connections') {
self.connections = value; self.connections = value;
} else if (name === 'identity') {
self.whoami = value;
} }
}); });
this.initial_load(); this.initial_load();
} }
async initial_load() { async initial_load() {
let whoami = await tfrpc.rpc.getActiveIdentity(); let whoami = await tfrpc.rpc.localStorageGet('whoami');
let ids = (await tfrpc.rpc.getIdentities()) || []; let ids = (await tfrpc.rpc.getIdentities()) || [];
this.url = await tfrpc.rpc.url();
this.whoami = whoami ?? (ids.length ? ids[0] : undefined); this.whoami = whoami ?? (ids.length ? ids[0] : undefined);
this.guest = !this.whoami?.length;
this.ids = ids; this.ids = ids;
await this.load_channels();
}
async load_channels() {
let channels = await tfrpc.rpc.query(
`
SELECT
content ->> 'channel' AS channel,
content ->> 'subscribed' AS subscribed
FROM
messages
WHERE
author = ? AND
content ->> 'type' = 'channel'
ORDER BY sequence
`,
[this.whoami]
);
let channel_map = {};
for (let row of channels) {
if (row.subscribed) {
channel_map[row.channel] = true;
} else {
delete channel_map[row.channel];
}
}
this.channels = Object.keys(channel_map).sort();
}
connectedCallback() {
super.connectedCallback();
this._keydown = this.keydown.bind(this);
window.addEventListener('keydown', this._keydown);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('keydown', this._keydown);
}
keydown(event) {
if (event.altKey && event.key == 'ArrowUp') {
this.next_channel(-1);
event.preventDefault();
} else if (event.altKey && event.key == 'ArrowDown') {
this.next_channel(1);
event.preventDefault();
}
}
next_channel(delta) {
let channel_names = ['', '@', '🔐', ...this.channels.map((x) => '#' + x)];
let index = channel_names.indexOf(this.hash.substring(1));
index = index != -1 ? index + delta : 0;
tfrpc.rpc.setHash(
'#' +
encodeURIComponent(
channel_names[(index + channel_names.length) % channel_names.length]
)
);
} }
set_hash(hash) { set_hash(hash) {
this.hash = decodeURIComponent(hash || '#'); this.hash = hash || '#';
if (this.hash.startsWith('#q=')) { if (this.hash.startsWith('#q=')) {
this.tab = 'search'; this.tab = 'search';
} else if (this.hash === '#connections') { } else if (this.hash === '#connections') {
this.tab = 'connections'; this.tab = 'connections';
} else if (this.hash === '#mentions') {
this.tab = 'mentions';
} else if (this.hash.startsWith('#sql=')) { } else if (this.hash.startsWith('#sql=')) {
this.tab = 'query'; this.tab = 'query';
} else { } else {
@ -148,11 +79,9 @@ class TfElement extends LitElement {
} }
} }
async fetch_about(following, users) { async fetch_about(ids, users) {
let ids = Object.keys(following).sort();
const k_cache_version = 1; const k_cache_version = 1;
let cache = await tfrpc.rpc.databaseGet('about'); let cache = await tfrpc.rpc.databaseGet('about');
let original_cache = cache;
cache = cache ? JSON.parse(cache) : {}; cache = cache ? JSON.parse(cache) : {};
if (cache.version !== k_cache_version) { if (cache.version !== k_cache_version) {
cache = { cache = {
@ -164,8 +93,8 @@ class TfElement extends LitElement {
let max_row_id = ( let max_row_id = (
await tfrpc.rpc.query( await tfrpc.rpc.query(
` `
SELECT MAX(rowid) AS max_row_id FROM messages SELECT MAX(rowid) AS max_row_id FROM messages
`, `,
[] []
) )
)[0].max_row_id; )[0].max_row_id;
@ -175,98 +104,53 @@ class TfElement extends LitElement {
} }
} }
const k_chunk_size = 1024; let abouts = await tfrpc.rpc.query(
let min_row_id = 0; `
console.log( SELECT
'loading about for', messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
ids.length, FROM
'accounts', messages,
cache.last_row_id, json_each(?1) AS following
'=>', WHERE
max_row_id 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,
]
); );
try { for (let about of abouts) {
while (true) { let content = JSON.parse(about.content);
let abouts = await tfrpc.rpc.query( if (content.about === about.author) {
` delete content.type;
SELECT * FROM ( delete content.about;
SELECT cache.about[about.author] = Object.assign(
messages.rowid AS rowid, messages.author, json(messages.content) AS content, messages.sequence cache.about[about.author] || {},
FROM content
messages,
json_each(?1) AS following
WHERE
messages.author = following.value AND
messages.content ->> 'type' = 'about' AND
messages.rowid > ?3 AND
messages.rowid <= ?4
UNION
SELECT
messages.rowid AS rowid, messages.author, json(messages.content) AS content, messages.sequence
FROM
messages,
json_each(?2) AS following
WHERE
messages.author = following.value AND
messages.content ->> 'type' = 'about' AND
messages.rowid > ?6 AND
messages.rowid <= ?4
)
ORDER BY rowid LIMIT ?5
`,
[
JSON.stringify(ids.filter((id) => cache.about[id])),
JSON.stringify(ids.filter((id) => !cache.about[id])),
cache.last_row_id,
max_row_id,
k_chunk_size,
min_row_id,
]
); );
let max_seen;
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
);
}
max_seen = about.rowid;
}
console.log(
'cache =',
cache.last_row_id,
'seen =',
max_seen,
'max =',
max_row_id
);
cache.last_row_id = Math.max(cache.last_row_id, max_seen ?? max_row_id);
min_row_id = Math.max(min_row_id, max_seen ?? max_row_id);
let new_cache = JSON.stringify(cache);
if (new_cache !== original_cache) {
let start_time = new Date();
tfrpc.rpc.databaseSet('about', new_cache).then(function () {
console.log('saving about took', (new Date() - start_time) / 1000);
});
}
users = users || {};
for (let id of Object.keys(cache.about)) {
users[id] = Object.assign(
{follow_depth: following[id]?.d},
users[id] || {},
cache.about[id]
);
}
if (cache.last_row_id >= max_row_id) {
break;
}
} }
} catch (e) { }
console.log(e); 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]);
} }
return Object.assign({}, users); return Object.assign({}, users);
} }
@ -281,15 +165,10 @@ class TfElement extends LitElement {
`, `,
[JSON.stringify(this.following), id] [JSON.stringify(this.following), id]
); );
for (let message of messages) { if (messages && messages.length) {
if ( this.unread = [...this.unread, ...messages];
message.author == this.whoami && this.unread = this.unread.slice(this.unread.length - 1024);
JSON.parse(message.content)?.type == 'channel'
) {
this.load_channels();
}
} }
this.schedule_load_latest();
} }
async _handle_whoami_changed(event) { async _handle_whoami_changed(event) {
@ -304,233 +183,85 @@ class TfElement extends LitElement {
} }
} }
async get_latest_private(following) { async create_identity() {
const k_version = 1; if (confirm('Are you sure you want to create a new identity?')) {
// { "version": 1, "range": [1234, 5678], messages: [ "%1.sha256", "%2.sha256", ... ], latest: rowid } await tfrpc.rpc.createIdentity();
let cache = JSON.parse( this.ids = (await tfrpc.rpc.getIdentities()) || [];
(await tfrpc.rpc.databaseGet(`private:${this.whoami}`)) ?? '{}' if (this.ids && !this.whoami) {
); this.whoami = this.ids[0];
if (cache.version !== k_version) {
cache = {
version: k_version,
messages: [],
range: [],
};
}
let latest = (
await tfrpc.rpc.query('SELECT MAX(rowid) AS latest FROM messages')
)[0].latest;
let ranges = [];
const k_chunk_size = 512;
if (cache.range.length) {
for (let i = cache.range[1]; i < latest; i += k_chunk_size) {
ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
}
for (let i = cache.range[0]; i >= 0; i -= k_chunk_size) {
ranges.push([
Math.max(i - k_chunk_size, 0),
Math.min(cache.range[0], i + k_chunk_size),
false,
]);
}
} else {
for (let i = 0; i < latest; i += k_chunk_size) {
ranges.push([i, Math.min(i + k_chunk_size, latest), true]);
} }
} }
for (let range of ranges) {
let messages = await tfrpc.rpc.query(
`
SELECT messages.rowid, messages.id, json(content) AS content
FROM messages
WHERE
messages.rowid > ?1 AND
messages.rowid <= ?2 AND
json(messages.content) LIKE '"%'
ORDER BY sequence DESC
`,
[range[0], range[1]]
);
messages = (await this.decrypt(messages)).filter((x) => x.decrypted);
if (messages.length) {
cache.latest = Math.max(
cache.latest ?? 0,
...messages.map((x) => x.rowid)
);
if (range[2]) {
cache.messages = [...cache.messages, ...messages.map((x) => x.id)];
} else {
cache.messages = [...messages.map((x) => x.id), ...cache.messages];
}
}
cache.range[0] = Math.min(cache.range[0] ?? range[0], range[0]);
cache.range[1] = Math.max(cache.range[1] ?? range[1], range[1]);
await tfrpc.rpc.databaseSet(
`private:${this.whoami}`,
JSON.stringify(cache)
);
}
return [cache.latest, cache.messages];
} }
async load_channels_latest(following) { render_id_picker() {
let start_time = new Date(); return html`
let latest_private = this.get_latest_private(following); <div style="display: flex; gap: 8px">
let channels = await tfrpc.rpc.query( <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();
this.tags = await tfrpc.rpc.query(
` `
SELECT channels.value AS channel, MAX(messages.rowid) AS rowid FROM messages WITH
JOIN json_each(?1) AS channels ON messages.content ->> 'channel' = channels.value recent AS (SELECT id, json(content) AS content FROM messages
JOIN json_each(?2) AS following ON messages.author = following.value WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
WHERE ORDER BY timestamp DESC LIMIT 1024),
messages.content ->> 'type' = 'post' AND recent_channels AS (SELECT recent.id, '#' || json_extract(content, '$.channel') AS tag
messages.content ->> 'root' IS NULL AND FROM recent
messages.author != ?4 WHERE json_extract(content, '$.channel') IS NOT NULL),
GROUP by channel recent_mentions AS (SELECT recent.id, json_extract(mention.value, '$.link') AS tag
UNION FROM recent, json_each(recent.content, '$.mentions') AS mention
SELECT '' AS channel, MAX(messages.rowid) AS rowid FROM messages WHERE json_valid(mention.value) AND tag LIKE '#%'),
JOIN json_each(?2) AS following ON messages.author = following.value combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
WHERE by_message AS (SELECT DISTINCT id, tag FROM combined)
messages.content ->> 'type' = 'post' AND SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
messages.content ->> 'root' IS NULL AND
messages.author != ?4
UNION
SELECT '@' AS channel, MAX(messages.rowid) AS rowid FROM messages_fts(?3)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?2) AS following ON messages.author = following.value
WHERE messages.author != ?4
`, `,
[ [new Date() - 7 * 24 * 60 * 60 * 1000]
JSON.stringify(this.channels),
JSON.stringify(following),
'"' + this.whoami.replace('"', '""') + '"',
this.whoami,
]
); );
this.channels_latest = Object.fromEntries( console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
channels.map((x) => [x.channel, x.rowid])
);
console.log('channels took', (new Date() - start_time) / 1000.0);
let self = this;
start_time = new Date();
latest_private.then(function (latest) {
self.channels_latest = Object.assign({}, self.channels_latest, {
'🔐': latest[0],
});
console.log('private took', (new Date() - start_time) / 1000.0);
self.private_messages = latest[1];
});
}
_schedule_load_latest_timer() {
--this.loading_latest_scheduled;
this.schedule_load_latest();
}
schedule_load_latest() {
if (!this.loading_latest) {
this.shadowRoot.getElementById('tf-tab-news')?.load_latest();
this.load();
} else if (!this.loading_latest_scheduled) {
this.loading_latest_scheduled++;
setTimeout(this._schedule_load_latest_timer.bind(this), 5000);
}
}
async fetch_user_info(users) {
let info = await tfrpc.rpc.query(
`
SELECT messages_stats.author, messages_stats.max_sequence, messages_stats.max_timestamp AS max_ts FROM messages_stats
JOIN json_each(?) AS following
ON messages_stats.author = following.value
`,
[JSON.stringify(Object.keys(users))]
);
for (let row of info) {
users[row.author].seq = row.max_seq;
users[row.author].ts = row.max_ts;
}
return users;
} }
async load() { async load() {
this.loading_latest = true;
try {
let start_time = new Date();
let whoami = this.whoami;
let following = await tfrpc.rpc.following([whoami], 2);
let users = {};
let by_count = [];
for (let [id, v] of Object.entries(following)) {
users[id] = {
following: v.of,
blocking: v.ob,
followed: v.if,
blocked: v.ib,
};
by_count.push({count: v.of, id: id});
}
this.load_channels_latest(Object.keys(following));
this.channels_unread = JSON.parse(
(await tfrpc.rpc.databaseGet('unread')) ?? '{}'
);
this.following = Object.keys(following);
let about_start_time = new Date();
users = await this.fetch_about(following, users);
console.log(
'about took',
(new Date() - about_start_time) / 1000.0,
'seconds for',
Object.keys(users).length,
'users'
);
start_time = new Date();
users = await this.fetch_user_info(users);
console.log(
'user info took',
(new Date() - start_time) / 1000.0,
'seconds'
);
this.users = users;
console.log(
`load finished ${whoami} => ${this.whoami} in ${(new Date() - start_time) / 1000}`
);
this.whoami = whoami;
this.loaded = whoami;
} finally {
this.loading_latest = false;
}
}
channel_set_unread(event) {
this.channels_unread[event.detail.channel ?? ''] = event.detail.unread;
this.channels_unread = Object.assign({}, this.channels_unread);
tfrpc.rpc.databaseSet('unread', JSON.stringify(this.channels_unread));
}
async decrypt(messages) {
let whoami = this.whoami; let whoami = this.whoami;
return Promise.all( let tags = this.load_recent_tags();
messages.map(async function (message) { let following = await tfrpc.rpc.following([whoami], 2);
let content; let users = {};
try { let by_count = [];
content = JSON.parse(message?.content); for (let [id, v] of Object.entries(following)) {
} catch {} users[id] = {
if (typeof content === 'string') { following: v.of,
let decrypted; blocking: v.ob,
try { followed: v.if,
decrypted = await tfrpc.rpc.try_decrypt(whoami, content); blocked: v.ib,
} catch {} };
if (decrypted) { by_count.push({count: v.of, id: id});
try { }
message.decrypted = JSON.parse(decrypted); console.log(by_count.sort((x, y) => y.count - x.count).slice(0, 20));
} catch { users = await this.fetch_about(Object.keys(following).sort(), users);
message.decrypted = decrypted; this.following = Object.keys(following);
} this.users = users;
} await tags;
} console.log(`load finished ${whoami} => ${this.whoami}`);
return message; this.whoami = whoami;
}) this.loaded = whoami;
);
} }
render_tab() { render_tab() {
@ -544,13 +275,8 @@ class TfElement extends LitElement {
whoami=${this.whoami} whoami=${this.whoami}
.users=${this.users} .users=${this.users}
hash=${this.hash} hash=${this.hash}
?loading=${this.loading} .unread=${this.unread}
.channels=${this.channels} @refresh=${() => (this.unread = [])}
.channels_latest=${this.channels_latest}
.channels_unread=${this.channels_unread}
@channelsetunread=${this.channel_set_unread}
.connections=${this.connections}
.private_messages=${this.private_messages}
></tf-tab-news> ></tf-tab-news>
`; `;
} else if (this.tab === 'connections') { } else if (this.tab === 'connections') {
@ -561,6 +287,14 @@ class TfElement extends LitElement {
.broadcasts=${this.broadcasts} .broadcasts=${this.broadcasts}
></tf-tab-connections> ></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') { } else if (this.tab === 'search') {
return html` return html`
<tf-tab-search <tf-tab-search
@ -592,15 +326,13 @@ class TfElement extends LitElement {
await tfrpc.rpc.setHash('#'); await tfrpc.rpc.setHash('#');
} else if (tab === 'connections') { } else if (tab === 'connections') {
await tfrpc.rpc.setHash('#connections'); await tfrpc.rpc.setHash('#connections');
} else if (tab === 'mentions') {
await tfrpc.rpc.setHash('#mentions');
} else if (tab === 'query') { } else if (tab === 'query') {
await tfrpc.rpc.setHash('#sql='); await tfrpc.rpc.setHash('#sql=');
} }
} }
refresh() {
tfrpc.rpc.sync();
}
render() { render() {
let self = this; let self = this;
@ -614,72 +346,40 @@ class TfElement extends LitElement {
const k_tabs = { const k_tabs = {
'📰': 'news', '📰': 'news',
'📡': 'connections', '📡': 'connections',
'@': 'mentions',
'🔍': 'search', '🔍': 'search',
'👩‍💻': 'query', '👩‍💻': 'query',
}; };
let tabs = html` let tabs = html`
<div <div class="w3-bar w3-black">
class="w3-bar w3-theme-l1"
style="position: static; top: 0; z-index: 10"
>
<button
class=${'w3-bar-item w3-button w3-circle w3-ripple' +
(this.connections?.some((x) => x.flags.one_shot) ? ' w3-spin' : '')}
style="width: 1.5em; height: 1.5em; padding: 8px"
@click=${this.refresh}
>
</button>
${Object.entries(k_tabs).map( ${Object.entries(k_tabs).map(
([k, v]) => html` ([k, v]) => html`
<button <button
title=${v} title=${v}
class="w3-bar-item w3-padding w3-hover-theme tab ${self.tab == v class="w3-bar-item w3-padding-large w3-hover-gray tab ${self.tab ==
? 'w3-theme-l2' v
: 'w3-theme-l1'}" ? 'w3-red'
: 'w3-black'}"
@click=${() => self.set_tab(v)} @click=${() => self.set_tab(v)}
> >
${k} ${k}
<span class=${self.tab == v ? '' : 'w3-hide-small'}
>${v.charAt(0).toUpperCase() + v.substring(1)}</span
>
</button> </button>
` `
)} )}
</div> </div>
`; `;
let contents = this.guest let contents = !this.loaded
? html`<div ? this.loading
class="w3-display-middle w3-panel w3-theme-l5 w3-card-4 w3-padding-large w3-round-xlarge w3-xlarge w3-container" ? html`<div>Loading...</div>`
> : html`<div>Select or create an identity.</div>`
<p>🦀 Must be logged in to Tilde Friends to scuttle here. 🦀</p> : this.render_tab();
<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();
return html` return html`
<div ${this.render_id_picker()} ${tabs}
style="width: 100vw; min-height: 100vh; height: 100vh; display: flex; flex-direction: column" ${this.tags.map(
class="w3-theme-dark" (x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
> )}
<div style="flex: 0 0">${tabs}</div> ${contents}
<div style="flex: 1 1; overflow: auto; contain: layout">
${contents}
</div>
</div>
`; `;
} }
} }

View File

@ -1,4 +1,4 @@
import {LitElement, html, unsafeHTML, live} from './lit-all.min.js'; import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfutils from './tf-utils.js'; import * as tfutils from './tf-utils.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js'; import {styles} from './tf-styles.js';
@ -13,9 +13,6 @@ class TfComposeElement extends LitElement {
branch: {type: String}, branch: {type: String},
apps: {type: Object}, apps: {type: Object},
drafts: {type: Object}, drafts: {type: Object},
author: {type: String},
channel: {type: String},
new_thread: {type: Boolean},
}; };
} }
@ -28,8 +25,6 @@ class TfComposeElement extends LitElement {
this.branch = undefined; this.branch = undefined;
this.apps = undefined; this.apps = undefined;
this.drafts = {}; this.drafts = {};
this.author = undefined;
this.new_thread = false;
} }
process_text(text) { process_text(text) {
@ -69,7 +64,7 @@ class TfComposeElement extends LitElement {
updated = true; updated = true;
} }
if (updated) { if (updated) {
setTimeout(() => this.notify(draft), 0); this.requestUpdate();
} }
return tfutils.markdown(text); return tfutils.markdown(text);
} }
@ -77,12 +72,14 @@ class TfComposeElement extends LitElement {
input(event) { input(event) {
let edit = this.renderRoot.getElementById('edit'); let edit = this.renderRoot.getElementById('edit');
let preview = this.renderRoot.getElementById('preview'); 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 content_warning = this.renderRoot.getElementById('content_warning');
let draft = this.get_draft(); let content_warning_preview = this.renderRoot.getElementById(
draft.text = edit.innerText; 'content_warning_preview'
draft.content_warning = content_warning?.value; );
setTimeout(() => this.notify(draft), 0); if (content_warning && content_warning_preview) {
content_warning_preview.innerText = content_warning.value;
}
} }
notify(draft) { notify(draft) {
@ -98,6 +95,14 @@ class TfComposeElement extends LitElement {
); );
} }
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) { convert_to_format(buffer, type, mime_type) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
let img = new Image(); let img = new Image();
@ -164,7 +169,8 @@ class TfComposeElement extends LitElement {
size: buffer.length ?? buffer.byteLength, size: buffer.length ?? buffer.byteLength,
}; };
let edit = self.renderRoot.getElementById('edit'); let edit = self.renderRoot.getElementById('edit');
edit.innerText += `\n![${name}](${id})`; edit.value += `\n![${name}](${id})`;
self.change();
self.input(); self.input();
} catch (e) { } catch (e) {
alert(e?.message); alert(e?.message);
@ -183,13 +189,6 @@ class TfComposeElement extends LitElement {
break; break;
} }
} }
event.preventDefault();
document.execCommand(
'insertText',
false,
event.clipboardData.getData('text/plain')
);
} }
async submit() { async submit() {
@ -198,27 +197,12 @@ class TfComposeElement extends LitElement {
let edit = this.renderRoot.getElementById('edit'); let edit = this.renderRoot.getElementById('edit');
let message = { let message = {
type: 'post', type: 'post',
text: edit.innerText, text: edit.value,
channel: this.channel,
}; };
if (this.root || this.branch) { if (this.root || this.branch) {
message.root = this.new_thread ? (this.branch ?? this.root) : this.root; message.root = this.root;
message.branch = this.branch; 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) { if (Object.values(draft.mentions || {}).length) {
message.mentions = Object.values(draft.mentions); message.mentions = Object.values(draft.mentions);
} }
@ -240,27 +224,35 @@ class TfComposeElement extends LitElement {
console.log('encrypted as', message); console.log('encrypted as', message);
} }
try { try {
await tfrpc.rpc.appendMessage(this.whoami, message); await tfrpc.rpc.appendMessage(this.whoami, message).then(function () {
self.notify(undefined); edit.value = '';
self.change();
self.notify(undefined);
self.requestUpdate();
});
} catch (error) { } catch (error) {
alert(error.message); alert(error.message);
} }
} }
discard() { discard() {
let edit = this.renderRoot.getElementById('edit');
edit.value = '';
this.change();
let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = '';
this.notify(undefined); this.notify(undefined);
} }
attach() { attach() {
let self = this; let self = this;
let edit = this.renderRoot.getElementById('edit');
let input = document.createElement('input'); let input = document.createElement('input');
input.type = 'file'; input.type = 'file';
input.addEventListener('change', function (event) { input.onchange = function (event) {
input.parentNode.removeChild(input);
let file = event.target.files[0]; let file = event.target.files[0];
self.add_file(file); self.add_file(file);
}); };
document.body.appendChild(input);
input.click(); input.click();
} }
@ -270,9 +262,9 @@ class TfComposeElement extends LitElement {
try { try {
let rows = await tfrpc.rpc.query( 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 JOIN messages ON messages.rowid = messages_fts.rowid
WHERE json(messages.content) LIKE ? WHERE messages.content LIKE ?
ORDER BY timestamp DESC LIMIT 10 ORDER BY timestamp DESC LIMIT 10
`, `,
['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`] ['"' + text.replace('"', '""') + '"', `%![%${text}%](%)%`]
@ -292,39 +284,22 @@ class TfComposeElement extends LitElement {
} }
firstUpdated() { firstUpdated() {
let values = Object.entries(this.users).map((x) => ({
key: x[1].name ?? x[0],
value: x[0],
}));
if (this.author) {
values = [].concat(
[
{
key: this.users[this.author]?.name,
value: this.author,
},
],
values
);
}
let tribute = new Tribute({ let tribute = new Tribute({
iframe: this.shadowRoot,
collection: [ collection: [
{ {
values: values, values: Object.entries(this.users).map((x) => ({
key: x[1].name,
value: x[0],
})),
selectTemplate: function (item) { selectTemplate: function (item) {
return item return `[@${item.original.key}](${item.original.value})`;
? `[@${item.original.key}](${item.original.value})`
: undefined;
}, },
}, },
{ {
trigger: '&', trigger: '&',
values: this.autocomplete, values: this.autocomplete,
selectTemplate: function (item) { selectTemplate: function (item) {
return item return `![${item.original.key}](${item.original.value})`;
? `![${item.original.key}](${item.original.value})`
: undefined;
}, },
}, },
], ],
@ -335,15 +310,14 @@ class TfComposeElement extends LitElement {
updated() { updated() {
super.updated(); super.updated();
let edit = this.renderRoot.getElementById('edit'); 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'); let preview = this.renderRoot.getElementById('preview');
preview.innerHTML = this.process_text(edit.innerText); preview.innerHTML = this.process_text(edit.value);
this.last_updated_text = edit.innerText; this.last_updated_text = edit.value;
} }
let encrypt = this.renderRoot.getElementById('encrypt_to'); let encrypt = this.renderRoot.getElementById('encrypt_to');
if (encrypt) { if (encrypt) {
let tribute = new Tribute({ let tribute = new Tribute({
iframe: this.shadowRoot,
values: Object.entries(this.users).map((x) => ({ values: Object.entries(this.users).map((x) => ({
key: x[1].name, key: x[1].name,
value: x[0], value: x[0],
@ -359,7 +333,8 @@ class TfComposeElement extends LitElement {
remove_mention(id) { remove_mention(id) {
let draft = this.get_draft(); let draft = this.get_draft();
delete draft.mentions[id]; delete draft.mentions[id];
setTimeout(() => this.notify(draft), 0); this.notify(draft);
this.requestUpdate();
} }
render_mention(mention) { render_mention(mention) {
@ -367,7 +342,7 @@ class TfComposeElement extends LitElement {
return html` <div style="display: flex; flex-direction: row"> return html` <div style="display: flex; flex-direction: row">
<div style="align-self: center; margin: 0.5em"> <div style="align-self: center; margin: 0.5em">
<button <button
class="w3-button w3-theme-d1" class="w3-button w3-dark-grey"
title="Remove ${mention.name} mention" title="Remove ${mention.name} mention"
@click=${() => self.remove_mention(mention.link)} @click=${() => self.remove_mention(mention.link)}
> >
@ -421,16 +396,16 @@ class TfComposeElement extends LitElement {
if (this.apps) { if (this.apps) {
return html` return html`
<div class="w3-card-4 w3-margin w3-padding"> <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( ${Object.keys(self.apps).map(
(app) => html`<option value=${app}>${app}</option>` (app) => html`<option value=${app}>${app}</option>`
)} )}
</select> </select>
<button class="w3-button w3-theme-d1" @click=${attach_selected_app}> <button class="w3-button w3-dark-grey" @click=${attach_selected_app}>
Attach Attach
</button> </button>
<button <button
class="w3-button w3-theme-d1" class="w3-button w3-dark-grey"
@click=${() => (this.apps = null)} @click=${() => (this.apps = null)}
> >
Cancel Cancel
@ -446,12 +421,12 @@ class TfComposeElement extends LitElement {
self.apps = await tfrpc.rpc.apps(); self.apps = await tfrpc.rpc.apps();
} }
if (!this.apps) { if (!this.apps) {
return html`<button class="w3-button w3-theme-d1" @click=${attach_app}> return html`<button class="w3-button w3-dark-grey" @click=${attach_app}>
Attach App Attach App
</button>`; </button>`;
} else { } else {
return html`<button return html`<button
class="w3-button w3-theme-d1" class="w3-button w3-dark-grey"
@click=${() => (this.apps = null)} @click=${() => (this.apps = null)}
> >
Discard App Discard App
@ -473,34 +448,20 @@ class TfComposeElement extends LitElement {
return html` return html`
<div class="w3-container w3-padding"> <div class="w3-container w3-padding">
<p> <p>
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning(undefined)} checked="checked"></input> <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> <label for="cw">CW</label>
</p> </p>
<input type="text" class="w3-input w3-border w3-theme-d1" id="content_warning" placeholder="Enter a content warning here." @input=${self.input} value=${draft.content_warning}></input> <input type="text" class="w3-input w3-border w3-dark-grey" id="content_warning" placeholder="Enter a content warning here." @input=${this.input} @change=${this.change} value=${draft.content_warning}></input>
</div> </div>
`; `;
} else { } else {
return html` return html`
<input type="checkbox" class="w3-check w3-theme-d1" id="cw" @change=${() => self.set_content_warning('')}></input> <input type="checkbox" class="w3-check w3-dark-grey" id="cw" @change=${() => self.set_content_warning('')}></input>
<label for="cw">CW</label> <label for="cw">CW</label>
`; `;
} }
} }
render_new_thread() {
let self = this;
if (
this.root !== undefined &&
this.branch !== undefined &&
this.root != this.branch
) {
return html`
<input type="checkbox" class="w3-check w3-theme-d1" id="new_thread" @change=${() => (self.new_thread = !self.new_thread)} ?checked=${self.new_thread}></input>
<label for="new_thread">New Thread</label>
`;
}
}
get_draft() { get_draft() {
return this.drafts[this.branch || ''] || {}; return this.drafts[this.branch || ''] || {};
} }
@ -525,14 +486,14 @@ class TfComposeElement extends LitElement {
<div style="display: flex; flex-direction: row; width: 100%"> <div style="display: flex; flex-direction: row; width: 100%">
<label for="encrypt_to">🔐 To:</label> <label for="encrypt_to">🔐 To:</label>
<input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input> <input type="text" id="encrypt_to" style="display: flex; flex: 1 1" @input=${this.update_encrypt}></input>
<button class="w3-button w3-theme-d1" @click=${() => this.set_encrypt(undefined)}>🚮</button> <button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt(undefined)}>🚮</button>
</div> </div>
<ul> <ul>
${draft.encrypt_to.map( ${draft.encrypt_to.map(
(x) => html` (x) => html`
<li> <li>
<tf-user id=${x} .users=${this.users}></tf-user> <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>` </li>`
)} )}
</ul> </ul>
@ -551,7 +512,7 @@ class TfComposeElement extends LitElement {
let draft = self.get_draft(); let draft = self.get_draft();
let content_warning = let content_warning =
draft.content_warning !== undefined 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> <p id="content_warning_preview">${draft.content_warning}</p>
</div>` </div>`
: undefined; : undefined;
@ -559,69 +520,56 @@ class TfComposeElement extends LitElement {
draft.encrypt_to !== undefined draft.encrypt_to !== undefined
? undefined ? undefined
: html`<button : html`<button
class="w3-button w3-theme-d1" class="w3-button w3-dark-grey"
@click=${() => this.set_encrypt([])} @click=${() => this.set_encrypt([])}
> >
🔐 🔐
</button>`; </button>`;
let result = html` let result = html`
<style>
.w3-input:empty::before {
content: attr(placeholder);
}
.w3-input:empty:focus::before {
content: '';
}
</style>
<div <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" style="box-sizing: border-box"
> >
<header class="w3-container"> ${this.render_encrypt()}
${this.channel !== undefined <div style="display: flex; flex-direction: row; width: 100%; gap: 4px">
? html`<p>To #${this.channel}:</p>` <div style="flex: 1 0 50%">
: undefined} <p>
${this.render_encrypt()} <textarea
</header> class="w3-input w3-dark-grey w3-border"
<div class="w3-container w3-padding-small"> style="resize: vertical"
<div class="w3-half"> placeholder="Write a post here."
<span id="edit"
class="w3-input w3-theme-d1 w3-border" @input=${this.input}
style="resize: vertical; width: 100%; overflow: hidden; white-space: pre-wrap" @change=${this.change}
placeholder="Write a post here." @paste=${this.paste}
id="edit" >
@input=${this.input} ${draft.text}</textarea
@paste=${this.paste} >
contenteditable="plaintext-only" </p>
.innerText=${live(draft.text ?? '')}
></span>
</div> </div>
<div class="w3-half w3-container"> <div style="flex: 1 0 50%">
${content_warning} ${content_warning}
<p id="preview"></p> <div id="preview"></div>
</div> </div>
</div> </div>
${Object.values(draft.mentions || {}).map((x) => ${Object.values(draft.mentions || {}).map((x) =>
self.render_mention(x) self.render_mention(x)
)} )}
<footer class="w3-container"> ${this.render_attach_app()} ${this.render_content_warning()}
${this.render_attach_app()} ${this.render_content_warning()} <button
${this.render_new_thread()} class="w3-button w3-dark-grey"
<button id="submit"
class="w3-button w3-theme-d1" @click=${this.submit}
id="submit" >
@click=${this.submit} Submit
> </button>
Submit <button class="w3-button w3-dark-grey" @click=${this.attach}>
</button> Attach
<button class="w3-button w3-theme-d1" @click=${this.attach}> </button>
Attach ${this.render_attach_app_button()} ${encrypt}
</button> <button class="w3-button w3-dark-grey" @click=${this.discard}>
${this.render_attach_app_button()} ${encrypt} Discard
<button class="w3-button w3-theme-d1" @click=${this.discard}> </button>
Discard
</button>
</footer>
</div> </div>
`; `;
return result; return result;

54
apps/ssb/tf-id-picker.js Normal file
View 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);

View File

@ -1,4 +1,4 @@
import {LitElement, html, repeat, render, unsafeHTML} from './lit-all.min.js'; import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import * as tfutils from './tf-utils.js'; import * as tfutils from './tf-utils.js';
import * as emojis from './emojis.js'; import * as emojis from './emojis.js';
@ -14,8 +14,6 @@ class TfMessageElement extends LitElement {
format: {type: String}, format: {type: String},
blog_data: {type: String}, blog_data: {type: String},
expanded: {type: Object}, expanded: {type: Object},
channel: {type: String},
channel_unread: {type: Number},
}; };
} }
@ -30,26 +28,6 @@ class TfMessageElement extends LitElement {
this.drafts = {}; this.drafts = {};
this.format = 'message'; this.format = 'message';
this.expanded = {}; this.expanded = {};
this.channel_unread = -1;
}
connectedCallback() {
super.connectedCallback();
this._click_callback = this.document_click.bind(this);
document.body.addEventListener('mouseup', this._click_callback);
}
disconnectedCallback() {
super.disconnectedCallback();
document.body.removeEventListener('mouseup', this._click_callback);
}
document_click(event) {
let content = this.renderRoot.querySelector('.w3-dropdown-content');
let target = event.target;
if (content && !content.contains(target)) {
content.classList.remove('w3-show');
}
} }
show_reply() { show_reply() {
@ -76,12 +54,6 @@ class TfMessageElement extends LitElement {
); );
} }
show_reactions() {
let modal = document.getElementById('reactions_modal');
modal.users = this.users;
modal.votes = this.message?.votes || [];
}
render_votes() { render_votes() {
function normalize_expression(expression) { function normalize_expression(expression) {
if (expression === 'Like' || !expression) { if (expression === 'Like' || !expression) {
@ -94,33 +66,19 @@ class TfMessageElement extends LitElement {
return expression; return expression;
} }
} }
if (this.message?.votes?.length) { return html`<div>
return html` <div class="w3-container"> ${(this.message.votes || []).map(
<div (vote) => html`
class="w3-button w3-bar w3-padding-small" <span
@click=${this.show_reactions} title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
> vote.timestamp
${(this.message.votes || []).map( )}"
(vote) => html` >
<span ${normalize_expression(vote.content.vote.expression)}
class="w3-bar-item w3-padding-small" </span>
title="${this.users[vote.author]?.name ?? `
vote.author} ${new Date(vote.timestamp)}" )}
> </div>`;
${normalize_expression(vote.content.vote.expression)}
</span>
`
)}
</div>
</div>`;
}
}
render_json(value) {
let json = JSON.stringify(value, null, 2);
return html`
<pre style="white-space: pre-wrap; overflow-wrap: anywhere">${json}</pre>
`;
} }
render_raw() { render_raw() {
@ -134,28 +92,40 @@ class TfMessageElement extends LitElement {
content: this.message?.content, content: this.message?.content,
signature: this.message?.signature, signature: this.message?.signature,
}; };
return this.render_json(raw); return html`<div style="white-space: pre-wrap">
${JSON.stringify(raw, null, 2)}
</div>`;
} }
vote(emoji) { vote(emoji) {
let reaction = emoji; let reaction = emoji;
let message = this.message.id; let message = this.message.id;
tfrpc.rpc if (
.appendMessage(this.whoami, { confirm(
type: 'vote', 'Are you sure you want to react with ' +
vote: { reaction +
link: message, ' to ' +
value: 1, message +
expression: reaction, '?'
}, )
}) ) {
.catch(function (error) { tfrpc.rpc
alert(error?.message); .appendMessage(this.whoami, {
}); type: 'vote',
vote: {
link: message,
value: 1,
expression: reaction,
},
})
.catch(function (error) {
alert(error?.message);
});
}
} }
react(event) { react(event) {
emojis.picker((x) => this.vote(x), null, this.whoami); emojis.picker((x) => this.vote(x));
} }
show_image(link) { show_image(link) {
@ -194,7 +164,7 @@ class TfMessageElement extends LitElement {
event.srcElement.classList.contains('img_caption') event.srcElement.classList.contains('img_caption')
) { ) {
let next = event.srcElement.nextSibling; let next = event.srcElement.nextSibling;
if (next.style.display != 'none') { if (next.style.display == 'block') {
next.style.display = 'none'; next.style.display = 'none';
} else { } else {
next.style.display = 'block'; next.style.display = 'block';
@ -204,7 +174,7 @@ class TfMessageElement extends LitElement {
render_mention(mention) { render_mention(mention) {
if (!mention?.link || typeof mention.link != 'string') { if (!mention?.link || typeof mention.link != 'string') {
return this.render_json(mention); return html` <pre>${JSON.stringify(mention)}</pre>`;
} else if ( } else if (
mention?.link?.startsWith('&') && mention?.link?.startsWith('&') &&
mention?.type?.startsWith('image/') mention?.type?.startsWith('image/')
@ -245,7 +215,7 @@ class TfMessageElement extends LitElement {
>${mention.name}</a >${mention.name}</a
>`; >`;
} else if (mention.link?.startsWith('#')) { } else if (mention.link?.startsWith('#')) {
return html` <a href=${'#' + encodeURIComponent('#' + mention.link)} return html` <a href=${'#q=' + encodeURIComponent(mention.link)}
>${mention.link}</a >${mention.link}</a
>`; >`;
} else if ( } else if (
@ -255,22 +225,23 @@ class TfMessageElement extends LitElement {
) { ) {
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`; return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
} else { } else {
return this.render_json(mention); return html` <pre style="white-space: pre-wrap">
${JSON.stringify(mention, null, 2)}</pre
>`;
} }
} }
render_mentions() { render_mentions() {
let mentions = this.message?.content?.mentions || []; let mentions = this.message?.content?.mentions || [];
mentions = mentions.filter( mentions = mentions.filter(
(x) => (x) => this.message?.content?.text?.indexOf(x.link) === -1
this.message?.content?.text?.indexOf(
typeof x === 'string' ? x : x.link
) === -1
); );
if (mentions.length) { if (mentions.length) {
let self = this; let self = this;
return html` return html`
<fieldset style="padding: 0.5em; border: 1px solid black"> <fieldset
style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black"
>
<legend>Mentions</legend> <legend>Mentions</legend>
${mentions.map((x) => self.render_mention(x))} ${mentions.map((x) => self.render_mention(x))}
</fieldset> </fieldset>
@ -310,58 +281,32 @@ class TfMessageElement extends LitElement {
let self = this; let self = this;
if (this.message.child_messages?.length) { if (this.message.child_messages?.length) {
if (!this.expanded[this.message.id]) { if (!this.expanded[this.message.id]) {
return html` return html`<button
<button class="w3-button w3-dark-grey"
class="w3-button w3-theme-d1 w3-block w3-bar" @click=${() => self.set_expanded(true)}
style="box-sizing: border-box" >
@click=${() => self.set_expanded(true)} + ${this.total_child_messages(this.message) + ' More'}
> </button>`;
+ ${this.total_child_messages(this.message) + ' More'}
</button>
`;
} else { } else {
return html` <div class="w3-container w3-margin-bottom"> return html`<button
${repeat( class="w3-button w3-dark-grey"
this.message.child_messages || [],
(x) => x.id,
(x) =>
html`<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>`
)}
</div>
<button
class="w3-button w3-theme-d1 w3-block w3-bar"
style="box-sizing: border-box"
@click=${() => self.set_expanded(false)} @click=${() => self.set_expanded(false)}
> >
Collapse Collapse</button
</button>`; >${(this.message.child_messages || []).map(
(x) =>
html`<tf-message
.message=${x}
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
.expanded=${this.expanded}
></tf-message>`
)}`;
} }
} else {
return undefined;
} }
} }
mark_unread() {
this.dispatchEvent(
new CustomEvent('channelsetunread', {
bubbles: true,
composed: true,
detail: {
channel: this.channel,
unread: this.message.rowid,
},
})
);
}
render_channels() { render_channels() {
let content = this.message?.content; let content = this.message?.content;
if (this?.messsage?.decrypted?.type == 'post') { if (this?.messsage?.decrypted?.type == 'post') {
@ -381,192 +326,89 @@ class TfMessageElement extends LitElement {
return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`); return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
} }
class_background() {
return this.message?.decrypted
? 'w3-pale-red'
: this.message?.rowid >= this.channel_unread
? 'w3-theme-d2'
: 'w3-theme-d4';
}
get_content() {
let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted;
}
return content;
}
copy_id(event) {
navigator.clipboard.writeText(this.message?.id);
}
toggle_menu(event) {
event.srcElement.parentNode
.querySelector('.w3-dropdown-content')
.classList.toggle('w3-show');
}
render_menu() {
let content = this.get_content();
let formats = [['message', 'Message']];
if (content?.type == 'post' || content?.type == 'blog') {
formats.push(['md', 'Markdown']);
}
if (this.message?.decrypted) {
formats.push(['decrypted', 'Decrypted']);
}
formats.push(['raw', 'Raw']);
return html`
<div class="w3-bar-item w3-right">
<button class="w3-button w3-theme-d1" @click=${this.toggle_menu}>
%
</button>
<div
class="w3-dropdown-content w3-bar-block w3-card-4 w3-theme-l1"
style="right: 48px"
>
<a
target="_top"
class="w3-button w3-bar-item"
href=${'#' + encodeURIComponent(this.message?.id)}
>View Message</a
>
<button
class="w3-button w3-bar-item w3-border-bottom"
@click=${this.copy_id}
>
Copy ID
</button>
${this.drafts[this.message?.id] === undefined
? html`
<button class="w3-button w3-bar-item" @click=${this.show_reply}>
Reply
</button>
`
: undefined}
<button
class="w3-button w3-bar-item w3-border-bottom"
@click=${this.react}
>
👍 React
</button>
${formats.map(
([format, name]) => html`
<button
class="w3-button w3-bar-item"
style=${format == this.format ? 'font-weight: bold' : ''}
@click=${() => (this.format = format)}
>
${name}
</button>
`
)}
</div>
</div>
`;
}
render_header() {
let is_encrypted = this.message?.decrypted
? html`<span class="w3-bar-item">🔓</span>`
: typeof this.message?.content == 'string'
? html`<span class="w3-bar-item">🔒</span>`
: undefined;
return html`
<header class="w3-bar">
<span class="w3-bar-item">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
</span>
${is_encrypted} ${this.render_menu()}
<div class="w3-bar-item w3-right" style="text-wrap: nowrap">
${new Date(this.message.timestamp).toLocaleString()}
</div>
</header>
`;
}
render_frame(inner) {
return html`
<style>
code {
white-space: pre-wrap;
overflow-wrap: break-word;
}
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4 ${this.class_background()} w3-border-theme w3-margin-top"
style="overflow: auto; overflow-wrap: anywhere; display: block; max-width: 100%"
>
${inner}
</div>
`;
}
render_small_frame(inner) {
let self = this;
return this.render_frame(html`
${self.render_header()}
${self.format == 'raw'
? html`<div class="w3-container">${self.render_raw()}</div>`
: inner}
${self.render_votes()}
${(self.message.child_messages || []).map(
(x) => html`
<tf-message
.message=${x}
whoami=${self.whoami}
.users=${self.users}
.drafts=${self.drafts}
.expanded=${self.expanded}
channel=${self.channel}
channel_unread=${self.channel_unread}
></tf-message>
`
)}
`);
}
render_actions() {
let content = this.get_content();
let reply =
this.drafts[this.message?.id] !== undefined
? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
author=${this.message.author}
></tf-compose>
`
: undefined;
return html`
<div class="w3-section w3-container">${reply}</div>
<footer>${this.render_children()}</footer>
`;
}
render() { render() {
let content = this.message?.content; let content = this.message?.content;
if (this.message?.decrypted?.type == 'post') { if (this.message?.decrypted?.type == 'post') {
content = this.message.decrypted; content = this.message.decrypted;
} }
let class_background = this.class_background();
let self = this; let self = this;
let raw_button;
switch (this.format) {
case 'raw':
if (content?.type == 'post' || content?.type == 'blog') {
raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'md')}
>
Markdown
</button>`;
} else {
raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'message')}
>
Message
</button>`;
}
break;
case 'md':
raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'message')}
>
Message
</button>`;
break;
case 'decrypted':
raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'raw')}
>
Raw
</button>`;
break;
default:
if (this.message.decrypted) {
raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'decrypted')}
>
Decrypted
</button>`;
} else {
raw_button = html`<button
class="w3-button w3-dark-grey"
@click=${() => (self.format = 'raw')}
>
Raw
</button>`;
}
break;
}
function small_frame(inner) {
let body;
return html`
<div
class="w3-card-4"
style="background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; display: inline-block; overflow-wrap: anywhere"
>
<tf-user id=${self.message.author} .users=${self.users}></tf-user>
<span style="padding-right: 8px"
><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(
self.message.timestamp
).toLocaleString()}</span
>
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
${self.render_votes()}
</div>
`;
}
if (this.message?.type === 'contact_group') { if (this.message?.type === 'contact_group') {
return this.render_frame( return html` <div
html` ${this.message.messages.map( class="w3-card-4"
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
>
${this.message.messages.map(
(x) => (x) =>
html`<tf-message html`<tf-message
.message=${x} .message=${x}
@ -574,37 +416,30 @@ class TfMessageElement extends LitElement {
.users=${this.users} .users=${this.users}
.drafts=${this.drafts} .drafts=${this.drafts}
.expanded=${this.expanded} .expanded=${this.expanded}
channel=${this.channel}
channel_unread=${this.channel_unread}
></tf-message>` ></tf-message>`
)}` )}
); </div>`;
} else if (this.message.placeholder) { } else if (this.message.placeholder) {
return this.render_frame( return html` <div
html`<div class="w3-padding"> class="w3-card-4"
<p> style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
<a target="_top" href=${'#' + encodeURIComponent(this.message.id)} >
>${this.message.id}</a <a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
> (placeholder)
(placeholder) <div>${this.render_votes()}</div>
</p> ${(this.message.child_messages || []).map(
<div>${this.render_votes()}</div> (x) => html`
${(this.message.child_messages || []).map( <tf-message
(x) => html` .message=${x}
<tf-message whoami=${this.whoami}
.message=${x} .users=${this.users}
whoami=${this.whoami} .drafts=${this.drafts}
.users=${this.users} .expanded=${this.expanded}
.drafts=${this.drafts} ></tf-message>
.expanded=${this.expanded} `
channel=${this.channel} )}
channel_unread=${this.channel_unread} </div>`;
></tf-message> } else if (typeof (content?.type === 'string')) {
`
)}
</div>`
);
} else if (typeof content?.type === 'string') {
if (content.type == 'about') { if (content.type == 'about') {
let name; let name;
let image; let image;
@ -631,14 +466,10 @@ class TfMessageElement extends LitElement {
Updated profile for Updated profile for
<tf-user id=${content.about} .users=${this.users}></tf-user>. <tf-user id=${content.about} .users=${this.users}></tf-user>.
</div>`; </div>`;
return this.render_small_frame(html` return small_frame(html` ${update} ${name} ${image} ${description} `);
<div class="w3-container">
<p>${update} ${name} ${image} ${description}</p>
</div>
`);
} else if (content.type == 'contact') { } else if (content.type == 'contact') {
return html` return html`
<div class="w3-padding"> <div>
<tf-user id=${this.message.author} .users=${this.users}></tf-user> <tf-user id=${this.message.author} .users=${this.users}></tf-user>
is is
${content.blocking === true ${content.blocking === true
@ -657,6 +488,26 @@ class TfMessageElement extends LitElement {
</div> </div>
`; `;
} else if (content.type == 'post') { } else if (content.type == 'post') {
let reply =
this.drafts[this.message?.id] !== undefined
? html`
<tf-compose
whoami=${this.whoami}
.users=${this.users}
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
></tf-compose>
`
: html`
<button
class="w3-button w3-dark-grey"
@click=${this.show_reply}
>
Reply
</button>
`;
let self = this; let self = this;
let body; let body;
switch (this.format) { switch (this.format) {
@ -673,12 +524,16 @@ class TfMessageElement extends LitElement {
body = unsafeHTML(tfutils.markdown(content.text)); body = unsafeHTML(tfutils.markdown(content.text));
break; break;
case 'decrypted': case 'decrypted':
body = this.render_json(content); body = html`<pre
style="white-space: pre-wrap; overflow-wrap: anywhere"
>
${JSON.stringify(content, null, 2)}</pre
>`;
break; break;
} }
let content_warning = html` let content_warning = html`
<div <div
class="w3-panel w3-round-xlarge w3-theme-l4" class="w3-panel w3-round-xlarge w3-blue"
style="cursor: pointer" style="cursor: pointer"
@click=${(x) => this.toggle_expanded(':cw')} @click=${(x) => this.toggle_expanded(':cw')}
> >
@ -695,22 +550,96 @@ class TfMessageElement extends LitElement {
? html` ${content_warning} ${content_html} ` ? html` ${content_warning} ${content_html} `
: content_warning : content_warning
: content_html; : content_html;
return this.render_frame(html` let is_encrypted = this.message?.decrypted
${this.render_header()} ? html`<span style="align-self: center">🔓</span>`
<div class="w3-container">${payload}</div> : undefined;
${this.render_votes()} ${this.render_actions()} let style_background = this.message?.decrypted
</div> ? 'rgba(255, 0, 0, 0.2)'
`); : 'rgba(255, 255, 255, 0.1)';
} else if (content.type === 'issue') { return html`
return this.render_frame(html` <style>
${this.render_header()} ${content.text} ${this.render_votes()} code {
<footer class="w3-container"> white-space: pre-wrap;
<button class="w3-button w3-theme-d1" @click=${this.react}> overflow-wrap: break-word;
React }
</button> div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4"
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
<span style="flex: 1"></span>
<span style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
</div>
${payload} ${this.render_votes()}
<p>
${reply}
<button class="w3-button w3-dark-grey" @click=${this.react}>
React
</button>
</p>
${this.render_children()} ${this.render_children()}
</footer> </div>
`); `;
} else if (content.type === 'issue') {
let is_encrypted = this.message?.decrypted
? html`<span style="align-self: center">🔓</span>`
: undefined;
let style_background = this.message?.decrypted
? 'rgba(255, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.1)';
return html`
<style>
code {
white-space: pre-wrap;
overflow-wrap: break-word;
}
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4"
style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
${is_encrypted}
<span style="flex: 1"></span>
<span style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
</div>
${content.text} ${this.render_votes()}
<p>
<button class="w3-button w3-dark-grey" @click=${this.react}>
React
</button>
</p>
${this.render_children()}
</div>
`;
} else if (content.type === 'blog') { } else if (content.type === 'blog') {
let self = this; let self = this;
tfrpc.rpc.get_blob(content.blog).then(function (data) { tfrpc.rpc.get_blob(content.blog).then(function (data) {
@ -746,20 +675,74 @@ class TfMessageElement extends LitElement {
`; `;
break; break;
} }
return this.render_frame(html` let reply =
${this.render_header()} this.drafts[this.message?.id] !== undefined
<div>${body}</div> ? html`
${this.render_mentions()} ${this.render_votes()} <tf-compose
${this.render_actions()} whoami=${this.whoami}
`); .users=${this.users}
root=${content.root || this.message.id}
branch=${this.message.id}
.drafts=${this.drafts}
@tf-discard=${this.discard_reply}
></tf-compose>
`
: html`
<button
class="w3-button w3-dark-grey"
@click=${this.show_reply}
>
Reply
</button>
`;
return html`
<style>
code {
white-space: pre-wrap;
overflow-wrap: break-word;
}
div {
overflow-wrap: anywhere;
}
img {
max-width: 100%;
height: auto;
display: block;
}
</style>
<div
class="w3-card-4"
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px"
>
<div style="display: flex; flex-direction: row">
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
<span style="flex: 1"></span>
<span style="padding-right: 8px"
><a target="_top" href=${'#' + self.message.id}>%</a>
${new Date(this.message.timestamp).toLocaleString()}</span
>
<span>${raw_button}</span>
</div>
<div>${body}</div>
${this.render_mentions()}
<div>
${reply}
<button class="w3-button w3-dark-grey" @click=${this.react}>
React
</button>
</div>
${this.render_votes()} ${this.render_children()}
</div>
`;
} else if (content.type === 'pub') { } else if (content.type === 'pub') {
return this.render_small_frame( return small_frame(
html` <style> html` <style>
span { span {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
</style> </style>
<div class="w3-padding"> <span>
<div> <div>
🍻 🍻
<tf-user <tf-user
@ -768,47 +751,38 @@ class TfMessageElement extends LitElement {
></tf-user> ></tf-user>
</div> </div>
<pre>${content.address.host}:${content.address.port}</pre> <pre>${content.address.host}:${content.address.port}</pre>
</div>` </span>`
); );
} else if (content.type === 'channel') { } else if (content.type === 'channel') {
return this.render_small_frame(html` return small_frame(html`
<div class="w3-container"> <div>
<p> ${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} <a href=${'#q=' + encodeURIComponent('#' + content.channel)}
<a href=${'#' + encodeURIComponent('#' + content.channel)} >#${content.channel}</a
>#${content.channel}</a >
>
</p>
</div> </div>
`); `);
} else if (typeof this.message.content == 'string') { } else if (typeof this.message.content == 'string') {
if (this.message?.decrypted) { if (this.message?.decrypted) {
if (this.format == 'decrypted') { if (this.format == 'decrypted') {
return this.render_small_frame( return small_frame(
html`<span class="w3-container">🔓</span> ${this.render_json( html`<span>🔓</span>
this.message.decrypted <pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`
)}`
); );
} else { } else {
return this.render_small_frame( return small_frame(
html`<span class="w3-container">🔓</span> html`<span>🔓</span>
<div class="w3-container">${this.message.decrypted.type}</div>` <div>${this.message.decrypted.type}</div>`
); );
} }
} else { } else {
return this.render_small_frame(); return small_frame(html`<span>🔒</span>`);
} }
} else { } else {
return this.render_small_frame( return small_frame(html`<div><b>type</b>: ${content.type}</div>`);
html`<div class="w3-container">
<p><b>type</b>: ${content.type}</p>
</div>`
);
} }
} else if (typeof this.message.content == 'string') {
return this.render_small_frame();
} else { } else {
return this.render_small_frame(this.render_raw()); return small_frame(this.render_raw());
} }
} }
} }

View File

@ -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 * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js'; import {styles} from './tf-styles.js';
@ -11,8 +11,6 @@ class TfNewsElement extends LitElement {
following: {type: Array}, following: {type: Array},
drafts: {type: Object}, drafts: {type: Object},
expanded: {type: Object}, expanded: {type: Object},
channel: {type: String},
channel_unread: {type: Number},
}; };
} }
@ -27,7 +25,6 @@ class TfNewsElement extends LitElement {
this.following = []; this.following = [];
this.drafts = {}; this.drafts = {};
this.expanded = {}; this.expanded = {};
this.channel_unread = -1;
} }
process_messages(messages) { process_messages(messages) {
@ -36,13 +33,12 @@ class TfNewsElement extends LitElement {
console.log('processing', messages.length, 'messages'); console.log('processing', messages.length, 'messages');
function ensure_message(id, rowid) { function ensure_message(id) {
let found = messages_by_id[id]; let found = messages_by_id[id];
if (found) { if (found) {
return found; return found;
} else { } else {
let added = { let added = {
rowid: rowid,
id: id, id: id,
placeholder: true, placeholder: true,
content: '"placeholder"', content: '"placeholder"',
@ -57,7 +53,7 @@ class TfNewsElement extends LitElement {
function link_message(message) { function link_message(message) {
if (message.content.type === 'vote') { if (message.content.type === 'vote') {
let parent = ensure_message(message.content.vote.link, message.rowid); let parent = ensure_message(message.content.vote.link);
if (!parent.votes) { if (!parent.votes) {
parent.votes = []; parent.votes = [];
} }
@ -66,14 +62,14 @@ class TfNewsElement extends LitElement {
} else if (message.content.type == 'post') { } else if (message.content.type == 'post') {
if (message.content.root) { if (message.content.root) {
if (typeof message.content.root === 'string') { if (typeof message.content.root === 'string') {
let m = ensure_message(message.content.root, message.rowid); let m = ensure_message(message.content.root);
if (!m.child_messages) { if (!m.child_messages) {
m.child_messages = []; m.child_messages = [];
} }
m.child_messages.push(message); m.child_messages.push(message);
message.parent_message = message.content.root; message.parent_message = message.content.root;
} else { } else {
let m = ensure_message(message.content.root[0], message.rowid); let m = ensure_message(message.content.root[0]);
if (!m.child_messages) { if (!m.child_messages) {
m.child_messages = []; m.child_messages = [];
} }
@ -166,7 +162,6 @@ class TfNewsElement extends LitElement {
} else { } else {
if (group.length > 0) { if (group.length > 0) {
result.push({ result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: 'contact_group', type: 'contact_group',
messages: group, messages: group,
}); });
@ -175,13 +170,6 @@ class TfNewsElement extends LitElement {
result.push(message); result.push(message);
} }
} }
if (group.length > 0) {
result.push({
rowid: Math.max(...group.map((x) => x.rowid)),
type: 'contact_group',
messages: group,
});
}
return result; return result;
} }
@ -190,40 +178,18 @@ class TfNewsElement extends LitElement {
let final_messages = this.group_following( let final_messages = this.group_following(
this.finalize_messages(messages_by_id) this.finalize_messages(messages_by_id)
); );
let unread_rowid = -1;
for (let message of final_messages) {
if (message.rowid >= this.channel_unread) {
unread_rowid = message.rowid;
}
}
return html` return html`
<div> <div style="display: flex; flex-direction: column">
${repeat( ${final_messages.map(
final_messages, (x) =>
(x) => x.id, html`<tf-message
(x) => html`
<tf-message
.message=${x} .message=${x}
whoami=${this.whoami} whoami=${this.whoami}
.users=${this.users} .users=${this.users}
.drafts=${this.drafts} .drafts=${this.drafts}
.expanded=${this.expanded} .expanded=${this.expanded}
collapsed="true" collapsed="true"
channel=${this.channel} ></tf-message>`
channel_unread=${this.channel_unread}
></tf-message>
${x.rowid == unread_rowid
? html`<div style="display: flex; flex-direction: row">
<div
style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
></div>
<div style="color: #f00; padding: 8px">unread</div>
<div
style="border-bottom: 1px solid #f00; flex: 1; align-self: center; height: 1px"
></div>
</div>`
: undefined}
`
)} )}
</div> </div>
`; `;

View File

@ -11,7 +11,7 @@ class TfProfileElement extends LitElement {
id: {type: String}, id: {type: String},
users: {type: Object}, users: {type: Object},
size: {type: Number}, size: {type: Number},
sequence: {type: Number}, server_follows_me: {type: Boolean},
following: {type: Boolean}, following: {type: Boolean},
blocking: {type: Boolean}, blocking: {type: Boolean},
}; };
@ -27,7 +27,7 @@ class TfProfileElement extends LitElement {
this.id = null; this.id = null;
this.users = {}; this.users = {};
this.size = 0; this.size = 0;
this.sequence = 0; this.server_follows_me = undefined;
} }
async load() { async load() {
@ -63,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) { modify(change) {
let self = this;
tfrpc.rpc tfrpc.rpc
.appendMessage( .appendMessage(
this.whoami, this.whoami,
@ -76,10 +95,6 @@ class TfProfileElement extends LitElement {
change change
) )
) )
.then(function () {
self._follow_whoami = undefined;
self.load();
})
.catch(function (error) { .catch(function (error) {
alert(error?.message); alert(error?.message);
}); });
@ -141,8 +156,7 @@ class TfProfileElement extends LitElement {
let self = this; let self = this;
let input = document.createElement('input'); let input = document.createElement('input');
input.type = 'file'; input.type = 'file';
input.addEventListener('change', function (event) { input.onchange = function (event) {
input.parentNode.removeChild(input);
let file = event.target.files[0]; let file = event.target.files[0];
file file
.arrayBuffer() .arrayBuffer()
@ -157,88 +171,113 @@ class TfProfileElement extends LitElement {
.catch(function (e) { .catch(function (e) {
alert(e.message); alert(e.message);
}); });
}); };
document.body.appendChild(input);
input.click(); input.click();
} }
copy_id() { async server_follow_me(follow) {
navigator.clipboard.writeText(this.id); try {
await tfrpc.rpc.setServerFollowingMe(this.whoami, follow);
} catch (e) {
console.log(e);
}
try {
await this.initial_load();
} catch (e) {
console.log(e);
}
} }
render() { render() {
if (
this.id == this.whoami &&
this.editing &&
this.server_follows_me === undefined
) {
this.initial_load();
}
this.load(); this.load();
let self = this; let self = this;
let profile = this.users[this.id] || {}; let profile = this.users[this.id] || {};
tfrpc.rpc tfrpc.rpc
.query( .query(
`SELECT 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] [this.id]
) )
.then(function (result) { .then(function (result) {
self.size = result[0].size; self.size = result[0].size;
self.sequence = result[0].sequence;
}); });
let edit; let edit;
let follow; let follow;
let block; let block;
if (this.id === this.whoami) { if (this.id === this.whoami) {
if (this.editing) { if (this.editing) {
edit = html` let server_follow;
<button if (this.server_follows_me === true) {
id="save_profile" server_follow = html`<button
class="w3-button w3-theme-d1" class="w3-button w3-dark-grey"
@click=${this.save_edits} @click=${() => this.server_follow_me(false)}
> >
Server, Stop Following Me
</button>`;
} else if (this.server_follows_me === false) {
server_follow = html`<button
class="w3-button w3-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 Save Profile
</button> </button>
<button class="w3-button w3-theme-d1" @click=${this.discard_edits}> <button class="w3-button w3-dark-grey" @click=${this.discard_edits}>
Discard Discard
</button> </button>
${server_follow}
`; `;
} else { } else {
edit = html`<button edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>
id="edit_profile"
class="w3-button w3-theme-d1"
@click=${this.edit}
>
Edit Profile Edit Profile
</button>`; </button>`;
} }
} }
if (this.id !== this.whoami && this.following !== undefined) { if (this.id !== this.whoami && this.following !== undefined) {
follow = this.following 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 Unfollow
</button>` </button>`
: html`<button class="w3-button w3-theme-d1" @click=${this.follow}> : html`<button class="w3-button w3-dark-grey" @click=${this.follow}>
Follow Follow
</button>`; </button>`;
} }
if (this.id !== this.whoami && this.blocking !== undefined) { if (this.id !== this.whoami && this.blocking !== undefined) {
block = this.blocking 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 Unblock
</button>` </button>`
: html`<button class="w3-button w3-theme-d1" @click=${this.block}> : html`<button class="w3-button w3-dark-grey" @click=${this.block}>
Block Block
</button>`; </button>`;
} }
let edit_profile = this.editing let edit_profile = this.editing
? html` ? html`
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px"> <div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
<div> <div class="w3-container">
<label for="name">Name:</label> <div>
<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> <label for="name">Name:</label>
</div> <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><label for="description">Description:</label></div> </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="description">Description:</label></div>
<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>
<label for="public_web_hosting">Public Web Hosting:</label> <div>
<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> <label for="public_web_hosting">Public Web Hosting:</label>
</div> <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-theme-d1" @click=${this.attach_image}>Attach Image</button> <div>
<button class="w3-button w3-dark-grey" @click=${this.attach_image}>Attach Image</button>
</div>
</div> </div>
</div>` </div>`
: null; : null;
@ -246,43 +285,26 @@ class TfProfileElement extends LitElement {
typeof profile.image == 'string' ? profile.image : profile.image?.link; typeof profile.image == 'string' ? profile.image : profile.image?.link;
image = this.editing?.image ?? image; image = this.editing?.image ?? image;
let description = this.editing?.description ?? profile.description; let description = this.editing?.description ?? profile.description;
return html`<div class="w3-card-4 w3-container w3-theme-d3" style="box-sizing: border-box"> return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
<header class="w3-container"> <tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
<p><tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)} in ${this.sequence} messages)</p> <div style="display: flex; flex-direction: row; gap: 1em">
</header> ${edit_profile}
<div class="w3-container"> <div style="flex: 1 0 50%">
<div class="w3-margin-bottom" style="display: flex; flex-direction: row"> <div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
<input type="text" class="w3-input w3-border w3-theme-d1" style="display: flex 1 1" readonly value=${this.id}></input> <div>${unsafeHTML(tfutils.markdown(description))}</div>
<button class="w3-button w3-theme-d1 w3-ripple" style="flex: 0 0 auto" @click=${this.copy_id}>Copy</button>
</div>
<div style="display: flex; flex-direction: row; gap: 1em">
${edit_profile}
<div style="flex: 1 0 50%">
${
image
? html`<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>`
: html`<div>
<div class="w3-jumbo">😎</div>
<div><i>Profile image not set.</i></div>
</div>`
}
<div>${unsafeHTML(tfutils.markdown(description))}</div>
</div>
</div>
<div>
Following ${profile.following} identities.
Followed by ${profile.followed} identities.
Blocking ${profile.blocking} identities.
Blocked by ${profile.blocked} identities.
</div> </div>
</div> </div>
<footer class="w3-container"> <div>
<p> Following ${profile.following} identities.
${edit} Followed by ${profile.followed} identities.
${follow} Blocking ${profile.blocking} identities.
${block} Blocked by ${profile.blocked} identities.
</p> </div>
</footer> <div>
${edit}
${follow}
${block}
</div>
</div>`; </div>`;
} }
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -7,55 +7,35 @@ class TfTabConnectionsElement extends LitElement {
return { return {
broadcasts: {type: Array}, broadcasts: {type: Array},
identities: {type: Array}, identities: {type: Array},
my_identities: {type: Array},
connections: {type: Array}, connections: {type: Array},
stored_connections: {type: Array}, stored_connections: {type: Array},
users: {type: Object}, users: {type: Object},
server_identity: {type: String},
connect_attempt: {type: Object},
connect_message: {type: String},
connect_success: {type: Boolean},
}; };
} }
static styles = styles; static styles = styles;
static k_broadcast_emojis = {
discovery: '🏓',
room: '🚪',
peer_exchange: '🕸',
};
constructor() { constructor() {
super(); super();
let self = this; let self = this;
this.broadcasts = []; this.broadcasts = [];
this.identities = []; this.identities = [];
this.my_identities = [];
this.connections = []; this.connections = [];
this.stored_connections = []; this.stored_connections = [];
this.users = {}; this.users = {};
tfrpc.rpc.getIdentities().then(function (identities) {
self.my_identities = identities || [];
});
tfrpc.rpc.getAllIdentities().then(function (identities) { tfrpc.rpc.getAllIdentities().then(function (identities) {
self.identities = identities || []; self.identities = identities || [];
}); });
tfrpc.rpc.getStoredConnections().then(function (connections) { tfrpc.rpc.getStoredConnections().then(function (connections) {
self.stored_connections = connections || []; self.stored_connections = connections || [];
}); });
tfrpc.rpc.getServerIdentity().then(function (identity) {
self.server_identity = identity;
});
} }
render_connection_summary(connection) { render_connection_summary(connection) {
if (connection.address && connection.port) { if (connection.address && connection.port) {
return html`<div> return html`(<small>${connection.address}:${connection.port}</small>)`;
<small>${connection.address}:${connection.port}</small>
</div>`;
} else if (connection.tunnel) { } else if (connection.tunnel) {
return html`<div>room peer</div>`; return html`(room peer)`;
} else { } else {
return JSON.stringify(connection); return JSON.stringify(connection);
} }
@ -81,7 +61,7 @@ class TfTabConnectionsElement extends LitElement {
return html` return html`
<li> <li>
<button <button
class="w3-button w3-theme-d1" class="w3-button w3-dark-grey"
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)} @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
> >
Connect 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) { render_broadcast(connection) {
let self = this;
return html` return html`
<li> <li>
<div class="w3-bar" style="overflow: hidden; overflow-wrap: nowrap"> <button
<button class="w3-button w3-dark-grey"
class="w3-bar-item w3-button w3-theme-d1" @click=${() => tfrpc.rpc.connect(connection)}
@click=${() => self.connect(connection)} >
> Connect
Connect </button>
</button> <tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
<div class="w3-bar-item"> ${this.render_connection_summary(connection)}
${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)}
</li> </li>
`; `;
} }
@ -148,200 +92,81 @@ class TfTabConnectionsElement extends LitElement {
} }
render_connection(connection) { 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` return html`
${connection.connected <button
? html` class="w3-button w3-dark-grey"
<button @click=${() => tfrpc.rpc.closeConnection(connection.id)}
class="w3-button w3-theme-d1" >
@click=${() => tfrpc.rpc.closeConnection(connection.id)} Close
> </button>
Close
</button>
`
: undefined}
${connection.flags.one_shot ? '🔃' : undefined}
<tf-user id=${connection.id} .users=${this.users}></tf-user> <tf-user id=${connection.id} .users=${this.users}></tf-user>
${this.render_progress(
'recv',
connection.progress.in.total - connection.progress.in.current,
connection.progress.in.total
)}
${this.render_progress(
'send',
connection.progress.out.total - connection.progress.out.current,
connection.progress.out.total
)}
${connection.tunnel !== undefined ${connection.tunnel !== undefined
? '🚇' ? '🚇'
: html`(${connection.host}:${connection.port})`} : html`(${connection.host}:${connection.port})`}
<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> <ul>
${this.connections ${this.connections
.filter((x) => x.tunnel === this.connections.indexOf(connection)) .filter((x) => x.tunnel === this.connections.indexOf(connection))
.map((x) => html`<li>${this.render_connection(x)}</li>`)} .map((x) => html`<li>${this.render_connection(x)}</li>`)}
${this.render_room_peers(connection.id)} ${this.render_room_peers(connection.id)}
</ul> </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() { render() {
let self = this; let self = this;
return html` return html`
<div class="w3-container" style="box-sizing: border-box"> <div class="w3-container">
<h2>New Connection</h2> <h2>New Connection</h2>
<textarea class="w3-input w3-theme-d1" id="code"></textarea> <textarea class="w3-input w3-dark-grey" id="code"></textarea>
${this.render_message(this.renderRoot.getElementById('code')?.value)}
<button <button
class="w3-button w3-theme-d1" class="w3-button w3-dark-grey"
@click=${() => @click=${() =>
self.connect(self.renderRoot.getElementById('code')?.value)} tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
> >
Connect Connect
</button> </button>
<h2 <h2>Broadcasts</h2>
class="w3-button w3-block w3-theme-d1" <ul>
@click=${() => self.toggle_accordian('connections')} ${this.broadcasts
> .filter((x) => x.address)
Connections (${this.valid_connections().length}) .map((x) => self.render_broadcast(x))}
</h2>
<ul class="w3-ul w3-border" id="connections">
${this.valid_connections().map(
(x) => html` <li class="w3-bar">${this.render_connection(x)}</li> `
)}
</ul> </ul>
<h2 <h2>Connections</h2>
class="w3-button w3-block w3-theme-d1" <ul>
@click=${() => self.toggle_accordian('broadcasts')} ${this.connections
> .filter((x) => x.tunnel === undefined)
Broadcasts (${this.valid_broadcasts().length}) .map((x) => html` <li>${this.render_connection(x)}</li> `)}
</h2>
<ul class="w3-ul w3-border w3-hide" id="broadcasts">
${this.valid_broadcasts().map((x) => self.render_broadcast(x))}
</ul> </ul>
<h2 <h2>Stored Connections (WIP)</h2>
class="w3-button w3-block w3-theme-d1" <ul>
@click=${() => self.toggle_accordian('stored_connections')}
>
Stored Connections (${this.stored_connections.length})
</h2>
<ul class="w3-ul w3-border w3-hide" id="stored_connections">
${this.stored_connections.map( ${this.stored_connections.map(
(x) => html` (x) => html`
<li> <li>
<div class="w3-bar"> <button
<button class="w3-button w3-dark-grey"
class="w3-bar-item w3-button w3-theme-d1" @click=${() => self.forget_stored_connection(x)}
@click=${() => self.forget_stored_connection(x)} >
> Forget
Forget </button>
</button> <button
<button class="w3-button w3-dark-grey"
class="w3-bar-item w3-button w3-theme-d1" @click=${() => tfrpc.rpc.connect(x)}
@click=${() => this.connect(x)} >
> Connect
Connect </button>
</button> ${x.address}:${x.port}
<div class="w3-bar-item"> <tf-user id=${x.pubkey} .users=${self.users}></tf-user>
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
<div><small>${x.address}:${x.port}</small></div>
</div>
</div>
${this.render_message(x)}
</li> </li>
` `
)} )}
</ul> </ul>
<h2 <h2>Local Accounts</h2>
class="w3-button w3-block w3-theme-d1" <ul>
@click=${() => self.toggle_accordian('local_accounts')}
>
Local Accounts (${this.identities.length})
</h2>
<div class="w3-container w3-hide" id="local_accounts">
${this.identities.map( ${this.identities.map(
(x) => (x) =>
html`<div html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`
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>`
)} )}
</div> </ul>
</div> </div>
`; `;
} }

View 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);

View File

@ -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 * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js'; import {styles} from './tf-styles.js';
@ -12,12 +12,6 @@ class TfTabNewsFeedElement extends LitElement {
messages: {type: Array}, messages: {type: Array},
drafts: {type: Object}, drafts: {type: Object},
expanded: {type: Object}, expanded: {type: Object},
channels_unread: {type: Object},
channels_latest: {type: Object},
loading: {type: Number},
time_range: {type: Array},
time_loading: {type: Array},
private_messages: {type: Array},
}; };
} }
@ -32,256 +26,112 @@ class TfTabNewsFeedElement extends LitElement {
this.following = []; this.following = [];
this.drafts = {}; this.drafts = {};
this.expanded = {}; this.expanded = {};
this.channels_unread = {}; this.start_time = new Date().valueOf() - 24 * 60 * 60 * 1000;
this.channels_latest = {};
this.start_time = new Date().valueOf();
this.time_range = [0, 0];
this.time_loading = undefined;
this.loading = 0;
} }
channel() { async fetch_messages() {
return this.hash.startsWith('##') if (this.hash.startsWith('#@')) {
? this.hash.substring(2) let r = await tfrpc.rpc.query(
: this.hash.substring(1);
}
async _fetch_related_messages(messages) {
let refs = await tfrpc.rpc.query(
`
WITH
news AS (
SELECT value AS id FROM json_each(?)
)
SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
UNION
SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
`,
[JSON.stringify(messages.map((x) => x.id))]
);
let related_messages = await tfrpc.rpc.query(
`
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages
JOIN json_each(?2) refs ON messages.id = refs.value
JOIN json_each(?1) AS following ON messages.author = following.value
`,
[JSON.stringify(this.following), JSON.stringify(refs.map((x) => x.ref))]
);
let combined = [].concat(messages, related_messages);
let refs2 = await tfrpc.rpc.query(
`
WITH
news AS (
SELECT value AS id FROM json_each(?)
)
SELECT refs_out.ref AS ref FROM messages_refs refs_out JOIN news ON refs_out.message = news.id
UNION
SELECT refs_in.message AS ref FROM messages_refs refs_in JOIN news ON refs_in.ref = news.id
`,
[JSON.stringify(combined.map((x) => x.id))]
);
let t0 = new Date();
let result = [].concat(
combined,
await tfrpc.rpc.query(
` `
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature WITH mine AS (SELECT id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM json_each(?2) refs FROM messages
JOIN messages ON messages.id = refs.value WHERE messages.author = ?
JOIN json_each(?1) following ON messages.author = following.value ORDER BY sequence DESC
WHERE messages.content ->> 'type' != 'post' LIMIT 20)
`, SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
[ FROM mine
JSON.stringify(this.following), JOIN messages_refs ON mine.id = messages_refs.ref
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.time_loading = [start_time, end_time];
let result;
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 20)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM mentions
JOIN messages_refs ON mentions.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id JOIN messages ON messages_refs.message = messages.id
UNION UNION
SELECT TRUE AS is_primary, * FROM mentions SELECT * FROM mine
`, `,
[ [this.hash.substring(1)]
'"' + this.whoami.replace('"', '""') + '"',
JSON.stringify(this.following),
start_time,
end_time,
]
);
} else if (this.hash.startsWith('#@')) {
result = await tfrpc.rpc.query(
`
WITH
selected AS (SELECT rowid, id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
WHERE messages.author = ?1 AND (?2 IS NULL OR messages.timestamp >= 2) AND messages.timestamp < ?3
ORDER BY sequence DESC LIMIT 20
)
SELECT FALSE AS is_primary, messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM selected
JOIN messages_refs ON selected.id = messages_refs.ref
JOIN messages ON messages_refs.message = messages.id
UNION
SELECT TRUE AS is_primary, * FROM selected
`,
[this.hash.substring(1), start_time, end_time]
); );
return r;
} else if (this.hash.startsWith('#%')) { } 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 FROM messages
WHERE messages.id = ?1 WHERE id = ?1
UNION 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 FROM messages JOIN messages_refs
ON messages.id = messages_refs.message ON messages.id = messages_refs.message
WHERE messages_refs.ref = ?1 WHERE messages_refs.ref = ?1
`, `,
[this.hash.substring(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
UNION
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
FROM messages_fts(?5)
JOIN messages ON messages.rowid = messages_fts.rowid
JOIN json_each(?1) AS following ON messages.author = following.value
JOIN json_tree(messages.content, '$.mentions') AS mention ON mention.value = '#' || ?4
),
news AS (SELECT * FROM all_news
WHERE (?2 IS NULL OR all_news.timestamp >= ?2) AND all_news.timestamp < ?3
ORDER BY all_news.timestamp DESC LIMIT 20)
SELECT TRUE AS is_primary, news.* FROM news
`,
[
JSON.stringify(this.following),
start_time,
end_time,
this.hash.substring(2),
'"#' + this.hash.substring(2).replace('"', '""') + '"',
]
);
let t1 = new Date();
result = await this._fetch_related_messages(initial_messages);
let t2 = new Date();
console.log(
`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}`
);
} else if (this.hash == '#🔐') {
result = await tfrpc.rpc.query(
`
SELECT TRUE AS is_primary, messages.rowid, messages.id, previous, author, sequence, timestamp, hash, json(content) AS content, signature
FROM messages
JOIN json_each(?1) AS private_messages ON messages.id = private_messages.value
WHERE
(?2 IS NULL OR (messages.timestamp >= ?2)) AND messages.timestamp < ?3 AND
json(messages.content) LIKE '"%'
ORDER BY messages.sequence DESC LIMIT 20
`,
[JSON.stringify(this.private_messages), start_time, end_time]
);
result = (await this.decrypt(result)).filter((x) => x.decrypted);
} else { } else {
let t0 = new Date(); let promises = [];
let initial_messages = await tfrpc.rpc.query( const k_following_limit = 256;
` for (let i = 0; i < this.following.length; i += k_following_limit) {
WITH promises.push(
all_news AS ( tfrpc.rpc.query(
SELECT messages.rowid, messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature `
FROM messages WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
JOIN json_each(?) AS following ON messages.author = following.value FROM messages
), JOIN json_each(?) AS following ON messages.author = following.value
news AS ( WHERE messages.timestamp > ? AND messages.timestamp < ?
SELECT * FROM all_news ORDER BY messages.timestamp DESC)
WHERE all_news.timestamp < ?3 AND (?2 IS NULL OR all_news.timestamp >= ?2) SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
ORDER BY timestamp DESC LIMIT 20 FROM news
) JOIN messages_refs ON news.id = messages_refs.ref
SELECT TRUE AS is_primary, news.* FROM news JOIN messages ON messages_refs.message = messages.id
`, UNION
[JSON.stringify(this.following), start_time, end_time] SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
); FROM news
let t1 = new Date(); JOIN messages_refs ON news.id = messages_refs.message
result = await this._fetch_related_messages(initial_messages); JOIN messages ON messages_refs.ref = messages.id
let t2 = new Date(); UNION
console.log( SELECT news.* FROM news
`load of ${result.length} rows took ${(t2 - t0) / 1000} (${(t1 - t0) / 1000} to find ${initial_messages.length} initial messages, ${(t2 - t1) / 1000} to find ${result.length} total messages) following=${this.following.length} st=${start_time} et=${end_time}` `,
); [
JSON.stringify(this.following.slice(i, i + k_following_limit)),
this.start_time,
/*
** Don't show messages more than a day into the future to prevent
** messages with far-future timestamps from staying at the top forever.
*/
new Date().valueOf() + 24 * 60 * 60 * 1000,
]
)
);
}
return [].concat(...(await Promise.all(promises)));
} }
this.time_loading = undefined;
return result;
}
update_time_range_from_messages(messages) {
let only_primary = messages.filter((x) => x.is_primary);
this.time_range = [
only_primary.reduce(
(accumulator, current) => Math.min(accumulator, current.timestamp),
this.time_range[0]
),
only_primary.reduce(
(accumulator, current) => Math.max(accumulator, current.timestamp),
this.time_range[1]
),
];
} }
async load_more() { async load_more() {
this.loading++; let last_start_time = this.start_time;
this.loading_canceled = false; this.start_time = last_start_time - 24 * 60 * 60 * 1000;
try { let more = await tfrpc.rpc.query(
let more = []; `
let last_start_time = this.time_range[0]; WITH news AS (SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
try { FROM messages
more = await this.fetch_messages(null, last_start_time); JOIN json_each(?) AS following ON messages.author = following.value
} catch (e) { WHERE messages.timestamp > ?
console.log(e); AND messages.timestamp <= ?
} ORDER BY messages.timestamp DESC)
this.update_time_range_from_messages( SELECT messages.id, messages.previous, messages.author, messages.sequence, messages.timestamp, messages.hash, json(messages.content) AS content, messages.signature
more.filter((x) => x.timestamp < last_start_time) FROM news
); JOIN messages_refs ON news.id = messages_refs.ref
this.messages = await this.decrypt([...more, ...this.messages]); JOIN messages ON messages_refs.message = messages.id
} finally { UNION
this.loading--; 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
cancel_load() { UNION
this.loading_canceled = true; 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) { async decrypt(messages) {
console.log('decrypt');
let result = []; let result = [];
for (let message of messages) { for (let message of messages) {
let content; let content;
@ -306,143 +156,44 @@ class TfTabNewsFeedElement extends LitElement {
return result; return result;
} }
merge_messages(old_messages, new_messages) { async add_messages(messages) {
let old_by_id = Object.fromEntries(old_messages.map((x) => [x.id, x])); this.messages = await this.decrypt([...messages, ...this.messages]);
return new_messages.map((x) => (old_by_id[x.id] ? old_by_id[x.id] : x));
}
async load_latest() {
this.loading++;
let now = new Date().valueOf();
let end_time = now + 24 * 60 * 60 * 1000;
let messages = [];
try {
messages = await this.fetch_messages(this.time_range[0], end_time);
messages = await this.decrypt(messages);
this.update_time_range_from_messages(
messages.filter(
(x) => x.timestamp >= this.time_range[0] && x.timestamp < end_time
)
);
} finally {
this.loading--;
}
this.messages = this.merge_messages(
this.messages,
Object.values(
Object.fromEntries(
[...this.messages, ...messages]
.sort((x, y) => x.timestamp - y.timestamp)
.slice(-1024)
.map((x) => [x.id, x])
)
)
);
console.log('done loading latest messages.');
}
async load_messages() {
let start_time = new Date();
let self = this;
this.loading++;
let messages = [];
try {
if (this._messages_hash !== this.hash) {
this.messages = [];
this._messages_hash = this.hash;
}
this._messages_following = this.following;
let now = new Date().valueOf();
let start_time = now - 24 * 60 * 60 * 1000;
this.start_time = start_time;
this.time_range = [now + 24 * 60 * 60 * 1000, now + 24 * 60 * 60 * 1000];
messages = await this.fetch_messages(null, this.time_range[1]);
this.update_time_range_from_messages(
messages.filter((x) => x.timestamp < this.time_range[1])
);
messages = await this.decrypt(messages);
} finally {
this.loading--;
}
this.messages = this.merge_messages(this.messages, messages);
this.time_loading = undefined;
console.log(
`loading ${messages.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,
},
})
);
}
} }
render() { render() {
if ( if (
!this.messages || !this.messages ||
this._messages_hash !== this.hash || this._messages_hash !== this.hash ||
JSON.stringify(this._messages_following) !== this._messages_following !== this.following
JSON.stringify(this.following)
) { ) {
console.log( console.log(
`loading messages for ${this.whoami} (following ${this.following.length})` `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; let more;
if (!this.hash.startsWith('#%')) { if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
more = html` more = html`
<p> <p>
<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}> <button class="w3-button w3-dark-grey" @click=${this.load_more}>
Mark All Read
</button>
<button
?disabled=${this.loading}
class="w3-button w3-theme-d1"
@click=${this.load_more}
>
Load More Load More
</button> </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> </p>
`; `;
} }
return cache(html` return html`
<button class="w3-button w3-theme-d1" @click=${this.mark_all_read}>
Mark All Read
</button>
<tf-news <tf-news
id="news" id="news"
whoami=${this.whoami} whoami=${this.whoami}
@ -451,11 +202,9 @@ class TfTabNewsFeedElement extends LitElement {
.following=${this.following} .following=${this.following}
.drafts=${this.drafts} .drafts=${this.drafts}
.expanded=${this.expanded} .expanded=${this.expanded}
channel=${this.channel()}
channel_unread=${this.channels_unread?.[this.channel()]}
></tf-news> ></tf-news>
${more} ${more}
`); `;
} }
} }

View File

@ -1,11 +1,4 @@
import { import {LitElement, html, unsafeHTML, until} from './lit-all.min.js';
LitElement,
cache,
keyed,
html,
unsafeHTML,
until,
} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js'; import * as tfrpc from '/static/tfrpc.js';
import {styles} from './tf-styles.js'; import {styles} from './tf-styles.js';
@ -15,15 +8,10 @@ class TfTabNewsElement extends LitElement {
whoami: {type: String}, whoami: {type: String},
users: {type: Object}, users: {type: Object},
hash: {type: String}, hash: {type: String},
unread: {type: Array},
following: {type: Array}, following: {type: Array},
drafts: {type: Object}, drafts: {type: Object},
expanded: {type: Object}, expanded: {type: Object},
loading: {type: Boolean},
channels: {type: Array},
channels_unread: {type: Object},
channels_latest: {type: Object},
connections: {type: Array},
private_messages: {type: Array},
}; };
} }
@ -35,14 +23,11 @@ class TfTabNewsElement extends LitElement {
this.whoami = null; this.whoami = null;
this.users = {}; this.users = {};
this.hash = '#'; this.hash = '#';
this.unread = [];
this.following = []; this.following = [];
this.cache = {}; this.cache = {};
this.drafts = {}; this.drafts = {};
this.expanded = {}; this.expanded = {};
this.channels_unread = {};
this.channels_latest = {};
this.channels = [];
this.connections = [];
tfrpc.rpc.localStorageGet('drafts').then(function (d) { tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}'); self.drafts = JSON.parse(d || '{}');
}); });
@ -58,13 +43,39 @@ class TfTabNewsElement extends LitElement {
document.body.removeEventListener('keypress', this.on_keypress.bind(this)); document.body.removeEventListener('keypress', this.on_keypress.bind(this));
} }
load_latest() { show_more() {
let unread = this.unread;
let news = this.shadowRoot?.getElementById('news'); let news = this.shadowRoot?.getElementById('news');
if (news) { if (news) {
news.load_latest(); console.log('injecting messages', news.messages);
news.add_messages(
Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x])))
);
this.dispatchEvent(new CustomEvent('refresh'));
} }
} }
new_messages_text() {
if (!this.unread?.length) {
return 'No new messages.';
}
let counts = {};
for (let message of this.unread) {
let type = 'private';
try {
type = JSON.parse(message.content).type || type;
} catch {}
counts[type] = (counts[type] || 0) + 1;
}
return (
'↻ Show New: ' +
Object.keys(counts)
.sort()
.map((x) => counts[x].toString() + ' ' + x + 's')
.join(', ')
);
}
draft(event) { draft(event) {
let id = event.detail.id || ''; let id = event.detail.id || '';
let previous = this.drafts[id]; let previous = this.drafts[id];
@ -73,7 +84,10 @@ class TfTabNewsElement extends LitElement {
} else { } else {
delete this.drafts[id]; 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)); tfrpc.rpc.localStorageSet('drafts', JSON.stringify(this.drafts));
} }
@ -94,256 +108,48 @@ class TfTabNewsElement extends LitElement {
} }
} }
unread_status(channel) {
if (
this.channels_latest[channel] &&
this.channels_latest[channel] > 0 &&
(this.channels_unread[channel] === undefined ||
this.channels_unread[channel] <= this.channels_latest[channel])
) {
return '✉️ ';
}
}
show_sidebar() {
this.renderRoot.getElementById('sidebar').style.display = 'block';
this.renderRoot.getElementById('sidebar_overlay').style.display = 'block';
}
hide_sidebar() {
this.renderRoot.getElementById('sidebar').style.display = 'none';
this.renderRoot.getElementById('sidebar_overlay').style.display = 'none';
}
async channel_toggle_subscribed() {
let channel = this.hash.substring(2);
let subscribed = this.channels.indexOf(channel) != -1;
subscribed = !subscribed;
await tfrpc.rpc.appendMessage(this.whoami, {
type: 'channel',
channel: channel,
subscribed: subscribed,
});
if (subscribed) {
this.channels = [].concat([channel], this.channels).sort();
} else {
this.channels = this.channels.filter((x) => x != channel);
}
}
channel() {
return this.hash.startsWith('##') ? this.hash.substring(2) : undefined;
}
compare_follows() {
const now = new Date().valueOf();
return function (a, b) {
return (b[1].ts > now ? -1 : b[1].ts) - (a[1].ts > now ? -1 : a[1].ts);
};
}
suggested_follows() {
/*
** Filter out people who have used future timestamps so that they aren't
** pinned at the top.
*/
let self = this;
return Object.entries(this.users)
.filter((x) => x[1].follow_depth > 1)
.sort(self.compare_follows())
.slice(0, 8)
.map((x) => x[0]);
}
render_sidebar() {
return html`
<div
class="w3-sidebar w3-bar-block w3-theme-d1 w3-collapse w3-animate-left"
style="width: 2in; left: 0; z-index: 5; box-sizing: border-box; top: 0"
id="sidebar"
>
<div
class="w3-right w3-button w3-hide-large"
@click=${this.hide_sidebar}
>
&times;
</div>
${this.hash.startsWith('##') &&
this.channels.indexOf(this.hash.substring(2)) == -1
? html`
<div class="w3-bar-item w3-theme-d2">Viewing</div>
<a
href="#"
class="w3-bar-item w3-button"
style="font-weight: bold"
>${this.hash.substring(2)}</a
>
`
: undefined}
<h4 class="w3-bar-item w3-theme-d2">Channels</h4>
<a
href="#"
class="w3-bar-item w3-button"
style=${this.hash == '#' ? 'font-weight: bold' : undefined}
>${this.unread_status('')}general</a
>
<a
href="#@"
class="w3-bar-item w3-button"
style=${this.hash == '#@' ? 'font-weight: bold' : undefined}
>${this.unread_status('@')}@mentions</a
>
<a
href="#🔐"
class="w3-bar-item w3-button"
style=${this.hash == '#🔐' ? 'font-weight: bold' : undefined}
>${this.unread_status('🔐')}🔐private</a
>
${Object.keys(this.drafts)
.sort()
.map(
(x) => html`
<a
href=${'#' + encodeURIComponent(x)}
class="w3-bar-item w3-button"
style="text-wrap: nowrap; text-overflow: ellipsis"
>📝 ${this.drafts[x]?.text ?? x}</a
>
`
)}
${this.channels.map(
(x) => html`
<a
href=${'#' + encodeURIComponent('#' + x)}
class="w3-bar-item w3-button"
style=${this.hash == '##' + x ? 'font-weight: bold' : undefined}
>${this.unread_status(x)}#${x}</a
>
`
)}
<h4 class="w3-bar-item w3-theme-d2">Connections</h4>
${this.connections
.filter((x) => x.id && !x.destroy_reason)
.map(
(x) => html`
<tf-user
class="w3-bar-item"
style="max-width: 100%"
id=${x.id}
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() { render() {
let profile = let profile = this.hash.startsWith('#@')
this.hash.startsWith('#@') && this.hash != '#@' ? html`<tf-profile
? keyed( id=${this.hash.substring(1)}
this.hash.substring(1), whoami=${this.whoami}
html`<tf-profile .users=${this.users}
class="tf-profile" ></tf-profile>`
id=${this.hash.substring(1)} : undefined;
whoami=${this.whoami} return html`
.users=${this.users} <p class="w3-bar">
></tf-profile>` <button
) class="w3-bar-item w3-button w3-dark-grey"
: undefined; @click=${this.show_more}
let edit_profile; >
if ( ${this.new_messages_text()}
!this.loading && </button>
this.users[this.whoami]?.name === undefined && </p>
this.hash.substring(1) != this.whoami <div>
) { Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
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}
>
&#9776;
</div>
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
${edit_profile}
</div>
<div>
<tf-compose
id="tf-compose"
whoami=${this.whoami}
.users=${this.users}
.drafts=${this.drafts}
@tf-draft=${this.draft}
.channel=${this.channel()}
></tf-compose>
</div>
${profile}
<tf-tab-news-feed
id="news"
whoami=${this.whoami}
.users=${this.users}
.following=${this.following}
hash=${this.hash}
.drafts=${this.drafts}
.expanded=${this.expanded}
@tf-draft=${this.draft}
@tf-expand=${this.on_expand}
.channels_unread=${this.channels_unread}
.channels_latest=${this.channels_latest}
.private_messages=${this.private_messages}
></tf-tab-news-feed>
</div>
</div> </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>
`;
} }
} }

View File

@ -110,14 +110,14 @@ class TfTabQueryElement extends LitElement {
<textarea <textarea
id="search" id="search"
rows="8" rows="8"
class="w3-input w3-theme-d1" class="w3-input w3-dark-grey"
style="flex: 1; resize: vertical" style="flex: 1; resize: vertical"
@keydown=${this.search_keydown} @keydown=${this.search_keydown}
> >
${this.query}</textarea ${this.query}</textarea
> >
<button <button
class="w3-button w3-theme-d1" class="w3-button w3-dark-grey"
@click=${(event) => @click=${(event) =>
self.search(self.renderRoot.getElementById('search').value)} self.search(self.renderRoot.getElementById('search').value)}
> >

View File

@ -5,7 +5,6 @@ import {styles} from './tf-styles.js';
class TfTabSearchElement extends LitElement { class TfTabSearchElement extends LitElement {
static get properties() { static get properties() {
return { return {
drafts: {type: Object},
whoami: {type: String}, whoami: {type: String},
users: {type: Object}, users: {type: Object},
following: {type: Array}, following: {type: Array},
@ -23,10 +22,6 @@ class TfTabSearchElement extends LitElement {
this.users = {}; this.users = {};
this.following = []; this.following = [];
this.expanded = {}; this.expanded = {};
this.drafts = {};
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
self.drafts = JSON.parse(d || '{}');
});
} }
async search(query) { 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() { render() {
if (this.query !== this.last_query) { if (this.query !== this.last_query) {
this.last_query = this.query; this.last_query = this.query;
@ -95,10 +78,10 @@ class TfTabSearchElement extends LitElement {
let self = this; let self = this;
return html` return html`
<div style="display: flex; flex-direction: row; gap: 4px"> <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> <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-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button> <button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
</div> </div>
<tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} .drafts=${this.drafts} @tf-expand=${this.on_expand} @tf-draft=${this.draft}></tf-news> <tf-news id="news" whoami=${this.whoami} .messages=${this.messages} .users=${this.users} .expanded=${this.expanded} @tf-expand=${this.on_expand}></tf-news>
`; `;
} }
} }

View File

@ -18,10 +18,10 @@ class TfTagElement extends LitElement {
render() { render() {
let number = this.count ? html` (${this.count})` : undefined; let number = this.count ? html` (${this.count})` : undefined;
return html`<a return html`<a
href=${'#' + encodeURIComponent(this.tag)} href="#q=${this.tag}"
class="w3-tag w3-theme-d1 w3-round-4 w3-button" style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px"
>${this.tag}${number}</a >${this.tag}${number}</a
> `; >`;
} }
} }

View File

@ -6,7 +6,6 @@ class TfUserElement extends LitElement {
static get properties() { static get properties() {
return { return {
id: {type: String}, id: {type: String},
fallback_name: {type: String},
users: {type: Object}, users: {type: Object},
}; };
} }
@ -16,43 +15,32 @@ class TfUserElement extends LitElement {
constructor() { constructor() {
super(); super();
this.id = null; this.id = null;
this.fallback_name = null;
this.users = {}; this.users = {};
} }
render() { 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 = this.users?.[this.id]?.name;
name = html`<a target="_top" href=${'#' + this.id} name =
>${name ?? this.fallback_name ?? this.id}</a name !== undefined
>`; ? html`<a target="_top" href=${'#' + this.id}>${name}</a>`
: html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
if (user) { if (this.users[this.id]) {
let image_link = user.image; let image = this.users[this.id].image;
image_link = image = typeof image == 'string' ? image : image?.link;
typeof image_link == 'string' ? image_link : image_link?.link; return html` <div style="display: inline-block; font-weight: bold">
if (image_link !== undefined) { <img
image = html`<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%"
class=${'w3-theme-l4 ' + shape} ?hidden=${image === undefined}
style="width: 2em; height: 2em; vertical-align: middle; object-fit: cover" src="${image ? '/' + image + '/view' : undefined}"
src="/${image_link}/view" />
/>`; ${name}
} </div>`;
} else {
return html` <div style="display: inline-block; font-weight: bold">
${name}
</div>`;
} }
return html` <div
style="display: inline-block; vertical-align: middle; font-weight: bold; text-wrap: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis"
>
${image} ${name}
</div>`;
} }
} }

View File

@ -1,13 +1,6 @@
import * as linkify from './commonmark-linkify.js';
import * as hashtagify from './commonmark-hashtag.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) { function image(node, entering) {
if ( if (
node.firstChild?.type === 'text' && node.firstChild?.type === 'text' &&
@ -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) { export function markdown(md) {
let reader = new commonmark.Parser(); let reader = new commonmark.Parser({safe: true});
let writer = new commonmark.HtmlRenderer({safe: true}); let writer = new commonmark.HtmlRenderer();
writer.image = image; writer.image = image;
writer.code = code;
writer.attrs = attrs;
let parsed = reader.parse(md || ''); let parsed = reader.parse(md || '');
parsed = hashtagify.transform(parsed); parsed = hashtagify.transform(parsed);
parsed = linkify.transform(parsed);
let walker = parsed.walker(); let walker = parsed.walker();
let event, node; let event, node;
while ((event = walker.next())) { while ((event = walker.next())) {

View File

@ -482,7 +482,16 @@ class TributeRange {
} }
getDocument() { 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) { positionMenuAtCaret(scrollTo) {
@ -644,8 +653,8 @@ class TributeRange {
} }
getWindowSelection() { getWindowSelection() {
if (this.tribute.collection[0].iframe?.getSelection) { if (this.tribute.collection.iframe) {
return this.tribute.collection[0].iframe.getSelection() return this.tribute.collection.iframe.contentWindow.getSelection()
} }
return window.getSelection() return window.getSelection()

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
Hello, world!

1
apps/user_settings.json Normal file
View File

@ -0,0 +1 @@
{"type": "tildefriends-app", "emoji": "⚙️"}

60
apps/user_settings/app.js Normal file
View File

@ -0,0 +1,60 @@
import * as tfrpc from '/tfrpc.js';
tfrpc.register(async function getIdentities() {
return ssb.getIdentities();
});
tfrpc.register(async function createID(id) {
return await ssb.createIdentity();
});
tfrpc.register(async function getPrivateKey(id) {
return bip39Words(await ssb.getPrivateKey(id));
});
tfrpc.register(async function addID(id) {
return await ssb.addIdentity(bip39Bytes(id));
});
tfrpc.register(async function deleteID(id) {
return await ssb.deleteIdentity(id);
});
tfrpc.register(async function getThemes() {
// TODO
return ['solarized', 'gruvbox', 'light'];
});
tfrpc.register(async function getTheme() {
// TODO
return 'gruvbox';
});
tfrpc.register(async function setTheme() {
// TODO
console.warn('setTheme called - not implemented');
return null;
});
tfrpc.register(async function reload() {
await main();
});
async function main() {
// Get body.html
const body = utf8Decode(await getFile('body.html'));
// Build the document
const document = `
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/static/tildefriends-v1.css"/>
<script src="tf-theme-picker.js" type="module"></script>
<script src="tf-password-form.js" type="module"></script>
<script src="tf-delete-account-btn.js" type="module"></script>
<script src="tf-identity-manager.js" type="module"></script>
</head>
<body class="flex-column">
${body}
</body>
</html>`;
// Send it to the browser
app.setDocument(document);
}
main();

View File

@ -0,0 +1,20 @@
<h1>Your settings</h1>
<div class="box flex-column">
<h2>Appearance</h2>
<tf-theme-picker></tf-theme-picker>
</div>
<div class="box flex-column">
<h2>Danger Zone</h2>
<h3>Manage your identities</h3>
<tf-identity-manager></tf-identity-manager>
<h3>Change my password</h3>
<tf-password-form></tf-password-form>
<h3>Delete your account</h3>
<tf-delete-account-btn></tf-delete-account-btn>
</div>

120
apps/user_settings/lit-all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,36 @@
import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
class TfDeleteAccountButtonElement extends LitElement {
static get properties() {
return {};
}
constructor() {
super();
}
deleteAccount() {
const res = confirm(
'Are you really sure you want to delete your account ?'
);
if (!res) return;
console.warn('TODO');
}
render() {
return html`
<link rel="stylesheet" href="/static/tildefriends-v1.css" />
<span>This action is irreversible !</span>
<button class="red" @click=${this.deleteAccount}>
[Not implemented] Delete my Tilde Friends account
</button>
`;
}
}
customElements.define('tf-delete-account-btn', TfDeleteAccountButtonElement);

View File

@ -0,0 +1,118 @@
import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
class TfIdentityManagerElement extends LitElement {
static get properties() {
return {
ids: {type: Array},
};
}
constructor() {
super();
this.ids = [];
this.load();
}
async load() {
this.ids = await tfrpc.rpc.getIdentities();
}
async createIdentity() {
try {
const id = await tfrpc.rpc.createID();
alert('Successfully created: ' + id);
await tfrpc.rpc.reload();
} catch (err) {
alert('Error creating identity: ' + err);
}
}
async importIdentity() {
const words = this.renderRoot?.querySelector('#import-id-textarea').value;
if (!words) return;
try {
const newID = await tfrpc.rpc.addID(words);
if (newID) alert('Successfully imported a new identity.');
else alert('This identity already exists or is invalid.');
await tfrpc.rpc.reload();
} catch (err) {
alert('Error importing identity: ' + err);
}
}
async exportIdentity(id) {
alert(
'Your private key is:\n' +
(await tfrpc.rpc.getPrivateKey(id)) +
'\nDo not share it with anyone!'
);
}
async deleteIdentity(id) {
try {
if (
prompt(
'Are you sure you want to delete "' +
id +
'"? It cannot be recovered without the exported phrase.\\n\\nEnter the word "DELETE" to confirm you wish to delete it.'
) === 'DELETE'
) {
if (await tfrpc.rpc.deleteID(id)) {
alert('Successfully deleted ID: ' + id);
}
await tfrpc.rpc.reload();
}
} catch (e) {
alert('Error deleting ID: ' + e);
}
}
render() {
return html` <link rel="stylesheet" href="/static/tildefriends-v1.css" />
<style>
.id-span {
font-family: monospace;
margin-left: 8px;
}
</style>
<h4>Create a new identity</h4>
<button id="create-id" class="green" @click=${this.createIdentity}>
Create Identity
</button>
<h4>Import an SSB Identity from 12 BIP39 English Words</h4>
<textarea id="import-id-textarea" style="width: 100%" rows="4"></textarea>
<button class="green" @click=${this.importIdentity}>
Import Identity
</button>
<h4>Warning !</h4>
<strong
>Anybody that knows your private key can gain total access over your
account.</strong
>
<br /><br />
Tilde Friends' contributors will never ask you for your private key !
<ul>
${this.ids.map(
(id) =>
html` <li>
<button class="blue" @click=${() => this.exportIdentity(id)}>
Export Identity
</button>
<button class="red" @click=${() => this.deleteIdentity(id)}>
Delete Identity
</button>
<span class="id-span">${id}</span>
</li>`
)}
</ul>`;
}
}
customElements.define('tf-identity-manager', TfIdentityManagerElement);

View File

@ -0,0 +1,82 @@
import {LitElement, html} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
class TfPasswordFormElement extends LitElement {
static get properties() {
return {
//selected: {type: String},
};
}
constructor() {
super();
}
/**
* Checks a password against different requirements
* @param {string} password the password to validate
* @returns
*/
validatePassword(password) {
// TODO(tasiaiso)
return true;
}
submitPassword() {
const currentPwd = this.shadowRoot.getElementById('current').value;
const newPwd = this.shadowRoot.getElementById('new').value;
const repeatPwd = this.shadowRoot.getElementById('Repeat').value;
if (!(newPwd === repeatPwd)) {
return;
}
// TODO
// tfrpc.changePassword()
}
render() {
return html`
<link rel="stylesheet" href="/static/tildefriends-v1.css" />
<style>
.grid {
display: grid;
grid-template-columns: auto auto;
}
</style>
<div class="grid">
<label for="current">Current password:</label>
<input
type="password"
id="current"
name="current"
autocomplete="current-password"
/>
<label for="new">Enter new password:</label>
<input
type="password"
id="new"
name="new"
autocomplete="new-password"
/>
<label for="repeat">Repeat new password:</label>
<input
type="password"
id="repeat"
name="repeat"
autocomplete="new-password"
/>
</div>
<button @click=${this.submitPassword} class="red">
[Not implemented] Change my password
</button>
`;
}
}
customElements.define('tf-password-form', TfPasswordFormElement);

View File

@ -0,0 +1,51 @@
import {LitElement, html, nothing} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
class TfThemePickerElement extends LitElement {
static get properties() {
return {
selected: {type: String},
themes: {type: Array},
};
}
constructor() {
super();
this.load();
}
async load() {
this.themes = await tfrpc.rpc.getThemes();
this.selected = await tfrpc.rpc.getTheme();
let select = this.renderRoot?.querySelector('#theme-select');
select.value = this.selected;
}
changed(event) {
this.selected = event.srcElement.value;
console.log('selected theme', this.selected);
// TODO
}
render() {
return html`
<link rel="stylesheet" href="/static/tildefriends-v1.css" />
<label for="theme">[Not implemented] Choose your theme:</label>
<select
name="theme"
id="theme-select"
?hidden=${!this.themes?.length}
@change=${this.changed}
>
${(this.themes ?? []).map(
(name) => html`<option value=${name}>${name}</option>`
)}
</select>
`;
}
}
customElements.define('tf-theme-picker', TfThemePickerElement);

View File

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

View File

@ -1,100 +0,0 @@
let g_hash;
async function query(sql, params) {
let results = [];
await ssb.sqlAsync(sql, params, function (row) {
results.push(row);
});
return results;
}
async function resolve(id) {
try {
let blob = await ssb.blobGet(id);
if (blob) {
let json;
try {
json = JSON.parse(utf8Decode(blob));
} catch {
return {id: utf8Decode(blob)};
}
if (json?.links) {
for (let [key, value] of Object.entries(json.links)) {
json.links[key] = await resolve(value);
}
return json;
} else {
return 'huh?' + json;
}
} else {
return `missing<${id}>`;
}
} catch (e) {
return id + ': ' + e.message;
}
}
async function get_names(identities) {
return Object.fromEntries(
(
await query(
`
SELECT author, name FROM (
SELECT
messages.author,
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
messages.content ->> 'name' AS name
FROM messages
JOIN json_each(?) AS identities ON identities.value = messages.author
WHERE
json_extract(messages.content, '$.type') = 'about' AND
content ->> 'about' = messages.author AND name IS NOT NULL)
WHERE author_rank = 1
`,
[JSON.stringify(identities)]
)
).map((x) => [x.author, x.name])
);
}
async function render(hash) {
g_hash = hash;
if (!hash) {
let sites = await query(
`
SELECT site.author, site.id
FROM messages site
WHERE site.content ->> 'type' = 'web-init'
`,
[]
);
let names = await get_names(sites.map((x) => x.author));
if (hash === g_hash) {
await app.setDocument(
`<ul style="background-color: #ddd">${sites.map((x) => `<li><a target="_top" href="#${encodeURIComponent(x.id)}">${names[x.author] ?? x.author} - ${x.id}</a></li>`).join('\n')}</ul>`
);
}
} else {
let site_id =
hash.charAt(0) == '#'
? decodeURIComponent(hash.substring(1))
: decodeURIComponent(hash);
await app.setDocument(`<html style="margin: 0; padding: 0; width: 100vw; height: 100vh; margin: 0; padding: 0">
<body style="display: flex; flex-direction: column; width: 100vw; height: 100vh">
<iframe src="${encodeURIComponent(site_id)}/index.html" style="flex: 1 1; border: 0; background-color: #fff"></iframe>
</body>
</html>`);
}
}
core.register('message', async function message_handler(message) {
if (message.event == 'hashChange') {
await render(message.hash);
}
});
async function main() {
render(null);
}
main();

View File

@ -1,63 +0,0 @@
async function query(sql, params) {
let results = [];
await ssb.sqlAsync(sql, params, function (row) {
results.push(row);
});
return results;
}
function guess_content_type(name) {
if (name.endsWith('.html')) {
return 'text/html; charset=UTF-8';
} else if (name.endsWith('.js') || name.endsWith('.mjs')) {
return 'text/javascript; charset=UTF-8';
} else if (name.endsWith('.css')) {
return 'text/stylesheet; charset=UTF-8';
} else {
return 'application/binary';
}
}
async function main() {
let path = request.path.replaceAll(/(%[0-9a-fA-F]{2})/g, (x) =>
String.fromCharCode(parseInt(x.substring(1), 16))
);
let match = path.match(/^(%.{44}\.sha256)(?:\/)?(.*)$/);
let content_type = guess_content_type(request.path);
let root = await query(
`
SELECT root.content ->> 'root' AS root
FROM messages site
JOIN messages root
ON site.id = ? AND root.author = site.author AND root.content ->> 'site' = site.id
ORDER BY root.sequence DESC LIMIT 1
`,
[match[1]]
);
let root_id = root[0]['root'];
let last_id = root_id;
let blob = await ssb.blobGet(root_id);
try {
for (let part of match[2]?.split('/')) {
let dir = JSON.parse(utf8Decode(blob));
last_id = dir?.links[part];
blob = await ssb.blobGet(dir?.links[part]);
content_type = guess_content_type(part);
}
} catch {}
respond({
status_code: 200,
data: blob ? utf8Decode(blob) : `${last_id} not found`,
content_type: content_type,
});
}
main().catch(function (e) {
respond({
status_code: 200,
data: `${e.message}\n${e.stack}`,
content_type: 'text/plain',
});
});

View File

@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "👋", "emoji": "👋",
"previous": "&wAb7J6E35xEXpiXsQ6t1RaWTGIvlatUnyH8ipF6pVic=.sha256" "previous": "&zFISmRDAv+SXFonfZ9/sHNhrmMe+poTU22gwZzuSkT4=.sha256"
} }

View File

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

View File

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

View File

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

View File

@ -10,6 +10,17 @@
<link rel="stylesheet" href="brands.min.css" /> <link rel="stylesheet" href="brands.min.css" />
<style> <style>
body,
h1,
h2,
h3,
h4,
h5 {
font-family: 'Poppins', sans-serif;
}
body {
font-size: 16px;
}
img { img {
margin-bottom: -8px; margin-bottom: -8px;
} }
@ -28,14 +39,11 @@
<b>😎 Tilde Friends</b> <b>😎 Tilde Friends</b>
</h1> </h1>
<h1 class="w3-xxlarge w3-text-green"> <h1 class="w3-xxlarge w3-text-green">
<b <b>Make apps and friends from the comfort of your web browser.</b>
>the Secure Scuttlebutt decentralized social network client that's
<i>fancy🎩</i></b
>
</h1> </h1>
<p> <p>
In addition to participating in Secure Scuttlebutt, Tilde Friends is Tilde Friends is a platform for building, running, and sharing web
a platform for building, running, and sharing applications. applications.
</p> </p>
<p> <p>
Available for lots of devices: Available for lots of devices:
@ -47,46 +55,14 @@
</p> </p>
<a <a
class="w3-button w3-black w3-padding-large" class="w3-button w3-black w3-padding-large"
href="https://dev.tildefriends.net/cory/tildefriends/releases" href="https://www.tildefriends.net/~cory/releases/"
><i class="fa fa-download"></i> Download</a ><i class="fa fa-download"></i> Download</a
> >
<a <a
class="w3-button w3-black w3-padding-large" class="w3-button w3-black w3-padding-large"
href="https://www.tildefriends.net/~core/ssb/" href="https://www.tildefriends.net/~cory/apps/"
><i class="fa fa-link"></i> Try It</a ><i class="fa fa-link"></i> Try It</a
> >
<a
class="w3-button w3-black w3-padding-large"
href="https://dev.tildefriends.net/"
><i class="fa fa-mug-hot"></i> Development</a
>
<a
class="w3-button w3-black w3-padding-large"
href="https://docs.tildefriends.net/"
><i class="fa fa-book"></i> Documentation</a
>
<p>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://f-droid.org/en/packages/com.unprompted.tildefriends.fdroid/"
><img src="f-droid.svg" style="height: 2em; margin: 0" /> Get it
on F-Droid</a
>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://dev.tildefriends.net/releases/tildefriends-x86_64.AppImage"
>
<img src="appimage.svg" style="height: 2em; margin: 0" />
Get Linux 64-bit AppImage
</a>
<a
class="w3-button w3-round-large w3-padding w3-blue-gray w3-margin-top"
href="https://play.google.com/store/apps/details?id=com.unprompted.tildefriends"
>
<img src="googleplay.svg" style="height: 2em; margin: 0" />
Get it on Google Play (Open Testing)
</a>
</p>
</div> </div>
<div class="w3-col l4 m6"> <div class="w3-col l4 m6">
<img src="tildefriends.png" class="w3-image w3-right w3-hide-small" /> <img src="tildefriends.png" class="w3-image w3-right w3-hide-small" />
@ -94,55 +70,6 @@
</div> </div>
</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"
>Download</a
>
Tilde Friends or use
<a href="https://www.tildefriends.net/"
>https://www.tildefriends.net/</a
>.
</li>
<li>Create an account to identify yourself with that instance.</li>
<li>
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 --> <!-- SSB Section -->
<div class="w3-light-grey"> <div class="w3-light-grey">
<div class="w3-row-padding w3-padding-64"> <div class="w3-row-padding w3-padding-64">
@ -220,15 +147,11 @@
<!-- Technlology Section --> <!-- Technlology Section -->
<div class="w3-container w3-padding-64 w3-light-grey w3-center"> <div class="w3-container w3-padding-64 w3-light-grey w3-center">
<h1 class="w3-jumbo"><b>Built the Old Fashioned Way</b></h1> <h1 class="w3-jumbo"><b>Trusted Technology</b></h1>
<p> <p>Tilde Friends is built using boring, trusted tech.</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>
<p> <p>
Though of course for building Tilde Friends apps, you are free to use Though of course for building Tilde Friends apps, you are free to use
whatever fits on top. whatever fits.
</p> </p>
<div class="w3-row" style="margin-top: 64px"> <div class="w3-row" style="margin-top: 64px">
@ -262,7 +185,7 @@
<i class="fa fa-lock w3-text-purple w3-jumbo"></i> <i class="fa fa-lock w3-text-purple w3-jumbo"></i>
<p>libsodium</p> <p>libsodium</p>
</a> </a>
<a href="https://github.com/openssl/openssl/releases" class="w3-col s3"> <a href="https://www.openssl.org/" class="w3-col s3">
<i class="fa fa-shield-halved w3-text-green w3-jumbo"></i> <i class="fa fa-shield-halved w3-text-green w3-jumbo"></i>
<p>OpenSSL</p> <p>OpenSSL</p>
</a> </a>
@ -276,7 +199,7 @@
</div> </div>
<div class="w3-row" style="margin-top: 64px"> <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> <i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i>
<p>CodeMirror</p> <p>CodeMirror</p>
</a> </a>
@ -288,13 +211,6 @@
<i class="fa fa-fire w3-text-cyan w3-jumbo"></i> <i class="fa fa-fire w3-text-cyan w3-jumbo"></i>
<p>Lit</p> <p>Lit</p>
</a> </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"> <a href="https://www.gnu.org/software/make/" class="w3-col s3">
<i class="fa fa-hammer w3-text-teal w3-jumbo"></i> <i class="fa fa-hammer w3-text-teal w3-jumbo"></i>
<p>GNU Make</p> <p>GNU Make</p>

View File

@ -1,4 +1,4 @@
/* W3.CSS 5.01 March 14 2025 by Jan Egil and Borge Refsnes */ /* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit} html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ /* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
@ -108,10 +108,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px} .w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px} .w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px} .w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px} .w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word} .w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%} .w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
@ -153,9 +149,9 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important} .w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important} .w3-hover-none:hover{box-shadow:none!important}
/* Colors */ /* Colors */
.w3-amber,.w3-hover-amber:hover,.w3-warning{color:#000!important;background-color:#ffc107!important} .w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important} .w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
.w3-blue,.w3-hover-blue:hover,.w3-info,.w3-primary{color:#fff!important;background-color:#2196F3!important} .w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important} .w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important} .w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important} .w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
@ -170,24 +166,15 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important} .w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important} .w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important} .w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
.w3-red,.w3-hover-red:hover,.w3-danger{color:#fff!important;background-color:#f44336!important} .w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important} .w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important} .w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
.w3-yellow,.w3-hover-yellow:hover,.w3-note{color:#000!important;background-color:#ffeb3b!important} .w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important} .w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important} .w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover,.w3-secondary{color:#000!important;background-color:#9e9e9e!important}
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important} .w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important} .w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
.w3-emerald,.w3-hover-emerald:hover,.w3-success{color:#fff!important;background-color:#008a00!important}
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important} .w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important} .w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important} .w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
@ -245,4 +232,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important} .w3-border-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-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-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} .w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}

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