forked from cory/tildefriends
build: Add prettier to the project
This commit is contained in:
parent
17b92126de
commit
41024ddb79
1
.git-blame-ignore-revs
Normal file
1
.git-blame-ignore-revs
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Add prettier to the project
|
14
.prettierignore
Normal file
14
.prettierignore
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
node_modules
|
||||||
|
src
|
||||||
|
deps
|
||||||
|
.clang-format
|
||||||
|
|
||||||
|
# Minified files
|
||||||
|
**/*.min.css
|
||||||
|
**/*.min.js
|
||||||
|
**/leaflet.*
|
||||||
|
**/commonmark*
|
||||||
|
**/w3.css
|
||||||
|
apps/ssb/tribute.esm.js
|
||||||
|
apps/api/app.js
|
||||||
|
**/emojis.json
|
10
.prettierrc.yaml
Normal file
10
.prettierrc.yaml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
trailingComma: 'es5'
|
||||||
|
useTabs: true
|
||||||
|
semi: true
|
||||||
|
singleQuote: true
|
||||||
|
bracketSpacing: false
|
||||||
|
# overrides:
|
||||||
|
# - files: '**/*.json'
|
||||||
|
# options:
|
||||||
|
# useTabs: false
|
||||||
|
# tabWidth: 2
|
38
README.md
38
README.md
@ -1,43 +1,37 @@
|
|||||||
# Tilde Friends
|
# Tilde Friends
|
||||||
|
|
||||||
Tilde Friends is a tool for making and sharing.
|
Tilde Friends is a tool for making and sharing.
|
||||||
|
|
||||||
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
|
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.
|
||||||
Scuttlebutt, as well as a platform for writing and running web applications.
|
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
1. Make it easy and fun to run all sorts of web applications.
|
1. Make it easy and fun to run all sorts of web applications.
|
||||||
2. Provide security that is easy to understand and protects your data.
|
2. Provide security that is easy to understand and protects your data.
|
||||||
3. Make creating and sharing web applications accessible to anyone with a
|
3. Make creating and sharing web applications accessible to anyone with a browser.
|
||||||
browser.
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
|
|
||||||
all of those host platforms plus mingw64, iOS, and android.
|
|
||||||
|
|
||||||
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
|
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for all of those host platforms plus mingw64, iOS, and android.
|
||||||
are kept up to date in the tree.
|
|
||||||
2. To build, run `make debug` or `make release`. An executable will be
|
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies are kept up to date in the tree.
|
||||||
generated in a subdirectory of `out/`.
|
2. To build, run `make debug` or `make release`. An executable will be generated in a subdirectory of `out/`.
|
||||||
3. It's possible to build for Android, iOS, and Windows on Linux, if you have
|
3. It's possible to build for Android, iOS, and Windows on Linux, if you have the right dependencies in the right places. `make windebug winrelease iosdebug-ipa iosrelease-ipa release-apk`.
|
||||||
the right dependencies in the right places. `make windebug winrelease
|
|
||||||
iosdebug-ipa iosrelease-ipa release-apk`.
|
|
||||||
4. To build in docker, `docker build .`.
|
4. To build in docker, `docker build .`.
|
||||||
5. `make format` will normalize formatting to the coding standard.
|
5. `make format` will normalize formatting to the coding standard.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
By default, running the built `tildefriends` executable will start a web server
|
|
||||||
at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
|
||||||
|
|
||||||
The first user to create an account and log in will be granted administrative
|
By default, running the built `tildefriends` executable will start a web server at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
||||||
privileges. Further administration can be done at
|
|
||||||
<http://localhost:12345/~core/admin/>.
|
The first user to create an account and log in will be granted administrative privileges. Further administration can be done at <http://localhost:12345/~core/admin/>.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
Docs are a work in progress:
|
|
||||||
<https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
|
Docs are a work in progress: <https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
All code unless otherwise noted in is provided under the
|
|
||||||
[MIT](https://opensource.org/licenses/MIT) license.
|
All code unless otherwise noted in is provided under the [MIT](https://opensource.org/licenses/MIT) license.
|
||||||
|
@ -18,9 +18,13 @@ async function main() {
|
|||||||
for (let user of await core.users()) {
|
for (let user of await core.users()) {
|
||||||
data.users[user] = await core.permissionsForUser(user);
|
data.users[user] = await core.permissionsForUser(user);
|
||||||
}
|
}
|
||||||
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
|
await app.setDocument(
|
||||||
|
utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data))
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
await app.setDocument('<span style="color: #f00">Only an administrator can modify these settings.</span>');
|
await app.setDocument(
|
||||||
|
'<span style="color: #f00">Only an administrator can modify these settings.</span>'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
main();
|
main();
|
@ -1,7 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="width: 100%">
|
<html style="width: 100%">
|
||||||
<head>
|
<head>
|
||||||
<script>const g_data = $data;</script>
|
<script>
|
||||||
|
const g_data = $data;
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff; width: 100%">
|
<body style="color: #fff; width: 100%">
|
||||||
<h1>Tilde Friends Administration</h1>
|
<h1>Tilde Friends Administration</h1>
|
||||||
|
@ -3,25 +3,32 @@ import * as tfrpc from '/static/tfrpc.js';
|
|||||||
|
|
||||||
function delete_user(user) {
|
function delete_user(user) {
|
||||||
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
|
if (confirm(`Are you sure you want to delete the user "${user}"?`)) {
|
||||||
tfrpc.rpc.delete_user(user).then(function() {
|
tfrpc.rpc
|
||||||
|
.delete_user(user)
|
||||||
|
.then(function () {
|
||||||
alert(`User "${user}" deleted successfully.`);
|
alert(`User "${user}" deleted successfully.`);
|
||||||
}).catch(function(error) {
|
})
|
||||||
alert(`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`);
|
.catch(function (error) {
|
||||||
|
alert(
|
||||||
|
`Failed to delete user "${user}": ${JSON.stringify(error, null, 2)}.`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function global_settings_set(key, value) {
|
function global_settings_set(key, value) {
|
||||||
tfrpc.rpc.global_settings_set(key, value).then(function() {
|
tfrpc.rpc
|
||||||
|
.global_settings_set(key, value)
|
||||||
|
.then(function () {
|
||||||
alert(`Set "${key}" to "${value}".`);
|
alert(`Set "${key}" to "${value}".`);
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', function () {
|
||||||
const permission_template = (permission) =>
|
const permission_template = (permission) => html` <code>${permission}</code>`;
|
||||||
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`
|
||||||
@ -62,26 +69,24 @@ window.addEventListener('load', function() {
|
|||||||
}
|
}
|
||||||
const user_template = (user, permissions) => html`
|
const user_template = (user, permissions) => html`
|
||||||
<li>
|
<li>
|
||||||
<button @click=${(e) => delete_user(user)}>
|
<button @click=${(e) => delete_user(user)}>Delete</button>
|
||||||
Delete
|
${user}: ${permissions.map((x) => permission_template(x))}
|
||||||
</button>
|
|
||||||
${user}:
|
|
||||||
${permissions.map(x => permission_template(x))}
|
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
const users_template = (users) =>
|
const users_template = (users) =>
|
||||||
html`<h2>Users</h2>
|
html`<h2>Users</h2>
|
||||||
<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%">
|
||||||
<h2>Global Settings</h2>
|
<h2>Global Settings</h2>
|
||||||
<div>
|
<div>
|
||||||
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
|
${Object.keys(data.settings)
|
||||||
|
.sort()
|
||||||
|
.map((x) => html`${input_template(x, data.settings[x])}`)}
|
||||||
</div>
|
</div>
|
||||||
${users_template(data.users)}
|
${users_template(data.users)}
|
||||||
</div>
|
</div>`;
|
||||||
`;
|
|
||||||
render(page_template(g_data), document.body);
|
render(page_template(g_data), document.body);
|
||||||
});
|
});
|
@ -26,7 +26,8 @@ async function fetch_info(apps) {
|
|||||||
async function fetch_shared_apps() {
|
async function fetch_shared_apps() {
|
||||||
let messages = {};
|
let messages = {};
|
||||||
|
|
||||||
await ssb.sqlAsync(`
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
SELECT messages.*
|
SELECT messages.*
|
||||||
FROM messages_fts('"application/tildefriends"')
|
FROM messages_fts('"application/tildefriends"')
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
@ -44,10 +45,13 @@ async function fetch_shared_apps() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let result = {};
|
let result = {};
|
||||||
for (let app of Object.values(messages).sort((x, y) => y.message.timestamp - x.message.timestamp)) {
|
for (let app of Object.values(messages).sort(
|
||||||
|
(x, y) => y.message.timestamp - x.message.timestamp
|
||||||
|
)) {
|
||||||
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app.blob)));
|
let app_object = JSON.parse(utf8Decode(await ssb.blobGet(app.blob)));
|
||||||
if (app_object) {
|
if (app_object) {
|
||||||
app_object.blob_id = app.blob;
|
app_object.blob_id = app.blob;
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
import * as commonmark from './commonmark.min.js';
|
import * as commonmark from './commonmark.min.js';
|
||||||
|
|
||||||
function escape(text) {
|
function escape(text) {
|
||||||
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
return (text ?? '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeAttribute(text) {
|
function escapeAttribute(text) {
|
||||||
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
return (text ?? '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_blog_message(id) {
|
export async function get_blog_message(id) {
|
||||||
@ -21,7 +29,8 @@ export async function get_blog_message(id) {
|
|||||||
blog: content?.blog,
|
blog: content?.blog,
|
||||||
title: content?.title,
|
title: content?.title,
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
);
|
||||||
if (message) {
|
if (message) {
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
`
|
`
|
||||||
@ -36,7 +45,8 @@ export async function get_blog_message(id) {
|
|||||||
[message.author],
|
[message.author],
|
||||||
function (row) {
|
function (row) {
|
||||||
message.name = row.name;
|
message.name = row.name;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
@ -51,8 +61,12 @@ export function markdown(md) {
|
|||||||
node = event.node;
|
node = event.node;
|
||||||
if (event.entering) {
|
if (event.entering) {
|
||||||
if (node.destination?.startsWith('&')) {
|
if (node.destination?.startsWith('&')) {
|
||||||
node.destination = '/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
node.destination =
|
||||||
} else if (node.destination?.startsWith('@') || node.destination?.startsWith('%')) {
|
'/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
||||||
|
} else if (
|
||||||
|
node.destination?.startsWith('@') ||
|
||||||
|
node.destination?.startsWith('%')
|
||||||
|
) {
|
||||||
node.destination = '/~core/ssb/#' + escape(node.destination);
|
node.destination = '/~core/ssb/#' + escape(node.destination);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +121,7 @@ export function render_html(blogs) {
|
|||||||
<h1>🪵Tilde Friends Blog</h1>
|
<h1>🪵Tilde Friends Blog</h1>
|
||||||
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
|
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
|
||||||
</div>
|
</div>
|
||||||
${blogs.map(blog_post => render_blog_post(blog_post)).join('\n')}
|
${blogs.map((blog_post) => render_blog_post(blog_post)).join('\n')}
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
}
|
}
|
||||||
@ -135,14 +149,15 @@ export function render_atom(blogs) {
|
|||||||
<link href="${core.url}"/>
|
<link href="${core.url}"/>
|
||||||
<id>${core.url}</id>
|
<id>${core.url}</id>
|
||||||
<updated>${new Date().toString()}</updated>
|
<updated>${new Date().toString()}</updated>
|
||||||
${blogs.map(blog_post => render_blog_post_atom(blog_post)).join('\n')}
|
${blogs.map((blog_post) => render_blog_post_atom(blog_post)).join('\n')}
|
||||||
</feed>`;
|
</feed>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_posts() {
|
export async function get_posts() {
|
||||||
let blogs = [];
|
let blogs = [];
|
||||||
let ids = await ssb.getIdentities();
|
let ids = await ssb.getIdentities();
|
||||||
await ssb.sqlAsync(`
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
WITH
|
WITH
|
||||||
blogs AS (
|
blogs AS (
|
||||||
SELECT
|
SELECT
|
||||||
@ -182,8 +197,11 @@ export async function get_posts() {
|
|||||||
JOIN public ON public.author = blogs.author
|
JOIN public ON public.author = blogs.author
|
||||||
LEFT OUTER JOIN names ON names.author = blogs.author
|
LEFT OUTER JOIN names ON names.author = blogs.author
|
||||||
ORDER BY blogs.timestamp DESC LIMIT 20
|
ORDER BY blogs.timestamp DESC LIMIT 20
|
||||||
`, [JSON.stringify(ids)], function(row) {
|
`,
|
||||||
|
[JSON.stringify(ids)],
|
||||||
|
function (row) {
|
||||||
blogs.push(row);
|
blogs.push(row);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return blogs;
|
return blogs;
|
||||||
}
|
}
|
@ -2,30 +2,50 @@ import * as blog from './blog.js';
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (request.path.startsWith('%') && request.path.endsWith('.sha256')) {
|
if (request.path.startsWith('%') && request.path.endsWith('.sha256')) {
|
||||||
let id = request.path.startsWith('%25') ? '%' + request.path.substring(3) : request.path;
|
let id = request.path.startsWith('%25')
|
||||||
|
? '%' + request.path.substring(3)
|
||||||
|
: request.path;
|
||||||
let message = await blog.get_blog_message(id);
|
let message = await blog.get_blog_message(id);
|
||||||
if (message) {
|
if (message) {
|
||||||
respond({data: await blog.render_blog_post_html(message), content_type: 'text/html; charset=utf-8'});
|
respond({
|
||||||
|
data: await blog.render_blog_post_html(message),
|
||||||
|
content_type: 'text/html; charset=utf-8',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
respond({data: `Message ${id} not found.`, content_type: 'text/html; charset=utf-8'});
|
respond({
|
||||||
|
data: `Message ${id} not found.`,
|
||||||
|
content_type: 'text/html; charset=utf-8',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (request.path == 'atom') {
|
} else if (request.path == 'atom') {
|
||||||
let blogs = await blog.get_posts();
|
let blogs = await blog.get_posts();
|
||||||
respond({data: blog.render_atom(blogs), content_type: 'application/atom+xml'});
|
respond({
|
||||||
|
data: blog.render_atom(blogs),
|
||||||
|
content_type: 'application/atom+xml',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
let blogs = await blog.get_posts();
|
let blogs = await blog.get_posts();
|
||||||
for (let blog_post of blogs) {
|
for (let blog_post of blogs) {
|
||||||
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
|
let title = (blog_post.title || '').replaceAll(/\W/g, '_').toLowerCase();
|
||||||
if (request.path === title) {
|
if (request.path === title) {
|
||||||
respond({data: await blog.render_blog_post_html(blog_post), content_type: 'text/html; charset=utf-8'});
|
respond({
|
||||||
|
data: await blog.render_blog_post_html(blog_post),
|
||||||
|
content_type: 'text/html; charset=utf-8',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
respond({data: blog.render_html(blogs), content_type: 'text/html; charset=utf-8'});
|
respond({
|
||||||
|
data: blog.render_html(blogs),
|
||||||
|
content_type: 'text/html; charset=utf-8',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(function (error) {
|
main().catch(function (error) {
|
||||||
respond({data: `<!DOCTYPE html>
|
respond({
|
||||||
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`, content_type: 'text/html'});
|
data: `<!DOCTYPE html>
|
||||||
|
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`,
|
||||||
|
content_type: 'text/html',
|
||||||
|
});
|
||||||
});
|
});
|
@ -21,7 +21,8 @@ async function contacts_internal(id, last_row_id, following, max_row_id) {
|
|||||||
json_extract(content, '$.type') = 'contact'
|
json_extract(content, '$.type') = 'contact'
|
||||||
ORDER BY sequence
|
ORDER BY sequence
|
||||||
`,
|
`,
|
||||||
[id, last_row_id, max_row_id]);
|
[id, last_row_id, max_row_id]
|
||||||
|
);
|
||||||
for (let row of contacts) {
|
for (let row of contacts) {
|
||||||
let contact = JSON.parse(row.content);
|
let contact = JSON.parse(row.content);
|
||||||
if (contact.following === true) {
|
if (contact.following === true) {
|
||||||
@ -42,15 +43,34 @@ async function contact(id, last_row_id, following, max_row_id) {
|
|||||||
return await contacts_internal(id, last_row_id, following, max_row_id);
|
return await contacts_internal(id, last_row_id, following, max_row_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) {
|
async function following_deep_internal(
|
||||||
let contacts = await Promise.all([...new Set(ids)].map(x => contact(x, last_row_id, following, max_row_id)));
|
ids,
|
||||||
|
depth,
|
||||||
|
blocking,
|
||||||
|
last_row_id,
|
||||||
|
following,
|
||||||
|
max_row_id
|
||||||
|
) {
|
||||||
|
let contacts = await Promise.all(
|
||||||
|
[...new Set(ids)].map((x) => contact(x, last_row_id, following, max_row_id))
|
||||||
|
);
|
||||||
let result = {};
|
let result = {};
|
||||||
for (let i = 0; i < ids.length; i++) {
|
for (let i = 0; i < ids.length; i++) {
|
||||||
let id = ids[i];
|
let id = ids[i];
|
||||||
let contact = contacts[i];
|
let contact = contacts[i];
|
||||||
let all_blocking = Object.assign({}, contact.blocking, blocking);
|
let all_blocking = Object.assign({}, contact.blocking, blocking);
|
||||||
let found = Object.keys(contact.following).filter(y => !all_blocking[y]);
|
let found = Object.keys(contact.following).filter((y) => !all_blocking[y]);
|
||||||
let deeper = depth > 1 ? await following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : [];
|
let deeper =
|
||||||
|
depth > 1
|
||||||
|
? await following_deep_internal(
|
||||||
|
found,
|
||||||
|
depth - 1,
|
||||||
|
all_blocking,
|
||||||
|
last_row_id,
|
||||||
|
following,
|
||||||
|
max_row_id
|
||||||
|
)
|
||||||
|
: [];
|
||||||
result[id] = [id, ...found, ...deeper];
|
result[id] = [id, ...found, ...deeper];
|
||||||
}
|
}
|
||||||
return [...new Set(Object.values(result).flat())];
|
return [...new Set(Object.values(result).flat())];
|
||||||
@ -68,10 +88,22 @@ async function following_deep(ids, depth, blocking) {
|
|||||||
last_row_id: 0,
|
last_row_id: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let max_row_id = (await query(`
|
let max_row_id = (
|
||||||
|
await query(
|
||||||
|
`
|
||||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
`, []))[0].max_row_id;
|
`,
|
||||||
let result = await following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
|
[]
|
||||||
|
)
|
||||||
|
)[0].max_row_id;
|
||||||
|
let result = await following_deep_internal(
|
||||||
|
ids,
|
||||||
|
depth,
|
||||||
|
blocking,
|
||||||
|
cache.last_row_id,
|
||||||
|
cache.following,
|
||||||
|
max_row_id
|
||||||
|
);
|
||||||
cache.last_row_id = max_row_id;
|
cache.last_row_id = max_row_id;
|
||||||
let store = JSON.stringify(cache);
|
let store = JSON.stringify(cache);
|
||||||
await db.set('following', store);
|
await db.set('following', store);
|
||||||
@ -90,13 +122,15 @@ async function fetch_about(db, ids, users) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
let max_row_id = 0;
|
let max_row_id = 0;
|
||||||
await ssb.sqlAsync(`
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
`,
|
`,
|
||||||
[],
|
[],
|
||||||
function (row) {
|
function (row) {
|
||||||
max_row_id = row.max_row_id;
|
max_row_id = row.max_row_id;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
for (let id of Object.keys(cache.about)) {
|
for (let id of Object.keys(cache.about)) {
|
||||||
if (ids.indexOf(id) == -1) {
|
if (ids.indexOf(id) == -1) {
|
||||||
delete cache.about[id];
|
delete cache.about[id];
|
||||||
@ -129,17 +163,21 @@ async function fetch_about(db, ids, users) {
|
|||||||
ORDER BY messages.author, messages.sequence
|
ORDER BY messages.author, messages.sequence
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
JSON.stringify(ids.filter(id => cache.about[id])),
|
JSON.stringify(ids.filter((id) => cache.about[id])),
|
||||||
JSON.stringify(ids.filter(id => !cache.about[id])),
|
JSON.stringify(ids.filter((id) => !cache.about[id])),
|
||||||
cache.last_row_id,
|
cache.last_row_id,
|
||||||
max_row_id,
|
max_row_id,
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
for (let about of abouts) {
|
for (let about of abouts) {
|
||||||
let content = JSON.parse(about.content);
|
let content = JSON.parse(about.content);
|
||||||
if (content.about === about.author) {
|
if (content.about === about.author) {
|
||||||
delete content.type;
|
delete content.type;
|
||||||
delete content.about;
|
delete content.about;
|
||||||
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
|
cache.about[about.author] = Object.assign(
|
||||||
|
cache.about[about.author] || {},
|
||||||
|
content
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache.last_row_id = max_row_id;
|
cache.last_row_id = max_row_id;
|
||||||
@ -155,24 +193,24 @@ async function getAbout(db, id) {
|
|||||||
if (g_about_cache[id]) {
|
if (g_about_cache[id]) {
|
||||||
return g_about_cache[id];
|
return g_about_cache[id];
|
||||||
}
|
}
|
||||||
let o = await db.get(id + ":about");
|
let o = await db.get(id + ':about');
|
||||||
const k_version = 4;
|
const k_version = 4;
|
||||||
let f = o ? JSON.parse(o) : o;
|
let f = o ? JSON.parse(o) : o;
|
||||||
if (!f || f.version != k_version) {
|
if (!f || f.version != k_version) {
|
||||||
f = {about: {}, sequence: 0, version: k_version};
|
f = {about: {}, sequence: 0, version: k_version};
|
||||||
}
|
}
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
"SELECT "+
|
'SELECT ' +
|
||||||
" sequence, "+
|
' sequence, ' +
|
||||||
" content "+
|
' content ' +
|
||||||
"FROM messages "+
|
'FROM messages ' +
|
||||||
"WHERE "+
|
'WHERE ' +
|
||||||
" author = ?1 AND "+
|
' author = ?1 AND ' +
|
||||||
" sequence > ?2 AND "+
|
' sequence > ?2 AND ' +
|
||||||
" json_extract(content, '$.type') = 'about' AND " +
|
" json_extract(content, '$.type') = 'about' AND " +
|
||||||
" json_extract(content, '$.about') = ?1 " +
|
" json_extract(content, '$.about') = ?1 " +
|
||||||
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
|
'UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 ' +
|
||||||
"ORDER BY sequence",
|
'ORDER BY sequence',
|
||||||
[id, f.sequence],
|
[id, f.sequence],
|
||||||
function (row) {
|
function (row) {
|
||||||
f.sequence = row.sequence;
|
f.sequence = row.sequence;
|
||||||
@ -180,16 +218,16 @@ async function getAbout(db, id) {
|
|||||||
let about = {};
|
let about = {};
|
||||||
try {
|
try {
|
||||||
about = JSON.parse(row.content);
|
about = JSON.parse(row.content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
delete about.about;
|
delete about.about;
|
||||||
delete about.type;
|
delete about.type;
|
||||||
f.about = Object.assign(f.about, about);
|
f.about = Object.assign(f.about, about);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
let j = JSON.stringify(f);
|
let j = JSON.stringify(f);
|
||||||
if (o != j) {
|
if (o != j) {
|
||||||
await db.set(id + ":about", j);
|
await db.set(id + ':about', j);
|
||||||
}
|
}
|
||||||
g_about_cache[id] = f.about;
|
g_about_cache[id] = f.about;
|
||||||
return f.about;
|
return f.about;
|
||||||
@ -198,15 +236,15 @@ async function getAbout(db, id) {
|
|||||||
async function getSize(db, id) {
|
async function getSize(db, id) {
|
||||||
let size = 0;
|
let size = 0;
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
"SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1",
|
'SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1',
|
||||||
[id],
|
[id],
|
||||||
function (row) {
|
function (row) {
|
||||||
size += row.size;
|
size += row.size;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function getSizes(ids) {
|
async function getSizes(ids) {
|
||||||
let sizes = {};
|
let sizes = {};
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
@ -221,7 +259,8 @@ async function getSizes(ids) {
|
|||||||
[JSON.stringify(ids)],
|
[JSON.stringify(ids)],
|
||||||
function (row) {
|
function (row) {
|
||||||
sizes[row.author] = row.size;
|
sizes[row.author] = row.size;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return sizes;
|
return sizes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +280,10 @@ function niceSize(bytes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function escape(value) {
|
function escape(value) {
|
||||||
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
return value
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@ -249,19 +291,27 @@ async function main() {
|
|||||||
let db = await database('ssb');
|
let db = await database('ssb');
|
||||||
let whoami = await ssb.getIdentities();
|
let whoami = await ssb.getIdentities();
|
||||||
let tree = '';
|
let tree = '';
|
||||||
await app.setDocument(`<pre style="color: #fff">Enumerating followed users...</pre>`);
|
await app.setDocument(
|
||||||
|
`<pre style="color: #fff">Enumerating followed users...</pre>`
|
||||||
|
);
|
||||||
let following = await following_deep(whoami, 2, {});
|
let following = await following_deep(whoami, 2, {});
|
||||||
await app.setDocument(`<pre style="color: #fff">Getting names and sizes...</pre>`);
|
await app.setDocument(
|
||||||
|
`<pre style="color: #fff">Getting names and sizes...</pre>`
|
||||||
|
);
|
||||||
let [about, sizes] = await Promise.all([
|
let [about, sizes] = await Promise.all([
|
||||||
fetch_about(db, following, {}),
|
fetch_about(db, following, {}),
|
||||||
getSizes(following),
|
getSizes(following),
|
||||||
]);
|
]);
|
||||||
await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
|
await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
|
||||||
following.sort((a, b) => ((sizes[b] ?? 0) - (sizes[a] ?? 0)));
|
following.sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0));
|
||||||
for (let id of following) {
|
for (let id of following) {
|
||||||
tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
|
tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
|
||||||
}
|
}
|
||||||
await app.setDocument('<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' + tree + '</ul>\n</body>\n</html>');
|
await app.setDocument(
|
||||||
|
'<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' +
|
||||||
|
tree +
|
||||||
|
'</ul>\n</body>\n</html>'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
@ -46,7 +46,7 @@ tfrpc.register(async function query(sql, args) {
|
|||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
tfrpc.register(async function store_blob(blob) {
|
tfrpc.register(async function store_blob(blob) {
|
||||||
if (typeof(blob) == 'string') {
|
if (typeof blob == 'string') {
|
||||||
blob = utf8Encode(blob);
|
blob = utf8Encode(blob);
|
||||||
}
|
}
|
||||||
if (Array.isArray(blob)) {
|
if (Array.isArray(blob)) {
|
||||||
@ -71,10 +71,15 @@ async function main() {
|
|||||||
let shared_db = await shared_database('state');
|
let shared_db = await shared_database('state');
|
||||||
attempt = await shared_db.get(core.user.credentials.session.name);
|
attempt = await shared_db.get(core.user.credentials.session.name);
|
||||||
}
|
}
|
||||||
app.setDocument(utf8Decode(getFile('index.html')).replace('${data}', JSON.stringify({
|
app.setDocument(
|
||||||
|
utf8Decode(getFile('index.html')).replace(
|
||||||
|
'${data}',
|
||||||
|
JSON.stringify({
|
||||||
attempt: attempt,
|
attempt: attempt,
|
||||||
state: core.user?.credentials?.session?.name,
|
state: core.user?.credentials?.session?.name,
|
||||||
})));
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
@ -63,7 +63,10 @@ export function gpx_parse(xml) {
|
|||||||
for (let trkseg of xml_each(trk, 'trkseg')) {
|
for (let trkseg of xml_each(trk, 'trkseg')) {
|
||||||
let segment = [];
|
let segment = [];
|
||||||
for (let trkpt of xml_each(trkseg, 'trkpt')) {
|
for (let trkpt of xml_each(trkseg, 'trkpt')) {
|
||||||
segment.push({lat: parseFloat(trkpt.attributes.lat), lon: parseFloat(trkpt.attributes.lon)});
|
segment.push({
|
||||||
|
lat: parseFloat(trkpt.attributes.lat),
|
||||||
|
lon: parseFloat(trkpt.attributes.lon),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
result.segments.push(segment);
|
result.segments.push(segment);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,26 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="width: 100%; height: 100%; margin: 0; padding: 0">
|
<html style="width: 100%; height: 100%; margin: 0; padding: 0">
|
||||||
<head>
|
<head>
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
let g_data = ${data};
|
let g_data = ${data};
|
||||||
</script>
|
</script>
|
||||||
<script src="script.js" type="module"></script>
|
<script src="script.js" type="module"></script>
|
||||||
<script src="leaflet.js"></script>
|
<script src="leaflet.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff; display: flex; flex-flow: column; height: 100%; width: 100%; margin: 0; padding: 0">
|
<body
|
||||||
|
style="
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
"
|
||||||
|
>
|
||||||
<gg-app style="width: 100%; height: 100%" id="ggapp"></gg-app>
|
<gg-app style="width: 100%; height: 100%" id="ggapp"></gg-app>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -19,7 +19,7 @@ function encode(current, previous, factor) {
|
|||||||
previous = py2_round(previous * factor);
|
previous = py2_round(previous * factor);
|
||||||
var coordinate = (current - previous) * 2;
|
var coordinate = (current - previous) * 2;
|
||||||
if (coordinate < 0) {
|
if (coordinate < 0) {
|
||||||
coordinate = -coordinate - 1
|
coordinate = -coordinate - 1;
|
||||||
}
|
}
|
||||||
var output = '';
|
var output = '';
|
||||||
while (coordinate >= 0x20) {
|
while (coordinate >= 0x20) {
|
||||||
@ -57,7 +57,6 @@ polyline.decode = function(str, precision) {
|
|||||||
// track of whether we've hit the end of the string. In each
|
// track of whether we've hit the end of the string. In each
|
||||||
// loop iteration, a single coordinate is decoded.
|
// loop iteration, a single coordinate is decoded.
|
||||||
while (index < str.length) {
|
while (index < str.length) {
|
||||||
|
|
||||||
// Reset shift, result, and byte
|
// Reset shift, result, and byte
|
||||||
byte = null;
|
byte = null;
|
||||||
shift = 1;
|
shift = 1;
|
||||||
@ -69,7 +68,7 @@ polyline.decode = function(str, precision) {
|
|||||||
shift *= 32;
|
shift *= 32;
|
||||||
} while (byte >= 0x20);
|
} while (byte >= 0x20);
|
||||||
|
|
||||||
latitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2);
|
latitude_change = result & 1 ? (-result - 1) / 2 : result / 2;
|
||||||
|
|
||||||
shift = 1;
|
shift = 1;
|
||||||
result = 0;
|
result = 0;
|
||||||
@ -80,7 +79,7 @@ polyline.decode = function(str, precision) {
|
|||||||
shift *= 32;
|
shift *= 32;
|
||||||
} while (byte >= 0x20);
|
} while (byte >= 0x20);
|
||||||
|
|
||||||
longitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2);
|
longitude_change = result & 1 ? (-result - 1) / 2 : result / 2;
|
||||||
|
|
||||||
lat += latitude_change;
|
lat += latitude_change;
|
||||||
lng += longitude_change;
|
lng += longitude_change;
|
||||||
@ -99,13 +98,18 @@ polyline.decode = function(str, precision) {
|
|||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
polyline.encode = function (coordinates, precision) {
|
polyline.encode = function (coordinates, precision) {
|
||||||
if (!coordinates.length) { return ''; }
|
if (!coordinates.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
var factor = Math.pow(10, Number.isInteger(precision) ? precision : 5),
|
var factor = Math.pow(10, Number.isInteger(precision) ? precision : 5),
|
||||||
output = encode(coordinates[0][0], 0, factor) + encode(coordinates[0][1], 0, factor);
|
output =
|
||||||
|
encode(coordinates[0][0], 0, factor) +
|
||||||
|
encode(coordinates[0][1], 0, factor);
|
||||||
|
|
||||||
for (var i = 1; i < coordinates.length; i++) {
|
for (var i = 1; i < coordinates.length; i++) {
|
||||||
var a = coordinates[i], b = coordinates[i - 1];
|
var a = coordinates[i],
|
||||||
|
b = coordinates[i - 1];
|
||||||
output += encode(a[0], b[0], factor);
|
output += encode(a[0], b[0], factor);
|
||||||
output += encode(a[1], b[1], factor);
|
output += encode(a[1], b[1], factor);
|
||||||
}
|
}
|
||||||
@ -150,7 +154,7 @@ polyline.toGeoJSON = function(str, precision) {
|
|||||||
var coords = polyline.decode(str, precision);
|
var coords = polyline.decode(str, precision);
|
||||||
return {
|
return {
|
||||||
type: 'LineString',
|
type: 'LineString',
|
||||||
coordinates: flipped(coords)
|
coordinates: flipped(coords),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import {LitElement, html, unsafeHTML, css, guard, until} from './lit-all.min.js';
|
import {
|
||||||
|
LitElement,
|
||||||
|
html,
|
||||||
|
unsafeHTML,
|
||||||
|
css,
|
||||||
|
guard,
|
||||||
|
until,
|
||||||
|
} from './lit-all.min.js';
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
import * as polyline from './polyline.js';
|
import * as polyline from './polyline.js';
|
||||||
import {gpx_parse} from './gpx.js';
|
import {gpx_parse} from './gpx.js';
|
||||||
@ -65,9 +72,12 @@ class GgAppElement extends LitElement {
|
|||||||
async load() {
|
async load() {
|
||||||
console.log('load');
|
console.log('load');
|
||||||
let emojis = await (await fetch('emojis.json')).json();
|
let emojis = await (await fetch('emojis.json')).json();
|
||||||
emojis = Object.values(emojis).map(x => Object.values(x)).flat();
|
emojis = Object.values(emojis)
|
||||||
|
.map((x) => Object.values(x))
|
||||||
|
.flat();
|
||||||
let today = new Date();
|
let today = new Date();
|
||||||
let date_index = today.getYear() * 356 + today.getMonth() * 31 + today.getDate();
|
let date_index =
|
||||||
|
today.getYear() * 356 + today.getMonth() * 31 + today.getDate();
|
||||||
this.emoji_of_the_day = emojis[(date_index * 123457) % emojis.length];
|
this.emoji_of_the_day = emojis[(date_index * 123457) % emojis.length];
|
||||||
this.user = await tfrpc.rpc.getUser();
|
this.user = await tfrpc.rpc.getUser();
|
||||||
this.url = (await tfrpc.rpc.url()).split('?')[0];
|
this.url = (await tfrpc.rpc.url()).split('?')[0];
|
||||||
@ -109,7 +119,8 @@ class GgAppElement extends LitElement {
|
|||||||
async get_activities_from_ssb() {
|
async get_activities_from_ssb() {
|
||||||
this.status = {text: 'loading activities'};
|
this.status = {text: 'loading activities'};
|
||||||
this.loaded_activities = [];
|
this.loaded_activities = [];
|
||||||
let rows = await tfrpc.rpc.query(`
|
let rows = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT messages.author, json_extract(mention.value, '$.link') AS blob_id
|
SELECT messages.author, json_extract(mention.value, '$.link') AS blob_id
|
||||||
FROM messages_fts('"gg-activity"')
|
FROM messages_fts('"gg-activity"')
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid,
|
JOIN messages ON messages.rowid = messages_fts.rowid,
|
||||||
@ -117,10 +128,15 @@ class GgAppElement extends LitElement {
|
|||||||
WHERE json_extract(messages.content, '$.type') = 'gg-activity' AND
|
WHERE json_extract(messages.content, '$.type') = 'gg-activity' AND
|
||||||
json_extract(mention.value, '$.name') = 'activity_data'
|
json_extract(mention.value, '$.name') = 'activity_data'
|
||||||
ORDER BY messages.timestamp DESC
|
ORDER BY messages.timestamp DESC
|
||||||
`, []);
|
`,
|
||||||
|
[]
|
||||||
|
);
|
||||||
this.status = {text: 'loading activity data'};
|
this.status = {text: 'loading activity data'};
|
||||||
let authors = rows.map(x => x.author);
|
let authors = rows.map((x) => x.author);
|
||||||
let blobs = await this.promise_all(rows.map(x => tfrpc.rpc.get_blob(x.blob_id)), 8);
|
let blobs = await this.promise_all(
|
||||||
|
rows.map((x) => tfrpc.rpc.get_blob(x.blob_id)),
|
||||||
|
8
|
||||||
|
);
|
||||||
this.status = {text: 'processing activity data'};
|
this.status = {text: 'processing activity data'};
|
||||||
for (let [index, blob] of blobs.entries()) {
|
for (let [index, blob] of blobs.entries()) {
|
||||||
let activity;
|
let activity;
|
||||||
@ -135,13 +151,19 @@ class GgAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.status = {text: 'calculating balance'};
|
this.status = {text: 'calculating balance'};
|
||||||
rows = await tfrpc.rpc.query(`
|
rows = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT count(*) AS currency FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-activity'
|
SELECT count(*) AS currency FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-activity'
|
||||||
`, [this.whoami]);
|
`,
|
||||||
|
[this.whoami]
|
||||||
|
);
|
||||||
let currency = rows[0].currency;
|
let currency = rows[0].currency;
|
||||||
rows = await tfrpc.rpc.query(`
|
rows = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT SUM(json_extract(content, '$.cost')) AS cost FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-place'
|
SELECT SUM(json_extract(content, '$.cost')) AS cost FROM messages WHERE author = ? AND json_extract(content, '$.type') = 'gg-place'
|
||||||
`, [this.whoami]);
|
`,
|
||||||
|
[this.whoami]
|
||||||
|
);
|
||||||
let spent = rows[0].cost;
|
let spent = rows[0].cost;
|
||||||
this.currency = currency - spent;
|
this.currency = currency - spent;
|
||||||
this.status = {text: 'getting placed emojis'};
|
this.status = {text: 'getting placed emojis'};
|
||||||
@ -166,8 +188,11 @@ class GgAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sync_activities() {
|
async sync_activities() {
|
||||||
let ids = this.activities.map(x => `https://www.strava.com/activities/${x.id}`);
|
let ids = this.activities.map(
|
||||||
let missing = await tfrpc.rpc.query(`
|
(x) => `https://www.strava.com/activities/${x.id}`
|
||||||
|
);
|
||||||
|
let missing = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
WITH my_activities AS (
|
WITH my_activities AS (
|
||||||
SELECT json_extract(mention.value, '$.link') AS url
|
SELECT json_extract(mention.value, '$.link') AS url
|
||||||
FROM messages, json_each(messages.content, '$.mentions') AS mention
|
FROM messages, json_each(messages.content, '$.mentions') AS mention
|
||||||
@ -178,17 +203,26 @@ class GgAppElement extends LitElement {
|
|||||||
SELECT from_strava.value FROM json_each(?) AS from_strava
|
SELECT from_strava.value FROM json_each(?) AS from_strava
|
||||||
LEFT OUTER JOIN my_activities ON from_strava.value = my_activities.url
|
LEFT OUTER JOIN my_activities ON from_strava.value = my_activities.url
|
||||||
WHERE my_activities.url IS NULL
|
WHERE my_activities.url IS NULL
|
||||||
`, [this.whoami, JSON.stringify(ids)]);
|
`,
|
||||||
|
[this.whoami, JSON.stringify(ids)]
|
||||||
|
);
|
||||||
console.log('missing = ', missing);
|
console.log('missing = ', missing);
|
||||||
for (let [index, row] of missing.entries()) {
|
for (let [index, row] of missing.entries()) {
|
||||||
this.status = {text: 'syncing from strava', value: index, max: missing.length};
|
this.status = {
|
||||||
|
text: 'syncing from strava',
|
||||||
|
value: index,
|
||||||
|
max: missing.length,
|
||||||
|
};
|
||||||
let url = row.value;
|
let url = row.value;
|
||||||
let id = url.match(/.*\/(\d+)/)[1];
|
let id = url.match(/.*\/(\d+)/)[1];
|
||||||
let response = await fetch(`https://www.strava.com/api/v3/activities/${id}`, {
|
let response = await fetch(
|
||||||
|
`https://www.strava.com/api/v3/activities/${id}`,
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${this.strava.access_token}`,
|
Authorization: `Bearer ${this.strava.access_token}`,
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
let activity = await response.json();
|
let activity = await response.json();
|
||||||
let blob_id = await tfrpc.rpc.store_blob(JSON.stringify(activity));
|
let blob_id = await tfrpc.rpc.store_blob(JSON.stringify(activity));
|
||||||
let message = {
|
let message = {
|
||||||
@ -201,7 +235,7 @@ class GgAppElement extends LitElement {
|
|||||||
{
|
{
|
||||||
link: blob_id,
|
link: blob_id,
|
||||||
name: 'activity_data',
|
name: 'activity_data',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message);
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
@ -215,13 +249,20 @@ class GgAppElement extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ids = await tfrpc.rpc.getIdentities();
|
let ids = await tfrpc.rpc.getIdentities();
|
||||||
let players = ids.length ? (await tfrpc.rpc.query(`
|
let players = ids.length
|
||||||
|
? (
|
||||||
|
await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT author FROM messages JOIN json_each(?) ON messages.author = json_each.value
|
SELECT author FROM messages JOIN json_each(?) ON messages.author = json_each.value
|
||||||
WHERE
|
WHERE
|
||||||
json_extract(messages.content, '$.type') = 'gg-player' AND
|
json_extract(messages.content, '$.type') = 'gg-player' AND
|
||||||
json_extract(messages.content, '$.active')
|
json_extract(messages.content, '$.active')
|
||||||
ORDER BY timestamp DESC limit 1
|
ORDER BY timestamp DESC limit 1
|
||||||
`, [JSON.stringify(ids)])).map(row => row.author) : [];
|
`,
|
||||||
|
[JSON.stringify(ids)]
|
||||||
|
)
|
||||||
|
).map((row) => row.author)
|
||||||
|
: [];
|
||||||
if (!players.length) {
|
if (!players.length) {
|
||||||
this.whoami = await tfrpc.rpc.createIdentity();
|
this.whoami = await tfrpc.rpc.createIdentity();
|
||||||
if (this.whoami) {
|
if (this.whoami) {
|
||||||
@ -246,9 +287,14 @@ class GgAppElement extends LitElement {
|
|||||||
await tfrpc.rpc.databaseSet('strava', shared);
|
await tfrpc.rpc.databaseSet('strava', shared);
|
||||||
await tfrpc.rpc.sharedDatabaseRemove(name);
|
await tfrpc.rpc.sharedDatabaseRemove(name);
|
||||||
}
|
}
|
||||||
this.strava = JSON.parse(await tfrpc.rpc.databaseGet('strava') || '{}');
|
this.strava = JSON.parse((await tfrpc.rpc.databaseGet('strava')) || '{}');
|
||||||
if (new Date().valueOf() / 1000 > this.strava.expires_at) {
|
if (new Date().valueOf() / 1000 > this.strava.expires_at) {
|
||||||
console.log('this looks expired', new Date().valueOf() / 1000, '>', this.strava.expires_at);
|
console.log(
|
||||||
|
'this looks expired',
|
||||||
|
new Date().valueOf() / 1000,
|
||||||
|
'>',
|
||||||
|
this.strava.expires_at
|
||||||
|
);
|
||||||
let x = await tfrpc.rpc.refresh_token(this.strava);
|
let x = await tfrpc.rpc.refresh_token(this.strava);
|
||||||
if (x) {
|
if (x) {
|
||||||
this.strava = x;
|
this.strava = x;
|
||||||
@ -261,13 +307,16 @@ class GgAppElement extends LitElement {
|
|||||||
|
|
||||||
async update_activities() {
|
async update_activities() {
|
||||||
if (this?.strava?.access_token) {
|
if (this?.strava?.access_token) {
|
||||||
let response = await fetch('https://www.strava.com/api/v3/athlete/activities', {
|
let response = await fetch(
|
||||||
|
'https://www.strava.com/api/v3/athlete/activities',
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${this.strava.access_token}`,
|
Authorization: `Bearer ${this.strava.access_token}`,
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
this.activities = await response.json();
|
this.activities = await response.json();
|
||||||
this.activities.sort((a, b) => (a.id - b.id));
|
this.activities.sort((a, b) => a.id - b.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,10 +331,12 @@ class GgAppElement extends LitElement {
|
|||||||
[k_color_default, '🟧'],
|
[k_color_default, '🟧'],
|
||||||
];
|
];
|
||||||
for (let m of k_map) {
|
for (let m of k_map) {
|
||||||
if (m[0][0] == color[0] &&
|
if (
|
||||||
|
m[0][0] == color[0] &&
|
||||||
m[0][1] == color[1] &&
|
m[0][1] == color[1] &&
|
||||||
m[0][2] == color[2] &&
|
m[0][2] == color[2] &&
|
||||||
m[0][3] == color[3]) {
|
m[0][3] == color[3]
|
||||||
|
) {
|
||||||
return m[1];
|
return m[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -329,9 +380,11 @@ class GgAppElement extends LitElement {
|
|||||||
on_click(event) {
|
on_click(event) {
|
||||||
let popup = L.popup()
|
let popup = L.popup()
|
||||||
.setLatLng(event.latlng)
|
.setLatLng(event.latlng)
|
||||||
.setContent(`
|
.setContent(
|
||||||
|
`
|
||||||
<div><a target="_top" href="https://www.google.com/maps/search/?api=1&query=${event.latlng.lat},${event.latlng.lng}">${event.latlng.lat}, ${event.latlng.lng}</a></div>
|
<div><a target="_top" href="https://www.google.com/maps/search/?api=1&query=${event.latlng.lat},${event.latlng.lng}">${event.latlng.lat}, ${event.latlng.lng}</a></div>
|
||||||
`)
|
`
|
||||||
|
)
|
||||||
.openOn(this.leaflet);
|
.openOn(this.leaflet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -368,31 +421,43 @@ class GgAppElement extends LitElement {
|
|||||||
on_marker_click(event) {
|
on_marker_click(event) {
|
||||||
this.popup = L.popup()
|
this.popup = L.popup()
|
||||||
.setLatLng(event.latlng)
|
.setLatLng(event.latlng)
|
||||||
.setContent(`
|
.setContent(
|
||||||
|
`
|
||||||
${this.to_build} (-${k_store[this.to_build]}) <input type="button" value="Build" onclick="document.getElementById('ggapp').build()"></input>
|
${this.to_build} (-${k_store[this.to_build]}) <input type="button" value="Build" onclick="document.getElementById('ggapp').build()"></input>
|
||||||
`)
|
`
|
||||||
|
)
|
||||||
.openOn(this.leaflet);
|
.openOn(this.leaflet);
|
||||||
}
|
}
|
||||||
|
|
||||||
snap_to_grid(latlng, fudge, zoom) {
|
snap_to_grid(latlng, fudge, zoom) {
|
||||||
let position = this.leaflet.options.crs.latLngToPoint(latlng, zoom ?? this.leaflet.getZoom());
|
let position = this.leaflet.options.crs.latLngToPoint(
|
||||||
|
latlng,
|
||||||
|
zoom ?? this.leaflet.getZoom()
|
||||||
|
);
|
||||||
position.x = Math.round(position.x / 16) * 16 + (fudge?.x ?? 0);
|
position.x = Math.round(position.x / 16) * 16 + (fudge?.x ?? 0);
|
||||||
position.y = Math.round(position.y / 16) * 16 + (fudge?.y ?? 0);
|
position.y = Math.round(position.y / 16) * 16 + (fudge?.y ?? 0);
|
||||||
position = this.leaflet.options.crs.pointToLatLng(position, zoom ?? this.leaflet.getZoom());
|
position = this.leaflet.options.crs.pointToLatLng(
|
||||||
|
position,
|
||||||
|
zoom ?? this.leaflet.getZoom()
|
||||||
|
);
|
||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
on_marker_move(event) {
|
on_marker_move(event) {
|
||||||
if (!this.no_snap && this.marker) {
|
if (!this.no_snap && this.marker) {
|
||||||
this.no_snap = true;
|
this.no_snap = true;
|
||||||
this.marker.setLatLng(this.snap_to_grid(this.marker.getLatLng(), k_marker_snap));
|
this.marker.setLatLng(
|
||||||
|
this.snap_to_grid(this.marker.getLatLng(), k_marker_snap)
|
||||||
|
);
|
||||||
this.no_snap = false;
|
this.no_snap = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
on_zoom(event) {
|
on_zoom(event) {
|
||||||
if (this.marker) {
|
if (this.marker) {
|
||||||
this.marker.setLatLng(this.snap_to_grid(this.marker.getLatLng(), k_marker_snap));
|
this.marker.setLatLng(
|
||||||
|
this.snap_to_grid(this.marker.getLatLng(), k_marker_snap)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,7 +468,10 @@ class GgAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.to_build) {
|
if (this.to_build) {
|
||||||
this.marker = L.marker(this.snap_to_grid(event.latlng, k_marker_snap), {icon: L.divIcon({className: 'build-icon'}), draggable: true}).addTo(this.leaflet);
|
this.marker = L.marker(this.snap_to_grid(event.latlng, k_marker_snap), {
|
||||||
|
icon: L.divIcon({className: 'build-icon'}),
|
||||||
|
draggable: true,
|
||||||
|
}).addTo(this.leaflet);
|
||||||
this.marker.on({click: this.on_marker_click.bind(this)});
|
this.marker.on({click: this.on_marker_click.bind(this)});
|
||||||
this.marker.on({drag: this.on_marker_move.bind(this)});
|
this.marker.on({drag: this.on_marker_move.bind(this)});
|
||||||
}
|
}
|
||||||
@ -417,7 +485,11 @@ class GgAppElement extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.leaflet) {
|
if (!this.leaflet) {
|
||||||
this.leaflet = L.map(map, {attributionControl: false, maxZoom: 16, bounceAtZoomLimits: false});
|
this.leaflet = L.map(map, {
|
||||||
|
attributionControl: false,
|
||||||
|
maxZoom: 16,
|
||||||
|
bounceAtZoomLimits: false,
|
||||||
|
});
|
||||||
this.leaflet.on({contextmenu: this.on_click.bind(this)});
|
this.leaflet.on({contextmenu: this.on_click.bind(this)});
|
||||||
this.leaflet.on({click: this.on_mouse_down.bind(this)});
|
this.leaflet.on({click: this.on_mouse_down.bind(this)});
|
||||||
this.leaflet.on({zoom: this.on_zoom.bind(this)});
|
this.leaflet.on({zoom: this.on_zoom.bind(this)});
|
||||||
@ -432,7 +504,7 @@ class GgAppElement extends LitElement {
|
|||||||
var context = tile.getContext('2d');
|
var context = tile.getContext('2d');
|
||||||
context.font = '10pt sans';
|
context.font = '10pt sans';
|
||||||
let bounds = this._tileCoordsToBounds(coords);
|
let bounds = this._tileCoordsToBounds(coords);
|
||||||
let degrees = 360.0 / (2 ** coords.z);
|
let degrees = 360.0 / 2 ** coords.z;
|
||||||
let ul = bounds.getNorthWest();
|
let ul = bounds.getNorthWest();
|
||||||
let lr = bounds.getSouthEast();
|
let lr = bounds.getSouthEast();
|
||||||
|
|
||||||
@ -442,33 +514,53 @@ class GgAppElement extends LitElement {
|
|||||||
let mini_context = mini.getContext('2d');
|
let mini_context = mini.getContext('2d');
|
||||||
let image_data = context.getImageData(0, 0, mini.width, mini.height);
|
let image_data = context.getImageData(0, 0, mini.width, mini.height);
|
||||||
for (let activity of self.loaded_activities) {
|
for (let activity of self.loaded_activities) {
|
||||||
self.draw_activity_to_tile(image_data, mini.width, mini.height, ul, lr, activity);
|
self.draw_activity_to_tile(
|
||||||
|
image_data,
|
||||||
|
mini.width,
|
||||||
|
mini.height,
|
||||||
|
ul,
|
||||||
|
lr,
|
||||||
|
activity
|
||||||
|
);
|
||||||
}
|
}
|
||||||
context.textAlign = 'left';
|
context.textAlign = 'left';
|
||||||
context.textBaseline = 'bottom';
|
context.textBaseline = 'bottom';
|
||||||
for (let x = 0; x < mini.width; x++) {
|
for (let x = 0; x < mini.width; x++) {
|
||||||
for (let y = 0; y < mini.height; y++) {
|
for (let y = 0; y < mini.height; y++) {
|
||||||
let start = (y * mini.width + x) * 4;
|
let start = (y * mini.width + x) * 4;
|
||||||
let pixel = self.color_to_emoji(image_data.data.slice(start, start + 4));
|
let pixel = self.color_to_emoji(
|
||||||
|
image_data.data.slice(start, start + 4)
|
||||||
|
);
|
||||||
if (pixel) {
|
if (pixel) {
|
||||||
//context.fillRect(x * size.x / mini.width, y * size.y / mini.height, size.x / mini.width, size.y / mini.height);
|
//context.fillRect(x * size.x / mini.width, y * size.y / mini.height, size.x / mini.width, size.y / mini.height);
|
||||||
context.fillText(pixel, x * size.x / mini.width, y * size.y / mini.height + mini.height);
|
context.fillText(
|
||||||
|
pixel,
|
||||||
|
(x * size.x) / mini.width,
|
||||||
|
(y * size.y) / mini.height + mini.height
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let placed of self.placed_emojis) {
|
for (let placed of self.placed_emojis) {
|
||||||
let position = self.leaflet.options.crs.latLngToPoint(self.snap_to_grid(placed.position, undefined, coords.z), coords.z);
|
let position = self.leaflet.options.crs.latLngToPoint(
|
||||||
|
self.snap_to_grid(placed.position, undefined, coords.z),
|
||||||
|
coords.z
|
||||||
|
);
|
||||||
let tile_x = Math.floor(position.x / size.x);
|
let tile_x = Math.floor(position.x / size.x);
|
||||||
let tile_y = Math.floor(position.y / size.y);
|
let tile_y = Math.floor(position.y / size.y);
|
||||||
position.x = position.x - tile_x * size.x;
|
position.x = position.x - tile_x * size.x;
|
||||||
position.y = position.y - tile_y * size.y;
|
position.y = position.y - tile_y * size.y;
|
||||||
if (tile_x == coords.x && tile_y == coords.y) {
|
if (tile_x == coords.x && tile_y == coords.y) {
|
||||||
//context.fillRect(position.x, position.y, size.x / mini.width, size.y / mini.height);
|
//context.fillRect(position.x, position.y, size.x / mini.width, size.y / mini.height);
|
||||||
context.fillText(placed.emoji, position.x, position.y + mini.height);
|
context.fillText(
|
||||||
|
placed.emoji,
|
||||||
|
position.x,
|
||||||
|
position.y + mini.height
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tile;
|
return tile;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
if (this.grid_layer) {
|
if (this.grid_layer) {
|
||||||
this.grid_layer.redraw();
|
this.grid_layer.redraw();
|
||||||
@ -484,10 +576,7 @@ class GgAppElement extends LitElement {
|
|||||||
this.max_lon = Math.max(this.max_lon, bounds.max.lng);
|
this.max_lon = Math.max(this.max_lon, bounds.max.lng);
|
||||||
}
|
}
|
||||||
if (this.focus) {
|
if (this.focus) {
|
||||||
this.leaflet.fitBounds([
|
this.leaflet.fitBounds([this.focus.min, this.focus.max]);
|
||||||
this.focus.min,
|
|
||||||
this.focus.max,
|
|
||||||
]);
|
|
||||||
this.focus = undefined;
|
this.focus = undefined;
|
||||||
} else {
|
} else {
|
||||||
this.leaflet.fitBounds([
|
this.leaflet.fitBounds([
|
||||||
@ -588,7 +677,12 @@ class GgAppElement extends LitElement {
|
|||||||
let sy = y0 < y1 ? 1 : -1;
|
let sy = y0 < y1 ? 1 : -1;
|
||||||
let error = dx + dy;
|
let error = dx + dy;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (x0 >= 0 && y0 >= 0 && x0 < image_data.width && y0 < image_data.height) {
|
if (
|
||||||
|
x0 >= 0 &&
|
||||||
|
y0 >= 0 &&
|
||||||
|
x0 < image_data.width &&
|
||||||
|
y0 < image_data.height
|
||||||
|
) {
|
||||||
let base = (y0 * image_data.width + x0) * 4;
|
let base = (y0 * image_data.width + x0) * 4;
|
||||||
image_data.data[base + 0] = value[0];
|
image_data.data[base + 0] = value[0];
|
||||||
image_data.data[base + 1] = value[1];
|
image_data.data[base + 1] = value[1];
|
||||||
@ -623,8 +717,8 @@ class GgAppElement extends LitElement {
|
|||||||
let last;
|
let last;
|
||||||
for (let pt of polyline.decode(activity.map.polyline)) {
|
for (let pt of polyline.decode(activity.map.polyline)) {
|
||||||
let px = [
|
let px = [
|
||||||
Math.floor(width * (pt[1] - ul.lng) / (lr.lng - ul.lng)),
|
Math.floor((width * (pt[1] - ul.lng)) / (lr.lng - ul.lng)),
|
||||||
Math.floor(height * (pt[0] - ul.lat) / (lr.lat - ul.lat)),
|
Math.floor((height * (pt[0] - ul.lat)) / (lr.lat - ul.lat)),
|
||||||
];
|
];
|
||||||
if (last) {
|
if (last) {
|
||||||
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
||||||
@ -637,8 +731,8 @@ class GgAppElement extends LitElement {
|
|||||||
let last;
|
let last;
|
||||||
for (let pt of segment) {
|
for (let pt of segment) {
|
||||||
let px = [
|
let px = [
|
||||||
Math.floor(width * (pt.lon - ul.lng) / (lr.lng - ul.lng)),
|
Math.floor((width * (pt.lon - ul.lng)) / (lr.lng - ul.lng)),
|
||||||
Math.floor(height * (pt.lat - ul.lat) / (lr.lat - ul.lat)),
|
Math.floor((height * (pt.lat - ul.lat)) / (lr.lat - ul.lat)),
|
||||||
];
|
];
|
||||||
if (last) {
|
if (last) {
|
||||||
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
this.line(image_data, last[0], last[1], px[0], px[1], color);
|
||||||
@ -667,7 +761,7 @@ class GgAppElement extends LitElement {
|
|||||||
{
|
{
|
||||||
link: blob_id,
|
link: blob_id,
|
||||||
name: 'activity_data',
|
name: 'activity_data',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
console.log('id =', this.whoami, 'message = ', message);
|
console.log('id =', this.whoami, 'message = ', message);
|
||||||
@ -693,8 +787,7 @@ class GgAppElement extends LitElement {
|
|||||||
|
|
||||||
focus_map(activity) {
|
focus_map(activity) {
|
||||||
let bounds = this.activity_bounds(activity);
|
let bounds = this.activity_bounds(activity);
|
||||||
if (bounds.min.lat < bounds.max.lat &&
|
if (bounds.min.lat < bounds.max.lat && bounds.min.lng < bounds.max.lng) {
|
||||||
bounds.min.lng < bounds.max.lng) {
|
|
||||||
this.tab = 'map';
|
this.tab = 'map';
|
||||||
this.focus = bounds;
|
this.focus = bounds;
|
||||||
}
|
}
|
||||||
@ -703,9 +796,13 @@ class GgAppElement extends LitElement {
|
|||||||
render_news() {
|
render_news() {
|
||||||
return html`
|
return html`
|
||||||
<ul>
|
<ul>
|
||||||
${this.loaded_activities.map(x => html`
|
${this.loaded_activities.map(
|
||||||
<li style="cursor: pointer" @click=${() => this.focus_map(x)}>${x.author} ${x.name ?? x.time}</li>
|
(x) => html`
|
||||||
`)}
|
<li style="cursor: pointer" @click=${() => this.focus_map(x)}>
|
||||||
|
${x.author} ${x.name ?? x.time}
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -714,7 +811,7 @@ class GgAppElement extends LitElement {
|
|||||||
let [emoji, cost] = item;
|
let [emoji, cost] = item;
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<input type="button" value="${emoji}" @click=${() => this.to_build = emoji}></input> ${cost} ${emoji == this.to_build ? '<-- Will be built next' : undefined}
|
<input type="button" value="${emoji}" @click=${() => (this.to_build = emoji)}></input> ${cost} ${emoji == this.to_build ? '<-- Will be built next' : undefined}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -732,7 +829,10 @@ class GgAppElement extends LitElement {
|
|||||||
render() {
|
render() {
|
||||||
let header;
|
let header;
|
||||||
if (!this.user?.credentials?.session?.name) {
|
if (!this.user?.credentials?.session?.name) {
|
||||||
header = html`<div style="flex: 1 0">Please <a target="_top" href="/login?return=${this.url}">login</a> to Tilde Friends, first.</div>`;
|
header = html`<div style="flex: 1 0">
|
||||||
|
Please <a target="_top" href="/login?return=${this.url}">login</a> to
|
||||||
|
Tilde Friends, first.
|
||||||
|
</div>`;
|
||||||
} else if (!this.strava?.access_token) {
|
} else if (!this.strava?.access_token) {
|
||||||
let strava_url = `https://www.strava.com/oauth/authorize?client_id=${k_client_id}&redirect_uri=${k_redirect_url}&response_type=code&approval_prompt=auto&scope=activity%3Aread&state=${g_data.state}`;
|
let strava_url = `https://www.strava.com/oauth/authorize?client_id=${k_client_id}&redirect_uri=${k_redirect_url}&response_type=code&approval_prompt=auto&scope=activity%3Aread&state=${g_data.state}`;
|
||||||
header = html`
|
header = html`
|
||||||
@ -765,10 +865,10 @@ class GgAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div id="navigation" style="display: flex; flex-direction: row">
|
<div id="navigation" style="display: flex; flex-direction: row">
|
||||||
<input type="button" id="button_map" @click=${() => this.tab = 'map'} value="🗺️Map"></input>
|
<input type="button" id="button_map" @click=${() => (this.tab = 'map')} value="🗺️Map"></input>
|
||||||
<input type="button" id="button_news" @click=${() => this.tab = 'news'} value="🏃News"></input>
|
<input type="button" id="button_news" @click=${() => (this.tab = 'news')} value="🏃News"></input>
|
||||||
<input type="button" id="button_friends" @click=${() => this.tab = 'friends'} value="👫Friends"></input>
|
<input type="button" id="button_friends" @click=${() => (this.tab = 'friends')} value="👫Friends"></input>
|
||||||
<input type="button" id="button_store" @click=${() => this.tab = 'store'} value="🏗️Store"></input>
|
<input type="button" id="button_store" @click=${() => (this.tab = 'store')} value="🏗️Store"></input>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -796,7 +896,9 @@ class GgAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="leaflet.css" />
|
<link rel="stylesheet" href="leaflet.css" />
|
||||||
<div style="width: 100%; height: 100%; display: flex; flex-direction: column">
|
<div
|
||||||
|
style="width: 100%; height: 100%; display: flex; flex-direction: column"
|
||||||
|
>
|
||||||
${header}
|
${header}
|
||||||
<div style="flex: 1 0; overflow: scroll">${content}</div>
|
<div style="flex: 1 0; overflow: scroll">${content}</div>
|
||||||
${navigation}
|
${navigation}
|
||||||
|
@ -18,7 +18,8 @@ tfrpc.register(async function reload() {
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
let ids = await ssb.getIdentities();
|
let ids = await ssb.getIdentities();
|
||||||
await app.setDocument(`<body style="color: #fff">
|
await app.setDocument(
|
||||||
|
`<body style="color: #fff">
|
||||||
<script>const handler = {};</script>
|
<script>const handler = {};</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.js';
|
||||||
@ -75,13 +76,18 @@ async function main() {
|
|||||||
<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button>
|
<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button>
|
||||||
<h2>Identities</h2>
|
<h2>Identities</h2>
|
||||||
<ul>` +
|
<ul>` +
|
||||||
ids.map(id => `<li>
|
ids
|
||||||
|
.map(
|
||||||
|
(id) => `<li>
|
||||||
<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button>
|
<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button>
|
||||||
<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button>
|
<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button>
|
||||||
${id}
|
${id}
|
||||||
</li>`).join('\n')+
|
</li>`
|
||||||
|
)
|
||||||
|
.join('\n') +
|
||||||
` </ul>
|
` </ul>
|
||||||
</body>`);
|
</body>`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
@ -97,7 +97,7 @@ core.register('onConnectionsChanged', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (typeof(database) !== 'undefined') {
|
if (typeof database !== 'undefined') {
|
||||||
g_database = await database('ssb');
|
g_database = await database('ssb');
|
||||||
}
|
}
|
||||||
await app.setDocument(utf8Decode(await getFile('index.html')));
|
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="color: #fff">
|
<html style="color: #fff">
|
||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends</title>
|
<title>Tilde Friends</title>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<tf-issues-app />
|
<tf-issues-app />
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="commonmark.min.js"></script>
|
<script src="commonmark.min.js"></script>
|
||||||
<script src="commonmark-linkify.js" type="module"></script>
|
<script src="commonmark-linkify.js" type="module"></script>
|
||||||
<script src="script.js" type="module"></script>
|
<script src="script.js" type="module"></script>
|
||||||
|
@ -31,7 +31,12 @@ class TfIdPickerElement extends LitElement {
|
|||||||
if (this.ids) {
|
if (this.ids) {
|
||||||
return html`
|
return html`
|
||||||
<select @change=${this.changed} style="max-width: 100%">
|
<select @change=${this.changed} style="max-width: 100%">
|
||||||
${(this.ids).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
${this.ids.map(
|
||||||
|
(id) =>
|
||||||
|
html`<option ?selected=${id == this.selected} value=${id}>
|
||||||
|
${id}
|
||||||
|
</option>`
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
@ -57,13 +62,15 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
this.dispatchEvent(new CustomEvent('tf-submit', {
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-submit', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
detail: {
|
detail: {
|
||||||
value: this.renderRoot.getElementById('input').value,
|
value: this.renderRoot.getElementById('input').value,
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
this.renderRoot.getElementById('input').value = '';
|
this.renderRoot.getElementById('input').value = '';
|
||||||
this.input();
|
this.input();
|
||||||
}
|
}
|
||||||
@ -96,7 +103,8 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
let issues = {};
|
let issues = {};
|
||||||
let messages = await tfrpc.rpc.query(`
|
let messages = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
WITH issues AS (SELECT messages.* 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'),
|
||||||
@ -107,7 +115,9 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
SELECT * FROM issues
|
SELECT * FROM issues
|
||||||
UNION
|
UNION
|
||||||
SELECT * FROM edits ORDER BY timestamp
|
SELECT * FROM edits ORDER BY timestamp
|
||||||
`, [k_project]);
|
`,
|
||||||
|
[k_project]
|
||||||
|
);
|
||||||
for (let message of messages) {
|
for (let message of messages) {
|
||||||
let content = JSON.parse(message.content);
|
let content = JSON.parse(message.content);
|
||||||
switch (content.type) {
|
switch (content.type) {
|
||||||
@ -123,7 +133,7 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
break;
|
break;
|
||||||
case 'issue-edit':
|
case 'issue-edit':
|
||||||
case 'post':
|
case 'post':
|
||||||
for (let issue of (content.issues || [])) {
|
for (let issue of content.issues || []) {
|
||||||
if (issues[issue.link]) {
|
if (issues[issue.link]) {
|
||||||
if (issue.open !== undefined) {
|
if (issue.open !== undefined) {
|
||||||
issues[issue.link].open = issue.open;
|
issues[issue.link].open = issue.open;
|
||||||
@ -136,7 +146,9 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.issues = Object.values(issues).sort((x, y) => (y.open - x.open) || (y.created - x.created));
|
this.issues = Object.values(issues).sort(
|
||||||
|
(x, y) => y.open - x.open || y.created - x.created
|
||||||
|
);
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
for (let issue of this.issues) {
|
for (let issue of this.issues) {
|
||||||
if (issue.id == this.selected.id) {
|
if (issue.id == this.selected.id) {
|
||||||
@ -150,11 +162,20 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<tr>
|
<tr>
|
||||||
<td>${issue.open ? '☐ open' : '☑ closed'}</td>
|
<td>${issue.open ? '☐ open' : '☑ closed'}</td>
|
||||||
<td style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis">${issue.author}</td>
|
<td
|
||||||
<td style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer" @click=${() => this.selected = issue}>
|
style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
|
||||||
|
>
|
||||||
|
${issue.author}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer"
|
||||||
|
@click=${() => (this.selected = issue)}
|
||||||
|
>
|
||||||
${issue.text.split('\n')?.[0]}
|
${issue.text.split('\n')?.[0]}
|
||||||
</td>
|
</td>
|
||||||
<td>${new Date(issue.updated ?? issue.created).toLocaleDateString()}</td>
|
<td>
|
||||||
|
${new Date(issue.updated ?? issue.created).toLocaleDateString()}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -170,13 +191,21 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
<div>${new Date(update.timestamp).toLocaleString()}</div>
|
<div>${new Date(update.timestamp).toLocaleString()}</div>
|
||||||
<div>${update.author}</div>
|
<div>${update.author}</div>
|
||||||
<div>${message}</div>
|
<div>${message}</div>
|
||||||
<div>${update.open !== undefined ? (update.open ? 'issue opened' : 'issue closed') : undefined}</div>
|
<div>
|
||||||
|
${update.open !== undefined
|
||||||
|
? update.open
|
||||||
|
? 'issue opened'
|
||||||
|
: 'issue closed'
|
||||||
|
: undefined}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async set_open(id, open) {
|
async set_open(id, open) {
|
||||||
if (confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)) {
|
if (
|
||||||
|
confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)
|
||||||
|
) {
|
||||||
let whoami = this.shadowRoot.getElementById('picker').selected;
|
let whoami = this.shadowRoot.getElementById('picker').selected;
|
||||||
await tfrpc.rpc.appendMessage(whoami, {
|
await tfrpc.rpc.appendMessage(whoami, {
|
||||||
type: 'issue-edit',
|
type: 'issue-edit',
|
||||||
@ -207,7 +236,9 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
type: 'post',
|
type: 'post',
|
||||||
text: event.detail.value,
|
text: event.detail.value,
|
||||||
root: this.selected.id,
|
root: this.selected.id,
|
||||||
branch: this.selected.updates.length ? this.selected.updates[this.selected.updates.length - 1].id : this.selected.id,
|
branch: this.selected.updates.length
|
||||||
|
? this.selected.updates[this.selected.updates.length - 1].id
|
||||||
|
: this.selected.id,
|
||||||
issues: [
|
issues: [
|
||||||
{
|
{
|
||||||
link: this.selected.id,
|
link: this.selected.id,
|
||||||
@ -226,16 +257,18 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
${header}
|
${header}
|
||||||
<div>
|
<div>
|
||||||
<input type="button" value="Back" @click=${() => this.selected = undefined}></input>
|
<input type="button" value="Back" @click=${() => (this.selected = undefined)}></input>
|
||||||
${this.selected.open ?
|
${
|
||||||
html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>` :
|
this.selected.open
|
||||||
html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`}
|
? html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></input>`
|
||||||
|
: html`<input type="button" value="Reopen Issue" @click=${() => this.set_open(this.selected.id, true)}></input>`
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div>${new Date(this.selected.created).toLocaleString()}</div>
|
<div>${new Date(this.selected.created).toLocaleString()}</div>
|
||||||
<div>${this.selected.author}</div>
|
<div>${this.selected.author}</div>
|
||||||
<div>${this.selected.id}</div>
|
<div>${this.selected.id}</div>
|
||||||
<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
|
<div>${unsafeHTML(tfutils.markdown(this.selected.text))}</div>
|
||||||
${this.selected.updates.map(x => this.render_update(x))}
|
${this.selected.updates.map((x) => this.render_update(x))}
|
||||||
<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
|
<tf-compose @tf-submit=${this.reply_to_issue}></tf-compose>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
@ -250,7 +283,7 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
</tr>
|
</tr>
|
||||||
${this.issues.map(x => this.render_issue_table_row(x))}
|
${this.issues.map((x) => this.render_issue_table_row(x))}
|
||||||
</table>
|
</table>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,32 @@
|
|||||||
import * as linkify from './commonmark-linkify.js';
|
import * as linkify from './commonmark-linkify.js';
|
||||||
|
|
||||||
function image(node, entering) {
|
function image(node, entering) {
|
||||||
if (node.firstChild?.type === 'text' &&
|
if (
|
||||||
node.firstChild.literal.startsWith('video:')) {
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('video:')
|
||||||
|
) {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
this.lit(
|
||||||
|
'<video style="max-width: 100%; max-height: 480px" title="' +
|
||||||
|
this.esc(node.firstChild?.literal) +
|
||||||
|
'" controls>'
|
||||||
|
);
|
||||||
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
this.disableTags += 1;
|
this.disableTags += 1;
|
||||||
} else {
|
} else {
|
||||||
this.disableTags -= 1;
|
this.disableTags -= 1;
|
||||||
this.lit('</video>');
|
this.lit('</video>');
|
||||||
}
|
}
|
||||||
} else if (node.firstChild?.type === 'text' &&
|
} else if (
|
||||||
node.firstChild.literal.startsWith('audio:')) {
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('audio:')
|
||||||
|
) {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
this.lit(
|
||||||
|
'<audio style="height: 32px; max-width: 100%" title="' +
|
||||||
|
this.esc(node.firstChild?.literal) +
|
||||||
|
'" controls>'
|
||||||
|
);
|
||||||
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
this.disableTags += 1;
|
this.disableTags += 1;
|
||||||
} else {
|
} else {
|
||||||
@ -24,7 +36,11 @@ function image(node, entering) {
|
|||||||
} else {
|
} else {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
if (this.disableTags === 0) {
|
if (this.disableTags === 0) {
|
||||||
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>');
|
this.lit(
|
||||||
|
'<div class="img_caption">' +
|
||||||
|
this.esc(node.firstChild?.literal || node.destination) +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
||||||
this.lit('<img src="" alt="');
|
this.lit('<img src="" alt="');
|
||||||
} else {
|
} else {
|
||||||
@ -56,14 +72,20 @@ export function markdown(md) {
|
|||||||
node = event.node;
|
node = event.node;
|
||||||
if (event.entering) {
|
if (event.entering) {
|
||||||
if (node.type == 'link') {
|
if (node.type == 'link') {
|
||||||
if (node.destination.startsWith('@') &&
|
if (
|
||||||
node.destination.endsWith('.ed25519')) {
|
node.destination.startsWith('@') &&
|
||||||
|
node.destination.endsWith('.ed25519')
|
||||||
|
) {
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (node.destination.startsWith('%') &&
|
} else if (
|
||||||
node.destination.endsWith('.sha256')) {
|
node.destination.startsWith('%') &&
|
||||||
|
node.destination.endsWith('.sha256')
|
||||||
|
) {
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (node.destination.startsWith('&') &&
|
} else if (
|
||||||
node.destination.endsWith('.sha256')) {
|
node.destination.startsWith('&') &&
|
||||||
|
node.destination.endsWith('.sha256')
|
||||||
|
) {
|
||||||
node.destination = '/' + node.destination + '/view';
|
node.destination = '/' + node.destination + '/view';
|
||||||
}
|
}
|
||||||
} else if (node.type == 'image') {
|
} else if (node.type == 'image') {
|
||||||
|
@ -104,8 +104,7 @@ async function process_message(whoami, collection, message, kind, parent) {
|
|||||||
if (!x) {
|
if (!x) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (content.type !== kind ||
|
if (content.type !== kind || (parent && content.parent !== parent)) {
|
||||||
(parent && content.parent !== parent)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,7 +112,10 @@ async function process_message(whoami, collection, message, kind, parent) {
|
|||||||
if (content?.tombstone) {
|
if (content?.tombstone) {
|
||||||
delete collection[content.key];
|
delete collection[content.key];
|
||||||
} else {
|
} else {
|
||||||
collection[content.key] = Object.assign(collection[content.key] || {}, content);
|
collection[content.key] = Object.assign(
|
||||||
|
collection[content.key] || {},
|
||||||
|
content
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
collection[message.id] = Object.assign(content, {id: message.id});
|
collection[message.id] = Object.assign(content, {id: message.id});
|
||||||
@ -125,20 +127,29 @@ tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
|
|||||||
let whoami = await ssb.getIdentities();
|
let whoami = await ssb.getIdentities();
|
||||||
data = data ?? {};
|
data = data ?? {};
|
||||||
let rowid = 0;
|
let rowid = 0;
|
||||||
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
await ssb.sqlAsync(
|
||||||
|
'SELECT MAX(rowid) AS rowid FROM messages',
|
||||||
|
[],
|
||||||
|
function (row) {
|
||||||
rowid = row.rowid;
|
rowid = row.rowid;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
while (true) {
|
while (true) {
|
||||||
if (rowid == max_rowid) {
|
if (rowid == max_rowid) {
|
||||||
await new_message();
|
await new_message();
|
||||||
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
await ssb.sqlAsync(
|
||||||
|
'SELECT MAX(rowid) AS rowid FROM messages',
|
||||||
|
[],
|
||||||
|
function (row) {
|
||||||
rowid = row.rowid;
|
rowid = row.rowid;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let modified = false;
|
let modified = false;
|
||||||
let rows = [];
|
let rows = [];
|
||||||
await ssb.sqlAsync(`
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
SELECT messages.id, author, content, timestamp
|
SELECT messages.id, author, content, timestamp
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?1) AS id ON messages.author = id.value
|
JOIN json_each(?1) AS id ON messages.author = id.value
|
||||||
@ -152,7 +163,8 @@ tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
|
|||||||
[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent],
|
[JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent],
|
||||||
function (row) {
|
function (row) {
|
||||||
rows.push(row);
|
rows.push(row);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
max_rowid = rowid;
|
max_rowid = rowid;
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
if (await process_message(whoami, data, row, kind, parent)) {
|
if (await process_message(whoami, data, row, kind, parent)) {
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff">
|
<body style="color: #fff">
|
||||||
<tf-journal-app></tf-journal-app>
|
<tf-journal-app></tf-journal-app>
|
||||||
<script src="commonmark.min.js"></script>
|
<script src="commonmark.min.js"></script>
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="tf-journal-app.js" type="module"></script>
|
<script src="tf-journal-app.js" type="module"></script>
|
||||||
<script src="tf-journal-entry.js" type="module"></script>
|
<script src="tf-journal-entry.js" type="module"></script>
|
||||||
<script src="tf-id-picker.js" type="module"></script>
|
<script src="tf-id-picker.js" type="module"></script>
|
||||||
|
@ -19,15 +19,22 @@ class TfIdentityPickerElement extends LitElement {
|
|||||||
|
|
||||||
changed(event) {
|
changed(event) {
|
||||||
this.selected = event.srcElement.value;
|
this.selected = event.srcElement.value;
|
||||||
this.dispatchEvent(new Event('change', {
|
this.dispatchEvent(
|
||||||
|
new Event('change', {
|
||||||
srcElement: this,
|
srcElement: this,
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<select @change=${this.changed} style="max-width: 100%">
|
<select @change=${this.changed} style="max-width: 100%">
|
||||||
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
${(this.ids ?? []).map(
|
||||||
|
(id) =>
|
||||||
|
html`<option ?selected=${id == this.selected} value=${id}>
|
||||||
|
${id}
|
||||||
|
</option>`
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -28,9 +28,14 @@ class TfJournalAppElement extends LitElement {
|
|||||||
async read_journals() {
|
async read_journals() {
|
||||||
let max_rowid;
|
let max_rowid;
|
||||||
let journals;
|
let journals;
|
||||||
while (true)
|
while (true) {
|
||||||
{
|
[max_rowid, journals] = await tfrpc.rpc.collection(
|
||||||
[max_rowid, journals] = await tfrpc.rpc.collection([this.whoami], 'journal-entry', undefined, max_rowid, journals);
|
[this.whoami],
|
||||||
|
'journal-entry',
|
||||||
|
undefined,
|
||||||
|
max_rowid,
|
||||||
|
journals
|
||||||
|
);
|
||||||
this.journals = Object.assign({}, journals);
|
this.journals = Object.assign({}, journals);
|
||||||
console.log('JOURNALS', this.journals);
|
console.log('JOURNALS', this.journals);
|
||||||
}
|
}
|
||||||
@ -52,7 +57,11 @@ class TfJournalAppElement extends LitElement {
|
|||||||
};
|
};
|
||||||
message.recps = [this.whoami];
|
message.recps = [this.whoami];
|
||||||
print(message);
|
print(message);
|
||||||
message = await tfrpc.rpc.encrypt(this.whoami, message.recps, JSON.stringify(message));
|
message = await tfrpc.rpc.encrypt(
|
||||||
|
this.whoami,
|
||||||
|
message.recps,
|
||||||
|
JSON.stringify(message)
|
||||||
|
);
|
||||||
print(message);
|
print(message);
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message);
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
}
|
}
|
||||||
@ -62,12 +71,17 @@ class TfJournalAppElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed}></tf-id-picker>
|
<tf-id-picker
|
||||||
|
.ids=${this.ids}
|
||||||
|
selected=${this.whoami}
|
||||||
|
@change=${this.on_whoami_changed}
|
||||||
|
></tf-id-picker>
|
||||||
</div>
|
</div>
|
||||||
<tf-journal-entry
|
<tf-journal-entry
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.journals=${this.journals}
|
.journals=${this.journals}
|
||||||
@publish=${this.on_journal_publish}></tf-journal-entry>
|
@publish=${this.on_journal_publish}
|
||||||
|
></tf-journal-entry>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,13 +30,15 @@ class TfJournalEntryElement extends LitElement {
|
|||||||
|
|
||||||
async on_publish() {
|
async on_publish() {
|
||||||
console.log('publish', this.text);
|
console.log('publish', this.text);
|
||||||
this.dispatchEvent(new CustomEvent('publish', {
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('publish', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: {
|
detail: {
|
||||||
key: this.shadowRoot.getElementById('date_picker').value,
|
key: this.shadowRoot.getElementById('date_picker').value,
|
||||||
text: this.text,
|
text: this.text,
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
back_dates(count) {
|
back_dates(count) {
|
||||||
@ -63,19 +65,30 @@ class TfJournalEntryElement extends LitElement {
|
|||||||
console.log('RENDER ENTRY', this.key, this.journals?.[this.key]);
|
console.log('RENDER ENTRY', this.key, this.journals?.[this.key]);
|
||||||
return html`
|
return html`
|
||||||
<select id="date_picker" @change=${this.on_date_change}>
|
<select id="date_picker" @change=${this.on_date_change}>
|
||||||
${this.back_dates(10).map(x => html`
|
${this.back_dates(10).map(
|
||||||
<option value=${x}>${x}</option>
|
(x) => html` <option value=${x}>${x}</option> `
|
||||||
`)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
<div style="display: inline-flex; flex-direction: row">
|
<div style="display: inline-flex; flex-direction: row">
|
||||||
<button ?disabled=${this.text == this.journals?.[this.key]?.text} @click=${this.on_publish}>Publish</button>
|
<button
|
||||||
|
?disabled=${this.text == this.journals?.[this.key]?.text}
|
||||||
|
@click=${this.on_publish}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
<button @click=${this.on_discard}>Discard</button>
|
<button @click=${this.on_discard}>Discard</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<textarea
|
<textarea
|
||||||
style="flex: 1 1; min-height: 10em"
|
style="flex: 1 1; min-height: 10em"
|
||||||
@input=${this.on_edit} .value=${this.text ?? this.journals?.[this.key]?.text ?? ''}></textarea>
|
@input=${this.on_edit}
|
||||||
<div style="flex: 1 1">${unsafeHTML(this.markdown(this.text ?? this.journals?.[this.key]?.text))}</div>
|
.value=${this.text ?? this.journals?.[this.key]?.text ?? ''}
|
||||||
|
></textarea>
|
||||||
|
<div style="flex: 1 1">
|
||||||
|
${unsafeHTML(
|
||||||
|
this.markdown(this.text ?? this.journals?.[this.key]?.text)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="color: #fff">
|
<html style="color: #fff">
|
||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends</title>
|
<title>Tilde Friends</title>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<tf-sneaker-app />
|
<tf-sneaker-app />
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="filesaver.min.js"></script>
|
<script src="filesaver.min.js"></script>
|
||||||
<script src="jszip.min.js"></script>
|
<script src="jszip.min.js"></script>
|
||||||
<script src="script.js" type="module"></script>
|
<script src="script.js" type="module"></script>
|
||||||
|
@ -19,7 +19,8 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
|
|
||||||
async search() {
|
async search() {
|
||||||
let q = this.renderRoot.getElementById('search').value;
|
let q = this.renderRoot.getElementById('search').value;
|
||||||
let result = await tfrpc.rpc.query(`
|
let result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name
|
SELECT messages.author AS id, json_extract(messages.content, '$.name') AS name
|
||||||
FROM messages_fts(?)
|
FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
@ -31,8 +32,9 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
HAVING MAX(messages.sequence)
|
HAVING MAX(messages.sequence)
|
||||||
ORDER BY COUNT(*) DESC
|
ORDER BY COUNT(*) DESC
|
||||||
`,
|
`,
|
||||||
[`"${q.replaceAll('"', '""')}"`]);
|
[`"${q.replaceAll('"', '""')}"`]
|
||||||
this.feeds = Object.fromEntries(result.map(x => [x.id, x.name]));
|
);
|
||||||
|
this.feeds = Object.fromEntries(result.map((x) => [x.id, x.name]));
|
||||||
}
|
}
|
||||||
|
|
||||||
format_message(message) {
|
format_message(message) {
|
||||||
@ -70,6 +72,7 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
if (startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
|
if (startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
|
||||||
startsWith(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
|
startsWith(data, [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01]) ||
|
||||||
startsWith(data, [0xff, 0xd8, 0xff, 0xee]) ||
|
startsWith(data, [0xff, 0xd8, 0xff, 0xee]) ||
|
||||||
@ -98,17 +101,29 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
let all_messages = '';
|
let all_messages = '';
|
||||||
let sequence = -1;
|
let sequence = -1;
|
||||||
let messages_done = 0;
|
let messages_done = 0;
|
||||||
let messages_max = (await tfrpc.rpc.query('SELECT MAX(sequence) AS total FROM messages WHERE author = ?', [id]))[0].total;
|
let messages_max = (
|
||||||
|
await tfrpc.rpc.query(
|
||||||
|
'SELECT MAX(sequence) AS total FROM messages WHERE author = ?',
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
)[0].total;
|
||||||
while (true) {
|
while (true) {
|
||||||
let messages = await tfrpc.rpc.query(
|
let messages = await tfrpc.rpc.query(
|
||||||
'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100',
|
'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100',
|
||||||
[id, sequence]
|
[id, sequence]
|
||||||
);
|
);
|
||||||
if (messages?.length) {
|
if (messages?.length) {
|
||||||
all_messages += messages.map(x => JSON.stringify(this.format_message(x))).join('\n') + '\n';
|
all_messages +=
|
||||||
|
messages
|
||||||
|
.map((x) => JSON.stringify(this.format_message(x)))
|
||||||
|
.join('\n') + '\n';
|
||||||
sequence = messages[messages.length - 1].sequence;
|
sequence = messages[messages.length - 1].sequence;
|
||||||
messages_done += messages.length;
|
messages_done += messages.length;
|
||||||
this.progress = {name: 'messages', value: messages_done, max: messages_max};
|
this.progress = {
|
||||||
|
name: 'messages',
|
||||||
|
value: messages_done,
|
||||||
|
max: messages_max,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -122,7 +137,8 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
FROM messages
|
FROM messages
|
||||||
JOIN messages_refs ON messages.id = messages_refs.message
|
JOIN messages_refs ON messages.id = messages_refs.message
|
||||||
WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
|
WHERE messages.author = ? AND messages_refs.ref LIKE '&%.sha256'`,
|
||||||
[id]);
|
[id]
|
||||||
|
);
|
||||||
let blobs_done = 0;
|
let blobs_done = 0;
|
||||||
for (let row of blobs) {
|
for (let row of blobs) {
|
||||||
this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
|
this.progress = {name: 'blobs', value: blobs_done, max: blobs.length};
|
||||||
@ -133,7 +149,10 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
console.log(`Failed to get ${row.id}: ${e.message}`);
|
console.log(`Failed to get ${row.id}: ${e.message}`);
|
||||||
}
|
}
|
||||||
if (blob) {
|
if (blob) {
|
||||||
zip.file(`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, new Uint8Array(blob));
|
zip.file(
|
||||||
|
`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`,
|
||||||
|
new Uint8Array(blob)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
blobs_done++;
|
blobs_done++;
|
||||||
}
|
}
|
||||||
@ -181,7 +200,11 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let message = JSON.parse(line);
|
let message = JSON.parse(line);
|
||||||
this.progress = {name: 'messages', value: progress++, max: total_messages};
|
this.progress = {
|
||||||
|
name: 'messages',
|
||||||
|
value: progress++,
|
||||||
|
max: total_messages,
|
||||||
|
};
|
||||||
if (await tfrpc.rpc.store_message(message.value)) {
|
if (await tfrpc.rpc.store_message(message.value)) {
|
||||||
success.messages++;
|
success.messages++;
|
||||||
}
|
}
|
||||||
@ -202,7 +225,13 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
let progress;
|
let progress;
|
||||||
if (this.progress) {
|
if (this.progress) {
|
||||||
if (this.progress.max) {
|
if (this.progress.max) {
|
||||||
progress = html`<div><label for="progress">${this.progress.name}</label><progress value=${this.progress.value} max=${this.progress.max}></progress></div>`;
|
progress = html`<div>
|
||||||
|
<label for="progress">${this.progress.name}</label
|
||||||
|
><progress
|
||||||
|
value=${this.progress.value}
|
||||||
|
max=${this.progress.max}
|
||||||
|
></progress>
|
||||||
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
progress = html`<div><span>${this.progress.name}</span></div>`;
|
progress = html`<div><span>${this.progress.name}</span></div>`;
|
||||||
}
|
}
|
||||||
@ -218,13 +247,17 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
<input type="text" id="search" @keypress=${this.keypress}></input>
|
<input type="text" id="search" @keypress=${this.keypress}></input>
|
||||||
<input type="button" value="Search Users" @click=${this.search}></input>
|
<input type="button" value="Search Users" @click=${this.search}></input>
|
||||||
<ul>
|
<ul>
|
||||||
${Object.entries(this.feeds).map(([id, name]) => html`
|
${Object.entries(this.feeds).map(
|
||||||
|
([id, name]) => html`
|
||||||
<li>
|
<li>
|
||||||
${this.progress ? undefined : html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
|
${this.progress
|
||||||
|
? undefined
|
||||||
|
: html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
|
||||||
${name}
|
${name}
|
||||||
<code style="color: #ccc">${id}</code>
|
<code style="color: #ccc">${id}</code>
|
||||||
</li>
|
</li>
|
||||||
`)}
|
`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ core.register('onConnectionsChanged', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
if (typeof(database) !== 'undefined') {
|
if (typeof database !== 'undefined') {
|
||||||
g_database = await database('ssb');
|
g_database = await database('ssb');
|
||||||
}
|
}
|
||||||
await app.setDocument(utf8Decode(await getFile('index.html')));
|
await app.setDocument(utf8Decode(await getFile('index.html')));
|
||||||
|
@ -72,9 +72,11 @@ export function picker(callback, anchor) {
|
|||||||
list.appendChild(header);
|
list.appendChild(header);
|
||||||
let any = false;
|
let any = false;
|
||||||
for (let entry of Object.entries(row[1])) {
|
for (let entry of Object.entries(row[1])) {
|
||||||
if (search &&
|
if (
|
||||||
|
search &&
|
||||||
search.length &&
|
search.length &&
|
||||||
entry[0].toLowerCase().indexOf(search) == -1) {
|
entry[0].toLowerCase().indexOf(search) == -1
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let emoji = document.createElement('span');
|
let emoji = document.createElement('span');
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html style="color: #fff">
|
<html style="color: #fff">
|
||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends</title>
|
<title>Tilde Friends</title>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
<link rel="stylesheet" href="tribute.css" />
|
<link rel="stylesheet" href="tribute.css" />
|
||||||
<style>
|
<style>
|
||||||
.tribute-container {
|
.tribute-container {
|
||||||
@ -12,7 +12,9 @@
|
|||||||
</head>
|
</head>
|
||||||
<body style="background-color: #223a5e">
|
<body style="background-color: #223a5e">
|
||||||
<tf-app class="w3-deep-purple" />
|
<tf-app class="w3-deep-purple" />
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</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-linkify.js" type="module"></script>
|
||||||
|
@ -34,9 +34,13 @@ class TfElement extends LitElement {
|
|||||||
this.users = {};
|
this.users = {};
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.tags = [];
|
this.tags = [];
|
||||||
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || []; });
|
tfrpc.rpc.getBroadcasts().then((b) => {
|
||||||
tfrpc.rpc.getConnections().then(c => { self.connections = c || []; });
|
self.broadcasts = b || [];
|
||||||
tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
|
});
|
||||||
|
tfrpc.rpc.getConnections().then((c) => {
|
||||||
|
self.connections = c || [];
|
||||||
|
});
|
||||||
|
tfrpc.rpc.getHash().then((hash) => self.set_hash(hash));
|
||||||
tfrpc.register(function hashChanged(hash) {
|
tfrpc.register(function hashChanged(hash) {
|
||||||
self.set_hash(hash);
|
self.set_hash(hash);
|
||||||
});
|
});
|
||||||
@ -86,9 +90,14 @@ class TfElement extends LitElement {
|
|||||||
last_row_id: 0,
|
last_row_id: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let max_row_id = (await tfrpc.rpc.query(`
|
let max_row_id = (
|
||||||
|
await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
`, []))[0].max_row_id;
|
`,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
)[0].max_row_id;
|
||||||
for (let id of Object.keys(cache.about)) {
|
for (let id of Object.keys(cache.about)) {
|
||||||
if (ids.indexOf(id) == -1) {
|
if (ids.indexOf(id) == -1) {
|
||||||
delete cache.about[id];
|
delete cache.about[id];
|
||||||
@ -120,17 +129,21 @@ class TfElement extends LitElement {
|
|||||||
ORDER BY messages.author, messages.sequence
|
ORDER BY messages.author, messages.sequence
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
JSON.stringify(ids.filter(id => cache.about[id])),
|
JSON.stringify(ids.filter((id) => cache.about[id])),
|
||||||
JSON.stringify(ids.filter(id => !cache.about[id])),
|
JSON.stringify(ids.filter((id) => !cache.about[id])),
|
||||||
cache.last_row_id,
|
cache.last_row_id,
|
||||||
max_row_id,
|
max_row_id,
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
for (let about of abouts) {
|
for (let about of abouts) {
|
||||||
let content = JSON.parse(about.content);
|
let content = JSON.parse(about.content);
|
||||||
if (content.about === about.author) {
|
if (content.about === about.author) {
|
||||||
delete content.type;
|
delete content.type;
|
||||||
delete content.about;
|
delete content.about;
|
||||||
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
|
cache.about[about.author] = Object.assign(
|
||||||
|
cache.about[about.author] || {},
|
||||||
|
content
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache.last_row_id = max_row_id;
|
cache.last_row_id = max_row_id;
|
||||||
@ -150,10 +163,8 @@ class TfElement extends LitElement {
|
|||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
WHERE messages.id = ?
|
WHERE messages.id = ?
|
||||||
`,
|
`,
|
||||||
[
|
[JSON.stringify(this.following), id]
|
||||||
JSON.stringify(this.following),
|
);
|
||||||
id,
|
|
||||||
]);
|
|
||||||
if (messages && messages.length) {
|
if (messages && messages.length) {
|
||||||
this.unread = [...this.unread, ...messages];
|
this.unread = [...this.unread, ...messages];
|
||||||
this.unread = this.unread.slice(this.unread.length - 1024);
|
this.unread = this.unread.slice(this.unread.length - 1024);
|
||||||
@ -173,7 +184,7 @@ class TfElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async create_identity() {
|
async create_identity() {
|
||||||
if (confirm("Are you sure you want to create a new identity?")) {
|
if (confirm('Are you sure you want to create a new identity?')) {
|
||||||
await tfrpc.rpc.createIdentity();
|
await tfrpc.rpc.createIdentity();
|
||||||
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
this.ids = (await tfrpc.rpc.getIdentities()) || [];
|
||||||
if (this.ids && !this.whoami) {
|
if (this.ids && !this.whoami) {
|
||||||
@ -185,15 +196,30 @@ class TfElement extends LitElement {
|
|||||||
render_id_picker() {
|
render_id_picker() {
|
||||||
return html`
|
return html`
|
||||||
<div style="display: flex; gap: 8px">
|
<div style="display: flex; gap: 8px">
|
||||||
<tf-id-picker id="picker" style="flex: 1 1 auto" selected=${this.whoami} .ids=${this.ids} .users=${this.users} @change=${this._handle_whoami_changed}></tf-id-picker>
|
<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>
|
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>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load_recent_tags() {
|
async load_recent_tags() {
|
||||||
let start = new Date();
|
let start = new Date();
|
||||||
this.tags = await tfrpc.rpc.query(`
|
this.tags = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
WITH
|
WITH
|
||||||
recent AS (SELECT id, content FROM messages
|
recent AS (SELECT id, content FROM messages
|
||||||
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
|
WHERE messages.timestamp > ? AND json_extract(content, '$.type') = 'post'
|
||||||
@ -207,7 +233,9 @@ class TfElement extends LitElement {
|
|||||||
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
|
combined AS (SELECT id, tag FROM recent_channels UNION ALL SELECT id, tag FROM recent_mentions),
|
||||||
by_message AS (SELECT DISTINCT id, tag FROM combined)
|
by_message AS (SELECT DISTINCT id, tag FROM combined)
|
||||||
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
|
SELECT tag, COUNT(*) AS count FROM by_message GROUP BY tag ORDER BY count DESC LIMIT 10
|
||||||
`, [new Date() - 7 * 24 * 60 * 60 * 1000]);
|
`,
|
||||||
|
[new Date() - 7 * 24 * 60 * 60 * 1000]
|
||||||
|
);
|
||||||
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
|
console.log('tags took', (new Date() - start) / 1000.0, 'seconds');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,23 +269,53 @@ class TfElement extends LitElement {
|
|||||||
let users = this.users;
|
let users = this.users;
|
||||||
if (this.tab === 'news') {
|
if (this.tab === 'news') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-news id="tf-tab-news" .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
|
<tf-tab-news
|
||||||
|
id="tf-tab-news"
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
hash=${this.hash}
|
||||||
|
.unread=${this.unread}
|
||||||
|
@refresh=${() => (this.unread = [])}
|
||||||
|
></tf-tab-news>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'connections') {
|
} else if (this.tab === 'connections') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
|
<tf-tab-connections
|
||||||
|
.users=${this.users}
|
||||||
|
.connections=${this.connections}
|
||||||
|
.broadcasts=${this.broadcasts}
|
||||||
|
></tf-tab-connections>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'mentions') {
|
} else if (this.tab === 'mentions') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-mentions .following=${this.following} whoami=${this.whoami} .users=${this.users}}></tf-tab-mentions>
|
<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 .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
|
<tf-tab-search
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
query=${this.hash?.startsWith('#q=')
|
||||||
|
? decodeURIComponent(this.hash.substring(3))
|
||||||
|
: null}
|
||||||
|
></tf-tab-search>
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'query') {
|
} else if (this.tab === 'query') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-query .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#sql=') ? decodeURIComponent(this.hash.substring(5)) : null}></tf-tab-query>
|
<tf-tab-query
|
||||||
|
.following=${this.following}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
query=${this.hash?.startsWith('#sql=')
|
||||||
|
? decodeURIComponent(this.hash.substring(5))
|
||||||
|
: null}
|
||||||
|
></tf-tab-query>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -295,21 +353,32 @@ class TfElement extends LitElement {
|
|||||||
|
|
||||||
let tabs = html`
|
let tabs = html`
|
||||||
<div class="w3-bar w3-black">
|
<div class="w3-bar w3-black">
|
||||||
${Object.entries(k_tabs).map(([k, v]) => html`
|
${Object.entries(k_tabs).map(
|
||||||
<button title=${v} class="w3-bar-item w3-padding-large w3-hover-gray tab ${self.tab == v ? 'w3-red' : 'w3-black'}" @click=${() => self.set_tab(v)}>${k}</button>
|
([k, v]) => html`
|
||||||
`)}
|
<button
|
||||||
|
title=${v}
|
||||||
|
class="w3-bar-item w3-padding-large w3-hover-gray tab ${self.tab ==
|
||||||
|
v
|
||||||
|
? 'w3-red'
|
||||||
|
: 'w3-black'}"
|
||||||
|
@click=${() => self.set_tab(v)}
|
||||||
|
>
|
||||||
|
${k}
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
let contents =
|
let contents = !this.loaded
|
||||||
!this.loaded ?
|
? this.loading
|
||||||
this.loading ?
|
? html`<div>Loading...</div>`
|
||||||
html`<div>Loading...</div>` :
|
: html`<div>Select or create an identity.</div>`
|
||||||
html`<div>Select or create an identity.</div>` :
|
: this.render_tab();
|
||||||
this.render_tab();
|
|
||||||
return html`
|
return html`
|
||||||
${this.render_id_picker()}
|
${this.render_id_picker()} ${tabs}
|
||||||
${tabs}
|
${this.tags.map(
|
||||||
${this.tags.map(x => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`)}
|
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
|
||||||
|
)}
|
||||||
${contents}
|
${contents}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,9 @@ class TfComposeElement extends LitElement {
|
|||||||
link: link,
|
link: link,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
|
draft.mentions[link].name = name.startsWith('@')
|
||||||
|
? name.substring(1)
|
||||||
|
: name;
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
if (updated) {
|
if (updated) {
|
||||||
@ -72,27 +74,32 @@ class TfComposeElement extends LitElement {
|
|||||||
let preview = this.renderRoot.getElementById('preview');
|
let preview = this.renderRoot.getElementById('preview');
|
||||||
preview.innerHTML = this.process_text(edit.value);
|
preview.innerHTML = this.process_text(edit.value);
|
||||||
let content_warning = this.renderRoot.getElementById('content_warning');
|
let content_warning = this.renderRoot.getElementById('content_warning');
|
||||||
let content_warning_preview = this.renderRoot.getElementById('content_warning_preview');
|
let content_warning_preview = this.renderRoot.getElementById(
|
||||||
|
'content_warning_preview'
|
||||||
|
);
|
||||||
if (content_warning && content_warning_preview) {
|
if (content_warning && content_warning_preview) {
|
||||||
content_warning_preview.innerText = content_warning.value;
|
content_warning_preview.innerText = content_warning.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(draft) {
|
notify(draft) {
|
||||||
this.dispatchEvent(new CustomEvent('tf-draft', {
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-draft', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true,
|
composed: true,
|
||||||
detail: {
|
detail: {
|
||||||
id: this.branch,
|
id: this.branch,
|
||||||
draft: draft
|
draft: draft,
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
change() {
|
change() {
|
||||||
let draft = this.get_draft();
|
let draft = this.get_draft();
|
||||||
draft.text = this.renderRoot.getElementById('edit')?.value;
|
draft.text = this.renderRoot.getElementById('edit')?.value;
|
||||||
draft.content_warning = this.renderRoot.getElementById('content_warning')?.value;
|
draft.content_warning =
|
||||||
|
this.renderRoot.getElementById('content_warning')?.value;
|
||||||
this.notify(draft);
|
this.notify(draft);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,13 +116,17 @@ class TfComposeElement extends LitElement {
|
|||||||
let context = canvas.getContext('2d');
|
let context = canvas.getContext('2d');
|
||||||
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
let data_url = canvas.toDataURL(mime_type);
|
let data_url = canvas.toDataURL(mime_type);
|
||||||
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
|
let result = atob(data_url.split(',')[1])
|
||||||
|
.split('')
|
||||||
|
.map((x) => x.charCodeAt(0));
|
||||||
resolve(result);
|
resolve(result);
|
||||||
};
|
};
|
||||||
img.onerror = function (event) {
|
img.onerror = function (event) {
|
||||||
reject(new Error('Failed to load image.'));
|
reject(new Error('Failed to load image.'));
|
||||||
};
|
};
|
||||||
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
|
let raw = Array.from(new Uint8Array(buffer))
|
||||||
|
.map((b) => String.fromCharCode(b))
|
||||||
|
.join('');
|
||||||
let original = `data:${type};base64,${btoa(raw)}`;
|
let original = `data:${type};base64,${btoa(raw)}`;
|
||||||
img.src = original;
|
img.src = original;
|
||||||
});
|
});
|
||||||
@ -131,7 +142,11 @@ class TfComposeElement extends LitElement {
|
|||||||
let best_buffer;
|
let best_buffer;
|
||||||
let best_type;
|
let best_type;
|
||||||
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
||||||
let test_buffer = await self.convert_to_format(buffer, file.type, format);
|
let test_buffer = await self.convert_to_format(
|
||||||
|
buffer,
|
||||||
|
file.type,
|
||||||
|
format
|
||||||
|
);
|
||||||
if (!best_buffer || test_buffer.length < best_buffer.length) {
|
if (!best_buffer || test_buffer.length < best_buffer.length) {
|
||||||
best_buffer = test_buffer;
|
best_buffer = test_buffer;
|
||||||
best_type = format;
|
best_type = format;
|
||||||
@ -201,7 +216,11 @@ class TfComposeElement extends LitElement {
|
|||||||
to = [...to];
|
to = [...to];
|
||||||
message.recps = to;
|
message.recps = to;
|
||||||
console.log('message is now', message);
|
console.log('message is now', message);
|
||||||
message = await tfrpc.rpc.encrypt(this.whoami, to, JSON.stringify(message));
|
message = await tfrpc.rpc.encrypt(
|
||||||
|
this.whoami,
|
||||||
|
to,
|
||||||
|
JSON.stringify(message)
|
||||||
|
);
|
||||||
console.log('encrypted as', message);
|
console.log('encrypted as', message);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -241,12 +260,15 @@ class TfComposeElement extends LitElement {
|
|||||||
this.last_autocomplete = text;
|
this.last_autocomplete = text;
|
||||||
let results = [];
|
let results = [];
|
||||||
try {
|
try {
|
||||||
let rows = await tfrpc.rpc.query(`
|
let rows = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT messages.content FROM messages_fts(?)
|
SELECT messages.content FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
WHERE 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}%](%)%`]
|
||||||
|
);
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
|
for (let match of row.content.matchAll(/!\[([^\]]*)\]\((&.*?)\)/g)) {
|
||||||
if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
|
if (match[1].toLowerCase().indexOf(text.toLowerCase()) != -1) {
|
||||||
@ -265,7 +287,10 @@ class TfComposeElement extends LitElement {
|
|||||||
let tribute = new Tribute({
|
let tribute = new Tribute({
|
||||||
collection: [
|
collection: [
|
||||||
{
|
{
|
||||||
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
values: Object.entries(this.users).map((x) => ({
|
||||||
|
key: x[1].name,
|
||||||
|
value: x[0],
|
||||||
|
})),
|
||||||
selectTemplate: function (item) {
|
selectTemplate: function (item) {
|
||||||
return `[@${item.original.key}](${item.original.value})`;
|
return `[@${item.original.key}](${item.original.value})`;
|
||||||
},
|
},
|
||||||
@ -293,7 +318,10 @@ class TfComposeElement extends LitElement {
|
|||||||
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({
|
||||||
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
values: Object.entries(this.users).map((x) => ({
|
||||||
|
key: x[1].name,
|
||||||
|
value: x[0],
|
||||||
|
})),
|
||||||
selectTemplate: function (item) {
|
selectTemplate: function (item) {
|
||||||
return item.original.value;
|
return item.original.value;
|
||||||
},
|
},
|
||||||
@ -311,17 +339,27 @@ class TfComposeElement extends LitElement {
|
|||||||
|
|
||||||
render_mention(mention) {
|
render_mention(mention) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html` <div style="display: flex; flex-direction: row">
|
||||||
<div style="display: flex; flex-direction: row">
|
|
||||||
<div style="align-self: center; margin: 0.5em">
|
<div style="align-self: center; margin: 0.5em">
|
||||||
<button class="w3-button w3-dark-grey" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}>🚮</button>
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
title="Remove ${mention.name} mention"
|
||||||
|
@click=${() => self.remove_mention(mention.link)}
|
||||||
|
>
|
||||||
|
🚮
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column">
|
<div style="display: flex; flex-direction: column">
|
||||||
<h3>${mention.name}</h3>
|
<h3>${mention.name}</h3>
|
||||||
<div style="padding-left: 1em">
|
<div style="padding-left: 1em">
|
||||||
${Object.entries(mention)
|
${Object.entries(mention)
|
||||||
.filter(x => x[0] != 'name')
|
.filter((x) => x[0] != 'name')
|
||||||
.map(x => html`<div><span style="font-weight: bold">${x[0]}</span>: ${x[1]}</div>`)}
|
.map(
|
||||||
|
(x) =>
|
||||||
|
html`<div>
|
||||||
|
<span style="font-weight: bold">${x[0]}</span>: ${x[1]}
|
||||||
|
</div>`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -359,10 +397,19 @@ class TfComposeElement extends LitElement {
|
|||||||
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-dark-grey">
|
<select id="select" class="w3-select w3-dark-grey">
|
||||||
${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
|
${Object.keys(self.apps).map(
|
||||||
|
(app) => html`<option value=${app}>${app}</option>`
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}>Attach</button>
|
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}>
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Cancel</button>
|
Attach
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (this.apps = null)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -374,9 +421,16 @@ 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-dark-grey" @click=${attach_app}>Attach App</button>`;
|
return html`<button class="w3-button w3-dark-grey" @click=${attach_app}>
|
||||||
|
Attach App
|
||||||
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
return html`<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Discard App</button>`;
|
return html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (this.apps = null)}
|
||||||
|
>
|
||||||
|
Discard App
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,11 +489,13 @@ class TfComposeElement extends LitElement {
|
|||||||
<button class="w3-button w3-dark-grey" @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(x => html`
|
${draft.encrypt_to.map(
|
||||||
|
(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-dark-grey" 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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -455,34 +511,65 @@ class TfComposeElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
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-blue">
|
? 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;
|
||||||
let encrypt = draft.encrypt_to !== undefined ?
|
let encrypt =
|
||||||
undefined :
|
draft.encrypt_to !== undefined
|
||||||
html`<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt([])}>🔐</button>`;
|
? undefined
|
||||||
|
: html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => this.set_encrypt([])}
|
||||||
|
>
|
||||||
|
🔐
|
||||||
|
</button>`;
|
||||||
let result = html`
|
let result = html`
|
||||||
<div class="w3-card-4 w3-blue-grey w3-padding" style="box-sizing: border-box">
|
<div
|
||||||
|
class="w3-card-4 w3-blue-grey w3-padding"
|
||||||
|
style="box-sizing: border-box"
|
||||||
|
>
|
||||||
${this.render_encrypt()}
|
${this.render_encrypt()}
|
||||||
<div style="display: flex; flex-direction: row; width: 100%; gap: 4px">
|
<div style="display: flex; flex-direction: row; width: 100%; gap: 4px">
|
||||||
<div style="flex: 1 0 50%">
|
<div style="flex: 1 0 50%">
|
||||||
<p><textarea class="w3-input w3-dark-grey w3-border" style="resize: vertical" placeholder="Write a post here." id="edit" @input=${this.input} @change=${this.change} @paste=${this.paste}>${draft.text}</textarea></p>
|
<p>
|
||||||
|
<textarea
|
||||||
|
class="w3-input w3-dark-grey w3-border"
|
||||||
|
style="resize: vertical"
|
||||||
|
placeholder="Write a post here."
|
||||||
|
id="edit"
|
||||||
|
@input=${this.input}
|
||||||
|
@change=${this.change}
|
||||||
|
@paste=${this.paste}
|
||||||
|
>
|
||||||
|
${draft.text}</textarea
|
||||||
|
>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex: 1 0 50%">
|
<div style="flex: 1 0 50%">
|
||||||
${content_warning}
|
${content_warning}
|
||||||
<div id="preview"></div>
|
<div id="preview"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
|
${Object.values(draft.mentions || {}).map((x) =>
|
||||||
${this.render_attach_app()}
|
self.render_mention(x)
|
||||||
${this.render_content_warning()}
|
)}
|
||||||
<button class="w3-button w3-dark-grey" id="submit" @click=${this.submit}>Submit</button>
|
${this.render_attach_app()} ${this.render_content_warning()}
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.attach}>Attach</button>
|
<button
|
||||||
${this.render_attach_app_button()}
|
class="w3-button w3-dark-grey"
|
||||||
${encrypt}
|
id="submit"
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.discard}>Discard</button>
|
@click=${this.submit}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.attach}>
|
||||||
|
Attach
|
||||||
|
</button>
|
||||||
|
${this.render_attach_app_button()} ${encrypt}
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.discard}>
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return result;
|
return result;
|
||||||
|
@ -3,7 +3,7 @@ import * as tfrpc from '/static/tfrpc.js';
|
|||||||
import {styles} from './tf-styles.js';
|
import {styles} from './tf-styles.js';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
** Provide a list of IDs, and this lets the user pick one.
|
* Provide a list of IDs, and this lets the user pick one.
|
||||||
*/
|
*/
|
||||||
class TfIdentityPickerElement extends LitElement {
|
class TfIdentityPickerElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
@ -24,15 +24,28 @@ class TfIdentityPickerElement extends LitElement {
|
|||||||
|
|
||||||
changed(event) {
|
changed(event) {
|
||||||
this.selected = event.srcElement.value;
|
this.selected = event.srcElement.value;
|
||||||
this.dispatchEvent(new Event('change', {
|
this.dispatchEvent(
|
||||||
|
new Event('change', {
|
||||||
srcElement: this,
|
srcElement: this,
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<select class="w3-select w3-dark-grey w3-padding w3-border" @change=${this.changed} style="max-width: 100%; overflow: hidden">
|
<select
|
||||||
${(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>`)}
|
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>
|
</select>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -31,14 +31,27 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show_reply() {
|
show_reply() {
|
||||||
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: {
|
let event = new CustomEvent('tf-draft', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
id: this.message?.id,
|
||||||
|
draft: {
|
||||||
encrypt_to: this.message?.decrypted?.recps,
|
encrypt_to: this.message?.decrypted?.recps,
|
||||||
}}});
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
discard_reply() {
|
discard_reply() {
|
||||||
this.dispatchEvent(new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.id, draft: undefined}}));
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-draft', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {id: this.id, draft: undefined},
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render_votes() {
|
render_votes() {
|
||||||
@ -53,12 +66,19 @@ class TfMessageElement extends LitElement {
|
|||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return html`<div>${(this.message.votes || []).map(
|
return html`<div>
|
||||||
vote => html`
|
${(this.message.votes || []).map(
|
||||||
<span title="${this.users[vote.author]?.name ?? vote.author} ${new Date(vote.timestamp)}">
|
(vote) => html`
|
||||||
|
<span
|
||||||
|
title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
|
||||||
|
vote.timestamp
|
||||||
|
)}"
|
||||||
|
>
|
||||||
${normalize_expression(vote.content.vote.expression)}
|
${normalize_expression(vote.content.vote.expression)}
|
||||||
</span>
|
</span>
|
||||||
`)}</div>`;
|
`
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render_raw() {
|
render_raw() {
|
||||||
@ -72,30 +92,40 @@ class TfMessageElement extends LitElement {
|
|||||||
content: this.message?.content,
|
content: this.message?.content,
|
||||||
signature: this.message?.signature,
|
signature: this.message?.signature,
|
||||||
};
|
};
|
||||||
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`;
|
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;
|
||||||
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
|
if (
|
||||||
tfrpc.rpc.appendMessage(
|
confirm(
|
||||||
this.whoami,
|
'Are you sure you want to react with ' +
|
||||||
{
|
reaction +
|
||||||
|
' to ' +
|
||||||
|
message +
|
||||||
|
'?'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
tfrpc.rpc
|
||||||
|
.appendMessage(this.whoami, {
|
||||||
type: 'vote',
|
type: 'vote',
|
||||||
vote: {
|
vote: {
|
||||||
link: message,
|
link: message,
|
||||||
value: 1,
|
value: 1,
|
||||||
expression: reaction,
|
expression: reaction,
|
||||||
},
|
},
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
react(event) {
|
react(event) {
|
||||||
emojis.picker(x => this.vote(x));
|
emojis.picker((x) => this.vote(x));
|
||||||
}
|
}
|
||||||
|
|
||||||
show_image(link) {
|
show_image(link) {
|
||||||
@ -129,7 +159,10 @@ class TfMessageElement extends LitElement {
|
|||||||
body_click(event) {
|
body_click(event) {
|
||||||
if (event.srcElement.tagName == 'IMG') {
|
if (event.srcElement.tagName == 'IMG') {
|
||||||
this.show_image(event.srcElement.src);
|
this.show_image(event.srcElement.src);
|
||||||
} else if (event.srcElement.tagName == 'DIV' && event.srcElement.classList.contains('img_caption')) {
|
} else if (
|
||||||
|
event.srcElement.tagName == 'DIV' &&
|
||||||
|
event.srcElement.classList.contains('img_caption')
|
||||||
|
) {
|
||||||
let next = event.srcElement.nextSibling;
|
let next = event.srcElement.nextSibling;
|
||||||
if (next.style.display == 'block') {
|
if (next.style.display == 'block') {
|
||||||
next.style.display = 'none';
|
next.style.display = 'none';
|
||||||
@ -140,50 +173,77 @@ 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 html` <pre>${JSON.stringify(mention)}</pre>`;
|
return html` <pre>${JSON.stringify(mention)}</pre>`;
|
||||||
} else if (mention?.link?.startsWith('&') &&
|
} else if (
|
||||||
mention?.type?.startsWith('image/')) {
|
mention?.link?.startsWith('&') &&
|
||||||
|
mention?.type?.startsWith('image/')
|
||||||
|
) {
|
||||||
return html`
|
return html`
|
||||||
<img src=${'/' + mention.link + '/view'} style="max-width: 128px; max-height: 128px" title=${mention.name} @click=${() => this.show_image('/' + mention.link + '/view')}>
|
<img
|
||||||
|
src=${'/' + mention.link + '/view'}
|
||||||
|
style="max-width: 128px; max-height: 128px"
|
||||||
|
title=${mention.name}
|
||||||
|
@click=${() => this.show_image('/' + mention.link + '/view')}
|
||||||
|
/>
|
||||||
`;
|
`;
|
||||||
} else if (mention.link?.startsWith('&') &&
|
} else if (
|
||||||
mention.name?.startsWith('audio:')) {
|
mention.link?.startsWith('&') &&
|
||||||
|
mention.name?.startsWith('audio:')
|
||||||
|
) {
|
||||||
return html`
|
return html`
|
||||||
<audio controls style="height: 32px">
|
<audio controls style="height: 32px">
|
||||||
<source src=${'/' + mention.link + '/view'}></source>
|
<source src=${'/' + mention.link + '/view'}></source>
|
||||||
</audio>
|
</audio>
|
||||||
`;
|
`;
|
||||||
} else if (mention.link?.startsWith('&') &&
|
} else if (
|
||||||
mention.name?.startsWith('video:')) {
|
mention.link?.startsWith('&') &&
|
||||||
|
mention.name?.startsWith('video:')
|
||||||
|
) {
|
||||||
return html`
|
return html`
|
||||||
<video controls style="max-height: 240px; max-width: 128px">
|
<video controls style="max-height: 240px; max-width: 128px">
|
||||||
<source src=${'/' + mention.link + '/view'}></source>
|
<source src=${'/' + mention.link + '/view'}></source>
|
||||||
</video>
|
</video>
|
||||||
`;
|
`;
|
||||||
} else if (mention.link?.startsWith('&') &&
|
} else if (
|
||||||
mention?.type === 'application/tildefriends') {
|
mention.link?.startsWith('&') &&
|
||||||
|
mention?.type === 'application/tildefriends'
|
||||||
|
) {
|
||||||
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
|
return html` <a href=${`/${mention.link}/`}>😎 ${mention.name}</a>`;
|
||||||
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
|
} else if (mention.link?.startsWith('%') || mention.link?.startsWith('@')) {
|
||||||
return html` <a href=${'#' + encodeURIComponent(mention.link)}>${mention.name}</a>`;
|
return html` <a href=${'#' + encodeURIComponent(mention.link)}
|
||||||
|
>${mention.name}</a
|
||||||
|
>`;
|
||||||
} else if (mention.link?.startsWith('#')) {
|
} else if (mention.link?.startsWith('#')) {
|
||||||
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}>${mention.link}</a>`;
|
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}
|
||||||
} else if (Object.keys(mention).length == 2 && mention.link && mention.name) {
|
>${mention.link}</a
|
||||||
|
>`;
|
||||||
|
} else if (
|
||||||
|
Object.keys(mention).length == 2 &&
|
||||||
|
mention.link &&
|
||||||
|
mention.name
|
||||||
|
) {
|
||||||
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
|
return html` <a href=${`/${mention.link}/view`}>${mention.name}</a>`;
|
||||||
} else {
|
} else {
|
||||||
return html` <pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>`;
|
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(x => this.message?.content?.text?.indexOf(x.link) === -1);
|
mentions = mentions.filter(
|
||||||
|
(x) => this.message?.content?.text?.indexOf(x.link) === -1
|
||||||
|
);
|
||||||
if (mentions.length) {
|
if (mentions.length) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<fieldset style="background-color: rgba(0, 0, 0, 0.1); 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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -194,28 +254,58 @@ class TfMessageElement extends LitElement {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let total = message.child_messages.length;
|
let total = message.child_messages.length;
|
||||||
for (let m of message.child_messages)
|
for (let m of message.child_messages) {
|
||||||
{
|
|
||||||
total += this.total_child_messages(m);
|
total += this.total_child_messages(m);
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_expanded(expanded, tag) {
|
set_expanded(expanded, tag) {
|
||||||
this.dispatchEvent(new CustomEvent('tf-expand', {bubbles: true, composed: true, detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded}}));
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tf-expand', {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: {
|
||||||
|
id: (this.message.id || '') + (tag || ''),
|
||||||
|
expanded: expanded,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle_expanded(tag) {
|
toggle_expanded(tag) {
|
||||||
this.set_expanded(!this.expanded[(this.message.id || '') + (tag || '')], tag);
|
this.set_expanded(
|
||||||
|
!this.expanded[(this.message.id || '') + (tag || '')],
|
||||||
|
tag
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render_children() {
|
render_children() {
|
||||||
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`<button class="w3-button w3-dark-grey" @click=${() => self.set_expanded(true)}>+ ${this.total_child_messages(this.message) + ' More'}</button>`;
|
return html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => self.set_expanded(true)}
|
||||||
|
>
|
||||||
|
+ ${this.total_child_messages(this.message) + ' More'}
|
||||||
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
return html`<button class="w3-button w3-dark-grey" @click=${() => self.set_expanded(false)}>Collapse</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>`)}`;
|
return html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => self.set_expanded(false)}
|
||||||
|
>
|
||||||
|
Collapse</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>`
|
||||||
|
)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -231,13 +321,12 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
if (Array.isArray(content.mentions)) {
|
if (Array.isArray(content.mentions)) {
|
||||||
for (let mention of content.mentions) {
|
for (let mention of content.mentions) {
|
||||||
if (typeof mention?.link === 'string' &&
|
if (typeof mention?.link === 'string' && mention.link.startsWith('#')) {
|
||||||
mention.link.startsWith('#')) {
|
|
||||||
channels.push(mention.link);
|
channels.push(mention.link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return channels.map(x => html`<tf-tag tag=${x}></tf-tag>`);
|
return channels.map((x) => html`<tf-tag tag=${x}></tf-tag>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -250,52 +339,108 @@ class TfMessageElement extends LitElement {
|
|||||||
switch (this.format) {
|
switch (this.format) {
|
||||||
case 'raw':
|
case 'raw':
|
||||||
if (content?.type == 'post' || content?.type == 'blog') {
|
if (content?.type == 'post' || content?.type == 'blog') {
|
||||||
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'md'}>Markdown</button>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (self.format = 'md')}
|
||||||
|
>
|
||||||
|
Markdown
|
||||||
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (self.format = 'message')}
|
||||||
|
>
|
||||||
|
Message
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'md':
|
case 'md':
|
||||||
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (self.format = 'message')}
|
||||||
|
>
|
||||||
|
Message
|
||||||
|
</button>`;
|
||||||
break;
|
break;
|
||||||
case 'decrypted':
|
case 'decrypted':
|
||||||
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</button>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (self.format = 'raw')}
|
||||||
|
>
|
||||||
|
Raw
|
||||||
|
</button>`;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (this.message.decrypted) {
|
if (this.message.decrypted) {
|
||||||
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'decrypted'}>Decrypted</button>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (self.format = 'decrypted')}
|
||||||
|
>
|
||||||
|
Decrypted
|
||||||
|
</button>`;
|
||||||
} else {
|
} else {
|
||||||
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</button>`;
|
raw_button = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => (self.format = 'raw')}
|
||||||
|
>
|
||||||
|
Raw
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
function small_frame(inner) {
|
function small_frame(inner) {
|
||||||
let body;
|
let body;
|
||||||
return html`
|
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">
|
<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>
|
<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>
|
<span style="padding-right: 8px"
|
||||||
${raw_button}
|
><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(
|
||||||
${self.format == 'raw' ? self.render_raw() : inner}
|
self.message.timestamp
|
||||||
|
).toLocaleString()}</span
|
||||||
|
>
|
||||||
|
${raw_button} ${self.format == 'raw' ? self.render_raw() : inner}
|
||||||
${self.render_votes()}
|
${self.render_votes()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (this.message?.type === 'contact_group') {
|
if (this.message?.type === 'contact_group') {
|
||||||
return html`
|
return html` <div
|
||||||
<div 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">
|
class="w3-card-4"
|
||||||
${this.message.messages.map(x =>
|
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
||||||
html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`
|
>
|
||||||
|
${this.message.messages.map(
|
||||||
|
(x) =>
|
||||||
|
html`<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-message>`
|
||||||
)}
|
)}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (this.message.placeholder) {
|
} else if (this.message.placeholder) {
|
||||||
return html`
|
return html` <div
|
||||||
<div 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">
|
class="w3-card-4"
|
||||||
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a> (placeholder)
|
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=${'#' + this.message.id}>${this.message.id}</a>
|
||||||
|
(placeholder)
|
||||||
<div>${this.render_votes()}</div>
|
<div>${this.render_votes()}</div>
|
||||||
${(this.message.child_messages || []).map(x => html`
|
${(this.message.child_messages || []).map(
|
||||||
<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>
|
(x) => html`
|
||||||
`)}
|
<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-message>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (typeof (content?.type === 'string')) {
|
} else if (typeof (content?.type === 'string')) {
|
||||||
if (content.type == 'about') {
|
if (content.type == 'about') {
|
||||||
@ -307,7 +452,7 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
if (content.image !== undefined) {
|
if (content.image !== undefined) {
|
||||||
image = html`
|
image = html`
|
||||||
<div><img src=${'/' + (typeof(content.image?.link) == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
|
<div><img src=${'/' + (typeof content.image?.link == 'string' ? content.image.link : content.image) + '/view'} style="width: 256px; height: auto"></img></div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
if (content.description !== undefined) {
|
if (content.description !== undefined) {
|
||||||
@ -317,41 +462,54 @@ class TfMessageElement extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
let update = content.about == this.message.author ?
|
let update =
|
||||||
html`<div style="font-weight: bold">Updated profile.</div>` :
|
content.about == this.message.author
|
||||||
html`<div style="font-weight: bold">Updated profile for <tf-user id=${content.about} .users=${this.users}></tf-user>.</div>`;
|
? html`<div style="font-weight: bold">Updated profile.</div>`
|
||||||
return small_frame(html`
|
: html`<div style="font-weight: bold">
|
||||||
${update}
|
Updated profile for
|
||||||
${name}
|
<tf-user id=${content.about} .users=${this.users}></tf-user>.
|
||||||
${image}
|
</div>`;
|
||||||
${description}
|
return small_frame(html` ${update} ${name} ${image} ${description} `);
|
||||||
`);
|
|
||||||
} else if (content.type == 'contact') {
|
} else if (content.type == 'contact') {
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<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 ? 'blocking' :
|
? 'blocking'
|
||||||
content.blocking === false ? 'no longer blocking' :
|
: content.blocking === false
|
||||||
content.following === true ? 'following' :
|
? 'no longer blocking'
|
||||||
content.following === false ? 'no longer following' :
|
: content.following === true
|
||||||
'?'
|
? 'following'
|
||||||
}
|
: content.following === false
|
||||||
<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user>
|
? 'no longer following'
|
||||||
|
: '?'}
|
||||||
|
<tf-user
|
||||||
|
id=${this.message.content.contact}
|
||||||
|
.users=${this.users}
|
||||||
|
></tf-user>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type == 'post') {
|
} else if (content.type == 'post') {
|
||||||
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
|
let reply =
|
||||||
|
this.drafts[this.message?.id] !== undefined
|
||||||
|
? html`
|
||||||
<tf-compose
|
<tf-compose
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.users=${this.users}
|
.users=${this.users}
|
||||||
root=${this.message.content.root || this.message.id}
|
root=${this.message.content.root || this.message.id}
|
||||||
branch=${this.message.id}
|
branch=${this.message.id}
|
||||||
.drafts=${this.drafts}
|
.drafts=${this.drafts}
|
||||||
@tf-discard=${this.discard_reply}></tf-compose>
|
@tf-discard=${this.discard_reply}
|
||||||
` : html`
|
></tf-compose>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button>
|
`
|
||||||
|
: html`
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${this.show_reply}
|
||||||
|
>
|
||||||
|
Reply
|
||||||
|
</button>
|
||||||
`;
|
`;
|
||||||
let self = this;
|
let self = this;
|
||||||
let body;
|
let body;
|
||||||
@ -360,35 +518,47 @@ class TfMessageElement extends LitElement {
|
|||||||
body = this.render_raw();
|
body = this.render_raw();
|
||||||
break;
|
break;
|
||||||
case 'md':
|
case 'md':
|
||||||
body = html`<code style="white-space: pre-wrap; overflow-wrap: anywhere">${content.text}</code>`;
|
body = html`<code
|
||||||
|
style="white-space: pre-wrap; overflow-wrap: anywhere"
|
||||||
|
>${content.text}</code
|
||||||
|
>`;
|
||||||
break;
|
break;
|
||||||
case 'message':
|
case 'message':
|
||||||
body = unsafeHTML(tfutils.markdown(content.text));
|
body = unsafeHTML(tfutils.markdown(content.text));
|
||||||
break;
|
break;
|
||||||
case 'decrypted':
|
case 'decrypted':
|
||||||
body = html`<pre style="white-space: pre-wrap; overflow-wrap: anywhere">${JSON.stringify(content, null, 2)}</pre>`;
|
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 class="w3-panel w3-round-xlarge w3-blue" style="cursor: pointer" @click=${x => this.toggle_expanded(':cw')}><p>${content.contentWarning}</p></div>
|
<div
|
||||||
|
class="w3-panel w3-round-xlarge w3-blue"
|
||||||
|
style="cursor: pointer"
|
||||||
|
@click=${(x) => this.toggle_expanded(':cw')}
|
||||||
|
>
|
||||||
|
<p>${content.contentWarning}</p>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
let content_html =
|
let content_html = html`
|
||||||
html`
|
|
||||||
${this.render_channels()}
|
${this.render_channels()}
|
||||||
<div @click=${this.body_click}>${body}</div>
|
<div @click=${this.body_click}>${body}</div>
|
||||||
${this.render_mentions()}
|
${this.render_mentions()}
|
||||||
`;
|
`;
|
||||||
let payload =
|
let payload = content.contentWarning
|
||||||
content.contentWarning ?
|
? self.expanded[(this.message.id || '') + ':cw']
|
||||||
self.expanded[(this.message.id || '') + ':cw'] ?
|
? html` ${content_warning} ${content_html} `
|
||||||
html`
|
: content_warning
|
||||||
${content_warning}
|
: content_html;
|
||||||
${content_html}
|
let is_encrypted = this.message?.decrypted
|
||||||
` :
|
? html`<span style="align-self: center">🔓</span>`
|
||||||
content_warning :
|
: undefined;
|
||||||
content_html;
|
let style_background = this.message?.decrypted
|
||||||
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
|
? 'rgba(255, 0, 0, 0.2)'
|
||||||
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
|
: 'rgba(255, 255, 255, 0.1)';
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
code {
|
code {
|
||||||
@ -404,26 +574,37 @@ class TfMessageElement extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="w3-card-4" style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
<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">
|
<div style="display: flex; flex-direction: row">
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
${is_encrypted}
|
${is_encrypted}
|
||||||
<span style="flex: 1"></span>
|
<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 style="padding-right: 8px"
|
||||||
|
><a target="_top" href=${'#' + self.message.id}>%</a>
|
||||||
|
${new Date(this.message.timestamp).toLocaleString()}</span
|
||||||
|
>
|
||||||
<span>${raw_button}</span>
|
<span>${raw_button}</span>
|
||||||
</div>
|
</div>
|
||||||
${payload}
|
${payload} ${this.render_votes()}
|
||||||
${this.render_votes()}
|
|
||||||
<p>
|
<p>
|
||||||
${reply}
|
${reply}
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
|
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
||||||
|
React
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
${this.render_children()}
|
${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type === 'issue') {
|
} else if (content.type === 'issue') {
|
||||||
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
|
let is_encrypted = this.message?.decrypted
|
||||||
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
|
? 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`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
code {
|
code {
|
||||||
@ -439,18 +620,25 @@ class TfMessageElement extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="w3-card-4" style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
<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">
|
<div style="display: flex; flex-direction: row">
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
${is_encrypted}
|
${is_encrypted}
|
||||||
<span style="flex: 1"></span>
|
<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 style="padding-right: 8px"
|
||||||
|
><a target="_top" href=${'#' + self.message.id}>%</a>
|
||||||
|
${new Date(this.message.timestamp).toLocaleString()}</span
|
||||||
|
>
|
||||||
<span>${raw_button}</span>
|
<span>${raw_button}</span>
|
||||||
</div>
|
</div>
|
||||||
${content.text}
|
${content.text} ${this.render_votes()}
|
||||||
${this.render_votes()}
|
|
||||||
<p>
|
<p>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
|
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
||||||
|
React
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
${this.render_children()}
|
${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
@ -460,10 +648,13 @@ class TfMessageElement extends LitElement {
|
|||||||
tfrpc.rpc.get_blob(content.blog).then(function (data) {
|
tfrpc.rpc.get_blob(content.blog).then(function (data) {
|
||||||
self.blog_data = data;
|
self.blog_data = data;
|
||||||
});
|
});
|
||||||
let payload =
|
let payload = this.expanded[(this.message.id || '') + ':blog']
|
||||||
this.expanded[(this.message.id || '') + ':blog'] ?
|
? html`<div>
|
||||||
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
|
${this.blog_data
|
||||||
undefined;
|
? unsafeHTML(tfutils.markdown(this.blog_data))
|
||||||
|
: 'Loading...'}
|
||||||
|
</div>`
|
||||||
|
: undefined;
|
||||||
let body;
|
let body;
|
||||||
switch (this.format) {
|
switch (this.format) {
|
||||||
case 'raw':
|
case 'raw':
|
||||||
@ -476,7 +667,7 @@ class TfMessageElement extends LitElement {
|
|||||||
body = html`
|
body = html`
|
||||||
<div
|
<div
|
||||||
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
style="border: 1px solid #fff; border-radius: 1em; padding: 8px; margin: 4px; cursor: pointer"
|
||||||
@click=${x => self.toggle_expanded(':blog')}>
|
@click=${(x) => self.toggle_expanded(':blog')}>
|
||||||
<h2>${content.title}</h2>
|
<h2>${content.title}</h2>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<img src=/${content.thumbnail}/view></img>
|
<img src=/${content.thumbnail}/view></img>
|
||||||
@ -487,16 +678,25 @@ class TfMessageElement extends LitElement {
|
|||||||
`;
|
`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
|
let reply =
|
||||||
|
this.drafts[this.message?.id] !== undefined
|
||||||
|
? html`
|
||||||
<tf-compose
|
<tf-compose
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.users=${this.users}
|
.users=${this.users}
|
||||||
root=${this.message.content.root || this.message.id}
|
root=${this.message.content.root || this.message.id}
|
||||||
branch=${this.message.id}
|
branch=${this.message.id}
|
||||||
.drafts=${this.drafts}
|
.drafts=${this.drafts}
|
||||||
@tf-discard=${this.discard_reply}></tf-compose>
|
@tf-discard=${this.discard_reply}
|
||||||
` : html`
|
></tf-compose>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button>
|
`
|
||||||
|
: html`
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${this.show_reply}
|
||||||
|
>
|
||||||
|
Reply
|
||||||
|
</button>
|
||||||
`;
|
`;
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
@ -513,11 +713,17 @@ class TfMessageElement extends LitElement {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</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
|
||||||
|
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">
|
<div style="display: flex; flex-direction: row">
|
||||||
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
<tf-user id=${this.message.author} .users=${this.users}></tf-user>
|
||||||
<span style="flex: 1"></span>
|
<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 style="padding-right: 8px"
|
||||||
|
><a target="_top" href=${'#' + self.message.id}>%</a>
|
||||||
|
${new Date(this.message.timestamp).toLocaleString()}</span
|
||||||
|
>
|
||||||
<span>${raw_button}</span>
|
<span>${raw_button}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -525,37 +731,52 @@ class TfMessageElement extends LitElement {
|
|||||||
${this.render_mentions()}
|
${this.render_mentions()}
|
||||||
<div>
|
<div>
|
||||||
${reply}
|
${reply}
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
|
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
||||||
|
React
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
${this.render_votes()}
|
${this.render_votes()} ${this.render_children()}
|
||||||
${this.render_children()}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type === 'pub') {
|
} else if (content.type === 'pub') {
|
||||||
return small_frame(html`
|
return small_frame(
|
||||||
<style>
|
html` <style>
|
||||||
span {
|
span {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<span>
|
<span>
|
||||||
<div>
|
<div>
|
||||||
🍻 <tf-user .users=${this.users} id=${content.address.key}></tf-user>
|
🍻
|
||||||
|
<tf-user
|
||||||
|
.users=${this.users}
|
||||||
|
id=${content.address.key}
|
||||||
|
></tf-user>
|
||||||
</div>
|
</div>
|
||||||
<pre>${content.address.host}:${content.address.port}</pre>
|
<pre>${content.address.host}:${content.address.port}</pre>
|
||||||
</span>`);
|
</span>`
|
||||||
|
);
|
||||||
} else if (content.type === 'channel') {
|
} else if (content.type === 'channel') {
|
||||||
return small_frame(html`
|
return small_frame(html`
|
||||||
<div>
|
<div>
|
||||||
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} <a href=${'#q=' + encodeURIComponent('#' + content.channel)}>#${content.channel}</a>
|
${content.subscribed ? 'subscribed to' : 'unsubscribed from'}
|
||||||
|
<a href=${'#q=' + encodeURIComponent('#' + content.channel)}
|
||||||
|
>#${content.channel}</a
|
||||||
|
>
|
||||||
</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 small_frame(html`<span>🔓</span><pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`);
|
return small_frame(
|
||||||
|
html`<span>🔓</span>
|
||||||
|
<pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return small_frame(html`<span>🔓</span><div>${this.message.decrypted.type}</div>`);
|
return small_frame(
|
||||||
|
html`<span>🔓</span>
|
||||||
|
<div>${this.message.decrypted.type}</div>`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return small_frame(html`<span>🔒</span>`);
|
return small_frame(html`<span>🔒</span>`);
|
||||||
|
@ -61,7 +61,7 @@ class TfNewsElement extends LitElement {
|
|||||||
message.parent_message = message.content.vote.link;
|
message.parent_message = message.content.vote.link;
|
||||||
} else if (message.content.type == 'post') {
|
} else if (message.content.type == 'post') {
|
||||||
if (message.content.root) {
|
if (message.content.root) {
|
||||||
if (typeof(message.content.root) === 'string') {
|
if (typeof message.content.root === 'string') {
|
||||||
let m = ensure_message(message.content.root);
|
let m = ensure_message(message.content.root);
|
||||||
if (!m.child_messages) {
|
if (!m.child_messages) {
|
||||||
m.child_messages = [];
|
m.child_messages = [];
|
||||||
@ -89,8 +89,7 @@ class TfNewsElement extends LitElement {
|
|||||||
for (let message of messages) {
|
for (let message of messages) {
|
||||||
try {
|
try {
|
||||||
message.content = JSON.parse(message.content);
|
message.content = JSON.parse(message.content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
if (!messages_by_id[message.id]) {
|
if (!messages_by_id[message.id]) {
|
||||||
messages_by_id[message.id] = message;
|
messages_by_id[message.id] = message;
|
||||||
link_message(message);
|
link_message(message);
|
||||||
@ -100,8 +99,12 @@ class TfNewsElement extends LitElement {
|
|||||||
message.parent_message = placeholder.parent_message;
|
message.parent_message = placeholder.parent_message;
|
||||||
message.child_messages = placeholder.child_messages;
|
message.child_messages = placeholder.child_messages;
|
||||||
message.votes = placeholder.votes;
|
message.votes = placeholder.votes;
|
||||||
if (placeholder.parent_message && messages_by_id[placeholder.parent_message]) {
|
if (
|
||||||
let children = messages_by_id[placeholder.parent_message].child_messages;
|
placeholder.parent_message &&
|
||||||
|
messages_by_id[placeholder.parent_message]
|
||||||
|
) {
|
||||||
|
let children =
|
||||||
|
messages_by_id[placeholder.parent_message].child_messages;
|
||||||
children.splice(children.indexOf(placeholder), 1);
|
children.splice(children.indexOf(placeholder), 1);
|
||||||
children.push(message);
|
children.push(message);
|
||||||
}
|
}
|
||||||
@ -116,7 +119,10 @@ class TfNewsElement extends LitElement {
|
|||||||
let latest = 0;
|
let latest = 0;
|
||||||
for (let message of messages || []) {
|
for (let message of messages || []) {
|
||||||
if (message.latest_subtree_timestamp === undefined) {
|
if (message.latest_subtree_timestamp === undefined) {
|
||||||
message.latest_subtree_timestamp = Math.max(message.timestamp ?? 0, this.update_latest_subtree_timestamp(message.child_messages));
|
message.latest_subtree_timestamp = Math.max(
|
||||||
|
message.timestamp ?? 0,
|
||||||
|
this.update_latest_subtree_timestamp(message.child_messages)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
latest = Math.max(latest, message.latest_subtree_timestamp);
|
latest = Math.max(latest, message.latest_subtree_timestamp);
|
||||||
}
|
}
|
||||||
@ -127,20 +133,22 @@ class TfNewsElement extends LitElement {
|
|||||||
function recursive_sort(messages, top) {
|
function recursive_sort(messages, top) {
|
||||||
if (messages) {
|
if (messages) {
|
||||||
if (top) {
|
if (top) {
|
||||||
messages.sort((a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp);
|
messages.sort(
|
||||||
|
(a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
messages.sort((a, b) => a.timestamp - b.timestamp);
|
messages.sort((a, b) => a.timestamp - b.timestamp);
|
||||||
}
|
}
|
||||||
for (let message of messages) {
|
for (let message of messages) {
|
||||||
recursive_sort(message.child_messages, false);
|
recursive_sort(message.child_messages, false);
|
||||||
}
|
}
|
||||||
return messages.map(x => Object.assign({}, x));
|
return messages.map((x) => Object.assign({}, x));
|
||||||
} else {
|
} else {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let roots = Object.values(messages_by_id).filter(x => !x.parent_message);
|
let roots = Object.values(messages_by_id).filter((x) => !x.parent_message);
|
||||||
this.update_latest_subtree_timestamp(roots);
|
this.update_latest_subtree_timestamp(roots);
|
||||||
return recursive_sort(roots, true);
|
return recursive_sort(roots, true);
|
||||||
}
|
}
|
||||||
@ -167,10 +175,22 @@ class TfNewsElement extends LitElement {
|
|||||||
|
|
||||||
load_and_render(messages) {
|
load_and_render(messages) {
|
||||||
let messages_by_id = this.process_messages(messages);
|
let messages_by_id = this.process_messages(messages);
|
||||||
let final_messages = this.group_following(this.finalize_messages(messages_by_id));
|
let final_messages = this.group_following(
|
||||||
|
this.finalize_messages(messages_by_id)
|
||||||
|
);
|
||||||
return html`
|
return html`
|
||||||
<div style="display: flex; flex-direction: column">
|
<div style="display: flex; flex-direction: column">
|
||||||
${final_messages.map(x => html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded} collapsed=true></tf-message>`)}
|
${final_messages.map(
|
||||||
|
(x) =>
|
||||||
|
html`<tf-message
|
||||||
|
.message=${x}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
collapsed="true"
|
||||||
|
></tf-message>`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -36,23 +36,29 @@ class TfProfileElement extends LitElement {
|
|||||||
this.following = undefined;
|
this.following = undefined;
|
||||||
this.blocking = undefined;
|
this.blocking = undefined;
|
||||||
|
|
||||||
let result = await tfrpc.rpc.query(`
|
let result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT json_extract(content, '$.following') AS following
|
SELECT json_extract(content, '$.following') AS following
|
||||||
FROM messages WHERE author = ? AND
|
FROM messages WHERE author = ? AND
|
||||||
json_extract(content, '$.type') = 'contact' AND
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
json_extract(content, '$.contact') = ? AND
|
json_extract(content, '$.contact') = ? AND
|
||||||
following IS NOT NULL
|
following IS NOT NULL
|
||||||
ORDER BY sequence DESC LIMIT 1
|
ORDER BY sequence DESC LIMIT 1
|
||||||
`, [this.whoami, this.id]);
|
`,
|
||||||
|
[this.whoami, this.id]
|
||||||
|
);
|
||||||
this.following = result?.[0]?.following ?? false;
|
this.following = result?.[0]?.following ?? false;
|
||||||
result = await tfrpc.rpc.query(`
|
result = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT json_extract(content, '$.blocking') AS blocking
|
SELECT json_extract(content, '$.blocking') AS blocking
|
||||||
FROM messages WHERE author = ? AND
|
FROM messages WHERE author = ? AND
|
||||||
json_extract(content, '$.type') = 'contact' AND
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
json_extract(content, '$.contact') = ? AND
|
json_extract(content, '$.contact') = ? AND
|
||||||
blocking IS NOT NULL
|
blocking IS NOT NULL
|
||||||
ORDER BY sequence DESC LIMIT 1
|
ORDER BY sequence DESC LIMIT 1
|
||||||
`, [this.whoami, this.id]);
|
`,
|
||||||
|
[this.whoami, this.id]
|
||||||
|
);
|
||||||
this.blocking = result?.[0]?.blocking ?? false;
|
this.blocking = result?.[0]?.blocking ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,13 +66,16 @@ class TfProfileElement extends LitElement {
|
|||||||
async initial_load() {
|
async initial_load() {
|
||||||
this.server_follows_me = undefined;
|
this.server_follows_me = undefined;
|
||||||
let server_id = await tfrpc.rpc.getServerIdentity();
|
let server_id = await tfrpc.rpc.getServerIdentity();
|
||||||
let followed = await tfrpc.rpc.query(`
|
let followed = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT json_extract(content, '$.following') AS following
|
SELECT json_extract(content, '$.following') AS following
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE author = ? AND
|
WHERE author = ? AND
|
||||||
json_extract(content, '$.type') = 'contact' AND
|
json_extract(content, '$.type') = 'contact' AND
|
||||||
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
|
json_extract(content, '$.contact') = ? ORDER BY sequence DESC LIMIT 1
|
||||||
`, [server_id, this.whoami]);
|
`,
|
||||||
|
[server_id, this.whoami]
|
||||||
|
);
|
||||||
let is_followed = false;
|
let is_followed = false;
|
||||||
for (let row of followed) {
|
for (let row of followed) {
|
||||||
is_followed = row.following != 0;
|
is_followed = row.following != 0;
|
||||||
@ -75,11 +84,18 @@ class TfProfileElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
modify(change) {
|
modify(change) {
|
||||||
tfrpc.rpc.appendMessage(this.whoami,
|
tfrpc.rpc
|
||||||
Object.assign({
|
.appendMessage(
|
||||||
|
this.whoami,
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
type: 'contact',
|
type: 'contact',
|
||||||
contact: this.id,
|
contact: this.id,
|
||||||
}, change)).catch(function(error) {
|
},
|
||||||
|
change
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.catch(function (error) {
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -122,9 +138,12 @@ class TfProfileElement extends LitElement {
|
|||||||
message[key] = this.editing[key];
|
message[key] = this.editing[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
tfrpc.rpc
|
||||||
|
.appendMessage(this.whoami, message)
|
||||||
|
.then(function () {
|
||||||
self.editing = null;
|
self.editing = null;
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -139,13 +158,17 @@ class TfProfileElement extends LitElement {
|
|||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.onchange = function (event) {
|
input.onchange = function (event) {
|
||||||
let file = event.target.files[0];
|
let file = event.target.files[0];
|
||||||
file.arrayBuffer().then(function(buffer) {
|
file
|
||||||
|
.arrayBuffer()
|
||||||
|
.then(function (buffer) {
|
||||||
let bin = Array.from(new Uint8Array(buffer));
|
let bin = Array.from(new Uint8Array(buffer));
|
||||||
return tfrpc.rpc.store_blob(bin);
|
return tfrpc.rpc.store_blob(bin);
|
||||||
}).then(function(id) {
|
})
|
||||||
|
.then(function (id) {
|
||||||
self.editing = Object.assign({}, self.editing, {image: id});
|
self.editing = Object.assign({}, self.editing, {image: id});
|
||||||
console.log(self.editing);
|
console.log(self.editing);
|
||||||
}).catch(function(e) {
|
})
|
||||||
|
.catch(function (e) {
|
||||||
alert(e.message);
|
alert(e.message);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -166,15 +189,22 @@ class TfProfileElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.id == this.whoami && this.editing && this.server_follows_me === undefined) {
|
if (
|
||||||
|
this.id == this.whoami &&
|
||||||
|
this.editing &&
|
||||||
|
this.server_follows_me === undefined
|
||||||
|
) {
|
||||||
this.initial_load();
|
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.query(
|
tfrpc.rpc
|
||||||
|
.query(
|
||||||
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
|
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
|
||||||
[this.id]).then(function(result) {
|
[this.id]
|
||||||
|
)
|
||||||
|
.then(function (result) {
|
||||||
self.size = result[0].size;
|
self.size = result[0].size;
|
||||||
});
|
});
|
||||||
let edit;
|
let edit;
|
||||||
@ -184,52 +214,75 @@ class TfProfileElement extends LitElement {
|
|||||||
if (this.editing) {
|
if (this.editing) {
|
||||||
let server_follow;
|
let server_follow;
|
||||||
if (this.server_follows_me === true) {
|
if (this.server_follows_me === true) {
|
||||||
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(false)}>Server, Stop Following Me</button>`;
|
server_follow = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => this.server_follow_me(false)}
|
||||||
|
>
|
||||||
|
Server, Stop Following Me
|
||||||
|
</button>`;
|
||||||
} else if (this.server_follows_me === false) {
|
} 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>`;
|
server_follow = html`<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => this.server_follow_me(true)}
|
||||||
|
>
|
||||||
|
Server, Follow Me
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
edit = html`
|
edit = html`
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.save_edits}>Save Profile</button>
|
<button class="w3-button w3-dark-grey" @click=${this.save_edits}>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>Discard</button>
|
Save Profile
|
||||||
|
</button>
|
||||||
|
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
${server_follow}
|
${server_follow}
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>Edit Profile</button>`;
|
edit = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>
|
||||||
|
Edit Profile
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami &&
|
if (this.id !== this.whoami && this.following !== undefined) {
|
||||||
this.following !== undefined) {
|
follow = this.following
|
||||||
follow =
|
? html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>
|
||||||
this.following ?
|
Unfollow
|
||||||
html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>Unfollow</button>` :
|
</button>`
|
||||||
html`<button class="w3-button w3-dark-grey" @click=${this.follow}>Follow</button>`;
|
: html`<button class="w3-button w3-dark-grey" @click=${this.follow}>
|
||||||
|
Follow
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami &&
|
if (this.id !== this.whoami && this.blocking !== undefined) {
|
||||||
this.blocking !== undefined) {
|
block = this.blocking
|
||||||
block =
|
? html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>
|
||||||
this.blocking ?
|
Unblock
|
||||||
html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>Unblock</button>` :
|
</button>`
|
||||||
html`<button class="w3-button w3-dark-grey" @click=${this.block}>Block</button>`;
|
: html`<button class="w3-button w3-dark-grey" @click=${this.block}>
|
||||||
|
Block
|
||||||
|
</button>`;
|
||||||
}
|
}
|
||||||
let edit_profile = this.editing ? html`
|
let edit_profile = this.editing
|
||||||
|
? 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 class="w3-container">
|
<div class="w3-container">
|
||||||
<div>
|
<div>
|
||||||
<label for="name">Name:</label>
|
<label for="name">Name:</label>
|
||||||
<input class="w3-input w3-dark-grey" type="text" id="name" value=${this.editing.name} @input=${event => this.editing = Object.assign({}, this.editing, {name: event.srcElement.value})}></input>
|
<input class="w3-input w3-dark-grey" type="text" id="name" value=${this.editing.name} @input=${(event) => (this.editing = Object.assign({}, this.editing, {name: event.srcElement.value}))}></input>
|
||||||
</div>
|
</div>
|
||||||
<div><label for="description">Description:</label></div>
|
<div><label for="description">Description:</label></div>
|
||||||
<textarea class="w3-input w3-dark-grey" style="resize: vertical" rows="8" id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
|
<textarea class="w3-input w3-dark-grey" style="resize: vertical" rows="8" id="description" @input=${(event) => (this.editing = Object.assign({}, this.editing, {description: event.srcElement.value}))}>${this.editing.description}</textarea>
|
||||||
<div>
|
<div>
|
||||||
<label for="public_web_hosting">Public Web Hosting:</label>
|
<label for="public_web_hosting">Public Web Hosting:</label>
|
||||||
<input class="w3-check w3-dark-grey" type="checkbox" id="public_web_hosting" ?checked=${this.editing.publicWebHosting} @input=${event => self.editing = Object.assign({}, self.editing, {publicWebHosting: event.srcElement.checked})}></input>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.attach_image}>Attach Image</button>
|
<button class="w3-button w3-dark-grey" @click=${this.attach_image}>Attach Image</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>` : null;
|
</div>`
|
||||||
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
|
: null;
|
||||||
|
let image =
|
||||||
|
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 style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
|
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
|
||||||
|
@ -62,6 +62,7 @@ blockquote {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// prettier-ignore
|
||||||
const w3 = css`
|
const w3 = css`
|
||||||
/* W3.CSS 4.15 December 2020 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}
|
||||||
|
@ -43,10 +43,12 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
|
|
||||||
render_room_peers(connection) {
|
render_room_peers(connection) {
|
||||||
let self = this;
|
let self = this;
|
||||||
let peers = this.broadcasts.filter(x => x.tunnel?.id == connection);
|
let peers = this.broadcasts.filter((x) => x.tunnel?.id == connection);
|
||||||
if (peers.length) {
|
if (peers.length) {
|
||||||
let connections = this.connections.map(x => x.id);
|
let connections = this.connections.map((x) => x.id);
|
||||||
return html`${peers.filter(x => connections.indexOf(x.pubkey) == -1).map(x => html`${self.render_room_peer(x)}`)}`;
|
return html`${peers
|
||||||
|
.filter((x) => connections.indexOf(x.pubkey) == -1)
|
||||||
|
.map((x) => html`${self.render_room_peer(x)}`)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +60,12 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<li>
|
<li>
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}>Connect</button>
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user> 📡
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
@ -67,7 +74,12 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
render_broadcast(connection) {
|
render_broadcast(connection) {
|
||||||
return html`
|
return html`
|
||||||
<li>
|
<li>
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(connection)}>Connect</button>
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => tfrpc.rpc.connect(connection)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
<tf-user id=${connection.pubkey} .users=${this.users}></tf-user>
|
||||||
${this.render_connection_summary(connection)}
|
${this.render_connection_summary(connection)}
|
||||||
</li>
|
</li>
|
||||||
@ -81,11 +93,20 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
|
|
||||||
render_connection(connection) {
|
render_connection(connection) {
|
||||||
return html`
|
return html`
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.closeConnection(connection.id)}>Close</button>
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => tfrpc.rpc.closeConnection(connection.id)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
<tf-user id=${connection.id} .users=${this.users}></tf-user>
|
<tf-user id=${connection.id} .users=${this.users}></tf-user>
|
||||||
${connection.tunnel !== undefined ? '🚇' : html`(${connection.host}:${connection.port})`}
|
${connection.tunnel !== undefined
|
||||||
|
? '🚇'
|
||||||
|
: html`(${connection.host}:${connection.port})`}
|
||||||
<ul>
|
<ul>
|
||||||
${this.connections.filter(x => x.tunnel === this.connections.indexOf(connection)).map(x => html`<li>${this.render_connection(x)}</li>`)}
|
${this.connections
|
||||||
|
.filter((x) => x.tunnel === this.connections.indexOf(connection))
|
||||||
|
.map((x) => html`<li>${this.render_connection(x)}</li>`)}
|
||||||
${this.render_room_peers(connection.id)}
|
${this.render_room_peers(connection.id)}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
@ -97,30 +118,54 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
<div class="w3-container">
|
<div class="w3-container">
|
||||||
<h2>New Connection</h2>
|
<h2>New Connection</h2>
|
||||||
<textarea class="w3-input w3-dark-grey" id="code"></textarea>
|
<textarea class="w3-input w3-dark-grey" id="code"></textarea>
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}>Connect</button>
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() =>
|
||||||
|
tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
<h2>Broadcasts</h2>
|
<h2>Broadcasts</h2>
|
||||||
<ul>
|
<ul>
|
||||||
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
|
${this.broadcasts
|
||||||
|
.filter((x) => x.address)
|
||||||
|
.map((x) => self.render_broadcast(x))}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Connections</h2>
|
<h2>Connections</h2>
|
||||||
<ul>
|
<ul>
|
||||||
${this.connections.filter(x => x.tunnel === undefined).map(x => html`
|
${this.connections
|
||||||
<li>${this.render_connection(x)}</li>
|
.filter((x) => x.tunnel === undefined)
|
||||||
`)}
|
.map((x) => html` <li>${this.render_connection(x)}</li> `)}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Stored Connections (WIP)</h2>
|
<h2>Stored Connections (WIP)</h2>
|
||||||
<ul>
|
<ul>
|
||||||
${this.stored_connections.map(x => html`
|
${this.stored_connections.map(
|
||||||
|
(x) => html`
|
||||||
<li>
|
<li>
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => self.forget_stored_connection(x)}>Forget</button>
|
<button
|
||||||
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(x)}>Connect</button>
|
class="w3-button w3-dark-grey"
|
||||||
${x.address}:${x.port} <tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
@click=${() => self.forget_stored_connection(x)}
|
||||||
|
>
|
||||||
|
Forget
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${() => tfrpc.rpc.connect(x)}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
${x.address}:${x.port}
|
||||||
|
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
||||||
</li>
|
</li>
|
||||||
`)}
|
`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Local Accounts</h2>
|
<h2>Local Accounts</h2>
|
||||||
<ul>
|
<ul>
|
||||||
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
${this.identities.map(
|
||||||
|
(x) =>
|
||||||
|
html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -27,7 +27,8 @@ class TfTabMentionsElement extends LitElement {
|
|||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
console.log('Loading...', this.whoami);
|
console.log('Loading...', this.whoami);
|
||||||
let results = await tfrpc.rpc.query(`
|
let results = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT messages.*
|
SELECT messages.*
|
||||||
FROM messages_fts(?)
|
FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
@ -35,7 +36,12 @@ class TfTabMentionsElement extends LitElement {
|
|||||||
WHERE messages.author != ?
|
WHERE messages.author != ?
|
||||||
ORDER BY timestamp DESC limit 20
|
ORDER BY timestamp DESC limit 20
|
||||||
`,
|
`,
|
||||||
['"' + this.whoami.replace('"', '""') + '"', JSON.stringify(this.following), this.whoami]);
|
[
|
||||||
|
'"' + this.whoami.replace('"', '""') + '"',
|
||||||
|
JSON.stringify(this.following),
|
||||||
|
this.whoami,
|
||||||
|
]
|
||||||
|
);
|
||||||
console.log('Done.');
|
console.log('Done.');
|
||||||
this.messages = results;
|
this.messages = results;
|
||||||
}
|
}
|
||||||
@ -58,7 +64,14 @@ class TfTabMentionsElement extends LitElement {
|
|||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
return html`
|
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>
|
<tf-news
|
||||||
|
id="news"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.messages=${this.messages}
|
||||||
|
.users=${this.users}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@tf-expand=${this.on_expand}
|
||||||
|
></tf-news>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,9 +45,8 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
UNION
|
UNION
|
||||||
SELECT * FROM mine
|
SELECT * FROM mine
|
||||||
`,
|
`,
|
||||||
[
|
[this.hash.substring(1)]
|
||||||
this.hash.substring(1),
|
);
|
||||||
]);
|
|
||||||
return r;
|
return r;
|
||||||
} else if (this.hash.startsWith('#%')) {
|
} else if (this.hash.startsWith('#%')) {
|
||||||
return await tfrpc.rpc.query(
|
return await tfrpc.rpc.query(
|
||||||
@ -61,14 +60,14 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
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 {
|
} else {
|
||||||
let promises = [];
|
let promises = [];
|
||||||
const k_following_limit = 256;
|
const k_following_limit = 256;
|
||||||
for (let i = 0; i < this.following.length; i += k_following_limit) {
|
for (let i = 0; i < this.following.length; i += k_following_limit) {
|
||||||
promises.push(tfrpc.rpc.query(
|
promises.push(
|
||||||
|
tfrpc.rpc.query(
|
||||||
`
|
`
|
||||||
WITH news AS (SELECT messages.*
|
WITH news AS (SELECT messages.*
|
||||||
FROM messages
|
FROM messages
|
||||||
@ -95,7 +94,9 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
** messages with far-future timestamps from staying at the top forever.
|
** messages with far-future timestamps from staying at the top forever.
|
||||||
*/
|
*/
|
||||||
new Date().valueOf() + 24 * 60 * 60 * 1000,
|
new Date().valueOf() + 24 * 60 * 60 * 1000,
|
||||||
]));
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return [].concat(...(await Promise.all(promises)));
|
return [].concat(...(await Promise.all(promises)));
|
||||||
}
|
}
|
||||||
@ -124,11 +125,8 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
UNION
|
UNION
|
||||||
SELECT news.* FROM news
|
SELECT news.* FROM news
|
||||||
`,
|
`,
|
||||||
[
|
[JSON.stringify(this.following), this.start_time, last_start_time]
|
||||||
JSON.stringify(this.following),
|
);
|
||||||
this.start_time,
|
|
||||||
last_start_time,
|
|
||||||
]);
|
|
||||||
this.messages = await this.decrypt([...more, ...this.messages]);
|
this.messages = await this.decrypt([...more, ...this.messages]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,14 +137,12 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
let content;
|
let content;
|
||||||
try {
|
try {
|
||||||
content = JSON.parse(message?.content);
|
content = JSON.parse(message?.content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
if (typeof content === 'string') {
|
||||||
if (typeof(content) === 'string') {
|
|
||||||
let decrypted;
|
let decrypted;
|
||||||
try {
|
try {
|
||||||
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
|
decrypted = await tfrpc.rpc.try_decrypt(this.whoami, content);
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
if (decrypted) {
|
if (decrypted) {
|
||||||
try {
|
try {
|
||||||
message.decrypted = JSON.parse(decrypted);
|
message.decrypted = JSON.parse(decrypted);
|
||||||
@ -165,18 +161,25 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.messages ||
|
if (
|
||||||
|
!this.messages ||
|
||||||
this._messages_hash !== this.hash ||
|
this._messages_hash !== this.hash ||
|
||||||
this._messages_following !== this.following) {
|
this._messages_following !== this.following
|
||||||
console.log(`loading messages for ${this.whoami} (following ${this.following.length})`);
|
) {
|
||||||
|
console.log(
|
||||||
|
`loading messages for ${this.whoami} (following ${this.following.length})`
|
||||||
|
);
|
||||||
let self = this;
|
let self = this;
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this._messages_hash = this.hash;
|
this._messages_hash = this.hash;
|
||||||
this._messages_following = this.following;
|
this._messages_following = this.following;
|
||||||
this.fetch_messages().then(this.decrypt.bind(this)).then(function(messages) {
|
this.fetch_messages()
|
||||||
|
.then(this.decrypt.bind(this))
|
||||||
|
.then(function (messages) {
|
||||||
self.messages = messages;
|
self.messages = messages;
|
||||||
console.log(`loading mesages done for ${self.whoami}`);
|
console.log(`loading mesages done for ${self.whoami}`);
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
alert(JSON.stringify(error, null, 2));
|
alert(JSON.stringify(error, null, 2));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -184,12 +187,22 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
if (!this.hash.startsWith('#@') && !this.hash.startsWith('#%')) {
|
||||||
more = html`
|
more = html`
|
||||||
<p>
|
<p>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.load_more}>Load More</button>
|
<button class="w3-button w3-dark-grey" @click=${this.load_more}>
|
||||||
|
Load More
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<tf-news id="news" whoami=${this.whoami} .users=${this.users} .messages=${this.messages} .following=${this.following} .drafts=${this.drafts} .expanded=${this.expanded}></tf-news>
|
<tf-news
|
||||||
|
id="news"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.messages=${this.messages}
|
||||||
|
.following=${this.following}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
></tf-news>
|
||||||
${more}
|
${more}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,9 @@ class TfTabNewsElement extends LitElement {
|
|||||||
let news = this.shadowRoot?.getElementById('news');
|
let news = this.shadowRoot?.getElementById('news');
|
||||||
if (news) {
|
if (news) {
|
||||||
console.log('injecting messages', news.messages);
|
console.log('injecting messages', news.messages);
|
||||||
news.add_messages(Object.values(Object.fromEntries(this.unread.map(x => [x.id, x]))));
|
news.add_messages(
|
||||||
|
Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x])))
|
||||||
|
);
|
||||||
this.dispatchEvent(new CustomEvent('refresh'));
|
this.dispatchEvent(new CustomEvent('refresh'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,11 +64,16 @@ class TfTabNewsElement extends LitElement {
|
|||||||
let type = 'private';
|
let type = 'private';
|
||||||
try {
|
try {
|
||||||
type = JSON.parse(message.content).type || type;
|
type = JSON.parse(message.content).type || type;
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
counts[type] = (counts[type] || 0) + 1;
|
counts[type] = (counts[type] || 0) + 1;
|
||||||
}
|
}
|
||||||
return '↻ Show New: ' + Object.keys(counts).sort().map(x => (counts[x].toString() + ' ' + x + 's')).join(', ');
|
return (
|
||||||
|
'↻ Show New: ' +
|
||||||
|
Object.keys(counts)
|
||||||
|
.sort()
|
||||||
|
.map((x) => counts[x].toString() + ' ' + x + 's')
|
||||||
|
.join(', ')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
draft(event) {
|
draft(event) {
|
||||||
@ -96,23 +103,52 @@ class TfTabNewsElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
on_keypress(event) {
|
on_keypress(event) {
|
||||||
if (event.target === document.body &&
|
if (event.target === document.body && event.key == '.') {
|
||||||
event.key == '.') {
|
|
||||||
this.show_more();
|
this.show_more();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let profile = this.hash.startsWith('#@') ?
|
let profile = this.hash.startsWith('#@')
|
||||||
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
|
? html`<tf-profile
|
||||||
|
id=${this.hash.substring(1)}
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
></tf-profile>`
|
||||||
|
: undefined;
|
||||||
return html`
|
return html`
|
||||||
<p class="w3-bar">
|
<p class="w3-bar">
|
||||||
<button class="w3-bar-item w3-button w3-dark-grey" @click=${this.show_more}>${this.new_messages_text()}</button>
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-dark-grey"
|
||||||
|
@click=${this.show_more}
|
||||||
|
>
|
||||||
|
${this.new_messages_text()}
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div>
|
<div>
|
||||||
<div><tf-compose id="tf-compose" whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
|
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<tf-compose
|
||||||
|
id="tf-compose"
|
||||||
|
whoami=${this.whoami}
|
||||||
|
.users=${this.users}
|
||||||
|
.drafts=${this.drafts}
|
||||||
|
@tf-draft=${this.draft}
|
||||||
|
></tf-compose>
|
||||||
|
</div>
|
||||||
${profile}
|
${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>
|
<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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ class TfTabQueryElement extends LitElement {
|
|||||||
await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query));
|
await tfrpc.rpc.setHash('#sql=' + encodeURIComponent(query));
|
||||||
let start_time = new Date();
|
let start_time = new Date();
|
||||||
try {
|
try {
|
||||||
this.results = await tfrpc.rpc.query(query, [])
|
this.results = await tfrpc.rpc.query(query, []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error = error;
|
this.error = error;
|
||||||
}
|
}
|
||||||
@ -79,8 +79,15 @@ class TfTabQueryElement extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
let keys = Object.keys(this.results[0]).sort();
|
let keys = Object.keys(this.results[0]).sort();
|
||||||
return html`<table style="width: 100%; max-width: 100%">
|
return html`<table style="width: 100%; max-width: 100%">
|
||||||
<tr>${keys.map(key => html`<th>${key}</th>`)}</tr>
|
<tr>
|
||||||
${this.results.map(row => html`<tr>${keys.map(key => html`<td>${row[key]}</td>`)}</tr>`)}
|
${keys.map((key) => html`<th>${key}</th>`)}
|
||||||
|
</tr>
|
||||||
|
${this.results.map(
|
||||||
|
(row) =>
|
||||||
|
html`<tr>
|
||||||
|
${keys.map((key) => html`<td>${row[key]}</td>`)}
|
||||||
|
</tr>`
|
||||||
|
)}
|
||||||
</table>`;
|
</table>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -100,13 +107,28 @@ class TfTabQueryElement 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">
|
||||||
<textarea id="search" rows=8 class="w3-input w3-dark-grey" style="flex: 1; resize: vertical" @keydown=${this.search_keydown}>${this.query}</textarea>
|
<textarea
|
||||||
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Execute</button>
|
id="search"
|
||||||
|
rows="8"
|
||||||
|
class="w3-input w3-dark-grey"
|
||||||
|
style="flex: 1; resize: vertical"
|
||||||
|
@keydown=${this.search_keydown}
|
||||||
|
>
|
||||||
|
${this.query}</textarea
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w3-button w3-dark-grey"
|
||||||
|
@click=${(event) =>
|
||||||
|
self.search(self.renderRoot.getElementById('search').value)}
|
||||||
|
>
|
||||||
|
Execute
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div ?hidden=${this.duration === undefined}>
|
||||||
|
Took ${this.duration / 1000.0} seconds.
|
||||||
</div>
|
</div>
|
||||||
<div ?hidden=${this.duration === undefined}>Took ${this.duration / 1000.0} seconds.</div>
|
|
||||||
<div ?hidden=${this.duration !== undefined}>Executing...</div>
|
<div ?hidden=${this.duration !== undefined}>Executing...</div>
|
||||||
${this.render_error()}
|
${this.render_error()} ${this.render_results()}
|
||||||
${this.render_results()}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,14 +33,16 @@ class TfTabSearchElement extends LitElement {
|
|||||||
search.select();
|
search.select();
|
||||||
}
|
}
|
||||||
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
|
await tfrpc.rpc.setHash('#q=' + encodeURIComponent(query));
|
||||||
let results = await tfrpc.rpc.query(`
|
let results = await tfrpc.rpc.query(
|
||||||
|
`
|
||||||
SELECT messages.*
|
SELECT messages.*
|
||||||
FROM messages_fts(?)
|
FROM messages_fts(?)
|
||||||
JOIN messages ON messages.rowid = messages_fts.rowid
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
ORDER BY timestamp DESC limit 100
|
ORDER BY timestamp DESC limit 100
|
||||||
`,
|
`,
|
||||||
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]);
|
['"' + query.replace('"', '""') + '"', JSON.stringify(this.following)]
|
||||||
|
);
|
||||||
console.log('Done.');
|
console.log('Done.');
|
||||||
search = this.renderRoot.getElementById('search');
|
search = this.renderRoot.getElementById('search');
|
||||||
if (search) {
|
if (search) {
|
||||||
|
@ -17,7 +17,11 @@ class TfTagElement extends LitElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let number = this.count ? html` (${this.count})` : undefined;
|
let number = this.count ? html` (${this.count})` : undefined;
|
||||||
return html`<a href="#q=${this.tag}" style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px">${this.tag}${number}</a>`;
|
return html`<a
|
||||||
|
href="#q=${this.tag}"
|
||||||
|
style="display: inline-block; margin: 3px; border: 1px solid black; background-color: #444; padding: 4px; border-radius: 3px"
|
||||||
|
>${this.tag}${number}</a
|
||||||
|
>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,21 +20,24 @@ class TfUserElement extends LitElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let name = this.users?.[this.id]?.name;
|
let name = this.users?.[this.id]?.name;
|
||||||
name = name !== undefined ?
|
name =
|
||||||
html`<a target="_top" href=${'#' + this.id}>${name}</a>` :
|
name !== undefined
|
||||||
html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
|
? html`<a target="_top" href=${'#' + this.id}>${name}</a>`
|
||||||
|
: html`<a target="_top" href=${'#' + this.id}>${this.id}</a>`;
|
||||||
|
|
||||||
if (this.users[this.id]) {
|
if (this.users[this.id]) {
|
||||||
let image = this.users[this.id].image;
|
let image = this.users[this.id].image;
|
||||||
image = typeof(image) == 'string' ? image : image?.link;
|
image = typeof image == 'string' ? image : image?.link;
|
||||||
return html`
|
return html` <div style="display: inline-block; font-weight: bold">
|
||||||
<div style="display: inline-block; font-weight: bold">
|
<img
|
||||||
<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" ?hidden=${image === undefined} src="${image ? '/' + image + '/view' : undefined}">
|
style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%"
|
||||||
|
?hidden=${image === undefined}
|
||||||
|
src="${image ? '/' + image + '/view' : undefined}"
|
||||||
|
/>
|
||||||
${name}
|
${name}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html` <div style="display: inline-block; font-weight: bold">
|
||||||
<div style="display: inline-block; font-weight: bold">
|
|
||||||
${name}
|
${name}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
@ -2,20 +2,32 @@ import * as linkify from './commonmark-linkify.js';
|
|||||||
import * as hashtagify from './commonmark-hashtag.js';
|
import * as hashtagify from './commonmark-hashtag.js';
|
||||||
|
|
||||||
function image(node, entering) {
|
function image(node, entering) {
|
||||||
if (node.firstChild?.type === 'text' &&
|
if (
|
||||||
node.firstChild.literal.startsWith('video:')) {
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('video:')
|
||||||
|
) {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
this.lit(
|
||||||
|
'<video style="max-width: 100%; max-height: 480px" title="' +
|
||||||
|
this.esc(node.firstChild?.literal) +
|
||||||
|
'" controls>'
|
||||||
|
);
|
||||||
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
this.disableTags += 1;
|
this.disableTags += 1;
|
||||||
} else {
|
} else {
|
||||||
this.disableTags -= 1;
|
this.disableTags -= 1;
|
||||||
this.lit('</video>');
|
this.lit('</video>');
|
||||||
}
|
}
|
||||||
} else if (node.firstChild?.type === 'text' &&
|
} else if (
|
||||||
node.firstChild.literal.startsWith('audio:')) {
|
node.firstChild?.type === 'text' &&
|
||||||
|
node.firstChild.literal.startsWith('audio:')
|
||||||
|
) {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
this.lit(
|
||||||
|
'<audio style="height: 32px; max-width: 100%" title="' +
|
||||||
|
this.esc(node.firstChild?.literal) +
|
||||||
|
'" controls>'
|
||||||
|
);
|
||||||
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
|
||||||
this.disableTags += 1;
|
this.disableTags += 1;
|
||||||
} else {
|
} else {
|
||||||
@ -25,7 +37,11 @@ function image(node, entering) {
|
|||||||
} else {
|
} else {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
if (this.disableTags === 0) {
|
if (this.disableTags === 0) {
|
||||||
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>');
|
this.lit(
|
||||||
|
'<div class="img_caption">' +
|
||||||
|
this.esc(node.firstChild?.literal || node.destination) +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
if (this.options.safe && potentiallyUnsafe(node.destination)) {
|
||||||
this.lit('<img src="" alt="');
|
this.lit('<img src="" alt="');
|
||||||
} else {
|
} else {
|
||||||
@ -58,14 +74,20 @@ export function markdown(md) {
|
|||||||
node = event.node;
|
node = event.node;
|
||||||
if (event.entering) {
|
if (event.entering) {
|
||||||
if (node.type == 'link') {
|
if (node.type == 'link') {
|
||||||
if (node.destination.startsWith('@') &&
|
if (
|
||||||
node.destination.endsWith('.ed25519')) {
|
node.destination.startsWith('@') &&
|
||||||
|
node.destination.endsWith('.ed25519')
|
||||||
|
) {
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (node.destination.startsWith('%') &&
|
} else if (
|
||||||
node.destination.endsWith('.sha256')) {
|
node.destination.startsWith('%') &&
|
||||||
|
node.destination.endsWith('.sha256')
|
||||||
|
) {
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (node.destination.startsWith('&') &&
|
} else if (
|
||||||
node.destination.endsWith('.sha256')) {
|
node.destination.startsWith('&') &&
|
||||||
|
node.destination.endsWith('.sha256')
|
||||||
|
) {
|
||||||
node.destination = '/' + node.destination + '/view';
|
node.destination = '/' + node.destination + '/view';
|
||||||
}
|
}
|
||||||
} else if (node.type == 'image') {
|
} else if (node.type == 'image') {
|
||||||
|
@ -27,7 +27,8 @@ async function todo_add(list) {
|
|||||||
let set = new Set(names);
|
let set = new Set(names);
|
||||||
set.add(list);
|
set.add(list);
|
||||||
names = JSON.stringify([...set].sort());
|
names = JSON.stringify([...set].sort());
|
||||||
exchanged = original === names || await g_db.exchange('files', original, names);
|
exchanged =
|
||||||
|
original === names || (await g_db.exchange('files', original, names));
|
||||||
}
|
}
|
||||||
return exchanged;
|
return exchanged;
|
||||||
}
|
}
|
||||||
@ -42,7 +43,8 @@ async function todo_remove(list) {
|
|||||||
let set = new Set(names);
|
let set = new Set(names);
|
||||||
set.delete(list);
|
set.delete(list);
|
||||||
names = JSON.stringify([...set].sort());
|
names = JSON.stringify([...set].sort());
|
||||||
exchanged = original === names || await g_db.exchange('files', original, names);
|
exchanged =
|
||||||
|
original === names || (await g_db.exchange('files', original, names));
|
||||||
}
|
}
|
||||||
await g_db.remove('list:' + list);
|
await g_db.remove('list:' + list);
|
||||||
return exchanged;
|
return exchanged;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>TODO</title>
|
<title>TODO</title>
|
||||||
|
@ -4,7 +4,7 @@ import * as tfrpc from '/static/tfrpc.js';
|
|||||||
class TodosElement extends LitElement {
|
class TodosElement extends LitElement {
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
lists: {type: Array}
|
lists: {type: Array},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12,9 +12,12 @@ class TodosElement extends LitElement {
|
|||||||
super();
|
super();
|
||||||
this.lists = [];
|
this.lists = [];
|
||||||
let self = this;
|
let self = this;
|
||||||
tfrpc.rpc.todo_get_all().then(function(lists) {
|
tfrpc.rpc
|
||||||
|
.todo_get_all()
|
||||||
|
.then(function (lists) {
|
||||||
self.lists = lists;
|
self.lists = lists;
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -32,9 +35,15 @@ class TodosElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<div style="display: flex">
|
<div style="display: flex">
|
||||||
${this.lists.map(x => html`
|
${this.lists.map(
|
||||||
<tf-todo-list name=${x.name} .items=${x.items} @change=${this.refresh}></tf-todo-list>
|
(x) => html`
|
||||||
`)}
|
<tf-todo-list
|
||||||
|
name=${x.name}
|
||||||
|
.items=${x.items}
|
||||||
|
@change=${this.refresh}
|
||||||
|
></tf-todo-list>
|
||||||
|
`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<input type="button" @click=${this.new_list} value="+ List"></input>
|
<input type="button" @click=${this.new_list} value="+ List"></input>
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -59,16 +68,22 @@ class TodoListElement extends LitElement {
|
|||||||
save() {
|
save() {
|
||||||
let self = this;
|
let self = this;
|
||||||
console.log('saving', self.name, self.items);
|
console.log('saving', self.name, self.items);
|
||||||
tfrpc.rpc.todo_set(self.name, self.items).then(function() {
|
tfrpc.rpc
|
||||||
|
.todo_set(self.name, self.items)
|
||||||
|
.then(function () {
|
||||||
console.log('saved', self.name, self.items);
|
console.log('saved', self.name, self.items);
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_item(item) {
|
remove_item(item) {
|
||||||
let index = this.items.indexOf(item);
|
let index = this.items.indexOf(item);
|
||||||
this.items = [].concat(this.items.slice(0, index), this.items.slice(index + 1));
|
this.items = [].concat(
|
||||||
|
this.items.slice(0, index),
|
||||||
|
this.items.slice(index + 1)
|
||||||
|
);
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,20 +121,20 @@ class TodoListElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
if (index === this.editing) {
|
if (index === this.editing) {
|
||||||
return html`
|
return html`
|
||||||
<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input>
|
<div><input type="checkbox" ?checked=${item.x} @change=${(x) => self.handle_check(x, item)}></input>
|
||||||
<input
|
<input
|
||||||
id="edit"
|
id="edit"
|
||||||
type="text"
|
type="text"
|
||||||
value=${item.text}
|
value=${item.text}
|
||||||
@change=${event => self.input_change(event, item)}
|
@change=${(event) => self.input_change(event, item)}
|
||||||
@keydown=${event => self.input_keydown(event, item)}
|
@keydown=${(event) => self.input_keydown(event, item)}
|
||||||
@blur=${x => self.input_blur(item)}></input>
|
@blur=${(x) => self.input_blur(item)}></input>
|
||||||
<span @click=${x => self.remove_item(item)} style="cursor: pointer">❎</span></div>
|
<span @click=${(x) => self.remove_item(item)} style="cursor: pointer">❎</span></div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return html`
|
return html`
|
||||||
<div><input type="checkbox" ?checked=${item.x} @change=${x => self.handle_check(x, item)}></input>
|
<div><input type="checkbox" ?checked=${item.x} @change=${(x) => self.handle_check(x, item)}></input>
|
||||||
<span @click=${x => self.editing = index}>${item.text || '(empty)'}</span>
|
<span @click=${(x) => (self.editing = index)}>${item.text || '(empty)'}</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,10 +154,13 @@ class TodoListElement extends LitElement {
|
|||||||
|
|
||||||
rename(new_name) {
|
rename(new_name) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return tfrpc.rpc.todo_rename(this.name, new_name).then(function() {
|
return tfrpc.rpc
|
||||||
|
.todo_rename(this.name, new_name)
|
||||||
|
.then(function () {
|
||||||
self.dispatchEvent(new Event('change'));
|
self.dispatchEvent(new Event('change'));
|
||||||
self.editing_name = false;
|
self.editing_name = false;
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
alert(error.message);
|
alert(error.message);
|
||||||
self.editing_name = false;
|
self.editing_name = false;
|
||||||
@ -163,19 +181,25 @@ class TodoListElement extends LitElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let self = this;
|
let self = this;
|
||||||
let name = this.editing_name ?
|
let name = this.editing_name
|
||||||
html`<input
|
? html`<input
|
||||||
type="text"
|
type="text"
|
||||||
id="edit"
|
id="edit"
|
||||||
@keydown=${event => self.name_keydown(event)}
|
@keydown=${(event) => self.name_keydown(event)}
|
||||||
@blur=${event => self.name_blur(event.srcElement.value)}
|
@blur=${(event) => self.name_blur(event.srcElement.value)}
|
||||||
value=${this.name}></input>` :
|
value=${this.name}></input>`
|
||||||
html`<h2 @click=${x => this.editing_name = true}>${this.name}</h2>`;
|
: html`<h2 @click=${(x) => (this.editing_name = true)}>${this.name}</h2>`;
|
||||||
return html`
|
return html`
|
||||||
<div style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444">
|
<div
|
||||||
|
style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444"
|
||||||
|
>
|
||||||
${name}
|
${name}
|
||||||
${(this.items || []).filter(item => !item.x).map(x => self.render_item(x))}
|
${(this.items || [])
|
||||||
${(this.items || []).filter(item => item.x).map(x => self.render_item(x))}
|
.filter((item) => !item.x)
|
||||||
|
.map((x) => self.render_item(x))}
|
||||||
|
${(this.items || [])
|
||||||
|
.filter((item) => item.x)
|
||||||
|
.map((x) => self.render_item(x))}
|
||||||
<button @click=${self.add_item}>+ Item</button>
|
<button @click=${self.add_item}>+ Item</button>
|
||||||
<button @click=${self.remove_list}>- List</button>
|
<button @click=${self.remove_list}>- List</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,23 +1,36 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="w3.css">
|
<link rel="stylesheet" href="w3.css" />
|
||||||
<link rel="stylesheet" href="fontawesome.min.css">
|
<link rel="stylesheet" href="fontawesome.min.css" />
|
||||||
<link rel="stylesheet" href="regular.min.css">
|
<link rel="stylesheet" href="regular.min.css" />
|
||||||
<link rel="stylesheet" href="solid.min.css">
|
<link rel="stylesheet" href="solid.min.css" />
|
||||||
<link rel="stylesheet" href="brands.min.css">
|
<link rel="stylesheet" href="brands.min.css" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body,h1,h2,h3,h4,h5 {font-family: "Poppins", sans-serif}
|
body,
|
||||||
body {font-size: 16px;}
|
h1,
|
||||||
img {margin-bottom: -8px;}
|
h2,
|
||||||
.mySlides {display: none;}
|
h3,
|
||||||
|
h4,
|
||||||
|
h5 {
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
margin-bottom: -8px;
|
||||||
|
}
|
||||||
|
.mySlides {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
</head>
|
</head>
|
||||||
<body class="w3-content w3-black" style="max-width:1500px;">
|
<body class="w3-content w3-black" style="max-width: 1500px">
|
||||||
<!-- The App Section -->
|
<!-- The App Section -->
|
||||||
<div class="w3-padding-64 w3-white">
|
<div class="w3-padding-64 w3-white">
|
||||||
<div class="w3-row-padding">
|
<div class="w3-row-padding">
|
||||||
@ -25,20 +38,34 @@
|
|||||||
<h1 class="w3-jumbo">
|
<h1 class="w3-jumbo">
|
||||||
<b>😎 Tilde Friends</b>
|
<b>😎 Tilde Friends</b>
|
||||||
</h1>
|
</h1>
|
||||||
<h1 class="w3-xxlarge w3-text-green"><b>Make apps and friends from the comfort of your web browser.</b></h1>
|
<h1 class="w3-xxlarge w3-text-green">
|
||||||
<p>Tilde Friends is a platform for building, running, and sharing web applications.</p>
|
<b>Make apps and friends from the comfort of your web browser.</b>
|
||||||
<p>Available for lots of devices:
|
</h1>
|
||||||
|
<p>
|
||||||
|
Tilde Friends is a platform for building, running, and sharing web
|
||||||
|
applications.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Available for lots of devices:
|
||||||
<i class="fa-brands fa-linux w3-xlarge"></i>
|
<i class="fa-brands fa-linux w3-xlarge"></i>
|
||||||
<i class="fa-brands fa-android w3-xlarge"></i>
|
<i class="fa-brands fa-android w3-xlarge"></i>
|
||||||
<i class="fa-brands fa-apple w3-xlarge"></i>
|
<i class="fa-brands fa-apple w3-xlarge"></i>
|
||||||
<i class="fa fa-mobile-screen w3-xlarge"></i>
|
<i class="fa fa-mobile-screen w3-xlarge"></i>
|
||||||
<i class="fa-brands fa-windows w3-xlarge"></i>
|
<i class="fa-brands fa-windows w3-xlarge"></i>
|
||||||
</p>
|
</p>
|
||||||
<a class="w3-button w3-black w3-padding-large" href="https://www.tildefriends.net/~cory/releases/"><i class="fa fa-download"></i> Download</a>
|
<a
|
||||||
<a class="w3-button w3-black w3-padding-large" href="https://www.tildefriends.net/~cory/apps/"><i class="fa fa-link"></i> Try It</a>
|
class="w3-button w3-black w3-padding-large"
|
||||||
|
href="https://www.tildefriends.net/~cory/releases/"
|
||||||
|
><i class="fa fa-download"></i> Download</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="w3-button w3-black w3-padding-large"
|
||||||
|
href="https://www.tildefriends.net/~cory/apps/"
|
||||||
|
><i class="fa fa-link"></i> Try It</a
|
||||||
|
>
|
||||||
</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" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -47,19 +74,28 @@
|
|||||||
<div class="w3-light-grey">
|
<div class="w3-light-grey">
|
||||||
<div class="w3-row-padding w3-padding-64">
|
<div class="w3-row-padding w3-padding-64">
|
||||||
<div class="w3-col l4 m6 s4">
|
<div class="w3-col l4 m6 s4">
|
||||||
<a href="https://scuttlebutt.nz/"><img class="w3-image w3-round-large" src="ssb.png" alt="Secure Scuttlebutt"></a>
|
<a href="https://scuttlebutt.nz/"
|
||||||
|
><img
|
||||||
|
class="w3-image w3-round-large"
|
||||||
|
src="ssb.png"
|
||||||
|
alt="Secure Scuttlebutt"
|
||||||
|
/></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-col l8 m6" style="height: auto">
|
<div class="w3-col l8 m6" style="height: auto">
|
||||||
<h1 class="w3-jumbo"><b>Built for Sharing</b></h1>
|
<h1 class="w3-jumbo"><b>Built for Sharing</b></h1>
|
||||||
<p>
|
<p>
|
||||||
Tilde Friends participates in the <a href="https://scuttlebutt.nz/">Secure Scuttlebutt</a> distributed social network.
|
Tilde Friends participates in the
|
||||||
|
<a href="https://scuttlebutt.nz/">Secure Scuttlebutt</a> distributed
|
||||||
|
social network.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Share apps with friends. Discover new apps made by enemies. Post pictures of your coffee. Or just lurk.
|
Share apps with friends. Discover new apps made by enemies. Post
|
||||||
|
pictures of your coffee. Or just lurk.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The social network integration provides tools for connecting with other people world-wide
|
The social network integration provides tools for connecting with
|
||||||
while still allowing apps and everything to operate offline.
|
other people world-wide while still allowing apps and everything to
|
||||||
|
operate offline.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -70,14 +106,16 @@
|
|||||||
<div class="w3-row-padding">
|
<div class="w3-row-padding">
|
||||||
<div class="w3-col l8 m6">
|
<div class="w3-col l8 m6">
|
||||||
<h1 class="w3-jumbo"><b>Edit Anything</b></h1>
|
<h1 class="w3-jumbo"><b>Edit Anything</b></h1>
|
||||||
<i class="fa fa-pen-to-square w3-left w3-jumbo w3-text-gray" style="padding: 32px"></i>
|
<i
|
||||||
|
class="fa fa-pen-to-square w3-left w3-jumbo w3-text-gray"
|
||||||
|
style="padding: 32px"
|
||||||
|
></i>
|
||||||
<p>
|
<p>
|
||||||
See that <code><b>edit</b></code> link near the top left corner of this page? It's there for
|
See that <code><b>edit</b></code> link near the top left corner of
|
||||||
every Tilde Friends app, so you can modify and see your changes right away.
|
this page? It's there for every Tilde Friends app, so you can modify
|
||||||
</p>
|
and see your changes right away.
|
||||||
<p>
|
|
||||||
It's kind of like a wiki, but for code!
|
|
||||||
</p>
|
</p>
|
||||||
|
<p>It's kind of like a wiki, but for code!</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -86,16 +124,22 @@
|
|||||||
<div class="w3-padding-64 w3-grey">
|
<div class="w3-padding-64 w3-grey">
|
||||||
<div class="w3-row-padding">
|
<div class="w3-row-padding">
|
||||||
<div class="w3-col">
|
<div class="w3-col">
|
||||||
<h1 class="w3-jumbo" style="text-align: right"><b>Sandbox Security</b></h1>
|
<h1 class="w3-jumbo" style="text-align: right">
|
||||||
<i class="fa fa-road-barrier w3-right w3-jumbo w3-text-yellow" style="padding: 32px"></i>
|
<b>Sandbox Security</b>
|
||||||
|
</h1>
|
||||||
|
<i
|
||||||
|
class="fa fa-road-barrier w3-right w3-jumbo w3-text-yellow"
|
||||||
|
style="padding: 32px"
|
||||||
|
></i>
|
||||||
<p>
|
<p>
|
||||||
Tilde Friends tries to make sure apps can be trusted using similar techniques to how web
|
Tilde Friends tries to make sure apps can be trusted using similar
|
||||||
browsers and operating systems do it.
|
techniques to how web browsers and operating systems do it.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This is all a work in progress, and it varies by platform, so don't give it all your
|
This is all a work in progress, and it varies by platform, so don't
|
||||||
innermost secrets yet, but do kick its tires and
|
give it all your innermost secrets yet, but do kick its tires and
|
||||||
<a href="mailto:cory@tildefriends.net">share</a> any surprises you find.
|
<a href="mailto:cory@tildefriends.net">share</a> any surprises you
|
||||||
|
find.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -105,10 +149,16 @@
|
|||||||
<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>Trusted Technology</b></h1>
|
<h1 class="w3-jumbo"><b>Trusted Technology</b></h1>
|
||||||
<p>Tilde Friends is built using boring, trusted tech.</p>
|
<p>Tilde Friends is built using boring, trusted tech.</p>
|
||||||
<p>Though of course for building Tilde Friends apps, you are free to use whatever fits.</p>
|
<p>
|
||||||
|
Though of course for building Tilde Friends apps, you are free to use
|
||||||
|
whatever fits.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="w3-row" style="margin-top: 64px">
|
<div class="w3-row" style="margin-top: 64px">
|
||||||
<a href="https://en.wikipedia.org/wiki/C_(programming_language)" class="w3-col s3">
|
<a
|
||||||
|
href="https://en.wikipedia.org/wiki/C_(programming_language)"
|
||||||
|
class="w3-col s3"
|
||||||
|
>
|
||||||
<i class="fa fa-c w3-text-blue w3-jumbo"></i>
|
<i class="fa fa-c w3-text-blue w3-jumbo"></i>
|
||||||
<p>C</p>
|
<p>C</p>
|
||||||
</a>
|
</a>
|
||||||
@ -139,7 +189,10 @@
|
|||||||
<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>
|
||||||
<a href="https://github.com/ianlancetaylor/libbacktrace" class="w3-col s3">
|
<a
|
||||||
|
href="https://github.com/ianlancetaylor/libbacktrace"
|
||||||
|
class="w3-col s3"
|
||||||
|
>
|
||||||
<i class="fa fa-burst w3-text-pink w3-jumbo"></i>
|
<i class="fa fa-burst w3-text-pink w3-jumbo"></i>
|
||||||
<p>libbacktrace</p>
|
<p>libbacktrace</p>
|
||||||
</a>
|
</a>
|
||||||
@ -167,7 +220,10 @@
|
|||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="w3-container w3-padding-32 w3-blue-grey w3-center w3-xlarge">
|
<footer class="w3-container w3-padding-32 w3-blue-grey w3-center w3-xlarge">
|
||||||
<p class="w3-medium">This page and Tilde Friends itself was made by Cory mostly in coffee shops and a local pizza place.</p>
|
<p class="w3-medium">
|
||||||
|
This page and Tilde Friends itself was made by Cory mostly in coffee
|
||||||
|
shops and a local pizza place.
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -11,10 +11,13 @@ function markdown(md) {
|
|||||||
let node = event.node;
|
let node = event.node;
|
||||||
if (event.entering) {
|
if (event.entering) {
|
||||||
if (node.destination?.startsWith('&')) {
|
if (node.destination?.startsWith('&')) {
|
||||||
node.destination = '/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
node.destination =
|
||||||
|
'/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
||||||
} else if (node.type === 'link') {
|
} else if (node.type === 'link') {
|
||||||
if (node.destination.indexOf(':') == -1 &&
|
if (
|
||||||
node.destination.indexOf('/') == -1) {
|
node.destination.indexOf(':') == -1 &&
|
||||||
|
node.destination.indexOf('/') == -1
|
||||||
|
) {
|
||||||
node.destination = `${node.destination}`;
|
node.destination = `${node.destination}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -29,7 +32,9 @@ async function main() {
|
|||||||
let wiki_name = request.path.substring(0, slash);
|
let wiki_name = request.path.substring(0, slash);
|
||||||
let wiki_doc_name = request.path.substring(slash + 1);
|
let wiki_doc_name = request.path.substring(slash + 1);
|
||||||
|
|
||||||
let ids = Object.keys(await ssb.following(await ssb.getOwnerIdentities(), 1));
|
let ids = Object.keys(
|
||||||
|
await ssb.following(await ssb.getOwnerIdentities(), 1)
|
||||||
|
);
|
||||||
let [max_row_id, wikis] = await utils.collection(ids, 'wiki', null, -1, {});
|
let [max_row_id, wikis] = await utils.collection(ids, 'wiki', null, -1, {});
|
||||||
let wiki;
|
let wiki;
|
||||||
for (let w of Object.values(wikis)) {
|
for (let w of Object.values(wikis)) {
|
||||||
@ -40,7 +45,13 @@ async function main() {
|
|||||||
}
|
}
|
||||||
let wiki_doc;
|
let wiki_doc;
|
||||||
if (wiki) {
|
if (wiki) {
|
||||||
let [max_row_id, wiki_docs] = await utils.collection(ids, 'wiki-doc', wiki.id, -1, {});
|
let [max_row_id, wiki_docs] = await utils.collection(
|
||||||
|
ids,
|
||||||
|
'wiki-doc',
|
||||||
|
wiki.id,
|
||||||
|
-1,
|
||||||
|
{}
|
||||||
|
);
|
||||||
for (let w of Object.values(wiki_docs)) {
|
for (let w of Object.values(wiki_docs)) {
|
||||||
if (w.name === wiki_doc_name && !w.tombstone) {
|
if (w.name === wiki_doc_name && !w.tombstone) {
|
||||||
wiki_doc = w;
|
wiki_doc = w;
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<base target="_top">
|
<base target="_top" />
|
||||||
</head>
|
</head>
|
||||||
<body style="color: #fff">
|
<body style="color: #fff">
|
||||||
<tf-collections-app></tf-collections-app>
|
<tf-collections-app></tf-collections-app>
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="tf-collection.js" type="module"></script>
|
<script src="tf-collection.js" type="module"></script>
|
||||||
<script src="tf-id-picker.js" type="module"></script>
|
<script src="tf-id-picker.js" type="module"></script>
|
||||||
<script src="tf-wiki-doc.js" type="module"></script>
|
<script src="tf-wiki-doc.js" type="module"></script>
|
||||||
|
@ -14,52 +14,62 @@ class TfCollectionElement extends LitElement {
|
|||||||
|
|
||||||
on_create(event) {
|
on_create(event) {
|
||||||
let name = this.shadowRoot.getElementById('create_name').value;
|
let name = this.shadowRoot.getElementById('create_name').value;
|
||||||
this.dispatchEvent(new CustomEvent('create', {
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('create', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: {
|
detail: {
|
||||||
name: name,
|
name: name,
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
this.is_creating = false;
|
this.is_creating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
on_rename(event) {
|
on_rename(event) {
|
||||||
let id = this.shadowRoot.getElementById('select').value;
|
let id = this.shadowRoot.getElementById('select').value;
|
||||||
let name = this.shadowRoot.getElementById('rename_name').value;
|
let name = this.shadowRoot.getElementById('rename_name').value;
|
||||||
this.dispatchEvent(new CustomEvent('rename', {
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('rename', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: {
|
detail: {
|
||||||
id: id,
|
id: id,
|
||||||
value: this.collection[id],
|
value: this.collection[id],
|
||||||
name: name,
|
name: name,
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
this.is_renaming = false;
|
this.is_renaming = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
on_tombstone(event) {
|
on_tombstone(event) {
|
||||||
let id = this.shadowRoot.getElementById('select').value;
|
let id = this.shadowRoot.getElementById('select').value;
|
||||||
if (confirm(`Are you sure you want to delete '${this.collection[id].name}'?`)) {
|
if (
|
||||||
this.dispatchEvent(new CustomEvent('tombstone', {
|
confirm(`Are you sure you want to delete '${this.collection[id].name}'?`)
|
||||||
|
) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('tombstone', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: {
|
detail: {
|
||||||
id: id,
|
id: id,
|
||||||
value: this.collection[id],
|
value: this.collection[id],
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
on_selected(event) {
|
on_selected(event) {
|
||||||
let id = event.srcElement.value;
|
let id = event.srcElement.value;
|
||||||
this.selected_id = id != '' ? id : undefined;
|
this.selected_id = id != '' ? id : undefined;
|
||||||
this.dispatchEvent(new CustomEvent('change', {
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('change', {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
detail: {
|
detail: {
|
||||||
id: id,
|
id: id,
|
||||||
value: this.collection[id],
|
value: this.collection[id],
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -68,25 +78,35 @@ class TfCollectionElement extends LitElement {
|
|||||||
<span style="display: inline-flex; flex-direction: row">
|
<span style="display: inline-flex; flex-direction: row">
|
||||||
<select @change=${this.on_selected} id="select" value=${this.selected_id}>
|
<select @change=${this.on_selected} id="select" value=${this.selected_id}>
|
||||||
<option value="" ?selected=${this.selected_id === ''} disabled hidden>(select)</option>
|
<option value="" ?selected=${this.selected_id === ''} disabled hidden>(select)</option>
|
||||||
${Object.values(this.collection ?? {}).sort((x, y) => x.name.localeCompare(y.name)).map(x => html`<option value=${x.id} ?selected=${this.selected_id === x.id}>${x.name}</option>`)}
|
${Object.values(this.collection ?? {})
|
||||||
|
.sort((x, y) => x.name.localeCompare(y.name))
|
||||||
|
.map(
|
||||||
|
(x) =>
|
||||||
|
html`<option
|
||||||
|
value=${x.id}
|
||||||
|
?selected=${this.selected_id === x.id}
|
||||||
|
>
|
||||||
|
${x.name}
|
||||||
|
</option>`
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
<span ?hidden=${!this.is_renaming || !this.whoami}>
|
<span ?hidden=${!this.is_renaming || !this.whoami}>
|
||||||
<span style="display: inline-flex; flex-direction: row; margin-left: 8px; margin-right: 8px">
|
<span style="display: inline-flex; flex-direction: row; margin-left: 8px; margin-right: 8px">
|
||||||
<label for="rename_name">🏷Rename to:</label>
|
<label for="rename_name">🏷Rename to:</label>
|
||||||
<input type="text" id="rename_name"></input>
|
<input type="text" id="rename_name"></input>
|
||||||
<button @click=${this.on_rename}>Rename ${this.type}</button>
|
<button @click=${this.on_rename}>Rename ${this.type}</button>
|
||||||
<button @click=${() => self.is_renaming = false}>x</button>
|
<button @click=${() => (self.is_renaming = false)}>x</button>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<button @click=${() => self.is_renaming = true} ?disabled=${this.is_renaming || !this.selected_id} ?hidden=${!this.whoami}>🏷</button>
|
<button @click=${() => (self.is_renaming = true)} ?disabled=${this.is_renaming || !this.selected_id} ?hidden=${!this.whoami}>🏷</button>
|
||||||
<button @click=${self.on_tombstone} ?disabled=${!this.selected_id} ?hidden=${!this.whoami}>🪦</button>
|
<button @click=${self.on_tombstone} ?disabled=${!this.selected_id} ?hidden=${!this.whoami}>🪦</button>
|
||||||
<span ?hidden=${!this.is_creating || !this.whoami}>
|
<span ?hidden=${!this.is_creating || !this.whoami}>
|
||||||
<label for="create_name">New ${this.type} name:</label>
|
<label for="create_name">New ${this.type} name:</label>
|
||||||
<input type="text" id="create_name"></input>
|
<input type="text" id="create_name"></input>
|
||||||
<button @click=${this.on_create}>Create ${this.type}</button>
|
<button @click=${this.on_create}>Create ${this.type}</button>
|
||||||
<button @click=${() => self.is_creating = false}>x</button>
|
<button @click=${() => (self.is_creating = false)}>x</button>
|
||||||
</span>
|
</span>
|
||||||
<button @click=${() => self.is_creating = true} ?hidden=${this.is_creating || !this.whoami}>+</button>
|
<button @click=${() => (self.is_creating = true)} ?hidden=${this.is_creating || !this.whoami}>+</button>
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -19,15 +19,22 @@ class TfIdentityPickerElement extends LitElement {
|
|||||||
|
|
||||||
changed(event) {
|
changed(event) {
|
||||||
this.selected = event.srcElement.value;
|
this.selected = event.srcElement.value;
|
||||||
this.dispatchEvent(new Event('change', {
|
this.dispatchEvent(
|
||||||
|
new Event('change', {
|
||||||
srcElement: this,
|
srcElement: this,
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<select @change=${this.changed} style="max-width: 100%">
|
<select @change=${this.changed} style="max-width: 100%">
|
||||||
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
${(this.ids ?? []).map(
|
||||||
|
(id) =>
|
||||||
|
html`<option ?selected=${id == this.selected} value=${id}>
|
||||||
|
${id}
|
||||||
|
</option>`
|
||||||
|
)}
|
||||||
</select>
|
</select>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
tfrpc.register(function hash_changed(hash) {
|
tfrpc.register(function hash_changed(hash) {
|
||||||
self.notify_hash_changed(hash);
|
self.notify_hash_changed(hash);
|
||||||
});
|
});
|
||||||
tfrpc.rpc.get_hash().then(hash => self.notify_hash_changed(hash));
|
tfrpc.rpc.get_hash().then((hash) => self.notify_hash_changed(hash));
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
@ -49,10 +49,16 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
let max_rowid;
|
let max_rowid;
|
||||||
let wikis;
|
let wikis;
|
||||||
let start_whoami = this.whoami;
|
let start_whoami = this.whoami;
|
||||||
while (true)
|
while (true) {
|
||||||
{
|
|
||||||
console.log('read_wikis', this.whoami);
|
console.log('read_wikis', this.whoami);
|
||||||
[max_rowid, wikis] = await tfrpc.rpc.collection(this.following, 'wiki', undefined, max_rowid, wikis, false);
|
[max_rowid, wikis] = await tfrpc.rpc.collection(
|
||||||
|
this.following,
|
||||||
|
'wiki',
|
||||||
|
undefined,
|
||||||
|
max_rowid,
|
||||||
|
wikis,
|
||||||
|
false
|
||||||
|
);
|
||||||
console.log('read ->', wikis);
|
console.log('read ->', wikis);
|
||||||
if (this.whoami !== start_whoami) {
|
if (this.whoami !== start_whoami) {
|
||||||
break;
|
break;
|
||||||
@ -70,9 +76,14 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
let start_id = this.wiki.id;
|
let start_id = this.wiki.id;
|
||||||
let max_rowid;
|
let max_rowid;
|
||||||
let wiki_docs;
|
let wiki_docs;
|
||||||
while (true)
|
while (true) {
|
||||||
{
|
[max_rowid, wiki_docs] = await tfrpc.rpc.collection(
|
||||||
[max_rowid, wiki_docs] = await tfrpc.rpc.collection(this.wiki?.editors, 'wiki-doc', this.wiki?.id, max_rowid, wiki_docs);
|
this.wiki?.editors,
|
||||||
|
'wiki-doc',
|
||||||
|
this.wiki?.id,
|
||||||
|
max_rowid,
|
||||||
|
wiki_docs
|
||||||
|
);
|
||||||
if (this.wiki?.id !== start_id) {
|
if (this.wiki?.id !== start_id) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -128,7 +139,11 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update_hash() {
|
update_hash() {
|
||||||
tfrpc.rpc.set_hash(this.wiki_doc ? `${this.wiki.name}/${this.wiki_doc.name}` : `${this.wiki.name}`);
|
tfrpc.rpc.set_hash(
|
||||||
|
this.wiki_doc
|
||||||
|
? `${this.wiki.name}/${this.wiki_doc.name}`
|
||||||
|
: `${this.wiki.name}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async on_wiki_changed(event) {
|
async on_wiki_changed(event) {
|
||||||
@ -174,7 +189,7 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
if (confirm(`Are you sure you want to remove ${id} as an editor?`)) {
|
if (confirm(`Are you sure you want to remove ${id} as an editor?`)) {
|
||||||
let editors = [...this.wiki.editors];
|
let editors = [...this.wiki.editors];
|
||||||
if (editors.indexOf(id) != -1) {
|
if (editors.indexOf(id) != -1) {
|
||||||
editors = editors.filter(x => x !== id);
|
editors = editors.filter((x) => x !== id);
|
||||||
}
|
}
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, {
|
await tfrpc.rpc.appendMessage(this.whoami, {
|
||||||
type: 'wiki',
|
type: 'wiki',
|
||||||
@ -252,34 +267,45 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
<tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed} ?hidden=${!this.ids?.length}></tf-id-picker>
|
<tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed} ?hidden=${!this.ids?.length}></tf-id-picker>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
${keyed(this.whoami, html`<tf-collection
|
${keyed(
|
||||||
|
this.whoami,
|
||||||
|
html`<tf-collection
|
||||||
.collection=${this.wikis}
|
.collection=${this.wikis}
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
selected_id=${this.wiki?.id}
|
selected_id=${this.wiki?.id}
|
||||||
@create=${this.on_wiki_create}
|
@create=${this.on_wiki_create}
|
||||||
@rename=${this.on_wiki_rename}
|
@rename=${this.on_wiki_rename}
|
||||||
@tombstone=${this.on_wiki_tombstone}
|
@tombstone=${this.on_wiki_tombstone}
|
||||||
@change=${this.on_wiki_changed}></tf-collection>`)}
|
@change=${this.on_wiki_changed}
|
||||||
${keyed(this.wiki_doc?.id, html`<tf-collection
|
></tf-collection>`
|
||||||
|
)}
|
||||||
|
${keyed(
|
||||||
|
this.wiki_doc?.id,
|
||||||
|
html`<tf-collection
|
||||||
.collection=${this.wiki_docs}
|
.collection=${this.wiki_docs}
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
selected_id=${(this.wiki_doc && this.wiki_doc?.parent == this.wiki?.id) ? this.wiki_doc?.id : ''}
|
selected_id=${this.wiki_doc &&
|
||||||
|
this.wiki_doc?.parent == this.wiki?.id
|
||||||
|
? this.wiki_doc?.id
|
||||||
|
: ''}
|
||||||
@create=${this.on_wiki_doc_create}
|
@create=${this.on_wiki_doc_create}
|
||||||
@rename=${this.on_wiki_doc_rename}
|
@rename=${this.on_wiki_doc_rename}
|
||||||
@tombstone=${this.on_wiki_doc_tombstone}
|
@tombstone=${this.on_wiki_doc_tombstone}
|
||||||
@change=${this.on_wiki_doc_changed}></tf-collection>`)}
|
@change=${this.on_wiki_doc_changed}
|
||||||
<button @click=${() => self.expand_editors = !self.expand_editors}>${this.wiki?.editors?.length} editor${this.wiki?.editors?.length > 1 ? 's' : ''}</button>
|
></tf-collection>`
|
||||||
|
)}
|
||||||
|
<button @click=${() => (self.expand_editors = !self.expand_editors)}>${this.wiki?.editors?.length} editor${this.wiki?.editors?.length > 1 ? 's' : ''}</button>
|
||||||
<div ?hidden=${!this.wiki?.editors || !this.expand_editors}>
|
<div ?hidden=${!this.wiki?.editors || !this.expand_editors}>
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
${this.wiki?.editors.map(id => html`<li><button ?hidden=${id == this.whoami} @click=${() => self.on_remove_editor(id)}>x</button> ${id}</li>`)}
|
${this.wiki?.editors.map((id) => html`<li><button ?hidden=${id == this.whoami} @click=${() => self.on_remove_editor(id)}>x</button> ${id}</li>`)}
|
||||||
<li>
|
<li>
|
||||||
<button @click=${() => self.adding_editor = true} ?hidden=${this.wiki?.editors?.indexOf(this.whoami) == -1 || this.adding_editor}>+</button>
|
<button @click=${() => (self.adding_editor = true)} ?hidden=${this.wiki?.editors?.indexOf(this.whoami) == -1 || this.adding_editor}>+</button>
|
||||||
<div ?hidden=${!this.adding_editor}>
|
<div ?hidden=${!this.adding_editor}>
|
||||||
<label for="add_editor">Add Editor:</label>
|
<label for="add_editor">Add Editor:</label>
|
||||||
<input type="text" id="add_editor"></input>
|
<input type="text" id="add_editor"></input>
|
||||||
<button @click=${this.on_add_editor}>Add Editor</button>
|
<button @click=${this.on_add_editor}>Add Editor</button>
|
||||||
<button @click=${() => self.adding_editor = false}>x</button>
|
<button @click=${() => (self.adding_editor = false)}>x</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -288,22 +314,53 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: row">
|
<div style="display: flex; flex-direction: row">
|
||||||
<div style="flex: 0 0">
|
<div style="flex: 0 0">
|
||||||
${Object.values(this.wikis || {}).sort((x, y) => x.name.localeCompare(y.name)).map(wiki => html`
|
${Object.values(this.wikis || {})
|
||||||
<div class="toc ${self.wiki?.id === wiki.id ? 'selected' : ''}" style="white-space: nowrap; cursor: pointer" @click=${() => self.on_wiki_changed({detail: {value: wiki}})}>${wiki.name}</div>
|
.sort((x, y) => x.name.localeCompare(y.name))
|
||||||
<ul>
|
.map(
|
||||||
${Object.values(self.wiki_docs || {}).filter(doc => doc.parent === wiki?.id).sort((x, y) => x.name.localeCompare(y.name)).map(doc => html`
|
(wiki) => html`
|
||||||
<li class="toc ${self.wiki_doc?.id === doc.id ? 'selected' : ''}" style="white-space: nowrap; cursor: pointer; list-style: none; text-indent: -1rem" @click=${() => self.on_wiki_doc_changed({detail: {value: doc}})}>${doc?.private ? '🔒' : '📄'} ${doc.name}</li>
|
<div
|
||||||
`)}
|
class="toc ${self.wiki?.id === wiki.id ? 'selected' : ''}"
|
||||||
</ul>
|
style="white-space: nowrap; cursor: pointer"
|
||||||
`)}
|
@click=${() => self.on_wiki_changed({detail: {value: wiki}})}
|
||||||
|
>
|
||||||
|
${wiki.name}
|
||||||
</div>
|
</div>
|
||||||
${this.wiki_doc && this.wiki_doc.parent === this.wiki?.id ? html`
|
<ul>
|
||||||
|
${Object.values(self.wiki_docs || {})
|
||||||
|
.filter((doc) => doc.parent === wiki?.id)
|
||||||
|
.sort((x, y) => x.name.localeCompare(y.name))
|
||||||
|
.map(
|
||||||
|
(doc) => html`
|
||||||
|
<li
|
||||||
|
class="toc ${self.wiki_doc?.id === doc.id
|
||||||
|
? 'selected'
|
||||||
|
: ''}"
|
||||||
|
style="white-space: nowrap; cursor: pointer; list-style: none; text-indent: -1rem"
|
||||||
|
@click=${() =>
|
||||||
|
self.on_wiki_doc_changed({
|
||||||
|
detail: {value: doc},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${doc?.private ? '🔒' : '📄'} ${doc.name}
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
${
|
||||||
|
this.wiki_doc && this.wiki_doc.parent === this.wiki?.id
|
||||||
|
? html`
|
||||||
<tf-wiki-doc
|
<tf-wiki-doc
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
whoami=${this.whoami}
|
whoami=${this.whoami}
|
||||||
.wiki=${this.wiki}
|
.wiki=${this.wiki}
|
||||||
.value=${this.wiki_doc}></tf-wiki-doc>
|
.value=${this.wiki_doc}
|
||||||
` : undefined}
|
></tf-wiki-doc>
|
||||||
|
`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -29,10 +29,16 @@ class TfWikiDocElement extends LitElement {
|
|||||||
let node = event.node;
|
let node = event.node;
|
||||||
if (event.entering) {
|
if (event.entering) {
|
||||||
if (node.destination?.startsWith('&')) {
|
if (node.destination?.startsWith('&')) {
|
||||||
node.destination = '/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
node.destination =
|
||||||
|
'/' +
|
||||||
|
node.destination +
|
||||||
|
'/view?filename=' +
|
||||||
|
node.firstChild?.literal;
|
||||||
} else if (node.type === 'link') {
|
} else if (node.type === 'link') {
|
||||||
if (node.destination.indexOf(':') == -1 &&
|
if (
|
||||||
node.destination.indexOf('/') == -1) {
|
node.destination.indexOf(':') == -1 &&
|
||||||
|
node.destination.indexOf('/') == -1
|
||||||
|
) {
|
||||||
node.destination = `#${this.wiki?.name}/${node.destination}`;
|
node.destination = `#${this.wiki?.name}/${node.destination}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,7 +76,9 @@ class TfWikiDocElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
thumbnail(md) {
|
thumbnail(md) {
|
||||||
let m = md ? md.match(/\!\[image:[^\]]+\]\((\&.{44}\.sha256)\).*/) : undefined;
|
let m = md
|
||||||
|
? md.match(/\!\[image:[^\]]+\]\((\&.{44}\.sha256)\).*/)
|
||||||
|
: undefined;
|
||||||
return m ? m[1] : undefined;
|
return m ? m[1] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,12 +114,16 @@ class TfWikiDocElement extends LitElement {
|
|||||||
key: this.value.id,
|
key: this.value.id,
|
||||||
parent: this.value.parent,
|
parent: this.value.parent,
|
||||||
blob: id,
|
blob: id,
|
||||||
mentions: this.blob.match(/(&.{44}.sha256)/g)?.map(x => ({link: x})),
|
mentions: this.blob.match(/(&.{44}.sha256)/g)?.map((x) => ({link: x})),
|
||||||
private: this.value?.private,
|
private: this.value?.private,
|
||||||
};
|
};
|
||||||
if (draft) {
|
if (draft) {
|
||||||
message.recps = this.value.editors;
|
message.recps = this.value.editors;
|
||||||
message = await tfrpc.rpc.encrypt(this.whoami, this.value.editors, JSON.stringify(message));
|
message = await tfrpc.rpc.encrypt(
|
||||||
|
this.whoami,
|
||||||
|
this.value.editors,
|
||||||
|
JSON.stringify(message)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message);
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
this.is_editing = false;
|
this.is_editing = false;
|
||||||
@ -136,7 +148,7 @@ class TfWikiDocElement extends LitElement {
|
|||||||
summary: this.summary(blob),
|
summary: this.summary(blob),
|
||||||
thumbnail: this.thumbnail(blob),
|
thumbnail: this.thumbnail(blob),
|
||||||
blog: id,
|
blog: id,
|
||||||
mentions: this.blob.match(/(&.{44}.sha256)/g)?.map(x => ({link: x})),
|
mentions: this.blob.match(/(&.{44}.sha256)/g)?.map((x) => ({link: x})),
|
||||||
};
|
};
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message);
|
await tfrpc.rpc.appendMessage(this.whoami, message);
|
||||||
this.is_editing = false;
|
this.is_editing = false;
|
||||||
@ -155,13 +167,17 @@ class TfWikiDocElement extends LitElement {
|
|||||||
let context = canvas.getContext('2d');
|
let context = canvas.getContext('2d');
|
||||||
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
let data_url = canvas.toDataURL(mime_type);
|
let data_url = canvas.toDataURL(mime_type);
|
||||||
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
|
let result = atob(data_url.split(',')[1])
|
||||||
|
.split('')
|
||||||
|
.map((x) => x.charCodeAt(0));
|
||||||
resolve(result);
|
resolve(result);
|
||||||
};
|
};
|
||||||
img.onerror = function (event) {
|
img.onerror = function (event) {
|
||||||
reject(new Error('Failed to load image.'));
|
reject(new Error('Failed to load image.'));
|
||||||
};
|
};
|
||||||
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
|
let raw = Array.from(new Uint8Array(buffer))
|
||||||
|
.map((b) => String.fromCharCode(b))
|
||||||
|
.join('');
|
||||||
let original = `data:${type};base64,${btoa(raw)}`;
|
let original = `data:${type};base64,${btoa(raw)}`;
|
||||||
img.src = original;
|
img.src = original;
|
||||||
});
|
});
|
||||||
@ -187,7 +203,11 @@ class TfWikiDocElement extends LitElement {
|
|||||||
let best_buffer;
|
let best_buffer;
|
||||||
let best_type;
|
let best_type;
|
||||||
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
for (let format of ['image/png', 'image/jpeg', 'image/webp']) {
|
||||||
let test_buffer = await self.convert_to_format(buffer, file.type, format);
|
let test_buffer = await self.convert_to_format(
|
||||||
|
buffer,
|
||||||
|
file.type,
|
||||||
|
format
|
||||||
|
);
|
||||||
if (!best_buffer || test_buffer.length < best_buffer.length) {
|
if (!best_buffer || test_buffer.length < best_buffer.length) {
|
||||||
best_buffer = test_buffer;
|
best_buffer = test_buffer;
|
||||||
best_type = format;
|
best_type = format;
|
||||||
@ -234,31 +254,84 @@ class TfWikiDocElement extends LitElement {
|
|||||||
let thumbnail_ref = this.thumbnail(this.blob);
|
let thumbnail_ref = this.thumbnail(this.blob);
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
a:link { color: #268bd2 }
|
a:link {
|
||||||
a:visited { color: #6c71c4 }
|
color: #268bd2;
|
||||||
a:hover { color: #859900 }
|
}
|
||||||
a:active { color: #2aa198 }
|
a:visited {
|
||||||
|
color: #6c71c4;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #859900;
|
||||||
|
}
|
||||||
|
a:active {
|
||||||
|
color: #2aa198;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<div style="display: inline-flex; flex-direction: row">
|
<div style="display: inline-flex; flex-direction: row">
|
||||||
<button ?disabled=${!this.whoami || this.is_editing} @click=${() => self.is_editing = true}>Edit</button>
|
<button
|
||||||
<button ?disabled=${this.blob == this.blob_original} @click=${this.on_save_draft}>Save Draft</button>
|
?disabled=${!this.whoami || this.is_editing}
|
||||||
<button ?disabled=${this.blob == this.blob_original && !this.value?.draft} @click=${this.on_publish}>Publish</button>
|
@click=${() => (self.is_editing = true)}
|
||||||
<button ?disabled=${!this.is_editing} @click=${this.on_discard}>Discard</button>
|
>
|
||||||
<button ?disabled=${!this.is_editing} @click=${() => self.value = Object.assign({}, self.value, {private: !self.value.private})}>${this.value?.private ? 'Make Public' : 'Make Private'}</button>
|
Edit
|
||||||
<button ?disabled=${!this.is_editing} @click=${this.on_blog_publish}>Publish Blog</button>
|
</button>
|
||||||
|
<button
|
||||||
|
?disabled=${this.blob == this.blob_original}
|
||||||
|
@click=${this.on_save_draft}
|
||||||
|
>
|
||||||
|
Save Draft
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
?disabled=${this.blob == this.blob_original && !this.value?.draft}
|
||||||
|
@click=${this.on_publish}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
|
<button ?disabled=${!this.is_editing} @click=${this.on_discard}>
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
?disabled=${!this.is_editing}
|
||||||
|
@click=${() =>
|
||||||
|
(self.value = Object.assign({}, self.value, {
|
||||||
|
private: !self.value.private,
|
||||||
|
}))}
|
||||||
|
>
|
||||||
|
${this.value?.private ? 'Make Public' : 'Make Private'}
|
||||||
|
</button>
|
||||||
|
<button ?disabled=${!this.is_editing} @click=${this.on_blog_publish}>
|
||||||
|
Publish Blog
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ?hidden=${!this.value?.private} style="color: #800">🔒 document is private</div>
|
<div ?hidden=${!this.value?.private} style="color: #800">
|
||||||
<div style="display: flex; flex-direction: row; ${this.value?.private ? 'border-top: 4px solid #800' : ''}">
|
🔒 document is private
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="display: flex; flex-direction: row; ${this.value?.private
|
||||||
|
? 'border-top: 4px solid #800'
|
||||||
|
: ''}"
|
||||||
|
>
|
||||||
<textarea
|
<textarea
|
||||||
?hidden=${!this.is_editing}
|
?hidden=${!this.is_editing}
|
||||||
style="flex: 1 1; min-height: 10em; ${this.value?.private ? 'border: 4px solid #800' : ''}"
|
style="flex: 1 1; min-height: 10em; ${this.value?.private
|
||||||
|
? 'border: 4px solid #800'
|
||||||
|
: ''}"
|
||||||
@input=${this.on_edit}
|
@input=${this.on_edit}
|
||||||
@paste=${this.paste}
|
@paste=${this.paste}
|
||||||
.value=${this.blob ?? ''}></textarea>
|
.value=${this.blob ?? ''}
|
||||||
|
></textarea>
|
||||||
<div style="flex: 1 1">
|
<div style="flex: 1 1">
|
||||||
<div ?hidden=${!this.is_editing} style="border: 1px solid #fff; border-radius: 1em; padding: 0.5em">
|
<div
|
||||||
<img ?hidden=${!thumbnail_ref} style="max-width: 128px; max-height: 128px; float: right" src="/${thumbnail_ref}/view">
|
?hidden=${!this.is_editing}
|
||||||
<h1 ?hidden=${!this.title(this.blob)}>${unsafeHTML(this.markdown(this.title(this.blob)))}</h1>
|
style="border: 1px solid #fff; border-radius: 1em; padding: 0.5em"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
?hidden=${!thumbnail_ref}
|
||||||
|
style="max-width: 128px; max-height: 128px; float: right"
|
||||||
|
src="/${thumbnail_ref}/view"
|
||||||
|
/>
|
||||||
|
<h1 ?hidden=${!this.title(this.blob)}>
|
||||||
|
${unsafeHTML(this.markdown(this.title(this.blob)))}
|
||||||
|
</h1>
|
||||||
${unsafeHTML(this.markdown(this.summary(this.blob)))}
|
${unsafeHTML(this.markdown(this.summary(this.blob)))}
|
||||||
</div>
|
</div>
|
||||||
${unsafeHTML(this.markdown(this.blob))}
|
${unsafeHTML(this.markdown(this.blob))}
|
||||||
|
@ -2,7 +2,7 @@ async function process_message(whoami, collection, message, kind, parent) {
|
|||||||
let content = JSON.parse(message.content);
|
let content = JSON.parse(message.content);
|
||||||
if (typeof content == 'string') {
|
if (typeof content == 'string') {
|
||||||
let x;
|
let x;
|
||||||
for (let id of (whoami || [])) {
|
for (let id of whoami || []) {
|
||||||
x = await ssb.privateMessageDecrypt(id, content);
|
x = await ssb.privateMessageDecrypt(id, content);
|
||||||
if (x) {
|
if (x) {
|
||||||
try {
|
try {
|
||||||
@ -17,8 +17,7 @@ async function process_message(whoami, collection, message, kind, parent) {
|
|||||||
if (!x) {
|
if (!x) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (content.type !== kind ||
|
if (content.type !== kind || (parent && content.parent !== parent)) {
|
||||||
(parent && content.parent !== parent)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -28,7 +27,10 @@ async function process_message(whoami, collection, message, kind, parent) {
|
|||||||
if (content?.tombstone) {
|
if (content?.tombstone) {
|
||||||
delete collection[content.key];
|
delete collection[content.key];
|
||||||
} else {
|
} else {
|
||||||
collection[content.key] = Object.assign(collection[content.key] || {}, content);
|
collection[content.key] = Object.assign(
|
||||||
|
collection[content.key] || {},
|
||||||
|
content
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
collection[message.id] = Object.assign(content, {id: message.id});
|
collection[message.id] = Object.assign(content, {id: message.id});
|
||||||
@ -58,26 +60,42 @@ ssb.addEventListener('message', function(id) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function collection(ids, kind, parent, max_rowid, data, include_private) {
|
export async function collection(
|
||||||
|
ids,
|
||||||
|
kind,
|
||||||
|
parent,
|
||||||
|
max_rowid,
|
||||||
|
data,
|
||||||
|
include_private
|
||||||
|
) {
|
||||||
let whoami = await ssb.getIdentities();
|
let whoami = await ssb.getIdentities();
|
||||||
data = data ?? {};
|
data = data ?? {};
|
||||||
let rowid = 0;
|
let rowid = 0;
|
||||||
let first = true;
|
let first = true;
|
||||||
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
await ssb.sqlAsync(
|
||||||
|
'SELECT MAX(rowid) AS rowid FROM messages',
|
||||||
|
[],
|
||||||
|
function (row) {
|
||||||
rowid = row.rowid;
|
rowid = row.rowid;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
while (true) {
|
while (true) {
|
||||||
if (rowid == max_rowid) {
|
if (rowid == max_rowid) {
|
||||||
await new_message();
|
await new_message();
|
||||||
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
await ssb.sqlAsync(
|
||||||
|
'SELECT MAX(rowid) AS rowid FROM messages',
|
||||||
|
[],
|
||||||
|
function (row) {
|
||||||
rowid = row.rowid;
|
rowid = row.rowid;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let modified = false;
|
let modified = false;
|
||||||
let rows = [];
|
let rows = [];
|
||||||
await ssb.sqlAsync(`
|
await ssb.sqlAsync(
|
||||||
|
`
|
||||||
SELECT messages.id, author, content, timestamp
|
SELECT messages.id, author, content, timestamp
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?1) AS id ON messages.author = id.value
|
JOIN json_each(?1) AS id ON messages.author = id.value
|
||||||
@ -88,9 +106,19 @@ export async function collection(ids, kind, parent, max_rowid, data, include_pri
|
|||||||
(?5 IS NULL OR json_extract(messages.content, '$.parent') = ?5)) OR
|
(?5 IS NULL OR json_extract(messages.content, '$.parent') = ?5)) OR
|
||||||
(?6 AND content LIKE '"%'))
|
(?6 AND content LIKE '"%'))
|
||||||
ORDER BY timestamp
|
ORDER BY timestamp
|
||||||
`, [JSON.stringify(ids), max_rowid ?? -1, rowid, kind, parent, include_private ? true : false], function(row) {
|
`,
|
||||||
|
[
|
||||||
|
JSON.stringify(ids),
|
||||||
|
max_rowid ?? -1,
|
||||||
|
rowid,
|
||||||
|
kind,
|
||||||
|
parent,
|
||||||
|
include_private ? true : false,
|
||||||
|
],
|
||||||
|
function (row) {
|
||||||
rows.push(row);
|
rows.push(row);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
max_rowid = rowid;
|
max_rowid = rowid;
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
if (await process_message(whoami, data, row, kind, parent)) {
|
if (await process_message(whoami, data, row, kind, parent)) {
|
||||||
|
64
core/app.js
64
core/app.js
@ -18,7 +18,7 @@ function App() {
|
|||||||
|
|
||||||
App.prototype.readOutput = function (callback) {
|
App.prototype.readOutput = function (callback) {
|
||||||
this._on_output = callback;
|
this._on_output = callback;
|
||||||
}
|
};
|
||||||
|
|
||||||
App.prototype.makeFunction = function (api) {
|
App.prototype.makeFunction = function (api) {
|
||||||
let self = this;
|
let self = this;
|
||||||
@ -41,12 +41,12 @@ App.prototype.makeFunction = function(api) {
|
|||||||
};
|
};
|
||||||
Object.defineProperty(result, 'name', {value: api[0], writable: false});
|
Object.defineProperty(result, 'name', {value: api[0], writable: false});
|
||||||
return result;
|
return result;
|
||||||
}
|
};
|
||||||
|
|
||||||
App.prototype.send = function (message) {
|
App.prototype.send = function (message) {
|
||||||
if (this._send_queue) {
|
if (this._send_queue) {
|
||||||
if (this._on_output) {
|
if (this._on_output) {
|
||||||
this._send_queue.forEach(x => this._on_output(x));
|
this._send_queue.forEach((x) => this._on_output(x));
|
||||||
this._send_queue = null;
|
this._send_queue = null;
|
||||||
} else if (message) {
|
} else if (message) {
|
||||||
this._send_queue.push(message);
|
this._send_queue.push(message);
|
||||||
@ -55,7 +55,7 @@ App.prototype.send = function(message) {
|
|||||||
if (message && this._on_output) {
|
if (message && this._on_output) {
|
||||||
this._on_output(message);
|
this._on_output(message);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function socket(request, response, client) {
|
function socket(request, response, client) {
|
||||||
let process;
|
let process;
|
||||||
@ -70,7 +70,7 @@ function socket(request, response, client) {
|
|||||||
if (process) {
|
if (process) {
|
||||||
process.timeout = 0;
|
process.timeout = 0;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
response.onMessage = async function (event) {
|
response.onMessage = async function (event) {
|
||||||
if (event.opCode == 0x1 || event.opCode == 0x2) {
|
if (event.opCode == 0x1 || event.opCode == 0x2) {
|
||||||
@ -78,28 +78,33 @@ function socket(request, response, client) {
|
|||||||
try {
|
try {
|
||||||
message = JSON.parse(event.data);
|
message = JSON.parse(event.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
print("ERROR", error, event.data, event.data.length, event.opCode);
|
print('ERROR', error, event.data, event.data.length, event.opCode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (message.action == "hello") {
|
if (message.action == 'hello') {
|
||||||
let packageOwner;
|
let packageOwner;
|
||||||
let packageName;
|
let packageName;
|
||||||
let blobId;
|
let blobId;
|
||||||
let match;
|
let match;
|
||||||
let parentApp;
|
let parentApp;
|
||||||
if (match = /^\/([&%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(message.path)) {
|
if (
|
||||||
|
(match = /^\/([&%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(message.path))
|
||||||
|
) {
|
||||||
blobId = match[1];
|
blobId = match[1];
|
||||||
} else if (match = /^\/\~([^\/]+)\/([^\/]+)\/$/.exec(message.path)) {
|
} else if ((match = /^\/\~([^\/]+)\/([^\/]+)\/$/.exec(message.path))) {
|
||||||
packageOwner = match[1];
|
packageOwner = match[1];
|
||||||
packageName = match[2];
|
packageName = match[2];
|
||||||
blobId = await new Database(packageOwner).get('path:' + packageName);
|
blobId = await new Database(packageOwner).get('path:' + packageName);
|
||||||
if (!blobId) {
|
if (!blobId) {
|
||||||
response.send(JSON.stringify({
|
response.send(
|
||||||
|
JSON.stringify({
|
||||||
message: 'tfrpc',
|
message: 'tfrpc',
|
||||||
method: "error",
|
method: 'error',
|
||||||
params: [message.path + ' not found'],
|
params: [message.path + ' not found'],
|
||||||
id: -1,
|
id: -1,
|
||||||
}), 0x1);
|
}),
|
||||||
|
0x1
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (packageOwner != 'core') {
|
if (packageOwner != 'core') {
|
||||||
@ -110,12 +115,15 @@ function socket(request, response, client) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response.send(JSON.stringify({
|
response.send(
|
||||||
action: "session",
|
JSON.stringify({
|
||||||
|
action: 'session',
|
||||||
credentials: credentials,
|
credentials: credentials,
|
||||||
parentApp: parentApp,
|
parentApp: parentApp,
|
||||||
id: blobId,
|
id: blobId,
|
||||||
}), 0x1);
|
}),
|
||||||
|
0x1
|
||||||
|
);
|
||||||
|
|
||||||
options.api = message.api || [];
|
options.api = message.api || [];
|
||||||
options.credentials = credentials;
|
options.credentials = credentials;
|
||||||
@ -125,9 +133,16 @@ function socket(request, response, client) {
|
|||||||
let sessionId = makeSessionId();
|
let sessionId = makeSessionId();
|
||||||
if (blobId) {
|
if (blobId) {
|
||||||
if (message.edit_only) {
|
if (message.edit_only) {
|
||||||
response.send(JSON.stringify({action: 'ready', edit_only: true}), 0x1);
|
response.send(
|
||||||
|
JSON.stringify({action: 'ready', edit_only: true}),
|
||||||
|
0x1
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
process = await core.getSessionProcessBlob(blobId, sessionId, options);
|
process = await core.getSessionProcessBlob(
|
||||||
|
blobId,
|
||||||
|
sessionId,
|
||||||
|
options
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (process) {
|
if (process) {
|
||||||
@ -150,14 +165,14 @@ function socket(request, response, client) {
|
|||||||
again = false;
|
again = false;
|
||||||
} else {
|
} else {
|
||||||
// Idle. Ping them.
|
// Idle. Ping them.
|
||||||
response.send("", 0x9);
|
response.send('', 0x9);
|
||||||
process.lastPing = now;
|
process.lastPing = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (again && process.timeout) {
|
if (again && process.timeout) {
|
||||||
setTimeout(ping, process.timeout);
|
setTimeout(ping, process.timeout);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (process && process.timeout > 0) {
|
if (process && process.timeout > 0) {
|
||||||
setTimeout(ping, process.timeout);
|
setTimeout(ping, process.timeout);
|
||||||
@ -197,11 +212,16 @@ function socket(request, response, client) {
|
|||||||
if (process) {
|
if (process) {
|
||||||
process.lastActive = Date.now();
|
process.lastActive = Date.now();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
response.upgrade(100, refresh ? {
|
response.upgrade(
|
||||||
|
100,
|
||||||
|
refresh
|
||||||
|
? {
|
||||||
'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`,
|
'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`,
|
||||||
} : {});
|
}
|
||||||
|
: {}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {socket, App};
|
export {socket, App};
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends Sign-in</title>
|
<title>Tilde Friends Sign-in</title>
|
||||||
<link type="text/css" rel="stylesheet" href="/static/style.css">
|
<link type="text/css" rel="stylesheet" href="/static/style.css" />
|
||||||
<link type="image/png" rel="shortcut icon" href="/static/favicon.png">
|
<link type="image/png" rel="shortcut icon" href="/static/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1 style="text-align: center">Tilde Friends Sign-in</h1>
|
<h1 style="text-align: center">Tilde Friends Sign-in</h1>
|
||||||
<tf-auth id="auth"></tf-auth>
|
<tf-auth id="auth"></tf-auth>
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import {LitElement, html} from '/lit/lit-all.min.js';
|
import {LitElement, html} from '/lit/lit-all.min.js';
|
||||||
let g_data = $AUTH_DATA;
|
let g_data = $AUTH_DATA;
|
||||||
|
145
core/auth.js
145
core/auth.js
@ -1,7 +1,7 @@
|
|||||||
import * as core from './core.js';
|
import * as core from './core.js';
|
||||||
import * as form from './form.js';
|
import * as form from './form.js';
|
||||||
|
|
||||||
let gDatabase = new Database("auth");
|
let gDatabase = new Database('auth');
|
||||||
|
|
||||||
const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;
|
const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
@ -36,8 +36,20 @@ function makeJwt(payload) {
|
|||||||
id = ssb.createIdentity(':auth');
|
id = ssb.createIdentity(':auth');
|
||||||
}
|
}
|
||||||
|
|
||||||
let final_payload = b64url(base64Encode(JSON.stringify(Object.assign({}, payload, {exp: (new Date().valueOf()) + kRefreshInterval}))));
|
let final_payload = b64url(
|
||||||
let jwt = [b64url(base64Encode(JSON.stringify({alg: 'HS256', typ: 'JWT'}))), final_payload, b64url(ssb.hmacsha256sign(final_payload, ':auth', id))].join('.');
|
base64Encode(
|
||||||
|
JSON.stringify(
|
||||||
|
Object.assign({}, payload, {
|
||||||
|
exp: new Date().valueOf() + kRefreshInterval,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let jwt = [
|
||||||
|
b64url(base64Encode(JSON.stringify({alg: 'HS256', typ: 'JWT'}))),
|
||||||
|
final_payload,
|
||||||
|
b64url(ssb.hmacsha256sign(final_payload, ':auth', id)),
|
||||||
|
].join('.');
|
||||||
return jwt;
|
return jwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +63,7 @@ function readSession(session) {
|
|||||||
let id = ssb.getIdentities(':auth');
|
let id = ssb.getIdentities(':auth');
|
||||||
if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) {
|
if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) {
|
||||||
let result = JSON.parse(utf8Decode(base64Decode(unb64url(payload))));
|
let result = JSON.parse(utf8Decode(base64Decode(unb64url(payload))));
|
||||||
let now = new Date().valueOf()
|
let now = new Date().valueOf();
|
||||||
if (now < result.exp) {
|
if (now < result.exp) {
|
||||||
print(`JWT valid for another ${(result.exp - now) / 1000} seconds.`);
|
print(`JWT valid for another ${(result.exp - now) / 1000} seconds.`);
|
||||||
return result;
|
return result;
|
||||||
@ -77,9 +89,15 @@ function hashPassword(password) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function noAdministrator() {
|
function noAdministrator() {
|
||||||
return !core.globalSettings || !core.globalSettings.permissions || !Object.keys(core.globalSettings.permissions).some(function(name) {
|
return (
|
||||||
return core.globalSettings.permissions[name].indexOf("administration") != -1;
|
!core.globalSettings ||
|
||||||
});
|
!core.globalSettings.permissions ||
|
||||||
|
!Object.keys(core.globalSettings.permissions).some(function (name) {
|
||||||
|
return (
|
||||||
|
core.globalSettings.permissions[name].indexOf('administration') != -1
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeAdministrator(name) {
|
function makeAdministrator(name) {
|
||||||
@ -89,8 +107,8 @@ function makeAdministrator(name) {
|
|||||||
if (!core.globalSettings.permissions[name]) {
|
if (!core.globalSettings.permissions[name]) {
|
||||||
core.globalSettings.permissions[name] = [];
|
core.globalSettings.permissions[name] = [];
|
||||||
}
|
}
|
||||||
if (core.globalSettings.permissions[name].indexOf("administration") == -1) {
|
if (core.globalSettings.permissions[name].indexOf('administration') == -1) {
|
||||||
core.globalSettings.permissions[name].push("administration");
|
core.globalSettings.permissions[name].push('administration');
|
||||||
}
|
}
|
||||||
core.setGlobalSettings(core.globalSettings);
|
core.setGlobalSettings(core.globalSettings);
|
||||||
}
|
}
|
||||||
@ -101,7 +119,7 @@ function getCookies(headers) {
|
|||||||
if (headers.cookie) {
|
if (headers.cookie) {
|
||||||
let parts = headers.cookie.split(/,|;/);
|
let parts = headers.cookie.split(/,|;/);
|
||||||
for (let i in parts) {
|
for (let i in parts) {
|
||||||
let equals = parts[i].indexOf("=");
|
let equals = parts[i].indexOf('=');
|
||||||
let name = parts[i].substring(0, equals).trim();
|
let name = parts[i].substring(0, equals).trim();
|
||||||
let value = parts[i].substring(equals + 1).trim();
|
let value = parts[i].substring(equals + 1).trim();
|
||||||
cookies[name] = value;
|
cookies[name] = value;
|
||||||
@ -113,18 +131,34 @@ function getCookies(headers) {
|
|||||||
|
|
||||||
function isNameValid(name) {
|
function isNameValid(name) {
|
||||||
let c = name.charAt(0);
|
let c = name.charAt(0);
|
||||||
return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) && name.split().map(x => x >= ('a' && x <= 'z') || x >= ('A' && x <= 'Z') || x >= ('0' && x <= '9'));
|
return (
|
||||||
|
((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) &&
|
||||||
|
name
|
||||||
|
.split()
|
||||||
|
.map(
|
||||||
|
(x) =>
|
||||||
|
x >= ('a' && x <= 'z') ||
|
||||||
|
x >= ('A' && x <= 'Z') ||
|
||||||
|
x >= ('0' && x <= '9')
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handler(request, response) {
|
function handler(request, response) {
|
||||||
let session = getCookies(request.headers).session;
|
let session = getCookies(request.headers).session;
|
||||||
if (request.uri == "/login") {
|
if (request.uri == '/login') {
|
||||||
let formData = form.decodeForm(request.query);
|
let formData = form.decodeForm(request.query);
|
||||||
if (query(request.headers)?.permissions?.authenticated) {
|
if (query(request.headers)?.permissions?.authenticated) {
|
||||||
if (formData.return) {
|
if (formData.return) {
|
||||||
response.writeHead(303, {"Location": formData.return});
|
response.writeHead(303, {Location: formData.return});
|
||||||
} else {
|
} else {
|
||||||
response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + '/', "Content-Length": "0"});
|
response.writeHead(303, {
|
||||||
|
Location:
|
||||||
|
(request.client.tls ? 'https://' : 'http://') +
|
||||||
|
request.headers.host +
|
||||||
|
'/',
|
||||||
|
'Content-Length': '0',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
response.end();
|
response.end();
|
||||||
return;
|
return;
|
||||||
@ -133,22 +167,23 @@ function handler(request, response) {
|
|||||||
let sessionIsNew = false;
|
let sessionIsNew = false;
|
||||||
let loginError;
|
let loginError;
|
||||||
|
|
||||||
if (request.method == "POST" || formData.submit) {
|
if (request.method == 'POST' || formData.submit) {
|
||||||
sessionIsNew = true;
|
sessionIsNew = true;
|
||||||
formData = form.decodeForm(utf8Decode(request.body), formData);
|
formData = form.decodeForm(utf8Decode(request.body), formData);
|
||||||
if (formData.submit == "Login") {
|
if (formData.submit == 'Login') {
|
||||||
let account = gDatabase.get("user:" + formData.name);
|
let account = gDatabase.get('user:' + formData.name);
|
||||||
account = account ? JSON.parse(account) : account;
|
account = account ? JSON.parse(account) : account;
|
||||||
if (formData.register == '1') {
|
if (formData.register == '1') {
|
||||||
if (!account &&
|
if (
|
||||||
|
!account &&
|
||||||
isNameValid(formData.name) &&
|
isNameValid(formData.name) &&
|
||||||
formData.password == formData.confirm) {
|
formData.password == formData.confirm
|
||||||
|
) {
|
||||||
let users = new Set();
|
let users = new Set();
|
||||||
let users_original = gDatabase.get('users');
|
let users_original = gDatabase.get('users');
|
||||||
try {
|
try {
|
||||||
users = new Set(JSON.parse(users_original));
|
users = new Set(JSON.parse(users_original));
|
||||||
} catch {
|
} catch {}
|
||||||
}
|
|
||||||
if (!users.has(formData.name)) {
|
if (!users.has(formData.name)) {
|
||||||
users.add(formData.name);
|
users.add(formData.name);
|
||||||
}
|
}
|
||||||
@ -166,10 +201,12 @@ function handler(request, response) {
|
|||||||
loginError = 'Error registering account.';
|
loginError = 'Error registering account.';
|
||||||
}
|
}
|
||||||
} else if (formData.change == '1') {
|
} else if (formData.change == '1') {
|
||||||
if (account &&
|
if (
|
||||||
|
account &&
|
||||||
isNameValid(formData.name) &&
|
isNameValid(formData.name) &&
|
||||||
formData.new_password == formData.confirm &&
|
formData.new_password == formData.confirm &&
|
||||||
verifyPassword(formData.password, account.password)) {
|
verifyPassword(formData.password, account.password)
|
||||||
|
) {
|
||||||
session = makeJwt({name: formData.name});
|
session = makeJwt({name: formData.name});
|
||||||
account = {password: hashPassword(formData.new_password)};
|
account = {password: hashPassword(formData.new_password)};
|
||||||
gDatabase.set('user:' + formData.name, JSON.stringify(account));
|
gDatabase.set('user:' + formData.name, JSON.stringify(account));
|
||||||
@ -177,9 +214,11 @@ function handler(request, response) {
|
|||||||
loginError = 'Error changing password.';
|
loginError = 'Error changing password.';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (account &&
|
if (
|
||||||
|
account &&
|
||||||
account.password &&
|
account.password &&
|
||||||
verifyPassword(formData.password, account.password)) {
|
verifyPassword(formData.password, account.password)
|
||||||
|
) {
|
||||||
session = makeJwt({name: formData.name});
|
session = makeJwt({name: formData.name});
|
||||||
if (noAdministrator()) {
|
if (noAdministrator()) {
|
||||||
makeAdministrator(formData.name);
|
makeAdministrator(formData.name);
|
||||||
@ -197,10 +236,14 @@ function handler(request, response) {
|
|||||||
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; HttpOnly`;
|
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; HttpOnly`;
|
||||||
let entry = readSession(session);
|
let entry = readSession(session);
|
||||||
if (entry && formData.return) {
|
if (entry && formData.return) {
|
||||||
response.writeHead(303, {"Location": formData.return, "Set-Cookie": cookie});
|
response.writeHead(303, {
|
||||||
|
Location: formData.return,
|
||||||
|
'Set-Cookie': cookie,
|
||||||
|
});
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
File.readFile("core/auth.html").then(function(data) {
|
File.readFile('core/auth.html')
|
||||||
|
.then(function (data) {
|
||||||
let html = utf8Decode(data);
|
let html = utf8Decode(data);
|
||||||
let auth_data = {
|
let auth_data = {
|
||||||
session_is_new: sessionIsNew,
|
session_is_new: sessionIsNew,
|
||||||
@ -209,20 +252,36 @@ function handler(request, response) {
|
|||||||
code_of_conduct: core.globalSettings.code_of_conduct,
|
code_of_conduct: core.globalSettings.code_of_conduct,
|
||||||
have_administrator: !noAdministrator(),
|
have_administrator: !noAdministrator(),
|
||||||
};
|
};
|
||||||
html = utf8Encode(html.replace('$AUTH_DATA', JSON.stringify(auth_data)));
|
html = utf8Encode(
|
||||||
response.writeHead(200, {"Content-Type": "text/html; charset=utf-8", "Set-Cookie": cookie, "Content-Length": html.length});
|
html.replace('$AUTH_DATA', JSON.stringify(auth_data))
|
||||||
|
);
|
||||||
|
response.writeHead(200, {
|
||||||
|
'Content-Type': 'text/html; charset=utf-8',
|
||||||
|
'Set-Cookie': cookie,
|
||||||
|
'Content-Length': html.length,
|
||||||
|
});
|
||||||
response.end(html);
|
response.end(html);
|
||||||
}).catch(function(error) {
|
})
|
||||||
response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
.catch(function (error) {
|
||||||
response.end("404 File not found");
|
response.writeHead(404, {
|
||||||
|
'Content-Type': 'text/plain; charset=utf-8',
|
||||||
|
Connection: 'close',
|
||||||
|
});
|
||||||
|
response.end('404 File not found');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (request.uri == "/login/logout") {
|
} else if (request.uri == '/login/logout') {
|
||||||
response.writeHead(303, {"Set-Cookie": `session=; path=/; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly`, "Location": "/login" + (request.query ? "?" + request.query : "")});
|
response.writeHead(303, {
|
||||||
|
'Set-Cookie': `session=; path=/; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly`,
|
||||||
|
Location: '/login' + (request.query ? '?' + request.query : ''),
|
||||||
|
});
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
response.writeHead(200, {
|
||||||
response.end("Hello, " + request.client.peerName + ".");
|
'Content-Type': 'text/plain; charset=utf-8',
|
||||||
|
Connection: 'close',
|
||||||
|
});
|
||||||
|
response.end('Hello, ' + request.client.peerName + '.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,14 +290,18 @@ function getPermissions(session) {
|
|||||||
let entry = readSession(session);
|
let entry = readSession(session);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
permissions = getPermissionsForUser(entry.name);
|
permissions = getPermissionsForUser(entry.name);
|
||||||
permissions.authenticated = entry.name !== "guest";
|
permissions.authenticated = entry.name !== 'guest';
|
||||||
}
|
}
|
||||||
return permissions || {};
|
return permissions || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPermissionsForUser(userName) {
|
function getPermissionsForUser(userName) {
|
||||||
let permissions = {};
|
let permissions = {};
|
||||||
if (core.globalSettings && core.globalSettings.permissions && core.globalSettings.permissions[userName]) {
|
if (
|
||||||
|
core.globalSettings &&
|
||||||
|
core.globalSettings.permissions &&
|
||||||
|
core.globalSettings.permissions[userName]
|
||||||
|
) {
|
||||||
for (let i in core.globalSettings.permissions[userName]) {
|
for (let i in core.globalSettings.permissions[userName]) {
|
||||||
permissions[core.globalSettings.permissions[userName][i]] = true;
|
permissions[core.globalSettings.permissions[userName][i]] = true;
|
||||||
}
|
}
|
||||||
@ -250,10 +313,12 @@ function query(headers) {
|
|||||||
let session = getCookies(headers).session;
|
let session = getCookies(headers).session;
|
||||||
let entry;
|
let entry;
|
||||||
let autologin = tildefriends.args.autologin;
|
let autologin = tildefriends.args.autologin;
|
||||||
if (entry = autologin ? {name: autologin} : readSession(session)) {
|
if ((entry = autologin ? {name: autologin} : readSession(session))) {
|
||||||
return {
|
return {
|
||||||
session: entry,
|
session: entry,
|
||||||
permissions: autologin ? getPermissionsForUser(autologin) : getPermissions(session),
|
permissions: autologin
|
||||||
|
? getPermissionsForUser(autologin)
|
||||||
|
: getPermissions(session),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
618
core/client.js
618
core/client.js
File diff suppressed because it is too large
Load Diff
628
core/core.js
628
core/core.js
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,10 @@
|
|||||||
function decode(encoded) {
|
function decode(encoded) {
|
||||||
let result = "";
|
let result = '';
|
||||||
for (let i = 0; i < encoded.length; i++) {
|
for (let i = 0; i < encoded.length; i++) {
|
||||||
let c = encoded[i];
|
let c = encoded[i];
|
||||||
if (c == "+") {
|
if (c == '+') {
|
||||||
result += " ";
|
result += ' ';
|
||||||
} else if (c == "%") {
|
} else if (c == '%') {
|
||||||
result += String.fromCharCode(parseInt(encoded.slice(i + 1, i + 3), 16));
|
result += String.fromCharCode(parseInt(encoded.slice(i + 1, i + 3), 16));
|
||||||
i += 2;
|
i += 2;
|
||||||
} else {
|
} else {
|
||||||
|
25
core/http.js
25
core/http.js
@ -1,11 +1,11 @@
|
|||||||
function parseUrl(url) {
|
function parseUrl(url) {
|
||||||
// XXX: Hack.
|
// XXX: Hack.
|
||||||
let match = url.match(new RegExp("(\\w+)://([^/:]+)(?::(\\d+))?(.*)"));
|
let match = url.match(new RegExp('(\\w+)://([^/:]+)(?::(\\d+))?(.*)'));
|
||||||
return {
|
return {
|
||||||
protocol: match[1],
|
protocol: match[1],
|
||||||
host: match[2],
|
host: match[2],
|
||||||
path: match[4],
|
path: match[4],
|
||||||
port: match[3] ? parseInt(match[3]) : match[1] == "http" ? 80 : 443,
|
port: match[3] ? parseInt(match[3]) : match[1] == 'http' ? 80 : 443,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ function parseResponse(data) {
|
|||||||
} else if (!firstLine) {
|
} else if (!firstLine) {
|
||||||
firstLine = line;
|
firstLine = line;
|
||||||
} else {
|
} else {
|
||||||
let colon = line.indexOf(":");
|
let colon = line.indexOf(':');
|
||||||
headers[line.substring(colon)] = line.substring(colon + 1);
|
headers[line.substring(colon)] = line.substring(colon + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,7 +37,9 @@ export function fetch(url, options, allowed_hosts) {
|
|||||||
let socket = new Socket();
|
let socket = new Socket();
|
||||||
let buffer = new Uint8Array(0);
|
let buffer = new Uint8Array(0);
|
||||||
|
|
||||||
return socket.connect(parsed.host, parsed.port).then(function() {
|
return socket
|
||||||
|
.connect(parsed.host, parsed.port)
|
||||||
|
.then(function () {
|
||||||
socket.read(function (data) {
|
socket.read(function (data) {
|
||||||
if (data && data.length) {
|
if (data && data.length) {
|
||||||
let newBuffer = new Uint8Array(buffer.length + data.length);
|
let newBuffer = new Uint8Array(buffer.length + data.length);
|
||||||
@ -72,14 +74,21 @@ export function fetch(url, options, allowed_hosts) {
|
|||||||
if (parsed.port == 443) {
|
if (parsed.port == 443) {
|
||||||
return socket.startTls();
|
return socket.startTls();
|
||||||
}
|
}
|
||||||
}).then(function() {
|
})
|
||||||
let body = typeof options?.body == 'string' ? utf8Encode(options.body) : (options.body || new Uint8Array(0));
|
.then(function () {
|
||||||
let headers = utf8Encode(`${options?.method ?? 'GET'} ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\nContent-Length: ${body.length}\r\n\r\n`);
|
let body =
|
||||||
|
typeof options?.body == 'string'
|
||||||
|
? utf8Encode(options.body)
|
||||||
|
: options.body || new Uint8Array(0);
|
||||||
|
let headers = utf8Encode(
|
||||||
|
`${options?.method ?? 'GET'} ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\nContent-Length: ${body.length}\r\n\r\n`
|
||||||
|
);
|
||||||
let fullRequest = new Uint8Array(headers.length + body.length);
|
let fullRequest = new Uint8Array(headers.length + body.length);
|
||||||
fullRequest.set(headers, 0);
|
fullRequest.set(headers, 0);
|
||||||
fullRequest.set(body, headers.length);
|
fullRequest.set(body, headers.length);
|
||||||
socket.write(fullRequest);
|
socket.write(fullRequest);
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
139
core/index.html
139
core/index.html
@ -1,46 +1,145 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Tilde Friends</title>
|
<title>Tilde Friends</title>
|
||||||
<link type="text/css" rel="stylesheet" href="/static/style.css">
|
<link type="text/css" rel="stylesheet" href="/static/style.css" />
|
||||||
<link type="text/css" rel="stylesheet" href="/static/w3.css">
|
<link type="text/css" rel="stylesheet" href="/static/w3.css" />
|
||||||
<link type="image/png" rel="shortcut icon" href="/static/favicon.png">
|
<link type="image/png" rel="shortcut icon" href="/static/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<script>
|
<script>
|
||||||
function set_access_key_title(event) {
|
function set_access_key_title(event) {
|
||||||
if (!event.srcElement.title) {
|
if (!event.srcElement.title) {
|
||||||
event.srcElement.title = `${event.srcElement.dataset.tip} [${(event.srcElement.accessKeyLabel || ('⌨️' + event.srcElement.accessKey)).toUpperCase()}]`;
|
event.srcElement.title = `${event.srcElement.dataset.tip} [${(event.srcElement.accessKeyLabel || '⌨️' + event.srcElement.accessKey).toUpperCase()}]`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body style="display: flex; flex-flow: column; width: 100vw; height: 100vh; position: absolute; max-width: 100%; max-height: 100%">
|
<body
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
position: absolute;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
"
|
||||||
|
>
|
||||||
<tf-navigation></tf-navigation>
|
<tf-navigation></tf-navigation>
|
||||||
<div id="content" class="hbox" style="flex: 1 0; overflow: auto">
|
<div id="content" class="hbox" style="flex: 1 0; overflow: auto">
|
||||||
<div id="editPane" class="vbox" style="flex: 0 1 100%; display: none; overflow: auto">
|
<div
|
||||||
|
id="editPane"
|
||||||
|
class="vbox"
|
||||||
|
style="flex: 0 1 100%; display: none; overflow: auto"
|
||||||
|
>
|
||||||
<div class="navigation w3-bar" style="display: flex">
|
<div class="navigation w3-bar" style="display: flex">
|
||||||
<button class="w3-bar-item w3-button w3-blue" id="closeEditor" name="closeEditor" accesskey="c" onmouseover="set_access_key_title(event)" data-tip="Close the editor">Close</button>
|
<button
|
||||||
<button class="w3-bar-item w3-button w3-blue" id="save" name="save" accesskey="s" onmouseover="set_access_key_title(event)" data-tip="Save the app under the given path">Save</button>
|
class="w3-bar-item w3-button w3-blue"
|
||||||
<button class="w3-bar-item w3-button w3-blue" id="icon" name="icon" accesskey="i" onmouseover="set_access_key_title(event)" data-tip="Set an icon/emoji for the app">📦</button>
|
id="closeEditor"
|
||||||
<button class="w3-bar-item w3-button w3-blue" id="export" name="export" accesskey="e" onmouseover="set_access_key_title(event)" data-tip="Export app to .zip file">Export</button>
|
name="closeEditor"
|
||||||
<button class="w3-bar-item w3-button w3-blue" id="import" name="import" accesskey="i" onmouseover="set_access_key_title(event)" data-tip="Import app from .zip file">Import</button>
|
accesskey="c"
|
||||||
<button class="w3-bar-item w3-button w3-blue" id="pretty" name="pretty" accesskey="p" onmouseover="set_access_key_title(event)" data-tip="Clean up source formatting">🧼</button>
|
onmouseover="set_access_key_title(event)"
|
||||||
<input class="w3-bar-item w3-input w3-border w3-blue" type="text" id="name" name="name" style="flex: 1 1; min-width: 1em"></input>
|
data-tip="Close the editor"
|
||||||
<button class="w3-bar-item w3-button w3-blue" id="delete" name="delete" accesskey="d" onmouseover="set_access_key_title(event)" data-tip="Delete the app">Delete</button>
|
>
|
||||||
<button class="w3-bar-item w3-button w3-blue" id="trace_button" accesskey="t" onmouseover="set_access_key_title(event)" data-tip="Open a performance trace for the server">Trace</button>
|
Close
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-blue"
|
||||||
|
id="save"
|
||||||
|
name="save"
|
||||||
|
accesskey="s"
|
||||||
|
onmouseover="set_access_key_title(event)"
|
||||||
|
data-tip="Save the app under the given path"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-blue"
|
||||||
|
id="icon"
|
||||||
|
name="icon"
|
||||||
|
accesskey="i"
|
||||||
|
onmouseover="set_access_key_title(event)"
|
||||||
|
data-tip="Set an icon/emoji for the app"
|
||||||
|
>
|
||||||
|
📦
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-blue"
|
||||||
|
id="export"
|
||||||
|
name="export"
|
||||||
|
accesskey="e"
|
||||||
|
onmouseover="set_access_key_title(event)"
|
||||||
|
data-tip="Export app to .zip file"
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-blue"
|
||||||
|
id="import"
|
||||||
|
name="import"
|
||||||
|
accesskey="i"
|
||||||
|
onmouseover="set_access_key_title(event)"
|
||||||
|
data-tip="Import app from .zip file"
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-blue"
|
||||||
|
id="pretty"
|
||||||
|
name="pretty"
|
||||||
|
accesskey="p"
|
||||||
|
onmouseover="set_access_key_title(event)"
|
||||||
|
data-tip="Clean up source formatting"
|
||||||
|
>
|
||||||
|
🧼
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
class="w3-bar-item w3-input w3-border w3-blue"
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
style="flex: 1 1; min-width: 1em"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-blue"
|
||||||
|
id="delete"
|
||||||
|
name="delete"
|
||||||
|
accesskey="d"
|
||||||
|
onmouseover="set_access_key_title(event)"
|
||||||
|
data-tip="Delete the app"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w3-bar-item w3-button w3-blue"
|
||||||
|
id="trace_button"
|
||||||
|
accesskey="t"
|
||||||
|
onmouseover="set_access_key_title(event)"
|
||||||
|
data-tip="Open a performance trace for the server"
|
||||||
|
>
|
||||||
|
Trace
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="hbox" style="flex: 1 1; overflow: auto">
|
<div class="hbox" style="flex: 1 1; overflow: auto">
|
||||||
<div style="overflow: auto">
|
<div style="overflow: auto">
|
||||||
<tf-files-pane style="overflow: auto"></tf-files-pane>
|
<tf-files-pane style="overflow: auto"></tf-files-pane>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex: 1 1; overflow: auto"><div id="editor" style="width: 100%; height: 100%"></div></div>
|
<div style="flex: 1 1; overflow: auto">
|
||||||
|
<div id="editor" style="width: 100%; height: 100%"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="viewPane" class="vbox" style="flex: 0 1 100%; overflow: auto">
|
<div id="viewPane" class="vbox" style="flex: 0 1 100%; overflow: auto">
|
||||||
<iframe id="document" sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-popups allow-downloads" style="width: 100%; height: 100%; border: 0"></iframe>
|
<iframe
|
||||||
|
id="document"
|
||||||
|
sandbox="allow-forms allow-scripts allow-top-navigation allow-modals allow-popups allow-downloads"
|
||||||
|
style="width: 100%; height: 100%; border: 0"
|
||||||
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>window.litDisableBundleWarning = true;</script>
|
<script>
|
||||||
|
window.litDisableBundleWarning = true;
|
||||||
|
</script>
|
||||||
<script src="/static/client.js" type="module"></script>
|
<script src="/static/client.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -102,22 +102,54 @@ a:active {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Solarized Color Scheme Colors */
|
/* Solarized Color Scheme Colors */
|
||||||
.base03 { color: #002b36; }
|
.base03 {
|
||||||
.base02 { color: #073642; }
|
color: #002b36;
|
||||||
.base01 { color: #586e75; }
|
}
|
||||||
.base00 { color: #657b83; }
|
.base02 {
|
||||||
.base0 { color: #839496; }
|
color: #073642;
|
||||||
.base1 { color: #93a1a1; }
|
}
|
||||||
.base2 { color: #eee8d5; }
|
.base01 {
|
||||||
.base3 { color: #fdf6e3; }
|
color: #586e75;
|
||||||
.yellow { color: #b58900; }
|
}
|
||||||
.orange { color: #cb4b16; }
|
.base00 {
|
||||||
.red { color: #dc322f; }
|
color: #657b83;
|
||||||
.magenta { color: #d33682; }
|
}
|
||||||
.violet { color: #6c71c4; }
|
.base0 {
|
||||||
.blue { color: #268bd2; }
|
color: #839496;
|
||||||
.cyan { color: #2aa198; }
|
}
|
||||||
.green { color: #859900; }
|
.base1 {
|
||||||
|
color: #93a1a1;
|
||||||
|
}
|
||||||
|
.base2 {
|
||||||
|
color: #eee8d5;
|
||||||
|
}
|
||||||
|
.base3 {
|
||||||
|
color: #fdf6e3;
|
||||||
|
}
|
||||||
|
.yellow {
|
||||||
|
color: #b58900;
|
||||||
|
}
|
||||||
|
.orange {
|
||||||
|
color: #cb4b16;
|
||||||
|
}
|
||||||
|
.red {
|
||||||
|
color: #dc322f;
|
||||||
|
}
|
||||||
|
.magenta {
|
||||||
|
color: #d33682;
|
||||||
|
}
|
||||||
|
.violet {
|
||||||
|
color: #6c71c4;
|
||||||
|
}
|
||||||
|
.blue {
|
||||||
|
color: #268bd2;
|
||||||
|
}
|
||||||
|
.cyan {
|
||||||
|
color: #2aa198;
|
||||||
|
}
|
||||||
|
.green {
|
||||||
|
color: #859900;
|
||||||
|
}
|
||||||
|
|
||||||
.permissions {
|
.permissions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -4,7 +4,11 @@ let g_next_id = 1;
|
|||||||
let g_calls = {};
|
let g_calls = {};
|
||||||
|
|
||||||
function get_is_browser() {
|
function get_is_browser() {
|
||||||
try { return window !== undefined && console !== undefined; } catch { return false; }
|
try {
|
||||||
|
return window !== undefined && console !== undefined;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (k_is_browser) {
|
if (k_is_browser) {
|
||||||
@ -21,12 +25,22 @@ function make_rpc(target, prop, receiver) {
|
|||||||
g_calls[id] = {resolve: resolve, reject: reject};
|
g_calls[id] = {resolve: resolve, reject: reject};
|
||||||
});
|
});
|
||||||
if (k_is_browser) {
|
if (k_is_browser) {
|
||||||
window.parent.postMessage({message: 'tfrpc', method: prop, params: [...arguments], id: id}, '*');
|
window.parent.postMessage(
|
||||||
|
{message: 'tfrpc', method: prop, params: [...arguments], id: id},
|
||||||
|
'*'
|
||||||
|
);
|
||||||
return promise;
|
return promise;
|
||||||
} else {
|
} else {
|
||||||
return app.postMessage({message: 'tfrpc', method: prop, params: [...arguments], id: id}).then(x => promise);
|
return app
|
||||||
}
|
.postMessage({
|
||||||
|
message: 'tfrpc',
|
||||||
|
method: prop,
|
||||||
|
params: [...arguments],
|
||||||
|
id: id,
|
||||||
|
})
|
||||||
|
.then((x) => promise);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function send(response) {
|
function send(response) {
|
||||||
@ -44,16 +58,22 @@ function call_rpc(message) {
|
|||||||
let method = g_api[message.method];
|
let method = g_api[message.method];
|
||||||
if (method) {
|
if (method) {
|
||||||
try {
|
try {
|
||||||
Promise.resolve(method(...message.params)).then(function(result) {
|
Promise.resolve(method(...message.params))
|
||||||
|
.then(function (result) {
|
||||||
send({message: 'tfrpc', id: id, result: result});
|
send({message: 'tfrpc', id: id, result: result});
|
||||||
}).catch(function(error) {
|
})
|
||||||
|
.catch(function (error) {
|
||||||
send({message: 'tfrpc', id: id, error: error});
|
send({message: 'tfrpc', id: id, error: error});
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
send({message: 'tfrpc', id: id, error: error});
|
send({message: 'tfrpc', id: id, error: error});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
send({message: 'tfrpc', id: id, error: `Method '${message.method}' not found.`});
|
send({
|
||||||
|
message: 'tfrpc',
|
||||||
|
id: id,
|
||||||
|
error: `Method '${message.method}' not found.`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (message.error !== undefined) {
|
} else if (message.error !== undefined) {
|
||||||
if (g_calls[id]) {
|
if (g_calls[id]) {
|
||||||
|
144
docs/guide.md
144
docs/guide.md
@ -2,187 +2,187 @@
|
|||||||
|
|
||||||
Tilde Friends is a platform for making, running, and sharing web applications.
|
Tilde Friends is a platform for making, running, and sharing web applications.
|
||||||
|
|
||||||
When you visit Tilde Friends in a web browser, you are presented with a
|
When you visit Tilde Friends in a web browser, you are presented with a terminal interface, typically with a big text output box covering most of the page and an input box at the bottom, into which text or commands can be entered. A script runs to produce text output and consume user input.
|
||||||
terminal interface, typically with a big text output box covering most of the
|
|
||||||
page and an input box at the bottom, into which text or commands can be
|
|
||||||
entered. A script runs to produce text output and consume user input.
|
|
||||||
|
|
||||||
The script is a Tilde Friends application, and it runs on the server, which
|
The script is a Tilde Friends application, and it runs on the server, which means that unlike client-side JavaScript, it can have the ability to read and write files on the server or create network connections to other machines. Unlike node.js or other server-side runtime environments, applications are limited for security reasons to not interfere with each other or bring the entire server down.
|
||||||
means that unlike client-side JavaScript, it can have the ability to read and
|
|
||||||
write files on the server or create network connections to other machines.
|
|
||||||
Unlike node.js or other server-side runtime environments, applications are
|
|
||||||
limited for security reasons to not interfere with each other or bring the
|
|
||||||
entire server down.
|
|
||||||
|
|
||||||
Above the terminal, an "Edit" link brings a visitor to the source code for the
|
Above the terminal, an "Edit" link brings a visitor to the source code for the current Tilde Friends application, which they can then edit, save as their own, and run.
|
||||||
current Tilde Friends application, which they can then edit, save as their own,
|
|
||||||
and run.
|
|
||||||
|
|
||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
Tilde Friends is a C++ application with a JavaScript runtime that provides
|
Tilde Friends is a C++ application with a JavaScript runtime that provides restricted access to filesystem, network, and other system resources. The core process runs a core set of scripts that implement a web server, typically starting a new process for each visitor's session which runs scripts for the active application and stopping it when the visitor leaves.
|
||||||
restricted access to filesystem, network, and other system resources. The core
|
|
||||||
process runs a core set of scripts that implement a web server, typically
|
|
||||||
starting a new process for each visitor's session which runs scripts for the
|
|
||||||
active application and stopping it when the visitor leaves.
|
|
||||||
|
|
||||||
Only the core process has access to most system resources, but session
|
Only the core process has access to most system resources, but session processes can be given accesss through the core process.
|
||||||
processes can be given accesss through the core process.
|
|
||||||
|
|
||||||
Service processes are identical to session processes, but they are not tied to
|
Service processes are identical to session processes, but they are not tied to a user session.
|
||||||
a user session.
|
|
||||||
|
|
||||||
## Communication
|
## Communication
|
||||||
|
|
||||||
In the same way that web browsers expose APIs for scripts running in the
|
In the same way that web browsers expose APIs for scripts running in the browser to modify the document, play sounds and video, and draw, Tilde Friends exposes APIs for scripts running on a Tilde Friends server to interact with a visitor's web browser, read and write files on the server, and otherwise interact with the world.
|
||||||
browser to modify the document, play sounds and video, and draw, Tilde Friends
|
|
||||||
exposes APIs for scripts running on a Tilde Friends server to interact with a
|
|
||||||
visitor's web browser, read and write files on the server, and otherwise
|
|
||||||
interact with the world.
|
|
||||||
|
|
||||||
There are several distinct classes of APIs.
|
There are several distinct classes of APIs.
|
||||||
|
|
||||||
First, there are low-level functions exposed from C++ to JavaScript. Most of
|
First, there are low-level functions exposed from C++ to JavaScript. Most of these are only available to the core process. These typically only go through a basic JavaScript to C++ transition and are relatively fast and immediate.
|
||||||
these are only available to the core process. These typically only go through
|
|
||||||
a basic JavaScript to C++ transition and are relatively fast and immediate.
|
|
||||||
|
|
||||||
|
```js
|
||||||
// Displays some text to the server's console.
|
// Displays some text to the server's console.
|
||||||
print("Hello, world!");
|
print('Hello, world!');
|
||||||
|
```
|
||||||
|
|
||||||
There is a mechanism for communicating between processes. Functions can be
|
There is a mechanism for communicating between processes. Functions can be exported and called across process boundaries. When this is done, any arguments are serialized to a network protocol, deserialized by the other process, the function called, and finally any return value is passed back in the same way. Any functions referenced by the arguments or return value are also exported and can be subsequently called across process boundaries. Functions called across process boundaries are always asynchronous, returning a Promise. Care must be taken for security reasons to not pass dangerous functions ("deleteAllMydata()") to untrusted processes, and it is best for performance reasons to minimize the data size transferred between processes.
|
||||||
exported and called across process boundaries. When this is done, any
|
|
||||||
arguments are serialized to a network protocol, deserialized by the other
|
|
||||||
process, the function called, and finally any return value is passed back in
|
|
||||||
the same way. Any functions referenced by the arguments or return value are
|
|
||||||
also exported and can be subsequently called across process boundaries.
|
|
||||||
Functions called across process boundaries are always asynchronous, returning a
|
|
||||||
Promise. Care must be taken for security reasons to not pass dangerous
|
|
||||||
functions ("deleteAllMydata()") to untrusted processes, and it is best for
|
|
||||||
performance reasons to minimize the data size transferred between processes.
|
|
||||||
|
|
||||||
|
```js
|
||||||
// Send an "add" function to any other running processes. When called, it
|
// Send an "add" function to any other running processes. When called, it
|
||||||
// will run in this process.
|
// will run in this process.
|
||||||
core.broadcast({add: function(x, y) { return x + y; }});
|
core.broadcast({
|
||||||
|
add: function (x, y) {
|
||||||
|
return x + y;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Receive the above message and call the function.
|
// Receive the above message and call the function.
|
||||||
core.register("onMessage", function(sender, message) {
|
core.register('onMessage', function (sender, message) {
|
||||||
message.add(3, 4).then(x => terminal.print(x.toString()));
|
message.add(3, 4).then((x) => terminal.print(x.toString()));
|
||||||
});
|
});
|
||||||
|
```
|
||||||
|
|
||||||
Finally, there is a core web interface that runs on the client's browser that
|
Finally, there is a core web interface that runs on the client's browser that
|
||||||
extends access to a running Tilde Friends script.
|
extends access to a running Tilde Friends script.
|
||||||
|
|
||||||
|
```js
|
||||||
// Displays a message in the client's browser.
|
// Displays a message in the client's browser.
|
||||||
terminal.print("Hello, world!");
|
terminal.print('Hello, world!');
|
||||||
|
```
|
||||||
|
|
||||||
## API Documentation
|
## API Documentation
|
||||||
|
|
||||||
The Tilde Friends API is very much evolving.
|
The Tilde Friends API is very much evolving.
|
||||||
|
|
||||||
All currently registered methods can be explored in the
|
All currently registered methods can be explored in the [documentation](https://www.tildefriends.net/~cory/documentation) app.
|
||||||
[documentation](https://www.tildefriends.net/~cory/documentation) app.
|
|
||||||
|
|
||||||
All browser-facing methods are implemented in [client.js](core/client.js).
|
All browser-facing methods are implemented in [client.js](core/client.js). Most process-related methods are implemented in [core.js](core/core.js).
|
||||||
Most process-related methods are implemented in [core.js](core/core.js).
|
|
||||||
|
|
||||||
Higher-level behaviors are often implemented within library-style apps
|
Higher-level behaviors are often implemented within library-style apps themselves and are beyond the scope of this document.
|
||||||
themselves and are beyond the scope of this document.
|
|
||||||
|
|
||||||
### Terminal
|
### Terminal
|
||||||
All interaction with a human user is through a terminal-like interface. Though
|
|
||||||
it is somewhat limiting, it makes simple things easy, and it is possible to
|
All interaction with a human user is through a terminal-like interface. Though it is somewhat limiting, it makes simple things easy, and it is possible to construct complicated interfaces by creating and interacting with an iframe.
|
||||||
construct complicated interfaces by creating and interacting with an iframe.
|
|
||||||
|
|
||||||
#### terminal.print(arguments...)
|
#### terminal.print(arguments...)
|
||||||
Print to the terminal. Arguments and lists are recursively expanded. Numerous
|
|
||||||
special values are supported as implemented in client.cs.
|
|
||||||
|
|
||||||
|
Print to the terminal. Arguments and lists are recursively expanded. Numerous special values are supported as implemented in client.cs.
|
||||||
|
|
||||||
|
```js
|
||||||
// Create a link.
|
// Create a link.
|
||||||
terminal.print({href: "http://www.tildefriends.net/", value: "Tilde Friends!"});
|
terminal.print({href: 'http://www.tildefriends.net/', value: 'Tilde Friends!'});
|
||||||
|
|
||||||
// Create an iframe.
|
// Create an iframe.
|
||||||
terminal.print({iframe: "<b>Hello, world!</b>", width: 640, height: 480});
|
terminal.print({
|
||||||
|
iframe: '<b>Hello, world!</b>',
|
||||||
|
width: 640,
|
||||||
|
height: 480,
|
||||||
|
});
|
||||||
|
|
||||||
// Use style.
|
// Use style.
|
||||||
terminal.print({style: "color: #f00", value: "Hello, world!"});
|
terminal.print({style: 'color: #f00', value: 'Hello, world!'});
|
||||||
|
|
||||||
// Create a link that when clicked will act as if the user typed a command.
|
// Create a link that when clicked will act as if the user typed a command.
|
||||||
terminal.print({command: "exit", value: "Get out of here."});
|
terminal.print({command: 'exit', value: 'Get out of here.'});
|
||||||
|
```
|
||||||
|
|
||||||
#### terminal.clear()
|
#### terminal.clear()
|
||||||
|
|
||||||
Clears the terminal output.
|
Clears the terminal output.
|
||||||
|
|
||||||
#### terminal.readLine()
|
#### terminal.readLine()
|
||||||
|
|
||||||
Read a line of input from the user.
|
Read a line of input from the user.
|
||||||
|
|
||||||
#### terminal.setEcho(echo)
|
#### terminal.setEcho(echo)
|
||||||
|
|
||||||
Controls whether the terminal will automatically echo user input. Defaults to true.
|
Controls whether the terminal will automatically echo user input. Defaults to true.
|
||||||
|
|
||||||
#### terminal.setPrompt(prompt)
|
#### terminal.setPrompt(prompt)
|
||||||
|
|
||||||
Sets the terminal prompt. The default is ">".
|
Sets the terminal prompt. The default is ">".
|
||||||
|
|
||||||
#### terminal.setTitle(title)
|
#### terminal.setTitle(title)
|
||||||
|
|
||||||
Sets the browser window/tab title.
|
Sets the browser window/tab title.
|
||||||
|
|
||||||
#### terminal.split(terminalList)
|
#### terminal.split(terminalList)
|
||||||
|
|
||||||
Reconfigures the terminal layout, potentially into multiple split panes.
|
Reconfigures the terminal layout, potentially into multiple split panes.
|
||||||
|
|
||||||
|
```js
|
||||||
terminal.split([
|
terminal.split([
|
||||||
{
|
{
|
||||||
type: "horizontal",
|
type: 'horizontal',
|
||||||
children: [
|
children: [
|
||||||
{name: "left", basis: "2in", grow: 0, shrink: 0},
|
{name: 'left', basis: '2in', grow: 0, shrink: 0},
|
||||||
{name: "middle", grow: 1},
|
{name: 'middle', grow: 1},
|
||||||
{name: "right", basis: "2in", grow: 0, shrink: 0},
|
{name: 'right', basis: '2in', grow: 0, shrink: 0},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
#### terminal.select(name)
|
#### terminal.select(name)
|
||||||
|
|
||||||
Directs subsequent output to the named terminal.
|
Directs subsequent output to the named terminal.
|
||||||
|
|
||||||
#### terminal.postMessageToIframe(iframeName, message)
|
#### terminal.postMessageToIframe(iframeName, message)
|
||||||
Sends a message to the iframe that was created with the given name, using the
|
|
||||||
browser's window.postMessage.
|
Sends a message to the iframe that was created with the given name, using the browser's window.postMessage.
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
Tilde Friends uses lmdb as a basic key value store. Keys and values are all
|
|
||||||
expected to be of type String. Each application gets its own isolated
|
Tilde Friends uses lmdb as a basic key value store. Keys and values are all expected to be of type String. Each application gets its own isolated database.
|
||||||
database.
|
|
||||||
|
|
||||||
#### database.get(key)
|
#### database.get(key)
|
||||||
|
|
||||||
Retrieve the database value associated with the given key.
|
Retrieve the database value associated with the given key.
|
||||||
|
|
||||||
#### database.set(key, value)
|
#### database.set(key, value)
|
||||||
|
|
||||||
Sets the database value for the given key, overwriting any existing value.
|
Sets the database value for the given key, overwriting any existing value.
|
||||||
|
|
||||||
#### database.remove(key)
|
#### database.remove(key)
|
||||||
|
|
||||||
Remove the database entry for the given key.
|
Remove the database entry for the given key.
|
||||||
|
|
||||||
#### database.getAlll()
|
#### database.getAlll()
|
||||||
|
|
||||||
Retrieve a list of all key names.
|
Retrieve a list of all key names.
|
||||||
|
|
||||||
### Network
|
### Network
|
||||||
|
|
||||||
Network access is generally not extended to untrusted users.
|
Network access is generally not extended to untrusted users.
|
||||||
|
|
||||||
It is necessary to grant network permissions to an app owner through the
|
It is necessary to grant network permissions to an app owner through the administration app.
|
||||||
administration app.
|
|
||||||
|
|
||||||
Apps that require network access must declare it like this:
|
Apps that require network access must declare it like this:
|
||||||
|
|
||||||
//! { "permissions": ["network"] }
|
```json
|
||||||
|
{"permissions": ["network"]}
|
||||||
|
```
|
||||||
|
|
||||||
#### network.newConnection()
|
#### network.newConnection()
|
||||||
|
|
||||||
Creates a Connection object.
|
Creates a Connection object.
|
||||||
|
|
||||||
#### connection.connect(host, port)
|
#### connection.connect(host, port)
|
||||||
|
|
||||||
Opens a TCP connection to host:port.
|
Opens a TCP connection to host:port.
|
||||||
|
|
||||||
#### connection.read(readCallback)
|
#### connection.read(readCallback)
|
||||||
|
|
||||||
Begins reading and calls readCallback(data) for all data received.
|
Begins reading and calls readCallback(data) for all data received.
|
||||||
|
|
||||||
#### connection.write(data)
|
#### connection.write(data)
|
||||||
|
|
||||||
Writes data to the connection.
|
Writes data to the connection.
|
||||||
|
|
||||||
#### connection.close()
|
#### connection.close()
|
||||||
|
|
||||||
Closes the connection.
|
Closes the connection.
|
||||||
|
28
package-lock.json
generated
Normal file
28
package-lock.json
generated
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "tildefriends",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "tildefriends",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prettier": "^3.2.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
|
||||||
|
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
package.json
Normal file
11
package.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "tildefriends",
|
||||||
|
"scripts": {
|
||||||
|
"prettier": "prettier . --check --cache --write"
|
||||||
|
},
|
||||||
|
"author": "Cory McWilliams",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prettier": "^3.2.5"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user