Compare commits
No commits in common. "70a3e7fc7d9b494e990474be86b024ac32897916" and "63ae186c76cdfd3462a3f21fc72a26cf0e5bd2ee" have entirely different histories.
70a3e7fc7d
...
63ae186c76
@ -1,2 +0,0 @@
|
|||||||
# Add prettier to the project
|
|
||||||
41024ddb7961b04a5688bbc997cb74de6fab4763
|
|
@ -1,14 +0,0 @@
|
|||||||
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
|
|
@ -1,5 +0,0 @@
|
|||||||
trailingComma: 'es5'
|
|
||||||
useTabs: true
|
|
||||||
semi: true
|
|
||||||
singleQuote: true
|
|
||||||
bracketSpacing: false
|
|
20
README.md
20
README.md
@ -1,5 +1,4 @@
|
|||||||
# 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/.
|
||||||
@ -8,42 +7,37 @@ 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
|
||||||
Builds on Linux (x86_64 and aarch64), MacOS, OpenBSD, and Haiku. Builds for
|
|
||||||
all of those host platforms plus mingw64, iOS, and android.
|
all of those host platforms plus mingw64, iOS, and android.
|
||||||
|
|
||||||
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
|
1. Requires openssl (`libssl-dev`, in debian-speak). All other dependencies
|
||||||
are kept up to date in the tree.
|
are kept up to date in the tree.
|
||||||
2. To build, run `make debug` or `make release`. An executable will be
|
2. To build, run `make debug` or `make release`. An executable will be
|
||||||
generated in a subdirectory of `out/`.
|
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
|
the right dependencies in the right places. `make windebug winrelease
|
||||||
iosdebug-ipa iosrelease-ipa release-apk`.
|
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
|
By default, running the built `tildefriends` executable will start a web server
|
||||||
at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
at <http://localhost:12345/>. `tildefriends -h` lists further options.
|
||||||
|
|
||||||
The first user to create an account and log in will be granted administrative
|
The first user to create an account and log in will be granted administrative
|
||||||
privileges. Further administration can be done at
|
privileges. Further administration can be done at
|
||||||
<http://localhost:12345/~core/admin/>.
|
<http://localhost:12345/~core/admin/>.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Docs are a work in progress:
|
Docs are a work in progress:
|
||||||
<https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
|
<https://www.tildefriends.net/~cory/wiki/#test-wiki/tf-app-quick-reference>.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
All code unless otherwise noted in is provided under the
|
All code unless otherwise noted in is provided under the
|
||||||
[MIT](https://opensource.org/licenses/MIT) license.
|
[MIT](https://opensource.org/licenses/MIT) license.
|
||||||
|
@ -18,13 +18,9 @@ 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(
|
await app.setDocument(utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data)));
|
||||||
utf8Decode(getFile('index.html')).replace('$data', JSON.stringify(data))
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
await app.setDocument(
|
await app.setDocument('<span style="color: #f00">Only an administrator can modify these settings.</span>');
|
||||||
'<span style="color: #f00">Only an administrator can modify these settings.</span>'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
main();
|
main();
|
@ -1,9 +1,7 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html style="width: 100%">
|
<html style="width: 100%">
|
||||||
<head>
|
<head>
|
||||||
<script>
|
<script>const g_data = $data;</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,32 +3,25 @@ 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
|
tfrpc.rpc.delete_user(user).then(function() {
|
||||||
.delete_user(user)
|
alert(`User "${user}" deleted successfully.`);
|
||||||
.then(function () {
|
}).catch(function(error) {
|
||||||
alert(`User "${user}" deleted successfully.`);
|
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
|
tfrpc.rpc.global_settings_set(key, value).then(function() {
|
||||||
.global_settings_set(key, value)
|
alert(`Set "${key}" to "${value}".`);
|
||||||
.then(function () {
|
}).catch(function(error) {
|
||||||
alert(`Set "${key}" to "${value}".`);
|
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
||||||
})
|
});
|
||||||
.catch(function (error) {
|
|
||||||
alert(`Failed to set "${key}": ${JSON.stringify(error, null, 2)}.`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', function() {
|
||||||
const permission_template = (permission) => html` <code>${permission}</code>`;
|
const permission_template = (permission) =>
|
||||||
|
html` <code>${permission}</code>`;
|
||||||
function input_template(key, description) {
|
function input_template(key, description) {
|
||||||
if (description.type === 'boolean') {
|
if (description.type === 'boolean') {
|
||||||
return html`
|
return html`
|
||||||
@ -69,24 +62,26 @@ window.addEventListener('load', function () {
|
|||||||
}
|
}
|
||||||
const user_template = (user, permissions) => html`
|
const user_template = (user, permissions) => html`
|
||||||
<li>
|
<li>
|
||||||
<button @click=${(e) => delete_user(user)}>Delete</button>
|
<button @click=${(e) => delete_user(user)}>
|
||||||
${user}: ${permissions.map((x) => permission_template(x))}
|
Delete
|
||||||
|
</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)
|
${Object.keys(data.settings).sort().map(x => html`${input_template(x, data.settings[x])}`)}
|
||||||
.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);
|
||||||
});
|
});
|
@ -219,7 +219,7 @@ Parses an HTTP response.
|
|||||||
* *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse.
|
* *Object* An object with **bytes_parsed**, **minor_version**, **status**, **message**, and **headers** fields on successful parse.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
docs['sha1Digest()'] = `
|
docs['sha1Digest()'] =`
|
||||||
Calculates a SHA1 digest.
|
Calculates a SHA1 digest.
|
||||||
|
|
||||||
Completes synchronously.
|
Completes synchronously.
|
||||||
|
@ -26,15 +26,14 @@ 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
|
||||||
ORDER BY timestamp
|
ORDER BY timestamp
|
||||||
`,
|
`,
|
||||||
[],
|
[],
|
||||||
function (row) {
|
function(row) {
|
||||||
let content = JSON.parse(row.content);
|
let content = JSON.parse(row.content);
|
||||||
for (let mention of content.mentions) {
|
for (let mention of content.mentions) {
|
||||||
if (mention?.type === 'application/tildefriends') {
|
if (mention?.type === 'application/tildefriends') {
|
||||||
@ -45,13 +44,10 @@ async function fetch_shared_apps() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
let result = {};
|
let result = {};
|
||||||
for (let app of Object.values(messages).sort(
|
for (let app of Object.values(messages).sort((x, y) => y.message.timestamp - x.message.timestamp)) {
|
||||||
(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,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🪵",
|
"emoji": "🪵",
|
||||||
"previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256"
|
"previous": "&TIrBnpN3iz3O9L9MCCteAcVJZjA83EKdcfu4SCM76VE=.sha256"
|
||||||
}
|
}
|
@ -1,19 +1,11 @@
|
|||||||
import * as commonmark from './commonmark.min.js';
|
import * as commonmark from './commonmark.min.js';
|
||||||
|
|
||||||
function escape(text) {
|
function escape(text) {
|
||||||
return (text ?? '')
|
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
||||||
.replaceAll('&', '&')
|
|
||||||
.replaceAll('<', '<')
|
|
||||||
.replaceAll('>', '>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeAttribute(text) {
|
function escapeAttribute(text) {
|
||||||
return (text ?? '')
|
return (text ?? '').replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
||||||
.replaceAll('&', '&')
|
|
||||||
.replaceAll('<', '<')
|
|
||||||
.replaceAll('>', '>')
|
|
||||||
.replaceAll('"', '"')
|
|
||||||
.replaceAll("'", ''');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_blog_message(id) {
|
export async function get_blog_message(id) {
|
||||||
@ -21,7 +13,7 @@ export async function get_blog_message(id) {
|
|||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync(
|
||||||
'SELECT author, timestamp, content FROM messages WHERE id = ?',
|
'SELECT author, timestamp, content FROM messages WHERE id = ?',
|
||||||
[id],
|
[id],
|
||||||
function (row) {
|
function(row) {
|
||||||
let content = JSON.parse(row.content);
|
let content = JSON.parse(row.content);
|
||||||
message = {
|
message = {
|
||||||
author: row.author,
|
author: row.author,
|
||||||
@ -29,8 +21,7 @@ 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(
|
||||||
`
|
`
|
||||||
@ -43,10 +34,9 @@ export async function get_blog_message(id) {
|
|||||||
ORDER BY sequence DESC LIMIT 1
|
ORDER BY sequence DESC LIMIT 1
|
||||||
`,
|
`,
|
||||||
[message.author],
|
[message.author],
|
||||||
function (row) {
|
function(row) {
|
||||||
message.name = row.name;
|
message.name = row.name;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
@ -61,12 +51,8 @@ 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 = '/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
||||||
'/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
} else if (node.destination?.startsWith('@') || node.destination?.startsWith('%')) {
|
||||||
} else if (
|
|
||||||
node.destination?.startsWith('@') ||
|
|
||||||
node.destination?.startsWith('%')
|
|
||||||
) {
|
|
||||||
node.destination = '/~core/ssb/#' + escape(node.destination);
|
node.destination = '/~core/ssb/#' + escape(node.destination);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -121,7 +107,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>`;
|
||||||
}
|
}
|
||||||
@ -149,15 +135,14 @@ 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
|
||||||
@ -197,11 +182,8 @@ 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)],
|
blogs.push(row);
|
||||||
function (row) {
|
});
|
||||||
blogs.push(row);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return blogs;
|
return blogs;
|
||||||
}
|
}
|
@ -2,50 +2,30 @@ 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')
|
let id = request.path.startsWith('%25') ? '%' + request.path.substring(3) : request.path;
|
||||||
? '%' + 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({
|
respond({data: await blog.render_blog_post_html(message), content_type: 'text/html; charset=utf-8'});
|
||||||
data: await blog.render_blog_post_html(message),
|
|
||||||
content_type: 'text/html; charset=utf-8',
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
respond({
|
respond({data: `Message ${id} not found.`, content_type: 'text/html; charset=utf-8'});
|
||||||
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({
|
respond({data: blog.render_atom(blogs), content_type: 'application/atom+xml'});
|
||||||
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({
|
respond({data: await blog.render_blog_post_html(blog_post), content_type: 'text/html; charset=utf-8'});
|
||||||
data: await blog.render_blog_post_html(blog_post),
|
|
||||||
content_type: 'text/html; charset=utf-8',
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
respond({
|
respond({data: blog.render_html(blogs), content_type: 'text/html; charset=utf-8'});
|
||||||
data: blog.render_html(blogs),
|
|
||||||
content_type: 'text/html; charset=utf-8',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(function (error) {
|
main().catch(function(error) {
|
||||||
respond({
|
respond({data: `<!DOCTYPE html>
|
||||||
data: `<!DOCTYPE html>
|
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`, content_type: 'text/html'});
|
||||||
<pre style="color: #f00">${error.message}\n${error.stack}</pre>`,
|
|
||||||
content_type: 'text/html',
|
|
||||||
});
|
|
||||||
});
|
});
|
@ -51,7 +51,7 @@ async function key_list(db) {
|
|||||||
app.setDocument(doc);
|
app.setDocument(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
core.register('message', async function (message) {
|
core.register('message', async function(message) {
|
||||||
if (message.event == 'hashChange') {
|
if (message.event == 'hashChange') {
|
||||||
let hash = message.hash.substring(1);
|
let hash = message.hash.substring(1);
|
||||||
if (hash.startsWith(':shared:')) {
|
if (hash.startsWith(':shared:')) {
|
||||||
|
@ -2,7 +2,7 @@ let g_about_cache = {};
|
|||||||
|
|
||||||
async function query(sql, args) {
|
async function query(sql, args) {
|
||||||
let result = [];
|
let result = [];
|
||||||
await ssb.sqlAsync(sql, args, function (row) {
|
await ssb.sqlAsync(sql, args, function(row) {
|
||||||
result.push(row);
|
result.push(row);
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
@ -21,8 +21,7 @@ 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) {
|
||||||
@ -43,34 +42,15 @@ 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(
|
async function following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) {
|
||||||
ids,
|
let contacts = await Promise.all([...new Set(ids)].map(x => contact(x, last_row_id, following, max_row_id)));
|
||||||
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 =
|
let deeper = depth > 1 ? await following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : [];
|
||||||
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())];
|
||||||
@ -88,22 +68,10 @@ async function following_deep(ids, depth, blocking) {
|
|||||||
last_row_id: 0,
|
last_row_id: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let max_row_id = (
|
let max_row_id = (await query(`
|
||||||
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);
|
||||||
@ -122,15 +90,13 @@ 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];
|
||||||
@ -163,21 +129,17 @@ 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] = Object.assign(cache.about[about.author] || {}, content);
|
||||||
cache.about[about.author] || {},
|
|
||||||
content
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache.last_row_id = max_row_id;
|
cache.last_row_id = max_row_id;
|
||||||
@ -193,41 +155,41 @@ 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;
|
||||||
if (row.content) {
|
if (row.content) {
|
||||||
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;
|
||||||
@ -236,15 +198,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(
|
||||||
@ -259,8 +221,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,10 +241,7 @@ function niceSize(bytes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function escape(value) {
|
function escape(value) {
|
||||||
return value
|
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
||||||
.replaceAll('&', '&')
|
|
||||||
.replaceAll('<', '<')
|
|
||||||
.replaceAll('>', '>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@ -291,27 +249,19 @@ 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(
|
await app.setDocument(`<pre style="color: #fff">Enumerating followed users...</pre>`);
|
||||||
`<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(
|
await app.setDocument(`<pre style="color: #fff">Getting names and sizes...</pre>`);
|
||||||
`<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(
|
await app.setDocument('<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' + tree + '</ul>\n</body>\n</html>');
|
||||||
'<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' +
|
|
||||||
tree +
|
|
||||||
'</ul>\n</body>\n</html>'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🗺",
|
"emoji": "🗺",
|
||||||
"previous": "&0XSp+xdQwVtQ88bXzvWdH15Ex63hv5zUKTa4zx7HBGM=.sha256"
|
"previous": "&0XSp+xdQwVtQ88bXzvWdH15Ex63hv5zUKTa4zx7HBGM=.sha256"
|
||||||
}
|
}
|
@ -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,15 +71,10 @@ 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(
|
app.setDocument(utf8Decode(getFile('index.html')).replace('${data}', JSON.stringify({
|
||||||
utf8Decode(getFile('index.html')).replace(
|
attempt: attempt,
|
||||||
'${data}',
|
state: core.user?.credentials?.session?.name,
|
||||||
JSON.stringify({
|
})));
|
||||||
attempt: attempt,
|
|
||||||
state: core.user?.credentials?.session?.name,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
@ -17,7 +17,7 @@ function xml_parse(xml) {
|
|||||||
let tag = xml.substring(tag_begin, i).trim();
|
let tag = xml.substring(tag_begin, i).trim();
|
||||||
if (tag.startsWith('?') && tag.endsWith('?')) {
|
if (tag.startsWith('?') && tag.endsWith('?')) {
|
||||||
/* Ignore directives. */
|
/* Ignore directives. */
|
||||||
} else if (tag.startsWith('/')) {
|
} else if (tag.startsWith('/')) {
|
||||||
path.pop();
|
path.pop();
|
||||||
} else {
|
} else {
|
||||||
let parts = tag.split(' ');
|
let parts = tag.split(' ');
|
||||||
@ -63,10 +63,7 @@ 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({
|
segment.push({lat: parseFloat(trkpt.attributes.lat), lon: parseFloat(trkpt.attributes.lon)});
|
||||||
lat: parseFloat(trkpt.attributes.lat),
|
|
||||||
lon: parseFloat(trkpt.attributes.lon),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
result.segments.push(segment);
|
result.segments.push(segment);
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,14 @@
|
|||||||
<!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>
|
<script>window.litDisableBundleWarning = true;</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
|
<body style="color: #fff; display: flex; flex-flow: column; height: 100%; width: 100%; margin: 0; padding: 0">
|
||||||
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>
|
@ -10,24 +10,24 @@
|
|||||||
var polyline = {};
|
var polyline = {};
|
||||||
|
|
||||||
function py2_round(value) {
|
function py2_round(value) {
|
||||||
// Google's polyline algorithm uses the same rounding strategy as Python 2, which is different from JS for negative values
|
// Google's polyline algorithm uses the same rounding strategy as Python 2, which is different from JS for negative values
|
||||||
return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1);
|
return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function encode(current, previous, factor) {
|
function encode(current, previous, factor) {
|
||||||
current = py2_round(current * factor);
|
current = py2_round(current * 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) {
|
||||||
output += String.fromCharCode((0x20 | (coordinate & 0x1f)) + 63);
|
output += String.fromCharCode((0x20 | (coordinate & 0x1f)) + 63);
|
||||||
coordinate /= 32;
|
coordinate /= 32;
|
||||||
}
|
}
|
||||||
output += String.fromCharCode((coordinate | 0) + 63);
|
output += String.fromCharCode((coordinate | 0) + 63);
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,53 +41,54 @@ function encode(current, previous, factor) {
|
|||||||
*
|
*
|
||||||
* @see https://github.com/Project-OSRM/osrm-frontend/blob/master/WebContent/routing/OSRM.RoutingGeometry.js
|
* @see https://github.com/Project-OSRM/osrm-frontend/blob/master/WebContent/routing/OSRM.RoutingGeometry.js
|
||||||
*/
|
*/
|
||||||
polyline.decode = function (str, precision) {
|
polyline.decode = function(str, precision) {
|
||||||
var index = 0,
|
var index = 0,
|
||||||
lat = 0,
|
lat = 0,
|
||||||
lng = 0,
|
lng = 0,
|
||||||
coordinates = [],
|
coordinates = [],
|
||||||
shift = 0,
|
shift = 0,
|
||||||
result = 0,
|
result = 0,
|
||||||
byte = null,
|
byte = null,
|
||||||
latitude_change,
|
latitude_change,
|
||||||
longitude_change,
|
longitude_change,
|
||||||
factor = Math.pow(10, Number.isInteger(precision) ? precision : 5);
|
factor = Math.pow(10, Number.isInteger(precision) ? precision : 5);
|
||||||
|
|
||||||
// Coordinates have variable length when encoded, so just keep
|
// Coordinates have variable length when encoded, so just keep
|
||||||
// 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
|
|
||||||
byte = null;
|
|
||||||
shift = 1;
|
|
||||||
result = 0;
|
|
||||||
|
|
||||||
do {
|
// Reset shift, result, and byte
|
||||||
byte = str.charCodeAt(index++) - 63;
|
byte = null;
|
||||||
result += (byte & 0x1f) * shift;
|
shift = 1;
|
||||||
shift *= 32;
|
result = 0;
|
||||||
} while (byte >= 0x20);
|
|
||||||
|
|
||||||
latitude_change = result & 1 ? (-result - 1) / 2 : result / 2;
|
do {
|
||||||
|
byte = str.charCodeAt(index++) - 63;
|
||||||
|
result += (byte & 0x1f) * shift;
|
||||||
|
shift *= 32;
|
||||||
|
} while (byte >= 0x20);
|
||||||
|
|
||||||
shift = 1;
|
latitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2);
|
||||||
result = 0;
|
|
||||||
|
|
||||||
do {
|
shift = 1;
|
||||||
byte = str.charCodeAt(index++) - 63;
|
result = 0;
|
||||||
result += (byte & 0x1f) * shift;
|
|
||||||
shift *= 32;
|
|
||||||
} while (byte >= 0x20);
|
|
||||||
|
|
||||||
longitude_change = result & 1 ? (-result - 1) / 2 : result / 2;
|
do {
|
||||||
|
byte = str.charCodeAt(index++) - 63;
|
||||||
|
result += (byte & 0x1f) * shift;
|
||||||
|
shift *= 32;
|
||||||
|
} while (byte >= 0x20);
|
||||||
|
|
||||||
lat += latitude_change;
|
longitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2);
|
||||||
lng += longitude_change;
|
|
||||||
|
|
||||||
coordinates.push([lat / factor, lng / factor]);
|
lat += latitude_change;
|
||||||
}
|
lng += longitude_change;
|
||||||
|
|
||||||
return coordinates;
|
coordinates.push([lat / factor, lng / factor]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return coordinates;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,33 +98,28 @@ polyline.decode = function (str, precision) {
|
|||||||
* @param {Number} precision
|
* @param {Number} precision
|
||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
polyline.encode = function (coordinates, precision) {
|
polyline.encode = function(coordinates, precision) {
|
||||||
if (!coordinates.length) {
|
if (!coordinates.length) { return ''; }
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
var factor = Math.pow(10, Number.isInteger(precision) ? precision : 5),
|
var factor = Math.pow(10, Number.isInteger(precision) ? precision : 5),
|
||||||
output =
|
output = encode(coordinates[0][0], 0, factor) + encode(coordinates[0][1], 0, factor);
|
||||||
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],
|
var a = coordinates[i], b = coordinates[i - 1];
|
||||||
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);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
};
|
};
|
||||||
|
|
||||||
function flipped(coords) {
|
function flipped(coords) {
|
||||||
var flipped = [];
|
var flipped = [];
|
||||||
for (var i = 0; i < coords.length; i++) {
|
for (var i = 0; i < coords.length; i++) {
|
||||||
var coord = coords[i].slice();
|
var coord = coords[i].slice();
|
||||||
flipped.push([coord[1], coord[0]]);
|
flipped.push([coord[1], coord[0]]);
|
||||||
}
|
}
|
||||||
return flipped;
|
return flipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,14 +129,14 @@ function flipped(coords) {
|
|||||||
* @param {Number} precision
|
* @param {Number} precision
|
||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
polyline.fromGeoJSON = function (geojson, precision) {
|
polyline.fromGeoJSON = function(geojson, precision) {
|
||||||
if (geojson && geojson.type === 'Feature') {
|
if (geojson && geojson.type === 'Feature') {
|
||||||
geojson = geojson.geometry;
|
geojson = geojson.geometry;
|
||||||
}
|
}
|
||||||
if (!geojson || geojson.type !== 'LineString') {
|
if (!geojson || geojson.type !== 'LineString') {
|
||||||
throw new Error('Input must be a GeoJSON LineString');
|
throw new Error('Input must be a GeoJSON LineString');
|
||||||
}
|
}
|
||||||
return polyline.encode(flipped(geojson.coordinates), precision);
|
return polyline.encode(flipped(geojson.coordinates), precision);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -150,13 +146,13 @@ polyline.fromGeoJSON = function (geojson, precision) {
|
|||||||
* @param {Number} precision
|
* @param {Number} precision
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
polyline.toGeoJSON = function (str, precision) {
|
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)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let polyline_decode = polyline.decode;
|
let polyline_decode = polyline.decode;
|
||||||
export {polyline_decode as decode};
|
export { polyline_decode as decode };
|
@ -1,11 +1,4 @@
|
|||||||
import {
|
import {LitElement, html, unsafeHTML, css, guard, until} from './lit-all.min.js';
|
||||||
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';
|
||||||
@ -63,7 +56,7 @@ class GgAppElement extends LitElement {
|
|||||||
this.focus = undefined;
|
this.focus = undefined;
|
||||||
this.status = undefined;
|
this.status = undefined;
|
||||||
this.tab = 'map';
|
this.tab = 'map';
|
||||||
this.load().catch(function (e) {
|
this.load().catch(function(e) {
|
||||||
console.log('load error', e);
|
console.log('load error', e);
|
||||||
});
|
});
|
||||||
this.to_build = '🏠';
|
this.to_build = '🏠';
|
||||||
@ -72,12 +65,9 @@ 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)
|
emojis = Object.values(emojis).map(x => Object.values(x)).flat();
|
||||||
.map((x) => Object.values(x))
|
|
||||||
.flat();
|
|
||||||
let today = new Date();
|
let today = new Date();
|
||||||
let date_index =
|
let date_index = today.getYear() * 356 + today.getMonth() * 31 + today.getDate();
|
||||||
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];
|
||||||
@ -119,8 +109,7 @@ 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,
|
||||||
@ -128,15 +117,10 @@ 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(
|
let blobs = await this.promise_all(rows.map(x => tfrpc.rpc.get_blob(x.blob_id)), 8);
|
||||||
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;
|
||||||
@ -151,19 +135,13 @@ 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'};
|
||||||
@ -188,11 +166,8 @@ class GgAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sync_activities() {
|
async sync_activities() {
|
||||||
let ids = this.activities.map(
|
let ids = this.activities.map(x => `https://www.strava.com/activities/${x.id}`);
|
||||||
(x) => `https://www.strava.com/activities/${x.id}`
|
let missing = await tfrpc.rpc.query(`
|
||||||
);
|
|
||||||
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
|
||||||
@ -203,26 +178,17 @@ 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 = {
|
this.status = {text: 'syncing from strava', value: index, max: missing.length};
|
||||||
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(
|
let response = await fetch(`https://www.strava.com/api/v3/activities/${id}`, {
|
||||||
`https://www.strava.com/api/v3/activities/${id}`,
|
headers: {
|
||||||
{
|
'Authorization': `Bearer ${this.strava.access_token}`,
|
||||||
headers: {
|
},
|
||||||
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 = {
|
||||||
@ -235,7 +201,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);
|
||||||
@ -249,20 +215,13 @@ class GgAppElement extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ids = await tfrpc.rpc.getIdentities();
|
let ids = await tfrpc.rpc.getIdentities();
|
||||||
let players = ids.length
|
let players = ids.length ? (await tfrpc.rpc.query(`
|
||||||
? (
|
|
||||||
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) {
|
||||||
@ -287,14 +246,9 @@ 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(
|
console.log('this looks expired', new Date().valueOf() / 1000, '>', this.strava.expires_at);
|
||||||
'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;
|
||||||
@ -307,16 +261,13 @@ class GgAppElement extends LitElement {
|
|||||||
|
|
||||||
async update_activities() {
|
async update_activities() {
|
||||||
if (this?.strava?.access_token) {
|
if (this?.strava?.access_token) {
|
||||||
let response = await fetch(
|
let response = await fetch('https://www.strava.com/api/v3/athlete/activities', {
|
||||||
'https://www.strava.com/api/v3/athlete/activities',
|
headers: {
|
||||||
{
|
'Authorization': `Bearer ${this.strava.access_token}`,
|
||||||
headers: {
|
},
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -331,12 +282,10 @@ class GgAppElement extends LitElement {
|
|||||||
[k_color_default, '🟧'],
|
[k_color_default, '🟧'],
|
||||||
];
|
];
|
||||||
for (let m of k_map) {
|
for (let m of k_map) {
|
||||||
if (
|
if (m[0][0] == color[0] &&
|
||||||
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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -380,11 +329,9 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,43 +368,31 @@ 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(
|
let position = this.leaflet.options.crs.latLngToPoint(latlng, zoom ?? this.leaflet.getZoom());
|
||||||
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 = this.leaflet.options.crs.pointToLatLng(position, zoom ?? this.leaflet.getZoom());
|
||||||
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.marker.setLatLng(this.snap_to_grid(this.marker.getLatLng(), k_marker_snap));
|
||||||
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.marker.setLatLng(this.snap_to_grid(this.marker.getLatLng(), k_marker_snap));
|
||||||
this.snap_to_grid(this.marker.getLatLng(), k_marker_snap)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -468,10 +403,7 @@ 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), {
|
this.marker = L.marker(this.snap_to_grid(event.latlng, k_marker_snap), {icon: L.divIcon({className: 'build-icon'}), draggable: true}).addTo(this.leaflet);
|
||||||
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)});
|
||||||
}
|
}
|
||||||
@ -485,18 +417,14 @@ class GgAppElement extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.leaflet) {
|
if (!this.leaflet) {
|
||||||
this.leaflet = L.map(map, {
|
this.leaflet = L.map(map, {attributionControl: false, maxZoom: 16, bounceAtZoomLimits: false});
|
||||||
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)});
|
||||||
}
|
}
|
||||||
let self = this;
|
let self = this;
|
||||||
let grid_layer = L.GridLayer.extend({
|
let grid_layer = L.GridLayer.extend({
|
||||||
createTile: function (coords) {
|
createTile: function(coords) {
|
||||||
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
|
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
|
||||||
var size = this.getTileSize();
|
var size = this.getTileSize();
|
||||||
tile.width = size.x;
|
tile.width = size.x;
|
||||||
@ -504,7 +432,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();
|
||||||
|
|
||||||
@ -514,53 +442,33 @@ 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(
|
self.draw_activity_to_tile(image_data, mini.width, mini.height, ul, lr, activity);
|
||||||
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(
|
let pixel = self.color_to_emoji(image_data.data.slice(start, start + 4));
|
||||||
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(
|
context.fillText(pixel, x * size.x / mini.width, y * size.y / mini.height + mini.height);
|
||||||
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(
|
let position = self.leaflet.options.crs.latLngToPoint(self.snap_to_grid(placed.position, undefined, coords.z), coords.z);
|
||||||
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(
|
context.fillText(placed.emoji, position.x, position.y + mini.height);
|
||||||
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();
|
||||||
@ -576,7 +484,10 @@ 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.focus.min, this.focus.max]);
|
this.leaflet.fitBounds([
|
||||||
|
this.focus.min,
|
||||||
|
this.focus.max,
|
||||||
|
]);
|
||||||
this.focus = undefined;
|
this.focus = undefined;
|
||||||
} else {
|
} else {
|
||||||
this.leaflet.fitBounds([
|
this.leaflet.fitBounds([
|
||||||
@ -677,12 +588,7 @@ 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 (
|
if (x0 >= 0 && y0 >= 0 && x0 < image_data.width && y0 < image_data.height) {
|
||||||
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];
|
||||||
@ -717,8 +623,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);
|
||||||
@ -731,8 +637,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);
|
||||||
@ -761,7 +667,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);
|
||||||
@ -787,7 +693,8 @@ 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 && bounds.min.lng < bounds.max.lng) {
|
if (bounds.min.lat < bounds.max.lat &&
|
||||||
|
bounds.min.lng < bounds.max.lng) {
|
||||||
this.tab = 'map';
|
this.tab = 'map';
|
||||||
this.focus = bounds;
|
this.focus = bounds;
|
||||||
}
|
}
|
||||||
@ -796,13 +703,9 @@ class GgAppElement extends LitElement {
|
|||||||
render_news() {
|
render_news() {
|
||||||
return html`
|
return html`
|
||||||
<ul>
|
<ul>
|
||||||
${this.loaded_activities.map(
|
${this.loaded_activities.map(x => html`
|
||||||
(x) => html`
|
<li style="cursor: pointer" @click=${() => this.focus_map(x)}>${x.author} ${x.name ?? x.time}</li>
|
||||||
<li style="cursor: pointer" @click=${() => this.focus_map(x)}>
|
`)}
|
||||||
${x.author} ${x.name ?? x.time}
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -811,7 +714,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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -829,10 +732,7 @@ 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">
|
header = html`<div style="flex: 1 0">Please <a target="_top" href="/login?return=${this.url}">login</a> to Tilde Friends, first.</div>`;
|
||||||
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`
|
||||||
@ -865,10 +765,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>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -890,15 +790,13 @@ class GgAppElement extends LitElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
.build-icon::before {
|
.build-icon::before {
|
||||||
content: '📍';
|
content: '📍';
|
||||||
border: 2px solid red;
|
border: 2px solid red;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="leaflet.css" />
|
<link rel="stylesheet" href="leaflet.css"/>
|
||||||
<div
|
<div style="width: 100%; height: 100%; display: flex; flex-direction: column">
|
||||||
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}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🪪",
|
"emoji": "🪪",
|
||||||
"previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256"
|
"previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256"
|
||||||
}
|
}
|
@ -18,8 +18,7 @@ tfrpc.register(async function reload() {
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
let ids = await ssb.getIdentities();
|
let ids = await ssb.getIdentities();
|
||||||
await app.setDocument(
|
await app.setDocument(`<body style="color: #fff">
|
||||||
`<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,19 +74,14 @@ async function main() {
|
|||||||
<h2>Import an SSB Identity from 12 BIP39 English Words</h2>
|
<h2>Import an SSB Identity from 12 BIP39 English Words</h2>
|
||||||
<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button>
|
<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
|
ids.map(id => `<li>
|
||||||
.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>`
|
</li>`).join('\n')+
|
||||||
)
|
` </ul>
|
||||||
.join('\n') +
|
</body>`);
|
||||||
` </ul>
|
|
||||||
</body>`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "🦟",
|
"emoji": "🦟",
|
||||||
"previous": "&TegdzvFE+im94shygaHkgDYSaSrwY2h0OKUXSRPBQDM=.sha256"
|
"previous": "&TegdzvFE+im94shygaHkgDYSaSrwY2h0OKUXSRPBQDM=.sha256"
|
||||||
}
|
}
|
@ -67,7 +67,7 @@ tfrpc.register(function getHash(id, message) {
|
|||||||
tfrpc.register(function setHash(hash) {
|
tfrpc.register(function setHash(hash) {
|
||||||
return app.setHash(hash);
|
return app.setHash(hash);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('message', async function (id) {
|
ssb.addEventListener('message', async function(id) {
|
||||||
await tfrpc.rpc.notifyNewMessage(id);
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
});
|
});
|
||||||
tfrpc.register(async function store_blob(blob) {
|
tfrpc.register(async function store_blob(blob) {
|
||||||
@ -88,16 +88,16 @@ tfrpc.register(function apps() {
|
|||||||
tfrpc.register(async function try_decrypt(id, content) {
|
tfrpc.register(async function try_decrypt(id, content) {
|
||||||
return await ssb.privateMessageDecrypt(id, content);
|
return await ssb.privateMessageDecrypt(id, content);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('broadcasts', async function () {
|
ssb.addEventListener('broadcasts', async function() {
|
||||||
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
});
|
});
|
||||||
|
|
||||||
core.register('onConnectionsChanged', async function () {
|
core.register('onConnectionsChanged', async function() {
|
||||||
await tfrpc.rpc.set('connections', await ssb.connections());
|
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||||
});
|
});
|
||||||
|
|
||||||
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,14 +1,12 @@
|
|||||||
<!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>
|
<script>window.litDisableBundleWarning = true;</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,12 +31,7 @@ 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(
|
${(this.ids).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
||||||
(id) =>
|
|
||||||
html`<option ?selected=${id == this.selected} value=${id}>
|
|
||||||
${id}
|
|
||||||
</option>`
|
|
||||||
)}
|
|
||||||
</select>
|
</select>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
@ -62,15 +57,13 @@ class TfComposeElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(new CustomEvent('tf-submit', {
|
||||||
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();
|
||||||
}
|
}
|
||||||
@ -103,8 +96,7 @@ 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'),
|
||||||
@ -115,9 +107,7 @@ 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) {
|
||||||
@ -133,7 +123,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;
|
||||||
@ -146,9 +136,7 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.issues = Object.values(issues).sort(
|
this.issues = Object.values(issues).sort((x, y) => (y.open - x.open) || (y.created - x.created));
|
||||||
(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) {
|
||||||
@ -162,20 +150,11 @@ class TfIssuesAppElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<tr>
|
<tr>
|
||||||
<td>${issue.open ? '☐ open' : '☑ closed'}</td>
|
<td>${issue.open ? '☐ open' : '☑ closed'}</td>
|
||||||
<td
|
<td style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis">${issue.author}</td>
|
||||||
style="max-width: 8em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
|
<td style="max-width: 40em; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; cursor: pointer" @click=${() => this.selected = issue}>
|
||||||
>
|
|
||||||
${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>
|
<td>${new Date(issue.updated ?? issue.created).toLocaleDateString()}</td>
|
||||||
${new Date(issue.updated ?? issue.created).toLocaleDateString()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -191,21 +170,13 @@ 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>
|
<div>${update.open !== undefined ? (update.open ? 'issue opened' : 'issue closed') : undefined}</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 (
|
if (confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)) {
|
||||||
confirm(`Are you sure you want to ${open ? 'open' : 'close'} this issue?`)
|
|
||||||
) {
|
|
||||||
let whoami = 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',
|
||||||
@ -236,9 +207,7 @@ 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
|
branch: this.selected.updates.length ? this.selected.updates[this.selected.updates.length - 1].id : this.selected.id,
|
||||||
? this.selected.updates[this.selected.updates.length - 1].id
|
|
||||||
: this.selected.id,
|
|
||||||
issues: [
|
issues: [
|
||||||
{
|
{
|
||||||
link: this.selected.id,
|
link: this.selected.id,
|
||||||
@ -257,18 +226,16 @@ 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 ?
|
||||||
this.selected.open
|
html`<input type="button" value="Close Issue" @click=${() => this.set_open(this.selected.id, false)}></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>`}
|
||||||
: 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 {
|
||||||
@ -283,7 +250,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,32 +1,20 @@
|
|||||||
import * as linkify from './commonmark-linkify.js';
|
import * as linkify from './commonmark-linkify.js';
|
||||||
|
|
||||||
function image(node, entering) {
|
function image(node, entering) {
|
||||||
if (
|
if (node.firstChild?.type === 'text' &&
|
||||||
node.firstChild?.type === 'text' &&
|
node.firstChild.literal.startsWith('video:')) {
|
||||||
node.firstChild.literal.startsWith('video:')
|
|
||||||
) {
|
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit(
|
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
||||||
'<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 (
|
} else if (node.firstChild?.type === 'text' &&
|
||||||
node.firstChild?.type === 'text' &&
|
node.firstChild.literal.startsWith('audio:')) {
|
||||||
node.firstChild.literal.startsWith('audio:')
|
|
||||||
) {
|
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit(
|
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
||||||
'<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 {
|
||||||
@ -36,11 +24,7 @@ function image(node, entering) {
|
|||||||
} else {
|
} else {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
if (this.disableTags === 0) {
|
if (this.disableTags === 0) {
|
||||||
this.lit(
|
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>');
|
||||||
'<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 {
|
||||||
@ -72,20 +56,14 @@ 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 (
|
if (node.destination.startsWith('@') &&
|
||||||
node.destination.startsWith('@') &&
|
node.destination.endsWith('.ed25519')) {
|
||||||
node.destination.endsWith('.ed25519')
|
|
||||||
) {
|
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (
|
} else if (node.destination.startsWith('%') &&
|
||||||
node.destination.startsWith('%') &&
|
node.destination.endsWith('.sha256')) {
|
||||||
node.destination.endsWith('.sha256')
|
|
||||||
) {
|
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (
|
} else if (node.destination.startsWith('&') &&
|
||||||
node.destination.startsWith('&') &&
|
node.destination.endsWith('.sha256')) {
|
||||||
node.destination.endsWith('.sha256')
|
|
||||||
) {
|
|
||||||
node.destination = '/' + node.destination + '/view';
|
node.destination = '/' + node.destination + '/view';
|
||||||
}
|
}
|
||||||
} else if (node.type == 'image') {
|
} else if (node.type == 'image') {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "📝",
|
"emoji": "📝",
|
||||||
"previous": "&2hdIDbBrAg63T2X1MzdGSF7yiqHvlnfF0PnInQLp0DA=.sha256"
|
"previous": "&2hdIDbBrAg63T2X1MzdGSF7yiqHvlnfF0PnInQLp0DA=.sha256"
|
||||||
}
|
}
|
@ -47,7 +47,7 @@ tfrpc.register(async function get_blob(id) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let g_new_message_resolve;
|
let g_new_message_resolve;
|
||||||
let g_new_message_promise = new Promise(function (resolve, reject) {
|
let g_new_message_promise = new Promise(function(resolve, reject) {
|
||||||
g_new_message_resolve = resolve;
|
g_new_message_resolve = resolve;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -55,9 +55,9 @@ function new_message() {
|
|||||||
return g_new_message_promise;
|
return g_new_message_promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssb.addEventListener('message', function (id) {
|
ssb.addEventListener('message', function(id) {
|
||||||
let resolve = g_new_message_resolve;
|
let resolve = g_new_message_resolve;
|
||||||
g_new_message_promise = new Promise(function (resolve, reject) {
|
g_new_message_promise = new Promise(function(resolve, reject) {
|
||||||
g_new_message_resolve = resolve;
|
g_new_message_resolve = resolve;
|
||||||
});
|
});
|
||||||
if (resolve) {
|
if (resolve) {
|
||||||
@ -104,7 +104,8 @@ async function process_message(whoami, collection, message, kind, parent) {
|
|||||||
if (!x) {
|
if (!x) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (content.type !== kind || (parent && content.parent !== parent)) {
|
if (content.type !== kind ||
|
||||||
|
(parent && content.parent !== parent)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,10 +113,7 @@ 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] = Object.assign(collection[content.key] || {}, content);
|
||||||
collection[content.key] || {},
|
|
||||||
content
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
collection[message.id] = Object.assign(content, {id: message.id});
|
collection[message.id] = Object.assign(content, {id: message.id});
|
||||||
@ -127,29 +125,20 @@ 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(
|
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
||||||
'SELECT MAX(rowid) AS rowid FROM messages',
|
rowid = row.rowid;
|
||||||
[],
|
});
|
||||||
function (row) {
|
|
||||||
rowid = row.rowid;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (rowid == max_rowid) {
|
if (rowid == max_rowid) {
|
||||||
await new_message();
|
await new_message();
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
||||||
'SELECT MAX(rowid) AS rowid FROM messages',
|
rowid = row.rowid;
|
||||||
[],
|
});
|
||||||
function (row) {
|
|
||||||
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
|
||||||
@ -161,10 +150,9 @@ tfrpc.register(async function collection(ids, kind, parent, max_rowid, data) {
|
|||||||
content LIKE '"%')
|
content LIKE '"%')
|
||||||
`,
|
`,
|
||||||
[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,14 +1,12 @@
|
|||||||
<!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>
|
<script>window.litDisableBundleWarning = true;</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>
|
||||||
|
@ -2,8 +2,8 @@ import {LitElement, html} from './lit-all.min.js';
|
|||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.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() {
|
||||||
return {
|
return {
|
||||||
@ -19,22 +19,15 @@ class TfIdentityPickerElement extends LitElement {
|
|||||||
|
|
||||||
changed(event) {
|
changed(event) {
|
||||||
this.selected = event.srcElement.value;
|
this.selected = event.srcElement.value;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(new Event('change', {
|
||||||
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(
|
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
||||||
(id) =>
|
|
||||||
html`<option ?selected=${id == this.selected} value=${id}>
|
|
||||||
${id}
|
|
||||||
</option>`
|
|
||||||
)}
|
|
||||||
</select>
|
</select>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -28,14 +28,9 @@ 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(
|
{
|
||||||
[this.whoami],
|
[max_rowid, journals] = await tfrpc.rpc.collection([this.whoami], 'journal-entry', undefined, max_rowid, journals);
|
||||||
'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);
|
||||||
}
|
}
|
||||||
@ -57,11 +52,7 @@ class TfJournalAppElement extends LitElement {
|
|||||||
};
|
};
|
||||||
message.recps = [this.whoami];
|
message.recps = [this.whoami];
|
||||||
print(message);
|
print(message);
|
||||||
message = await tfrpc.rpc.encrypt(
|
message = await tfrpc.rpc.encrypt(this.whoami, message.recps, JSON.stringify(message));
|
||||||
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);
|
||||||
}
|
}
|
||||||
@ -71,17 +62,12 @@ class TfJournalAppElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<tf-id-picker
|
<tf-id-picker .ids=${this.ids} selected=${this.whoami} @change=${this.on_whoami_changed}></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}
|
@publish=${this.on_journal_publish}></tf-journal-entry>
|
||||||
></tf-journal-entry>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,15 +30,13 @@ class TfJournalEntryElement extends LitElement {
|
|||||||
|
|
||||||
async on_publish() {
|
async on_publish() {
|
||||||
console.log('publish', this.text);
|
console.log('publish', this.text);
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(new CustomEvent('publish', {
|
||||||
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) {
|
||||||
@ -65,30 +63,19 @@ 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(
|
${this.back_dates(10).map(x => html`
|
||||||
(x) => html` <option value=${x}>${x}</option> `
|
<option value=${x}>${x}</option>
|
||||||
)}
|
`)}
|
||||||
</select>
|
</select>
|
||||||
<div style="display: inline-flex; flex-direction: row">
|
<div style="display: inline-flex; flex-direction: row">
|
||||||
<button
|
<button ?disabled=${this.text == this.journals?.[this.key]?.text} @click=${this.on_publish}>Publish</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}
|
@input=${this.on_edit} .value=${this.text ?? this.journals?.[this.key]?.text ?? ''}></textarea>
|
||||||
.value=${this.text ?? this.journals?.[this.key]?.text ?? ''}
|
<div style="flex: 1 1">${unsafeHTML(this.markdown(this.text ?? this.journals?.[this.key]?.text))}</div>
|
||||||
></textarea>
|
|
||||||
<div style="flex: 1 1">
|
|
||||||
${unsafeHTML(
|
|
||||||
this.markdown(this.text ?? this.journals?.[this.key]?.text)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "👟"
|
"emoji": "👟"
|
||||||
}
|
}
|
@ -1,14 +1,12 @@
|
|||||||
<!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>
|
<script>window.litDisableBundleWarning = true;</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,8 +19,7 @@ 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
|
||||||
@ -32,9 +31,8 @@ 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) {
|
||||||
@ -72,104 +70,24 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (startsWith(data, [0xff, 0xd8, 0xff, 0xdb]) ||
|
||||||
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]) ||
|
||||||
startsWith(data, [
|
startsWith(data, [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00])) {
|
||||||
0xff,
|
|
||||||
0xd8,
|
|
||||||
0xff,
|
|
||||||
0xe1,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
0x45,
|
|
||||||
0x78,
|
|
||||||
0x69,
|
|
||||||
0x66,
|
|
||||||
0x00,
|
|
||||||
0x00,
|
|
||||||
])
|
|
||||||
) {
|
|
||||||
return '.jpg';
|
return '.jpg';
|
||||||
} else if (
|
} else if (startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
||||||
startsWith(data, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
|
||||||
) {
|
|
||||||
return '.png';
|
return '.png';
|
||||||
} else if (
|
} else if (startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
||||||
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) ||
|
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])) {
|
||||||
startsWith(data, [0x47, 0x49, 0x46, 0x38, 0x39, 0x61])
|
|
||||||
) {
|
|
||||||
return '.gif';
|
return '.gif';
|
||||||
} else if (
|
} else if (startsWith(data, [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50])) {
|
||||||
startsWith(data, [
|
|
||||||
0x52,
|
|
||||||
0x49,
|
|
||||||
0x46,
|
|
||||||
0x46,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
0x57,
|
|
||||||
0x45,
|
|
||||||
0x42,
|
|
||||||
0x50,
|
|
||||||
])
|
|
||||||
) {
|
|
||||||
return '.webp';
|
return '.webp';
|
||||||
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
|
} else if (startsWith(data, [0x3c, 0x73, 0x76, 0x67])) {
|
||||||
return '.svg';
|
return '.svg';
|
||||||
} else if (
|
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
||||||
startsWith(data, [
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
0x66,
|
|
||||||
0x74,
|
|
||||||
0x79,
|
|
||||||
0x70,
|
|
||||||
0x6d,
|
|
||||||
0x70,
|
|
||||||
0x34,
|
|
||||||
0x32,
|
|
||||||
])
|
|
||||||
) {
|
|
||||||
return '.mp3';
|
return '.mp3';
|
||||||
} else if (
|
} else if (startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d]) ||
|
||||||
startsWith(data, [
|
startsWith(data, [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32])) {
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
0x66,
|
|
||||||
0x74,
|
|
||||||
0x79,
|
|
||||||
0x70,
|
|
||||||
0x69,
|
|
||||||
0x73,
|
|
||||||
0x6f,
|
|
||||||
0x6d,
|
|
||||||
]) ||
|
|
||||||
startsWith(data, [
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
0x66,
|
|
||||||
0x74,
|
|
||||||
0x79,
|
|
||||||
0x70,
|
|
||||||
0x6d,
|
|
||||||
0x70,
|
|
||||||
0x34,
|
|
||||||
0x32,
|
|
||||||
])
|
|
||||||
) {
|
|
||||||
return '.mp4';
|
return '.mp4';
|
||||||
} else {
|
} else {
|
||||||
return '.bin';
|
return '.bin';
|
||||||
@ -180,29 +98,17 @@ 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 = (
|
let messages_max = (await tfrpc.rpc.query('SELECT MAX(sequence) AS total FROM messages WHERE author = ?', [id]))[0].total;
|
||||||
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 +=
|
all_messages += messages.map(x => JSON.stringify(this.format_message(x))).join('\n') + '\n';
|
||||||
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 = {
|
this.progress = {name: 'messages', value: messages_done, max: messages_max};
|
||||||
name: 'messages',
|
|
||||||
value: messages_done,
|
|
||||||
max: messages_max,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -216,8 +122,7 @@ 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};
|
||||||
@ -228,10 +133,7 @@ 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(
|
zip.file(`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`, new Uint8Array(blob));
|
||||||
`blob/classic/${this.sanitize(row.id)}${this.guess_ext(blob)}`,
|
|
||||||
new Uint8Array(blob)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
blobs_done++;
|
blobs_done++;
|
||||||
}
|
}
|
||||||
@ -259,7 +161,7 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
file = await zip.loadAsync(file);
|
file = await zip.loadAsync(file);
|
||||||
let messages = [];
|
let messages = [];
|
||||||
let blobs = [];
|
let blobs = [];
|
||||||
file.forEach(function (path, entry) {
|
file.forEach(function(path, entry) {
|
||||||
if (!entry.dir) {
|
if (!entry.dir) {
|
||||||
if (path.startsWith('message/classic/')) {
|
if (path.startsWith('message/classic/')) {
|
||||||
messages.push(entry);
|
messages.push(entry);
|
||||||
@ -279,11 +181,7 @@ class TfSneakerAppElement extends LitElement {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let message = JSON.parse(line);
|
let message = JSON.parse(line);
|
||||||
this.progress = {
|
this.progress = {name: 'messages', value: progress++, max: total_messages};
|
||||||
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++;
|
||||||
}
|
}
|
||||||
@ -304,13 +202,7 @@ 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>
|
progress = html`<div><label for="progress">${this.progress.name}</label><progress value=${this.progress.value} max=${this.progress.max}></progress></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>`;
|
||||||
}
|
}
|
||||||
@ -326,17 +218,13 @@ 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(
|
${Object.entries(this.feeds).map(([id, name]) => html`
|
||||||
([id, name]) => html`
|
<li>
|
||||||
<li>
|
${this.progress ? undefined : html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
|
||||||
${this.progress
|
${name}
|
||||||
? undefined
|
<code style="color: #ccc">${id}</code>
|
||||||
: html`<input type="button" value="Export" @click=${() => this.export(id)}></input>`}
|
</li>
|
||||||
${name}
|
`)}
|
||||||
<code style="color: #ccc">${id}</code>
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ tfrpc.register(function getHash(id, message) {
|
|||||||
tfrpc.register(function setHash(hash) {
|
tfrpc.register(function setHash(hash) {
|
||||||
return app.setHash(hash);
|
return app.setHash(hash);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('message', async function (id) {
|
ssb.addEventListener('message', async function(id) {
|
||||||
await tfrpc.rpc.notifyNewMessage(id);
|
await tfrpc.rpc.notifyNewMessage(id);
|
||||||
});
|
});
|
||||||
tfrpc.register(async function store_blob(blob) {
|
tfrpc.register(async function store_blob(blob) {
|
||||||
@ -100,16 +100,16 @@ tfrpc.register(async function try_decrypt(id, content) {
|
|||||||
tfrpc.register(async function encrypt(id, recipients, content) {
|
tfrpc.register(async function encrypt(id, recipients, content) {
|
||||||
return await ssb.privateMessageEncrypt(id, recipients, content);
|
return await ssb.privateMessageEncrypt(id, recipients, content);
|
||||||
});
|
});
|
||||||
ssb.addEventListener('broadcasts', async function () {
|
ssb.addEventListener('broadcasts', async function() {
|
||||||
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
|
||||||
});
|
});
|
||||||
|
|
||||||
core.register('onConnectionsChanged', async function () {
|
core.register('onConnectionsChanged', async function() {
|
||||||
await tfrpc.rpc.set('connections', await ssb.connections());
|
await tfrpc.rpc.set('connections', await ssb.connections());
|
||||||
});
|
});
|
||||||
|
|
||||||
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')));
|
||||||
|
@ -4,14 +4,14 @@ function get_emojis() {
|
|||||||
if (g_emojis) {
|
if (g_emojis) {
|
||||||
return Promise.resolve(g_emojis);
|
return Promise.resolve(g_emojis);
|
||||||
}
|
}
|
||||||
return fetch('emojis.json').then(function (result) {
|
return fetch('emojis.json').then(function(result) {
|
||||||
g_emojis = result.json();
|
g_emojis = result.json();
|
||||||
return g_emojis;
|
return g_emojis;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function picker(callback, anchor) {
|
export function picker(callback, anchor) {
|
||||||
get_emojis().then(function (json) {
|
get_emojis().then(function(json) {
|
||||||
let div = document.createElement('div');
|
let div = document.createElement('div');
|
||||||
div.id = 'emoji_picker';
|
div.id = 'emoji_picker';
|
||||||
div.style.color = '#000';
|
div.style.color = '#000';
|
||||||
@ -36,7 +36,7 @@ export function picker(callback, anchor) {
|
|||||||
div.appendChild(input);
|
div.appendChild(input);
|
||||||
let list = document.createElement('div');
|
let list = document.createElement('div');
|
||||||
div.appendChild(list);
|
div.appendChild(list);
|
||||||
div.addEventListener('mousedown', function (event) {
|
div.addEventListener('mousedown', function(event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -72,11 +72,9 @@ 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 (
|
if (search &&
|
||||||
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,9 +1,9 @@
|
|||||||
<!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 {
|
||||||
color: #000;
|
color: #000;
|
||||||
@ -11,10 +11,8 @@
|
|||||||
</style>
|
</style>
|
||||||
</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>
|
<script>window.litDisableBundleWarning = true;</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,13 +34,9 @@ class TfElement extends LitElement {
|
|||||||
this.users = {};
|
this.users = {};
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.tags = [];
|
this.tags = [];
|
||||||
tfrpc.rpc.getBroadcasts().then((b) => {
|
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || []; });
|
||||||
self.broadcasts = b || [];
|
tfrpc.rpc.getConnections().then(c => { self.connections = c || []; });
|
||||||
});
|
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);
|
||||||
});
|
});
|
||||||
@ -90,14 +86,9 @@ class TfElement extends LitElement {
|
|||||||
last_row_id: 0,
|
last_row_id: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let max_row_id = (
|
let max_row_id = (await tfrpc.rpc.query(`
|
||||||
await tfrpc.rpc.query(
|
|
||||||
`
|
|
||||||
SELECT MAX(rowid) AS max_row_id FROM messages
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
||||||
`,
|
`, []))[0].max_row_id;
|
||||||
[]
|
|
||||||
)
|
|
||||||
)[0].max_row_id;
|
|
||||||
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,21 +120,17 @@ 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] = Object.assign(cache.about[about.author] || {}, content);
|
||||||
cache.about[about.author] || {},
|
|
||||||
content
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache.last_row_id = max_row_id;
|
cache.last_row_id = max_row_id;
|
||||||
@ -163,8 +150,10 @@ 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);
|
||||||
@ -184,7 +173,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) {
|
||||||
@ -196,30 +185,15 @@ 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
|
<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>
|
||||||
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>
|
||||||
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'
|
||||||
@ -233,9 +207,7 @@ 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');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,53 +241,23 @@ 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
|
<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>
|
||||||
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
|
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></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
|
<tf-tab-mentions .following=${this.following} whoami=${this.whoami} .users=${this.users}}></tf-tab-mentions>
|
||||||
.following=${this.following}
|
|
||||||
whoami=${this.whoami}
|
|
||||||
.users="${this.users}}"
|
|
||||||
></tf-tab-mentions>
|
|
||||||
`;
|
`;
|
||||||
} else if (this.tab === 'search') {
|
} else if (this.tab === 'search') {
|
||||||
return html`
|
return html`
|
||||||
<tf-tab-search
|
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></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
|
<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>
|
||||||
.following=${this.following}
|
|
||||||
whoami=${this.whoami}
|
|
||||||
.users=${this.users}
|
|
||||||
query=${this.hash?.startsWith('#sql=')
|
|
||||||
? decodeURIComponent(this.hash.substring(5))
|
|
||||||
: null}
|
|
||||||
></tf-tab-query>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -338,7 +280,7 @@ class TfElement extends LitElement {
|
|||||||
|
|
||||||
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.load().finally(function () {
|
this.load().finally(function() {
|
||||||
self.loading = false;
|
self.loading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -353,32 +295,21 @@ 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(
|
${Object.entries(k_tabs).map(([k, v]) => html`
|
||||||
([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>
|
||||||
<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 = !this.loaded
|
let contents =
|
||||||
? this.loading
|
!this.loaded ?
|
||||||
? html`<div>Loading...</div>`
|
this.loading ?
|
||||||
: html`<div>Select or create an identity.</div>`
|
html`<div>Loading...</div>` :
|
||||||
: this.render_tab();
|
html`<div>Select or create an identity.</div>` :
|
||||||
|
this.render_tab();
|
||||||
return html`
|
return html`
|
||||||
${this.render_id_picker()} ${tabs}
|
${this.render_id_picker()}
|
||||||
${this.tags.map(
|
${tabs}
|
||||||
(x) => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`
|
${this.tags.map(x => html`<tf-tag tag=${x.tag} count=${x.count}></tf-tag>`)}
|
||||||
)}
|
|
||||||
${contents}
|
${contents}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -58,9 +58,7 @@ class TfComposeElement extends LitElement {
|
|||||||
link: link,
|
link: link,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
draft.mentions[link].name = name.startsWith('@')
|
draft.mentions[link].name = name.startsWith('@') ? name.substring(1) : name;
|
||||||
? name.substring(1)
|
|
||||||
: name;
|
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
if (updated) {
|
if (updated) {
|
||||||
@ -74,39 +72,34 @@ 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(
|
let content_warning_preview = this.renderRoot.getElementById('content_warning_preview');
|
||||||
'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(
|
this.dispatchEvent(new CustomEvent('tf-draft', {
|
||||||
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 =
|
draft.content_warning = this.renderRoot.getElementById('content_warning')?.value;
|
||||||
this.renderRoot.getElementById('content_warning')?.value;
|
|
||||||
this.notify(draft);
|
this.notify(draft);
|
||||||
}
|
}
|
||||||
|
|
||||||
convert_to_format(buffer, type, mime_type) {
|
convert_to_format(buffer, type, mime_type) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
let img = new Image();
|
let img = new Image();
|
||||||
img.onload = function () {
|
img.onload = function() {
|
||||||
let canvas = document.createElement('canvas');
|
let canvas = document.createElement('canvas');
|
||||||
let width_scale = Math.min(img.width, 1024) / img.width;
|
let width_scale = Math.min(img.width, 1024) / img.width;
|
||||||
let height_scale = Math.min(img.height, 1024) / img.height;
|
let height_scale = Math.min(img.height, 1024) / img.height;
|
||||||
@ -116,17 +109,13 @@ 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])
|
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
|
||||||
.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))
|
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
|
||||||
.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;
|
||||||
});
|
});
|
||||||
@ -142,11 +131,7 @@ 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(
|
let test_buffer = await self.convert_to_format(buffer, file.type, 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;
|
||||||
@ -172,7 +157,7 @@ class TfComposeElement extends LitElement {
|
|||||||
edit.value += `\n![${name}](${id})`;
|
edit.value += `\n![${name}](${id})`;
|
||||||
self.change();
|
self.change();
|
||||||
self.input();
|
self.input();
|
||||||
} catch (e) {
|
} catch(e) {
|
||||||
alert(e?.message);
|
alert(e?.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -216,15 +201,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(
|
message = await tfrpc.rpc.encrypt(this.whoami, to, JSON.stringify(message));
|
||||||
this.whoami,
|
|
||||||
to,
|
|
||||||
JSON.stringify(message)
|
|
||||||
);
|
|
||||||
console.log('encrypted as', message);
|
console.log('encrypted as', message);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await tfrpc.rpc.appendMessage(this.whoami, message).then(function () {
|
await tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
||||||
edit.value = '';
|
edit.value = '';
|
||||||
self.change();
|
self.change();
|
||||||
self.notify(undefined);
|
self.notify(undefined);
|
||||||
@ -249,7 +230,7 @@ class TfComposeElement extends LitElement {
|
|||||||
let edit = this.renderRoot.getElementById('edit');
|
let edit = this.renderRoot.getElementById('edit');
|
||||||
let input = document.createElement('input');
|
let input = document.createElement('input');
|
||||||
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];
|
||||||
self.add_file(file);
|
self.add_file(file);
|
||||||
};
|
};
|
||||||
@ -260,15 +241,12 @@ 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) {
|
||||||
@ -287,18 +265,15 @@ class TfComposeElement extends LitElement {
|
|||||||
let tribute = new Tribute({
|
let tribute = new Tribute({
|
||||||
collection: [
|
collection: [
|
||||||
{
|
{
|
||||||
values: Object.entries(this.users).map((x) => ({
|
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
||||||
key: x[1].name,
|
selectTemplate: function(item) {
|
||||||
value: x[0],
|
|
||||||
})),
|
|
||||||
selectTemplate: function (item) {
|
|
||||||
return `[@${item.original.key}](${item.original.value})`;
|
return `[@${item.original.key}](${item.original.value})`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
trigger: '&',
|
trigger: '&',
|
||||||
values: this.autocomplete,
|
values: this.autocomplete,
|
||||||
selectTemplate: function (item) {
|
selectTemplate: function(item) {
|
||||||
return `![${item.original.key}](${item.original.value})`;
|
return `![${item.original.key}](${item.original.value})`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -318,11 +293,8 @@ 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) => ({
|
values: Object.entries(this.users).map(x => ({key: x[1].name, value: x[0]})),
|
||||||
key: x[1].name,
|
selectTemplate: function(item) {
|
||||||
value: x[0],
|
|
||||||
})),
|
|
||||||
selectTemplate: function (item) {
|
|
||||||
return item.original.value;
|
return item.original.value;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -339,30 +311,20 @@ class TfComposeElement extends LitElement {
|
|||||||
|
|
||||||
render_mention(mention) {
|
render_mention(mention) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return html` <div style="display: flex; flex-direction: row">
|
return html`
|
||||||
<div style="align-self: center; margin: 0.5em">
|
<div style="display: flex; flex-direction: row">
|
||||||
<button
|
<div style="align-self: center; margin: 0.5em">
|
||||||
class="w3-button w3-dark-grey"
|
<button class="w3-button w3-dark-grey" title="Remove ${mention.name} mention" @click=${() => self.remove_mention(mention.link)}>🚮</button>
|
||||||
title="Remove ${mention.name} mention"
|
|
||||||
@click=${() => self.remove_mention(mention.link)}
|
|
||||||
>
|
|
||||||
🚮
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; flex-direction: column">
|
|
||||||
<h3>${mention.name}</h3>
|
|
||||||
<div style="padding-left: 1em">
|
|
||||||
${Object.entries(mention)
|
|
||||||
.filter((x) => x[0] != 'name')
|
|
||||||
.map(
|
|
||||||
(x) =>
|
|
||||||
html`<div>
|
|
||||||
<span style="font-weight: bold">${x[0]}</span>: ${x[1]}
|
|
||||||
</div>`
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div style="display: flex; flex-direction: column">
|
||||||
</div>`;
|
<h3>${mention.name}</h3>
|
||||||
|
<div style="padding-left: 1em">
|
||||||
|
${Object.entries(mention)
|
||||||
|
.filter(x => x[0] != 'name')
|
||||||
|
.map(x => html`<div><span style="font-weight: bold">${x[0]}</span>: ${x[1]}</div>`)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render_attach_app() {
|
render_attach_app() {
|
||||||
@ -397,21 +359,12 @@ 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(
|
${Object.keys(self.apps).map(app => html`<option value=${app}>${app}</option>`)}
|
||||||
(app) => html`<option value=${app}>${app}</option>`
|
|
||||||
)}
|
|
||||||
</select>
|
</select>
|
||||||
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}>
|
<button class="w3-button w3-dark-grey" @click=${attach_selected_app}>Attach</button>
|
||||||
Attach
|
<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Cancel</button>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w3-button w3-dark-grey"
|
|
||||||
@click=${() => (this.apps = null)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,16 +374,9 @@ 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}>
|
return html`<button class="w3-button w3-dark-grey" @click=${attach_app}>Attach App</button>`;
|
||||||
Attach App
|
|
||||||
</button>`;
|
|
||||||
} else {
|
} else {
|
||||||
return html`<button
|
return html`<button class="w3-button w3-dark-grey" @click=${() => this.apps = null}>Discard App</button>`;
|
||||||
class="w3-button w3-dark-grey"
|
|
||||||
@click=${() => (this.apps = null)}
|
|
||||||
>
|
|
||||||
Discard App
|
|
||||||
</button>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -489,13 +435,11 @@ 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(
|
${draft.encrypt_to.map(x => html`
|
||||||
(x) => html`
|
|
||||||
<li>
|
<li>
|
||||||
<tf-user id=${x} .users=${this.users}></tf-user>
|
<tf-user id=${x} .users=${this.users}></tf-user>
|
||||||
<input type="button" class="w3-button w3-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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -511,65 +455,34 @@ 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 =
|
let encrypt = draft.encrypt_to !== undefined ?
|
||||||
draft.encrypt_to !== undefined
|
undefined :
|
||||||
? undefined
|
html`<button class="w3-button w3-dark-grey" @click=${() => this.set_encrypt([])}>🔐</button>`;
|
||||||
: html`<button
|
|
||||||
class="w3-button w3-dark-grey"
|
|
||||||
@click=${() => this.set_encrypt([])}
|
|
||||||
>
|
|
||||||
🔐
|
|
||||||
</button>`;
|
|
||||||
let result = html`
|
let result = html`
|
||||||
<div
|
<div class="w3-card-4 w3-blue-grey w3-padding" style="box-sizing: border-box">
|
||||||
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>
|
<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>
|
||||||
<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) =>
|
${Object.values(draft.mentions || {}).map(x => self.render_mention(x))}
|
||||||
self.render_mention(x)
|
${this.render_attach_app()}
|
||||||
)}
|
${this.render_content_warning()}
|
||||||
${this.render_attach_app()} ${this.render_content_warning()}
|
<button class="w3-button w3-dark-grey" id="submit" @click=${this.submit}>Submit</button>
|
||||||
<button
|
<button class="w3-button w3-dark-grey" @click=${this.attach}>Attach</button>
|
||||||
class="w3-button w3-dark-grey"
|
${this.render_attach_app_button()}
|
||||||
id="submit"
|
${encrypt}
|
||||||
@click=${this.submit}
|
<button class="w3-button w3-dark-grey" @click=${this.discard}>Discard</button>
|
||||||
>
|
|
||||||
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,8 +3,8 @@ 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() {
|
||||||
return {
|
return {
|
||||||
@ -24,28 +24,15 @@ class TfIdentityPickerElement extends LitElement {
|
|||||||
|
|
||||||
changed(event) {
|
changed(event) {
|
||||||
this.selected = event.srcElement.value;
|
this.selected = event.srcElement.value;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(new Event('change', {
|
||||||
new Event('change', {
|
srcElement: this,
|
||||||
srcElement: this,
|
}));
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<select
|
<select class="w3-select w3-dark-grey w3-padding w3-border" @change=${this.changed} style="max-width: 100%; overflow: hidden">
|
||||||
class="w3-select w3-dark-grey w3-padding w3-border"
|
${(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>`)}
|
||||||
@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,27 +31,14 @@ class TfMessageElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show_reply() {
|
show_reply() {
|
||||||
let event = new CustomEvent('tf-draft', {
|
let event = new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.message?.id, draft: {
|
||||||
bubbles: true,
|
encrypt_to: this.message?.decrypted?.recps,
|
||||||
composed: true,
|
}}});
|
||||||
detail: {
|
|
||||||
id: this.message?.id,
|
|
||||||
draft: {
|
|
||||||
encrypt_to: this.message?.decrypted?.recps,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.dispatchEvent(event);
|
this.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
discard_reply() {
|
discard_reply() {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(new CustomEvent('tf-draft', {bubbles: true, composed: true, detail: {id: this.id, draft: undefined}}));
|
||||||
new CustomEvent('tf-draft', {
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
detail: {id: this.id, draft: undefined},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render_votes() {
|
render_votes() {
|
||||||
@ -66,19 +53,12 @@ class TfMessageElement extends LitElement {
|
|||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return html`<div>
|
return html`<div>${(this.message.votes || []).map(
|
||||||
${(this.message.votes || []).map(
|
vote => html`
|
||||||
(vote) => html`
|
<span title="${this.users[vote.author]?.name ?? vote.author} ${new Date(vote.timestamp)}">
|
||||||
<span
|
${normalize_expression(vote.content.vote.expression)}
|
||||||
title="${this.users[vote.author]?.name ?? vote.author} ${new Date(
|
</span>
|
||||||
vote.timestamp
|
`)}</div>`;
|
||||||
)}"
|
|
||||||
>
|
|
||||||
${normalize_expression(vote.content.vote.expression)}
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render_raw() {
|
render_raw() {
|
||||||
@ -92,40 +72,30 @@ 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">
|
return html`<div style="white-space: pre-wrap">${JSON.stringify(raw, null, 2)}</div>`;
|
||||||
${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 (
|
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
|
||||||
confirm(
|
tfrpc.rpc.appendMessage(
|
||||||
'Are you sure you want to react with ' +
|
this.whoami,
|
||||||
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) {
|
||||||
@ -159,10 +129,7 @@ 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 (
|
} else if (event.srcElement.tagName == 'DIV' && event.srcElement.classList.contains('img_caption')) {
|
||||||
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';
|
||||||
@ -173,77 +140,50 @@ 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 (
|
} else if (mention?.link?.startsWith('&') &&
|
||||||
mention?.link?.startsWith('&') &&
|
mention?.type?.startsWith('image/')) {
|
||||||
mention?.type?.startsWith('image/')
|
|
||||||
) {
|
|
||||||
return html`
|
return html`
|
||||||
<img
|
<img src=${'/' + mention.link + '/view'} style="max-width: 128px; max-height: 128px" title=${mention.name} @click=${() => this.show_image('/' + mention.link + '/view')}>
|
||||||
src=${'/' + mention.link + '/view'}
|
|
||||||
style="max-width: 128px; max-height: 128px"
|
|
||||||
title=${mention.name}
|
|
||||||
@click=${() => this.show_image('/' + mention.link + '/view')}
|
|
||||||
/>
|
|
||||||
`;
|
`;
|
||||||
} else if (
|
} else if (mention.link?.startsWith('&') &&
|
||||||
mention.link?.startsWith('&') &&
|
mention.name?.startsWith('audio:')) {
|
||||||
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 (
|
} else if (mention.link?.startsWith('&') &&
|
||||||
mention.link?.startsWith('&') &&
|
mention.name?.startsWith('video:')) {
|
||||||
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 (
|
} else if (mention.link?.startsWith('&') &&
|
||||||
mention.link?.startsWith('&') &&
|
mention?.type === 'application/tildefriends') {
|
||||||
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)}
|
return html` <a href=${'#' + encodeURIComponent(mention.link)}>${mention.name}</a>`;
|
||||||
>${mention.name}</a
|
|
||||||
>`;
|
|
||||||
} else if (mention.link?.startsWith('#')) {
|
} else if (mention.link?.startsWith('#')) {
|
||||||
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}
|
return html` <a href=${'#q=' + encodeURIComponent(mention.link)}>${mention.link}</a>`;
|
||||||
>${mention.link}</a
|
} else if (Object.keys(mention).length == 2 && mention.link && mention.name) {
|
||||||
>`;
|
|
||||||
} 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">
|
return html` <pre style="white-space: pre-wrap">${JSON.stringify(mention, null, 2)}</pre>`;
|
||||||
${JSON.stringify(mention, null, 2)}</pre
|
|
||||||
>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render_mentions() {
|
render_mentions() {
|
||||||
let mentions = this.message?.content?.mentions || [];
|
let mentions = this.message?.content?.mentions || [];
|
||||||
mentions = mentions.filter(
|
mentions = mentions.filter(x => this.message?.content?.text?.indexOf(x.link) === -1);
|
||||||
(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
|
<fieldset style="background-color: rgba(0, 0, 0, 0.1); padding: 0.5em; border: 1px solid black">
|
||||||
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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -254,55 +194,28 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
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(
|
this.dispatchEvent(new CustomEvent('tf-expand', {bubbles: true, composed: true, detail: {id: (this.message.id || '') + (tag || ''), expanded: expanded}}));
|
||||||
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.set_expanded(!this.expanded[(this.message.id || '') + (tag || '')], tag);
|
||||||
!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
|
return html`<button class="w3-button w3-dark-grey" @click=${() => self.set_expanded(true)}>+ ${this.total_child_messages(this.message) + ' More'}</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
|
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>`)}`;
|
||||||
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>`
|
|
||||||
)}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -318,12 +231,13 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
}
|
}
|
||||||
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' && mention.link.startsWith('#')) {
|
if (typeof mention?.link === 'string' &&
|
||||||
|
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() {
|
||||||
@ -336,110 +250,54 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
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
|
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'md'}>Markdown</button>`;
|
||||||
class="w3-button w3-dark-grey"
|
|
||||||
@click=${() => (self.format = 'md')}
|
|
||||||
>
|
|
||||||
Markdown
|
|
||||||
</button>`;
|
|
||||||
} else {
|
} else {
|
||||||
raw_button = html`<button
|
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
|
||||||
class="w3-button w3-dark-grey"
|
|
||||||
@click=${() => (self.format = 'message')}
|
|
||||||
>
|
|
||||||
Message
|
|
||||||
</button>`;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'md':
|
case 'md':
|
||||||
raw_button = html`<button
|
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'message'}>Message</button>`;
|
||||||
class="w3-button w3-dark-grey"
|
|
||||||
@click=${() => (self.format = 'message')}
|
|
||||||
>
|
|
||||||
Message
|
|
||||||
</button>`;
|
|
||||||
break;
|
break;
|
||||||
case 'decrypted':
|
case 'decrypted':
|
||||||
raw_button = html`<button
|
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</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
|
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'decrypted'}>Decrypted</button>`;
|
||||||
class="w3-button w3-dark-grey"
|
|
||||||
@click=${() => (self.format = 'decrypted')}
|
|
||||||
>
|
|
||||||
Decrypted
|
|
||||||
</button>`;
|
|
||||||
} else {
|
} else {
|
||||||
raw_button = html`<button
|
raw_button = html`<button class="w3-button w3-dark-grey" @click=${() => self.format = 'raw'}>Raw</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
|
<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">
|
||||||
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"
|
<span style="padding-right: 8px"><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(self.message.timestamp).toLocaleString()}</span>
|
||||||
><a tfarget="_top" href=${'#' + self.message.id}>%</a> ${new Date(
|
${raw_button}
|
||||||
self.message.timestamp
|
${self.format == 'raw' ? self.render_raw() : inner}
|
||||||
).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` <div
|
return html`
|
||||||
class="w3-card-4"
|
<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">
|
||||||
style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px; overflow-wrap: anywhere"
|
${this.message.messages.map(x =>
|
||||||
>
|
html`<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>`
|
||||||
${this.message.messages.map(
|
)}
|
||||||
(x) =>
|
</div>`;
|
||||||
html`<tf-message
|
|
||||||
.message=${x}
|
|
||||||
whoami=${this.whoami}
|
|
||||||
.users=${this.users}
|
|
||||||
.drafts=${this.drafts}
|
|
||||||
.expanded=${this.expanded}
|
|
||||||
></tf-message>`
|
|
||||||
)}
|
|
||||||
</div>`;
|
|
||||||
} else if (this.message.placeholder) {
|
} else if (this.message.placeholder) {
|
||||||
return html` <div
|
return html`
|
||||||
class="w3-card-4"
|
<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">
|
||||||
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>
|
||||||
<a target="_top" href=${'#' + this.message.id}>${this.message.id}</a>
|
${(this.message.child_messages || []).map(x => html`
|
||||||
(placeholder)
|
<tf-message .message=${x} whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} .expanded=${this.expanded}></tf-message>
|
||||||
<div>${this.render_votes()}</div>
|
`)}
|
||||||
${(this.message.child_messages || []).map(
|
</div>`;
|
||||||
(x) => html`
|
} else if (typeof(content?.type === 'string')) {
|
||||||
<tf-message
|
|
||||||
.message=${x}
|
|
||||||
whoami=${this.whoami}
|
|
||||||
.users=${this.users}
|
|
||||||
.drafts=${this.drafts}
|
|
||||||
.expanded=${this.expanded}
|
|
||||||
></tf-message>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>`;
|
|
||||||
} else if (typeof (content?.type === 'string')) {
|
|
||||||
if (content.type == 'about') {
|
if (content.type == 'about') {
|
||||||
let name;
|
let name;
|
||||||
let image;
|
let image;
|
||||||
@ -449,7 +307,7 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
}
|
}
|
||||||
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) {
|
||||||
@ -459,55 +317,42 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
let update =
|
let update = content.about == this.message.author ?
|
||||||
content.about == this.message.author
|
html`<div style="font-weight: bold">Updated profile.</div>` :
|
||||||
? html`<div style="font-weight: bold">Updated profile.</div>`
|
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">
|
return small_frame(html`
|
||||||
Updated profile for
|
${update}
|
||||||
<tf-user id=${content.about} .users=${this.users}></tf-user>.
|
${name}
|
||||||
</div>`;
|
${image}
|
||||||
return small_frame(html` ${update} ${name} ${image} ${description} `);
|
${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
|
${
|
||||||
? 'blocking'
|
content.blocking === true ? 'blocking' :
|
||||||
: content.blocking === false
|
content.blocking === false ? 'no longer blocking' :
|
||||||
? 'no longer blocking'
|
content.following === true ? 'following' :
|
||||||
: content.following === true
|
content.following === false ? 'no longer following' :
|
||||||
? 'following'
|
'?'
|
||||||
: content.following === false
|
}
|
||||||
? 'no longer following'
|
<tf-user id=${this.message.content.contact} .users=${this.users}></tf-user>
|
||||||
: '?'}
|
|
||||||
<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 =
|
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
|
||||||
this.drafts[this.message?.id] !== undefined
|
<tf-compose
|
||||||
? html`
|
whoami=${this.whoami}
|
||||||
<tf-compose
|
.users=${this.users}
|
||||||
whoami=${this.whoami}
|
root=${this.message.content.root || this.message.id}
|
||||||
.users=${this.users}
|
branch=${this.message.id}
|
||||||
root=${this.message.content.root || this.message.id}
|
.drafts=${this.drafts}
|
||||||
branch=${this.message.id}
|
@tf-discard=${this.discard_reply}></tf-compose>
|
||||||
.drafts=${this.drafts}
|
` : html`
|
||||||
@tf-discard=${this.discard_reply}
|
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button>
|
||||||
></tf-compose>
|
`;
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<button
|
|
||||||
class="w3-button w3-dark-grey"
|
|
||||||
@click=${this.show_reply}
|
|
||||||
>
|
|
||||||
Reply
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
let self = this;
|
let self = this;
|
||||||
let body;
|
let body;
|
||||||
switch (this.format) {
|
switch (this.format) {
|
||||||
@ -515,47 +360,35 @@ ${JSON.stringify(mention, null, 2)}</pre
|
|||||||
body = this.render_raw();
|
body = this.render_raw();
|
||||||
break;
|
break;
|
||||||
case 'md':
|
case 'md':
|
||||||
body = html`<code
|
body = html`<code style="white-space: pre-wrap; overflow-wrap: anywhere">${content.text}</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
|
body = html`<pre style="white-space: pre-wrap; overflow-wrap: anywhere">${JSON.stringify(content, null, 2)}</pre>`;
|
||||||
style="white-space: pre-wrap; overflow-wrap: anywhere"
|
|
||||||
>
|
|
||||||
${JSON.stringify(content, null, 2)}</pre
|
|
||||||
>`;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let content_warning = html`
|
let content_warning = html`
|
||||||
<div
|
<div class="w3-panel w3-round-xlarge w3-blue" style="cursor: pointer" @click=${x => this.toggle_expanded(':cw')}><p>${content.contentWarning}</p></div>
|
||||||
class="w3-panel w3-round-xlarge w3-blue"
|
`;
|
||||||
style="cursor: pointer"
|
let content_html =
|
||||||
@click=${(x) => this.toggle_expanded(':cw')}
|
html`
|
||||||
>
|
${this.render_channels()}
|
||||||
<p>${content.contentWarning}</p>
|
<div @click=${this.body_click}>${body}</div>
|
||||||
</div>
|
${this.render_mentions()}
|
||||||
`;
|
`;
|
||||||
let content_html = html`
|
let payload =
|
||||||
${this.render_channels()}
|
content.contentWarning ?
|
||||||
<div @click=${this.body_click}>${body}</div>
|
self.expanded[(this.message.id || '') + ':cw'] ?
|
||||||
${this.render_mentions()}
|
html`
|
||||||
`;
|
${content_warning}
|
||||||
let payload = content.contentWarning
|
${content_html}
|
||||||
? self.expanded[(this.message.id || '') + ':cw']
|
` :
|
||||||
? html` ${content_warning} ${content_html} `
|
content_warning :
|
||||||
: content_warning
|
content_html;
|
||||||
: content_html;
|
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 {
|
||||||
@ -571,37 +404,26 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div
|
<div class="w3-card-4" style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
||||||
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"
|
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
||||||
><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} ${this.render_votes()}
|
${payload}
|
||||||
|
${this.render_votes()}
|
||||||
<p>
|
<p>
|
||||||
${reply}
|
${reply}
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
|
||||||
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
|
let is_encrypted = this.message?.decrypted ? html`<span style="align-self: center">🔓</span>` : undefined;
|
||||||
? html`<span style="align-self: center">🔓</span>`
|
let style_background = this.message?.decrypted ? 'rgba(255, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.1)';
|
||||||
: 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 {
|
||||||
@ -617,41 +439,31 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div
|
<div class="w3-card-4" style="border: 1px solid black; background-color: ${style_background}; margin-top: 8px; padding: 16px">
|
||||||
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"
|
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
||||||
><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} ${this.render_votes()}
|
${content.text}
|
||||||
|
${this.render_votes()}
|
||||||
<p>
|
<p>
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
|
||||||
React
|
|
||||||
</button>
|
|
||||||
</p>
|
</p>
|
||||||
${this.render_children()}
|
${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type === 'blog') {
|
} else if (content.type === 'blog') {
|
||||||
let self = this;
|
let self = this;
|
||||||
tfrpc.rpc.get_blob(content.blog).then(function (data) {
|
tfrpc.rpc.get_blob(content.blog).then(function(data) {
|
||||||
self.blog_data = data;
|
self.blog_data = data;
|
||||||
});
|
});
|
||||||
let payload = this.expanded[(this.message.id || '') + ':blog']
|
let payload =
|
||||||
? html`<div>
|
this.expanded[(this.message.id || '') + ':blog'] ?
|
||||||
${this.blog_data
|
html`<div>${this.blog_data ? unsafeHTML(tfutils.markdown(this.blog_data)) : 'Loading...'}</div>` :
|
||||||
? unsafeHTML(tfutils.markdown(this.blog_data))
|
undefined;
|
||||||
: 'Loading...'}
|
|
||||||
</div>`
|
|
||||||
: undefined;
|
|
||||||
let body;
|
let body;
|
||||||
switch (this.format) {
|
switch (this.format) {
|
||||||
case 'raw':
|
case 'raw':
|
||||||
@ -664,7 +476,7 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
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>
|
||||||
@ -675,26 +487,17 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
`;
|
`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let reply =
|
let reply = (this.drafts[this.message?.id] !== undefined) ? html`
|
||||||
this.drafts[this.message?.id] !== undefined
|
<tf-compose
|
||||||
? html`
|
whoami=${this.whoami}
|
||||||
<tf-compose
|
.users=${this.users}
|
||||||
whoami=${this.whoami}
|
root=${this.message.content.root || this.message.id}
|
||||||
.users=${this.users}
|
branch=${this.message.id}
|
||||||
root=${this.message.content.root || this.message.id}
|
.drafts=${this.drafts}
|
||||||
branch=${this.message.id}
|
@tf-discard=${this.discard_reply}></tf-compose>
|
||||||
.drafts=${this.drafts}
|
` : html`
|
||||||
@tf-discard=${this.discard_reply}
|
<button class="w3-button w3-dark-grey" @click=${this.show_reply}>Reply</button>
|
||||||
></tf-compose>
|
`;
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<button
|
|
||||||
class="w3-button w3-dark-grey"
|
|
||||||
@click=${this.show_reply}
|
|
||||||
>
|
|
||||||
Reply
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
code {
|
code {
|
||||||
@ -710,17 +513,11 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div
|
<div class="w3-card-4" style="border: 1px solid black; background-color: rgba(255, 255, 255, 0.1); margin-top: 8px; padding: 16px">
|
||||||
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"
|
<span style="padding-right: 8px"><a target="_top" href=${'#' + self.message.id}>%</a> ${new Date(this.message.timestamp).toLocaleString()}</span>
|
||||||
><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>
|
||||||
|
|
||||||
@ -728,52 +525,37 @@ ${JSON.stringify(content, null, 2)}</pre
|
|||||||
${this.render_mentions()}
|
${this.render_mentions()}
|
||||||
<div>
|
<div>
|
||||||
${reply}
|
${reply}
|
||||||
<button class="w3-button w3-dark-grey" @click=${this.react}>
|
<button class="w3-button w3-dark-grey" @click=${this.react}>React</button>
|
||||||
React
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
${this.render_votes()} ${this.render_children()}
|
${this.render_votes()}
|
||||||
|
${this.render_children()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (content.type === 'pub') {
|
} else if (content.type === 'pub') {
|
||||||
return small_frame(
|
return small_frame(html`
|
||||||
html` <style>
|
<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
|
</div>
|
||||||
.users=${this.users}
|
<pre>${content.address.host}:${content.address.port}</pre>
|
||||||
id=${content.address.key}
|
</span>`);
|
||||||
></tf-user>
|
|
||||||
</div>
|
|
||||||
<pre>${content.address.host}:${content.address.port}</pre>
|
|
||||||
</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'}
|
${content.subscribed ? 'subscribed to' : 'unsubscribed from'} <a href=${'#q=' + encodeURIComponent('#' + content.channel)}>#${content.channel}</a>
|
||||||
<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(
|
return small_frame(html`<span>🔓</span><pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`);
|
||||||
html`<span>🔓</span>
|
|
||||||
<pre>${JSON.stringify(this.message.decrypted, null, 2)}</pre>`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return small_frame(
|
return small_frame(html`<span>🔓</span><div>${this.message.decrypted.type}</div>`);
|
||||||
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,7 +89,8 @@ 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);
|
||||||
@ -99,12 +100,8 @@ 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 (
|
if (placeholder.parent_message && messages_by_id[placeholder.parent_message]) {
|
||||||
placeholder.parent_message &&
|
let children = messages_by_id[placeholder.parent_message].child_messages;
|
||||||
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);
|
||||||
}
|
}
|
||||||
@ -119,10 +116,7 @@ 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.latest_subtree_timestamp = Math.max(message.timestamp ?? 0, this.update_latest_subtree_timestamp(message.child_messages));
|
||||||
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);
|
||||||
}
|
}
|
||||||
@ -133,22 +127,20 @@ class TfNewsElement extends LitElement {
|
|||||||
function recursive_sort(messages, top) {
|
function recursive_sort(messages, top) {
|
||||||
if (messages) {
|
if (messages) {
|
||||||
if (top) {
|
if (top) {
|
||||||
messages.sort(
|
messages.sort((a, b) => b.latest_subtree_timestamp - a.latest_subtree_timestamp);
|
||||||
(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);
|
||||||
}
|
}
|
||||||
@ -175,22 +167,10 @@ 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(
|
let final_messages = this.group_following(this.finalize_messages(messages_by_id));
|
||||||
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(
|
${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>`)}
|
||||||
(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,29 +36,23 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,16 +60,13 @@ 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;
|
||||||
@ -84,18 +75,11 @@ class TfProfileElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
modify(change) {
|
modify(change) {
|
||||||
tfrpc.rpc
|
tfrpc.rpc.appendMessage(this.whoami,
|
||||||
.appendMessage(
|
Object.assign({
|
||||||
this.whoami,
|
type: 'contact',
|
||||||
Object.assign(
|
contact: this.id,
|
||||||
{
|
}, change)).catch(function(error) {
|
||||||
type: 'contact',
|
|
||||||
contact: this.id,
|
|
||||||
},
|
|
||||||
change
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.catch(function (error) {
|
|
||||||
alert(error?.message);
|
alert(error?.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -138,14 +122,11 @@ class TfProfileElement extends LitElement {
|
|||||||
message[key] = this.editing[key];
|
message[key] = this.editing[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tfrpc.rpc
|
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
|
||||||
.appendMessage(this.whoami, message)
|
self.editing = null;
|
||||||
.then(function () {
|
}).catch(function(error) {
|
||||||
self.editing = null;
|
alert(error?.message);
|
||||||
})
|
});
|
||||||
.catch(function (error) {
|
|
||||||
alert(error?.message);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
discard_edits() {
|
discard_edits() {
|
||||||
@ -156,21 +137,17 @@ class TfProfileElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
let input = document.createElement('input');
|
let input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.onchange = function (event) {
|
input.onchange = function(event) {
|
||||||
let file = event.target.files[0];
|
let file = event.target.files[0];
|
||||||
file
|
file.arrayBuffer().then(function(buffer) {
|
||||||
.arrayBuffer()
|
let bin = Array.from(new Uint8Array(buffer));
|
||||||
.then(function (buffer) {
|
return tfrpc.rpc.store_blob(bin);
|
||||||
let bin = Array.from(new Uint8Array(buffer));
|
}).then(function(id) {
|
||||||
return tfrpc.rpc.store_blob(bin);
|
self.editing = Object.assign({}, self.editing, {image: id});
|
||||||
})
|
console.log(self.editing);
|
||||||
.then(function (id) {
|
}).catch(function(e) {
|
||||||
self.editing = Object.assign({}, self.editing, {image: id});
|
alert(e.message);
|
||||||
console.log(self.editing);
|
});
|
||||||
})
|
|
||||||
.catch(function (e) {
|
|
||||||
alert(e.message);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
}
|
}
|
||||||
@ -189,22 +166,15 @@ class TfProfileElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (
|
if (this.id == this.whoami && this.editing && this.server_follows_me === undefined) {
|
||||||
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
|
tfrpc.rpc.query(
|
||||||
.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;
|
||||||
@ -214,75 +184,52 @@ 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
|
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(false)}>Server, Stop Following Me</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
|
server_follow = html`<button class="w3-button w3-dark-grey" @click=${() => this.server_follow_me(true)}>Server, Follow Me</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}>
|
<button class="w3-button w3-dark-grey" @click=${this.save_edits}>Save Profile</button>
|
||||||
Save Profile
|
<button class="w3-button w3-dark-grey" @click=${this.discard_edits}>Discard</button>
|
||||||
</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 = html`<button class="w3-button w3-dark-grey" @click=${this.edit}>Edit Profile</button>`;
|
||||||
Edit Profile
|
|
||||||
</button>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami && this.following !== undefined) {
|
if (this.id !== this.whoami &&
|
||||||
follow = this.following
|
this.following !== undefined) {
|
||||||
? html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>
|
follow =
|
||||||
Unfollow
|
this.following ?
|
||||||
</button>`
|
html`<button class="w3-button w3-dark-grey" @click=${this.unfollow}>Unfollow</button>` :
|
||||||
: html`<button class="w3-button w3-dark-grey" @click=${this.follow}>
|
html`<button class="w3-button w3-dark-grey" @click=${this.follow}>Follow</button>`;
|
||||||
Follow
|
|
||||||
</button>`;
|
|
||||||
}
|
}
|
||||||
if (this.id !== this.whoami && this.blocking !== undefined) {
|
if (this.id !== this.whoami &&
|
||||||
block = this.blocking
|
this.blocking !== undefined) {
|
||||||
? html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>
|
block =
|
||||||
Unblock
|
this.blocking ?
|
||||||
</button>`
|
html`<button class="w3-button w3-dark-grey" @click=${this.unblock}>Unblock</button>` :
|
||||||
: html`<button class="w3-button w3-dark-grey" @click=${this.block}>
|
html`<button class="w3-button w3-dark-grey" @click=${this.block}>Block</button>`;
|
||||||
Block
|
|
||||||
</button>`;
|
|
||||||
}
|
}
|
||||||
let edit_profile = this.editing
|
let edit_profile = this.editing ? html`
|
||||||
? html`
|
|
||||||
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
|
<div style="flex: 1 0 50%; display: flex; flex-direction: column; gap: 8px">
|
||||||
<div 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>`
|
</div>` : null;
|
||||||
: null;
|
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
|
||||||
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">
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -23,10 +23,10 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
this.connections = [];
|
this.connections = [];
|
||||||
this.stored_connections = [];
|
this.stored_connections = [];
|
||||||
this.users = {};
|
this.users = {};
|
||||||
tfrpc.rpc.getAllIdentities().then(function (identities) {
|
tfrpc.rpc.getAllIdentities().then(function(identities) {
|
||||||
self.identities = identities || [];
|
self.identities = identities || [];
|
||||||
});
|
});
|
||||||
tfrpc.rpc.getStoredConnections().then(function (connections) {
|
tfrpc.rpc.getStoredConnections().then(function(connections) {
|
||||||
self.stored_connections = connections || [];
|
self.stored_connections = connections || [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -43,12 +43,10 @@ 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
|
return html`${peers.filter(x => connections.indexOf(x.pubkey) == -1).map(x => html`${self.render_room_peer(x)}`)}`;
|
||||||
.filter((x) => connections.indexOf(x.pubkey) == -1)
|
|
||||||
.map((x) => html`${self.render_room_peer(x)}`)}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,12 +58,7 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
let self = this;
|
let self = this;
|
||||||
return html`
|
return html`
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button class="w3-button w3-dark-grey" @click=${() => self._tunnel(connection.tunnel.id, connection.pubkey)}>Connect</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>
|
||||||
`;
|
`;
|
||||||
@ -74,12 +67,7 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
render_broadcast(connection) {
|
render_broadcast(connection) {
|
||||||
return html`
|
return html`
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(connection)}>Connect</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>
|
||||||
@ -93,20 +81,11 @@ class TfTabConnectionsElement extends LitElement {
|
|||||||
|
|
||||||
render_connection(connection) {
|
render_connection(connection) {
|
||||||
return html`
|
return html`
|
||||||
<button
|
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.closeConnection(connection.id)}>Close</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
|
${connection.tunnel !== undefined ? '🚇' : html`(${connection.host}:${connection.port})`}
|
||||||
? '🚇'
|
|
||||||
: html`(${connection.host}:${connection.port})`}
|
|
||||||
<ul>
|
<ul>
|
||||||
${this.connections
|
${this.connections.filter(x => x.tunnel === this.connections.indexOf(connection)).map(x => html`<li>${this.render_connection(x)}</li>`)}
|
||||||
.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>
|
||||||
`;
|
`;
|
||||||
@ -118,54 +97,30 @@ 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
|
<button class="w3-button w3-dark-grey" @click=${() => tfrpc.rpc.connect(self.renderRoot.getElementById('code').value)}>Connect</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
|
${this.broadcasts.filter(x => x.address).map(x => self.render_broadcast(x))}
|
||||||
.filter((x) => x.address)
|
|
||||||
.map((x) => self.render_broadcast(x))}
|
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Connections</h2>
|
<h2>Connections</h2>
|
||||||
<ul>
|
<ul>
|
||||||
${this.connections
|
${this.connections.filter(x => x.tunnel === undefined).map(x => html`
|
||||||
.filter((x) => x.tunnel === undefined)
|
<li>${this.render_connection(x)}</li>
|
||||||
.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(
|
${this.stored_connections.map(x => html`
|
||||||
(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)}
|
</li>
|
||||||
>
|
`)}
|
||||||
Forget
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="w3-button w3-dark-grey"
|
|
||||||
@click=${() => tfrpc.rpc.connect(x)}
|
|
||||||
>
|
|
||||||
Connect
|
|
||||||
</button>
|
|
||||||
${x.address}:${x.port}
|
|
||||||
<tf-user id=${x.pubkey} .users=${self.users}></tf-user>
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Local Accounts</h2>
|
<h2>Local Accounts</h2>
|
||||||
<ul>
|
<ul>
|
||||||
${this.identities.map(
|
${this.identities.map(x => html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`)}
|
||||||
(x) =>
|
|
||||||
html`<li><tf-user id=${x} .users=${this.users}></tf-user></li>`
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -27,8 +27,7 @@ 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
|
||||||
@ -36,12 +35,7 @@ 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;
|
||||||
}
|
}
|
||||||
@ -64,14 +58,7 @@ class TfTabMentionsElement extends LitElement {
|
|||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<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>
|
||||||
id="news"
|
|
||||||
whoami=${this.whoami}
|
|
||||||
.messages=${this.messages}
|
|
||||||
.users=${this.users}
|
|
||||||
.expanded=${this.expanded}
|
|
||||||
@tf-expand=${this.on_expand}
|
|
||||||
></tf-news>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,8 +45,9 @@ 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(
|
||||||
@ -60,15 +61,15 @@ 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(
|
promises.push(tfrpc.rpc.query(
|
||||||
tfrpc.rpc.query(
|
`
|
||||||
`
|
|
||||||
WITH news AS (SELECT messages.*
|
WITH news AS (SELECT messages.*
|
||||||
FROM messages
|
FROM messages
|
||||||
JOIN json_each(?) AS following ON messages.author = following.value
|
JOIN json_each(?) AS following ON messages.author = following.value
|
||||||
@ -86,17 +87,15 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
UNION
|
UNION
|
||||||
SELECT news.* FROM news
|
SELECT news.* FROM news
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
JSON.stringify(this.following.slice(i, i + k_following_limit)),
|
JSON.stringify(this.following.slice(i, i + k_following_limit)),
|
||||||
this.start_time,
|
this.start_time,
|
||||||
/*
|
/*
|
||||||
** Don't show messages more than a day into the future to prevent
|
** Don't show messages more than a day into the future to prevent
|
||||||
** 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)));
|
||||||
}
|
}
|
||||||
@ -125,8 +124,11 @@ 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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,12 +139,14 @@ 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);
|
||||||
@ -161,48 +165,31 @@ class TfTabNewsFeedElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (
|
if (!this.messages ||
|
||||||
!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()
|
this.fetch_messages().then(this.decrypt.bind(this)).then(function(messages) {
|
||||||
.then(this.decrypt.bind(this))
|
self.messages = messages;
|
||||||
.then(function (messages) {
|
console.log(`loading mesages done for ${self.whoami}`);
|
||||||
self.messages = messages;
|
}).catch(function(error) {
|
||||||
console.log(`loading mesages done for ${self.whoami}`);
|
alert(JSON.stringify(error, null, 2));
|
||||||
})
|
});
|
||||||
.catch(function (error) {
|
|
||||||
alert(JSON.stringify(error, null, 2));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
let more;
|
let more;
|
||||||
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}>
|
<button class="w3-button w3-dark-grey" @click=${this.load_more}>Load More</button>
|
||||||
Load More
|
|
||||||
</button>
|
|
||||||
</p>
|
</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<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>
|
||||||
id="news"
|
|
||||||
whoami=${this.whoami}
|
|
||||||
.users=${this.users}
|
|
||||||
.messages=${this.messages}
|
|
||||||
.following=${this.following}
|
|
||||||
.drafts=${this.drafts}
|
|
||||||
.expanded=${this.expanded}
|
|
||||||
></tf-news>
|
|
||||||
${more}
|
${more}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ class TfTabNewsElement extends LitElement {
|
|||||||
this.cache = {};
|
this.cache = {};
|
||||||
this.drafts = {};
|
this.drafts = {};
|
||||||
this.expanded = {};
|
this.expanded = {};
|
||||||
tfrpc.rpc.localStorageGet('drafts').then(function (d) {
|
tfrpc.rpc.localStorageGet('drafts').then(function(d) {
|
||||||
self.drafts = JSON.parse(d || '{}');
|
self.drafts = JSON.parse(d || '{}');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -48,9 +48,7 @@ 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(
|
news.add_messages(Object.values(Object.fromEntries(this.unread.map(x => [x.id, x]))));
|
||||||
Object.values(Object.fromEntries(this.unread.map((x) => [x.id, x])))
|
|
||||||
);
|
|
||||||
this.dispatchEvent(new CustomEvent('refresh'));
|
this.dispatchEvent(new CustomEvent('refresh'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -64,16 +62,11 @@ 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 (
|
return '↻ Show New: ' + Object.keys(counts).sort().map(x => (counts[x].toString() + ' ' + x + 's')).join(', ');
|
||||||
'↻ Show New: ' +
|
|
||||||
Object.keys(counts)
|
|
||||||
.sort()
|
|
||||||
.map((x) => counts[x].toString() + ' ' + x + 's')
|
|
||||||
.join(', ')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
draft(event) {
|
draft(event) {
|
||||||
@ -103,52 +96,23 @@ class TfTabNewsElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
on_keypress(event) {
|
on_keypress(event) {
|
||||||
if (event.target === document.body && event.key == '.') {
|
if (event.target === document.body &&
|
||||||
|
event.key == '.') {
|
||||||
this.show_more();
|
this.show_more();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let profile = this.hash.startsWith('#@')
|
let profile = this.hash.startsWith('#@') ?
|
||||||
? html`<tf-profile
|
html`<tf-profile id=${this.hash.substring(1)} whoami=${this.whoami} .users=${this.users}></tf-profile>` : undefined;
|
||||||
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
|
<button class="w3-bar-item w3-button w3-dark-grey" @click=${this.show_more}>${this.new_messages_text()}</button>
|
||||||
class="w3-bar-item w3-button w3-dark-grey"
|
|
||||||
@click=${this.show_more}
|
|
||||||
>
|
|
||||||
${this.new_messages_text()}
|
|
||||||
</button>
|
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!</div>
|
||||||
Welcome, <tf-user id=${this.whoami} .users=${this.users}></tf-user>!
|
<div><tf-compose id="tf-compose" whoami=${this.whoami} .users=${this.users} .drafts=${this.drafts} @tf-draft=${this.draft}></tf-compose></div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<tf-compose
|
|
||||||
id="tf-compose"
|
|
||||||
whoami=${this.whoami}
|
|
||||||
.users=${this.users}
|
|
||||||
.drafts=${this.drafts}
|
|
||||||
@tf-draft=${this.draft}
|
|
||||||
></tf-compose>
|
|
||||||
</div>
|
|
||||||
${profile}
|
${profile}
|
||||||
<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>
|
||||||
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,15 +79,8 @@ 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>
|
<tr>${keys.map(key => html`<th>${key}</th>`)}</tr>
|
||||||
${keys.map((key) => html`<th>${key}</th>`)}
|
${this.results.map(row => html`<tr>${keys.map(key => html`<td>${row[key]}</td>`)}</tr>`)}
|
||||||
</tr>
|
|
||||||
${this.results.map(
|
|
||||||
(row) =>
|
|
||||||
html`<tr>
|
|
||||||
${keys.map((key) => html`<td>${row[key]}</td>`)}
|
|
||||||
</tr>`
|
|
||||||
)}
|
|
||||||
</table>`;
|
</table>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,28 +100,13 @@ 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
|
<textarea id="search" rows=8 class="w3-input w3-dark-grey" style="flex: 1; resize: vertical" @keydown=${this.search_keydown}>${this.query}</textarea>
|
||||||
id="search"
|
<button class="w3-button w3-dark-grey" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Execute</button>
|
||||||
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_results()}
|
${this.render_error()}
|
||||||
|
${this.render_results()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,25 +27,23 @@ class TfTabSearchElement extends LitElement {
|
|||||||
async search(query) {
|
async search(query) {
|
||||||
console.log('Searching...', this.whoami, query);
|
console.log('Searching...', this.whoami, query);
|
||||||
let search = this.renderRoot.getElementById('search');
|
let search = this.renderRoot.getElementById('search');
|
||||||
if (search) {
|
if (search ) {
|
||||||
search.value = query;
|
search.value = query;
|
||||||
search.focus();
|
search.focus();
|
||||||
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 ) {
|
||||||
search.value = query;
|
search.value = query;
|
||||||
search.focus();
|
search.focus();
|
||||||
search.select();
|
search.select();
|
||||||
|
@ -17,11 +17,7 @@ class TfTagElement extends LitElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let number = this.count ? html` (${this.count})` : undefined;
|
let number = this.count ? html` (${this.count})` : undefined;
|
||||||
return html`<a
|
return html`<a href="#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>`;
|
||||||
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,26 +20,23 @@ class TfUserElement extends LitElement {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let name = this.users?.[this.id]?.name;
|
let name = this.users?.[this.id]?.name;
|
||||||
name =
|
name = name !== undefined ?
|
||||||
name !== undefined
|
html`<a target="_top" href=${'#' + this.id}>${name}</a>` :
|
||||||
? html`<a target="_top" href=${'#' + this.id}>${name}</a>`
|
html`<a target="_top" href=${'#' + this.id}>${this.id}</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` <div style="display: inline-block; font-weight: bold">
|
return html`
|
||||||
<img
|
<div style="display: inline-block; font-weight: bold">
|
||||||
style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%"
|
<img style="width: 2em; height: 2em; vertical-align: middle; border-radius: 50%" ?hidden=${image === undefined} src="${image ? '/' + image + '/view' : undefined}">
|
||||||
?hidden=${image === undefined}
|
${name}
|
||||||
src="${image ? '/' + image + '/view' : undefined}"
|
</div>`;
|
||||||
/>
|
|
||||||
${name}
|
|
||||||
</div>`;
|
|
||||||
} else {
|
} else {
|
||||||
return html` <div style="display: inline-block; font-weight: bold">
|
return html`
|
||||||
${name}
|
<div style="display: inline-block; font-weight: bold">
|
||||||
</div>`;
|
${name}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,32 +2,20 @@ 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 (
|
if (node.firstChild?.type === 'text' &&
|
||||||
node.firstChild?.type === 'text' &&
|
node.firstChild.literal.startsWith('video:')) {
|
||||||
node.firstChild.literal.startsWith('video:')
|
|
||||||
) {
|
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit(
|
this.lit('<video style="max-width: 100%; max-height: 480px" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
||||||
'<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 (
|
} else if (node.firstChild?.type === 'text' &&
|
||||||
node.firstChild?.type === 'text' &&
|
node.firstChild.literal.startsWith('audio:')) {
|
||||||
node.firstChild.literal.startsWith('audio:')
|
|
||||||
) {
|
|
||||||
if (entering) {
|
if (entering) {
|
||||||
this.lit(
|
this.lit('<audio style="height: 32px; max-width: 100%" title="' + this.esc(node.firstChild?.literal) + '" controls>');
|
||||||
'<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 {
|
||||||
@ -37,11 +25,7 @@ function image(node, entering) {
|
|||||||
} else {
|
} else {
|
||||||
if (entering) {
|
if (entering) {
|
||||||
if (this.disableTags === 0) {
|
if (this.disableTags === 0) {
|
||||||
this.lit(
|
this.lit('<div class="img_caption">' + this.esc(node.firstChild?.literal || node.destination) + '</div>');
|
||||||
'<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 {
|
||||||
@ -74,20 +58,14 @@ 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 (
|
if (node.destination.startsWith('@') &&
|
||||||
node.destination.startsWith('@') &&
|
node.destination.endsWith('.ed25519')) {
|
||||||
node.destination.endsWith('.ed25519')
|
|
||||||
) {
|
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (
|
} else if (node.destination.startsWith('%') &&
|
||||||
node.destination.startsWith('%') &&
|
node.destination.endsWith('.sha256')) {
|
||||||
node.destination.endsWith('.sha256')
|
|
||||||
) {
|
|
||||||
node.destination = '#' + node.destination;
|
node.destination = '#' + node.destination;
|
||||||
} else if (
|
} else if (node.destination.startsWith('&') &&
|
||||||
node.destination.startsWith('&') &&
|
node.destination.endsWith('.sha256')) {
|
||||||
node.destination.endsWith('.sha256')
|
|
||||||
) {
|
|
||||||
node.destination = '/' + node.destination + '/view';
|
node.destination = '/' + node.destination + '/view';
|
||||||
}
|
}
|
||||||
} else if (node.type == 'image') {
|
} else if (node.type == 'image') {
|
||||||
|
@ -1,32 +1,32 @@
|
|||||||
.tribute-container {
|
.tribute-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: auto;
|
height: auto;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: block;
|
display: block;
|
||||||
z-index: 999999;
|
z-index: 999999;
|
||||||
}
|
}
|
||||||
.tribute-container ul {
|
.tribute-container ul {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
background: #efefef;
|
background: #efefef;
|
||||||
}
|
}
|
||||||
.tribute-container li {
|
.tribute-container li {
|
||||||
padding: 5px 5px;
|
padding: 5px 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.tribute-container li.highlight {
|
.tribute-container li.highlight {
|
||||||
background: #ddd;
|
background: #ddd;
|
||||||
}
|
}
|
||||||
.tribute-container li span {
|
.tribute-container li span {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.tribute-container li.no-match {
|
.tribute-container li.no-match {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
.tribute-container .menu-highlighted {
|
.tribute-container .menu-highlighted {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
@ -27,8 +27,7 @@ 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 =
|
exchanged = original === names || await g_db.exchange('files', original, names);
|
||||||
original === names || (await g_db.exchange('files', original, names));
|
|
||||||
}
|
}
|
||||||
return exchanged;
|
return exchanged;
|
||||||
}
|
}
|
||||||
@ -43,8 +42,7 @@ 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 =
|
exchanged = original === names || await g_db.exchange('files', original, names);
|
||||||
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,14 +12,11 @@ class TodosElement extends LitElement {
|
|||||||
super();
|
super();
|
||||||
this.lists = [];
|
this.lists = [];
|
||||||
let self = this;
|
let self = this;
|
||||||
tfrpc.rpc
|
tfrpc.rpc.todo_get_all().then(function(lists) {
|
||||||
.todo_get_all()
|
self.lists = lists;
|
||||||
.then(function (lists) {
|
}).catch(function(error) {
|
||||||
self.lists = lists;
|
console.log(error);
|
||||||
})
|
});
|
||||||
.catch(function (error) {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async new_list() {
|
async new_list() {
|
||||||
@ -35,15 +32,9 @@ class TodosElement extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
<div style="display: flex">
|
<div style="display: flex">
|
||||||
${this.lists.map(
|
${this.lists.map(x => html`
|
||||||
(x) => html`
|
<tf-todo-list name=${x.name} .items=${x.items} @change=${this.refresh}></tf-todo-list>
|
||||||
<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>`;
|
||||||
@ -68,22 +59,16 @@ 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
|
tfrpc.rpc.todo_set(self.name, self.items).then(function() {
|
||||||
.todo_set(self.name, self.items)
|
console.log('saved', self.name, self.items);
|
||||||
.then(function () {
|
}).catch(function(error) {
|
||||||
console.log('saved', self.name, self.items);
|
console.log(error);
|
||||||
})
|
});
|
||||||
.catch(function (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 = [].concat(this.items.slice(0, index), this.items.slice(index + 1));
|
||||||
this.items.slice(0, index),
|
|
||||||
this.items.slice(index + 1)
|
|
||||||
);
|
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,20 +106,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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,17 +139,14 @@ class TodoListElement extends LitElement {
|
|||||||
|
|
||||||
rename(new_name) {
|
rename(new_name) {
|
||||||
let self = this;
|
let self = this;
|
||||||
return tfrpc.rpc
|
return tfrpc.rpc.todo_rename(this.name, new_name).then(function() {
|
||||||
.todo_rename(this.name, new_name)
|
self.dispatchEvent(new Event('change'));
|
||||||
.then(function () {
|
self.editing_name = false;
|
||||||
self.dispatchEvent(new Event('change'));
|
}).catch(function(error) {
|
||||||
self.editing_name = false;
|
console.log(error);
|
||||||
})
|
alert(error.message);
|
||||||
.catch(function (error) {
|
self.editing_name = false;
|
||||||
console.log(error);
|
});
|
||||||
alert(error.message);
|
|
||||||
self.editing_name = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
name_blur(new_name) {
|
name_blur(new_name) {
|
||||||
@ -181,25 +163,19 @@ 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
|
<div style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444">
|
||||||
style="border: 3px solid black; padding: 8px; margin: 8px; border-radius: 8px; background-color: #444"
|
|
||||||
>
|
|
||||||
${name}
|
${name}
|
||||||
${(this.items || [])
|
${(this.items || []).filter(item => !item.x).map(x => self.render_item(x))}
|
||||||
.filter((item) => !item.x)
|
${(this.items || []).filter(item => item.x).map(x => self.render_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,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "👋",
|
"emoji": "👋",
|
||||||
"previous": "&zFISmRDAv+SXFonfZ9/sHNhrmMe+poTU22gwZzuSkT4=.sha256"
|
"previous": "&zFISmRDAv+SXFonfZ9/sHNhrmMe+poTU22gwZzuSkT4=.sha256"
|
||||||
}
|
}
|
@ -1,36 +1,23 @@
|
|||||||
<!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,
|
body,h1,h2,h3,h4,h5 {font-family: "Poppins", sans-serif}
|
||||||
h1,
|
body {font-size: 16px;}
|
||||||
h2,
|
img {margin-bottom: -8px;}
|
||||||
h3,
|
.mySlides {display: none;}
|
||||||
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">
|
||||||
@ -38,64 +25,41 @@
|
|||||||
<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">
|
<h1 class="w3-xxlarge w3-text-green"><b>Make apps and friends from the comfort of your web browser.</b></h1>
|
||||||
<b>Make apps and friends from the comfort of your web browser.</b>
|
<p>Tilde Friends is a platform for building, running, and sharing web applications.</p>
|
||||||
</h1>
|
<p>Available for lots of devices:
|
||||||
<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
|
<a class="w3-button w3-black w3-padding-large" href="https://www.tildefriends.net/~cory/releases/"><i class="fa fa-download"></i> Download</a>
|
||||||
class="w3-button w3-black w3-padding-large"
|
<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>
|
||||||
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>
|
||||||
|
|
||||||
<!-- SSB Section -->
|
<!-- SSB Section -->
|
||||||
<div class="w3-light-grey">
|
<div class="w3-light-grey">
|
||||||
<div class="w3-row-padding w3-padding-64">
|
<div class="w3-row-padding w3-padding-64 ">
|
||||||
<div class="w3-col l4 m6 s4">
|
<div class="w3-col l4 m6 s4">
|
||||||
<a href="https://scuttlebutt.nz/"
|
<a href="https://scuttlebutt.nz/"><img class="w3-image w3-round-large" src="ssb.png" alt="Secure Scuttlebutt"></a>
|
||||||
><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
|
Tilde Friends participates in the <a href="https://scuttlebutt.nz/">Secure Scuttlebutt</a> distributed social network.
|
||||||
<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
|
Share apps with friends. Discover new apps made by enemies. Post pictures of your coffee. Or just lurk.
|
||||||
pictures of your coffee. Or just lurk.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The social network integration provides tools for connecting with
|
The social network integration provides tools for connecting with other people world-wide
|
||||||
other people world-wide while still allowing apps and everything to
|
while still allowing apps and everything to operate offline.
|
||||||
operate offline.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -106,16 +70,14 @@
|
|||||||
<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
|
<i class="fa fa-pen-to-square w3-left w3-jumbo w3-text-gray" style="padding: 32px"></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
|
See that <code><b>edit</b></code> link near the top left corner of this page? It's there for
|
||||||
this page? It's there for every Tilde Friends app, so you can modify
|
every Tilde Friends app, so you can modify and see your changes right away.
|
||||||
and see your changes right away.
|
</p>
|
||||||
|
<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>
|
||||||
@ -124,22 +86,16 @@
|
|||||||
<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">
|
<h1 class="w3-jumbo" style="text-align: right"><b>Sandbox Security</b></h1>
|
||||||
<b>Sandbox Security</b>
|
<i class="fa fa-road-barrier w3-right w3-jumbo w3-text-yellow" style="padding: 32px"></i>
|
||||||
</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
|
Tilde Friends tries to make sure apps can be trusted using similar techniques to how web
|
||||||
techniques to how web browsers and operating systems do it.
|
browsers and operating systems do it.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This is all a work in progress, and it varies by platform, so don't
|
This is all a work in progress, and it varies by platform, so don't give it all your
|
||||||
give it all your innermost secrets yet, but do kick its tires and
|
innermost secrets yet, but do kick its tires and
|
||||||
<a href="mailto:cory@tildefriends.net">share</a> any surprises you
|
<a href="mailto:cory@tildefriends.net">share</a> any surprises you find.
|
||||||
find.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -149,16 +105,10 @@
|
|||||||
<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>
|
<p>Though of course for building Tilde Friends apps, you are free to use whatever fits.</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
|
<a href="https://en.wikipedia.org/wiki/C_(programming_language)" class="w3-col s3">
|
||||||
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>
|
||||||
@ -176,7 +126,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w3-row" style="margin-top: 64px">
|
<div class="w3-row" style="margin-top:64px">
|
||||||
<a href="https://www.zlib.net/" class="w3-col s3">
|
<a href="https://www.zlib.net/" class="w3-col s3">
|
||||||
<i class="fa fa-file-zipper w3-text-cyan w3-jumbo"></i>
|
<i class="fa fa-file-zipper w3-text-cyan w3-jumbo"></i>
|
||||||
<p>zlib</p>
|
<p>zlib</p>
|
||||||
@ -187,18 +137,15 @@
|
|||||||
</a>
|
</a>
|
||||||
<a href="https://www.openssl.org/" class="w3-col s3">
|
<a href="https://www.openssl.org/" class="w3-col s3">
|
||||||
<i class="fa fa-shield-halved w3-text-green w3-jumbo"></i>
|
<i class="fa fa-shield-halved w3-text-green w3-jumbo"></i>
|
||||||
<p>OpenSSL</p>
|
<p>OpenSSL </p>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a href="https://github.com/ianlancetaylor/libbacktrace" class="w3-col s3">
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w3-row" style="margin-top: 64px">
|
<div class="w3-row" style="margin-top:64px">
|
||||||
<a href="https://codemirror.net/5/" class="w3-col s3">
|
<a href="https://codemirror.net/5/" class="w3-col s3">
|
||||||
<i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i>
|
<i class="fa fa-keyboard w3-text-indigo w3-jumbo"></i>
|
||||||
<p>CodeMirror</p>
|
<p>CodeMirror</p>
|
||||||
@ -220,10 +167,7 @@
|
|||||||
|
|
||||||
<!-- 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">
|
<p class="w3-medium">This page and Tilde Friends itself was made by Cory mostly in coffee shops and a local pizza place.</p>
|
||||||
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>
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"type": "tildefriends-app",
|
"type": "tildefriends-app",
|
||||||
"emoji": "📝",
|
"emoji": "📝",
|
||||||
"previous": "&/wl8HE2jZShRXTYEVYRrK3pjHwi41Wbxl9HoSJaQP6Y=.sha256"
|
"previous": "&/wl8HE2jZShRXTYEVYRrK3pjHwi41Wbxl9HoSJaQP6Y=.sha256"
|
||||||
}
|
}
|
@ -11,13 +11,10 @@ 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 = '/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
||||||
'/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
|
||||||
} else if (node.type === 'link') {
|
} else if (node.type === 'link') {
|
||||||
if (
|
if (node.destination.indexOf(':') == -1 &&
|
||||||
node.destination.indexOf(':') == -1 &&
|
node.destination.indexOf('/') == -1) {
|
||||||
node.destination.indexOf('/') == -1
|
|
||||||
) {
|
|
||||||
node.destination = `${node.destination}`;
|
node.destination = `${node.destination}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,9 +29,7 @@ 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(
|
let ids = Object.keys(await ssb.following(await ssb.getOwnerIdentities(), 1));
|
||||||
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)) {
|
||||||
@ -45,13 +40,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
let wiki_doc;
|
let wiki_doc;
|
||||||
if (wiki) {
|
if (wiki) {
|
||||||
let [max_row_id, wiki_docs] = await utils.collection(
|
let [max_row_id, wiki_docs] = await utils.collection(ids, 'wiki-doc', wiki.id, -1, {});
|
||||||
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,13 +1,11 @@
|
|||||||
<!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>
|
<script>window.litDisableBundleWarning = true;</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,62 +14,52 @@ 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(
|
this.dispatchEvent(new CustomEvent('create', {
|
||||||
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(
|
this.dispatchEvent(new CustomEvent('rename', {
|
||||||
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 (
|
if (confirm(`Are you sure you want to delete '${this.collection[id].name}'?`)) {
|
||||||
confirm(`Are you sure you want to delete '${this.collection[id].name}'?`)
|
this.dispatchEvent(new CustomEvent('tombstone', {
|
||||||
) {
|
bubbles: true,
|
||||||
this.dispatchEvent(
|
detail: {
|
||||||
new CustomEvent('tombstone', {
|
id: id,
|
||||||
bubbles: true,
|
value: this.collection[id],
|
||||||
detail: {
|
},
|
||||||
id: 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(
|
this.dispatchEvent(new CustomEvent('change', {
|
||||||
new CustomEvent('change', {
|
bubbles: true,
|
||||||
bubbles: true,
|
detail: {
|
||||||
detail: {
|
id: id,
|
||||||
id: id,
|
value: this.collection[id],
|
||||||
value: this.collection[id],
|
},
|
||||||
},
|
}));
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -78,35 +68,25 @@ 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 ?? {})
|
${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>`)}
|
||||||
.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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@ import {LitElement, html} from './lit-all.min.js';
|
|||||||
import * as tfrpc from '/static/tfrpc.js';
|
import * as tfrpc from '/static/tfrpc.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() {
|
||||||
return {
|
return {
|
||||||
@ -19,22 +19,15 @@ class TfIdentityPickerElement extends LitElement {
|
|||||||
|
|
||||||
changed(event) {
|
changed(event) {
|
||||||
this.selected = event.srcElement.value;
|
this.selected = event.srcElement.value;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(new Event('change', {
|
||||||
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(
|
${(this.ids ?? []).map(id => html`<option ?selected=${id == this.selected} value=${id}>${id}</option>`)}
|
||||||
(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,16 +49,10 @@ 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(
|
[max_rowid, wikis] = await tfrpc.rpc.collection(this.following, 'wiki', undefined, max_rowid, wikis, false);
|
||||||
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;
|
||||||
@ -76,14 +70,9 @@ 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(
|
{
|
||||||
this.wiki?.editors,
|
[max_rowid, wiki_docs] = await tfrpc.rpc.collection(this.wiki?.editors, 'wiki-doc', this.wiki?.id, max_rowid, wiki_docs);
|
||||||
'wiki-doc',
|
|
||||||
this.wiki?.id,
|
|
||||||
max_rowid,
|
|
||||||
wiki_docs
|
|
||||||
);
|
|
||||||
if (this.wiki?.id !== start_id) {
|
if (this.wiki?.id !== start_id) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -139,11 +128,7 @@ class TfCollectionsAppElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update_hash() {
|
update_hash() {
|
||||||
tfrpc.rpc.set_hash(
|
tfrpc.rpc.set_hash(this.wiki_doc ? `${this.wiki.name}/${this.wiki_doc.name}` : `${this.wiki.name}`);
|
||||||
this.wiki_doc
|
|
||||||
? `${this.wiki.name}/${this.wiki_doc.name}`
|
|
||||||
: `${this.wiki.name}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async on_wiki_changed(event) {
|
async on_wiki_changed(event) {
|
||||||
@ -189,7 +174,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',
|
||||||
@ -267,45 +252,34 @@ 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(
|
${keyed(this.whoami, html`<tf-collection
|
||||||
this.whoami,
|
.collection=${this.wikis}
|
||||||
html`<tf-collection
|
whoami=${this.whoami}
|
||||||
.collection=${this.wikis}
|
selected_id=${this.wiki?.id}
|
||||||
whoami=${this.whoami}
|
@create=${this.on_wiki_create}
|
||||||
selected_id=${this.wiki?.id}
|
@rename=${this.on_wiki_rename}
|
||||||
@create=${this.on_wiki_create}
|
@tombstone=${this.on_wiki_tombstone}
|
||||||
@rename=${this.on_wiki_rename}
|
@change=${this.on_wiki_changed}></tf-collection>`)}
|
||||||
@tombstone=${this.on_wiki_tombstone}
|
${keyed(this.wiki_doc?.id, html`<tf-collection
|
||||||
@change=${this.on_wiki_changed}
|
.collection=${this.wiki_docs}
|
||||||
></tf-collection>`
|
whoami=${this.whoami}
|
||||||
)}
|
selected_id=${(this.wiki_doc && this.wiki_doc?.parent == this.wiki?.id) ? this.wiki_doc?.id : ''}
|
||||||
${keyed(
|
@create=${this.on_wiki_doc_create}
|
||||||
this.wiki_doc?.id,
|
@rename=${this.on_wiki_doc_rename}
|
||||||
html`<tf-collection
|
@tombstone=${this.on_wiki_doc_tombstone}
|
||||||
.collection=${this.wiki_docs}
|
@change=${this.on_wiki_doc_changed}></tf-collection>`)}
|
||||||
whoami=${this.whoami}
|
<button @click=${() => self.expand_editors = !self.expand_editors}>${this.wiki?.editors?.length} editor${this.wiki?.editors?.length > 1 ? 's' : ''}</button>
|
||||||
selected_id=${this.wiki_doc &&
|
|
||||||
this.wiki_doc?.parent == this.wiki?.id
|
|
||||||
? this.wiki_doc?.id
|
|
||||||
: ''}
|
|
||||||
@create=${this.on_wiki_doc_create}
|
|
||||||
@rename=${this.on_wiki_doc_rename}
|
|
||||||
@tombstone=${this.on_wiki_doc_tombstone}
|
|
||||||
@change=${this.on_wiki_doc_changed}
|
|
||||||
></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>
|
||||||
@ -314,51 +288,22 @@ 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 || {})
|
${Object.values(this.wikis || {}).sort((x, y) => x.name.localeCompare(y.name)).map(wiki => html`
|
||||||
.sort((x, y) => x.name.localeCompare(y.name))
|
<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>
|
||||||
.map(
|
<ul>
|
||||||
(wiki) => html`
|
${Object.values(self.wiki_docs || {}).filter(doc => doc.parent === wiki?.id).sort((x, y) => x.name.localeCompare(y.name)).map(doc => html`
|
||||||
<div
|
<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>
|
||||||
class="toc ${self.wiki?.id === wiki.id ? 'selected' : ''}"
|
`)}
|
||||||
style="white-space: nowrap; cursor: pointer"
|
</ul>
|
||||||
@click=${() => self.on_wiki_changed({detail: {value: wiki}})}
|
`)}
|
||||||
>
|
|
||||||
${wiki.name}
|
|
||||||
</div>
|
|
||||||
<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>
|
</div>
|
||||||
${
|
${this.wiki_doc && this.wiki_doc.parent === this.wiki?.id ? html`
|
||||||
this.wiki_doc && this.wiki_doc.parent === this.wiki?.id
|
<tf-wiki-doc
|
||||||
? html`
|
style="width: 100%"
|
||||||
<tf-wiki-doc
|
whoami=${this.whoami}
|
||||||
style="width: 100%"
|
.wiki=${this.wiki}
|
||||||
whoami=${this.whoami}
|
.value=${this.wiki_doc}></tf-wiki-doc>
|
||||||
.wiki=${this.wiki}
|
` : undefined}
|
||||||
.value=${this.wiki_doc}
|
|
||||||
></tf-wiki-doc>
|
|
||||||
`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -29,16 +29,10 @@ 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 = '/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
||||||
'/' +
|
|
||||||
node.destination +
|
|
||||||
'/view?filename=' +
|
|
||||||
node.firstChild?.literal;
|
|
||||||
} else if (node.type === 'link') {
|
} else if (node.type === 'link') {
|
||||||
if (
|
if (node.destination.indexOf(':') == -1 &&
|
||||||
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}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,9 +70,7 @@ class TfWikiDocElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
thumbnail(md) {
|
thumbnail(md) {
|
||||||
let m = md
|
let m = md ? md.match(/\!\[image:[^\]]+\]\((\&.{44}\.sha256)\).*/) : undefined;
|
||||||
? md.match(/\!\[image:[^\]]+\]\((\&.{44}\.sha256)\).*/)
|
|
||||||
: undefined;
|
|
||||||
return m ? m[1] : undefined;
|
return m ? m[1] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,16 +106,12 @@ 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(
|
message = await tfrpc.rpc.encrypt(this.whoami, this.value.editors, JSON.stringify(message));
|
||||||
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;
|
||||||
@ -148,16 +136,16 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
convert_to_format(buffer, type, mime_type) {
|
convert_to_format(buffer, type, mime_type) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
let img = new Image();
|
let img = new Image();
|
||||||
img.onload = function () {
|
img.onload = function() {
|
||||||
let canvas = document.createElement('canvas');
|
let canvas = document.createElement('canvas');
|
||||||
let width_scale = Math.min(img.width, 1024) / img.width;
|
let width_scale = Math.min(img.width, 1024) / img.width;
|
||||||
let height_scale = Math.min(img.height, 1024) / img.height;
|
let height_scale = Math.min(img.height, 1024) / img.height;
|
||||||
@ -167,17 +155,13 @@ 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])
|
let result = atob(data_url.split(',')[1]).split('').map(x => x.charCodeAt(0));
|
||||||
.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))
|
let raw = Array.from(new Uint8Array(buffer)).map(b => String.fromCharCode(b)).join('');
|
||||||
.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;
|
||||||
});
|
});
|
||||||
@ -203,11 +187,7 @@ 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(
|
let test_buffer = await self.convert_to_format(buffer, file.type, 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;
|
||||||
@ -226,7 +206,7 @@ class TfWikiDocElement extends LitElement {
|
|||||||
}
|
}
|
||||||
document.execCommand('insertText', false, insert);
|
document.execCommand('insertText', false, insert);
|
||||||
self.on_edit({srcElement: editor});
|
self.on_edit({srcElement: editor});
|
||||||
} catch (e) {
|
} catch(e) {
|
||||||
alert(e?.message);
|
alert(e?.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,84 +234,31 @@ 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 {
|
a:link { color: #268bd2 }
|
||||||
color: #268bd2;
|
a:visited { color: #6c71c4 }
|
||||||
}
|
a:hover { color: #859900 }
|
||||||
a:visited {
|
a:active { color: #2aa198 }
|
||||||
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
|
<button ?disabled=${!this.whoami || this.is_editing} @click=${() => self.is_editing = true}>Edit</button>
|
||||||
?disabled=${!this.whoami || this.is_editing}
|
<button ?disabled=${this.blob == this.blob_original} @click=${this.on_save_draft}>Save Draft</button>
|
||||||
@click=${() => (self.is_editing = true)}
|
<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>
|
||||||
Edit
|
<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>
|
<button ?disabled=${!this.is_editing} @click=${this.on_blog_publish}>Publish Blog</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">
|
<div ?hidden=${!this.value?.private} style="color: #800">🔒 document is private</div>
|
||||||
🔒 document is private
|
<div style="display: flex; flex-direction: row; ${this.value?.private ? 'border-top: 4px solid #800' : ''}">
|
||||||
</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
|
style="flex: 1 1; min-height: 10em; ${this.value?.private ? 'border: 4px solid #800' : ''}"
|
||||||
? 'border: 4px solid #800'
|
|
||||||
: ''}"
|
|
||||||
@input=${this.on_edit}
|
@input=${this.on_edit}
|
||||||
@paste=${this.paste}
|
@paste=${this.paste}
|
||||||
.value=${this.blob ?? ''}
|
.value=${this.blob ?? ''}></textarea>
|
||||||
></textarea>
|
|
||||||
<div style="flex: 1 1">
|
<div style="flex: 1 1">
|
||||||
<div
|
<div ?hidden=${!this.is_editing} style="border: 1px solid #fff; border-radius: 1em; padding: 0.5em">
|
||||||
?hidden=${!this.is_editing}
|
<img ?hidden=${!thumbnail_ref} style="max-width: 128px; max-height: 128px; float: right" src="/${thumbnail_ref}/view">
|
||||||
style="border: 1px solid #fff; border-radius: 1em; padding: 0.5em"
|
<h1 ?hidden=${!this.title(this.blob)}>${unsafeHTML(this.markdown(this.title(this.blob)))}</h1>
|
||||||
>
|
|
||||||
<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,7 +17,8 @@ async function process_message(whoami, collection, message, kind, parent) {
|
|||||||
if (!x) {
|
if (!x) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (content.type !== kind || (parent && content.parent !== parent)) {
|
if (content.type !== kind ||
|
||||||
|
(parent && content.parent !== parent)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -27,10 +28,7 @@ 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] = Object.assign(collection[content.key] || {}, content);
|
||||||
collection[content.key] || {},
|
|
||||||
content
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
collection[message.id] = Object.assign(content, {id: message.id});
|
collection[message.id] = Object.assign(content, {id: message.id});
|
||||||
@ -42,7 +40,7 @@ async function process_message(whoami, collection, message, kind, parent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let g_new_message_resolve;
|
let g_new_message_resolve;
|
||||||
let g_new_message_promise = new Promise(function (resolve, reject) {
|
let g_new_message_promise = new Promise(function(resolve, reject) {
|
||||||
g_new_message_resolve = resolve;
|
g_new_message_resolve = resolve;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -50,9 +48,9 @@ function new_message() {
|
|||||||
return g_new_message_promise;
|
return g_new_message_promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssb.addEventListener('message', function (id) {
|
ssb.addEventListener('message', function(id) {
|
||||||
let resolve = g_new_message_resolve;
|
let resolve = g_new_message_resolve;
|
||||||
g_new_message_promise = new Promise(function (resolve, reject) {
|
g_new_message_promise = new Promise(function(resolve, reject) {
|
||||||
g_new_message_resolve = resolve;
|
g_new_message_resolve = resolve;
|
||||||
});
|
});
|
||||||
if (resolve) {
|
if (resolve) {
|
||||||
@ -60,42 +58,26 @@ ssb.addEventListener('message', function (id) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function collection(
|
export async function collection(ids, kind, parent, max_rowid, data, include_private) {
|
||||||
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(
|
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
||||||
'SELECT MAX(rowid) AS rowid FROM messages',
|
rowid = row.rowid;
|
||||||
[],
|
});
|
||||||
function (row) {
|
|
||||||
rowid = row.rowid;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (rowid == max_rowid) {
|
if (rowid == max_rowid) {
|
||||||
await new_message();
|
await new_message();
|
||||||
await ssb.sqlAsync(
|
await ssb.sqlAsync('SELECT MAX(rowid) AS rowid FROM messages', [], function(row) {
|
||||||
'SELECT MAX(rowid) AS rowid FROM messages',
|
rowid = row.rowid;
|
||||||
[],
|
});
|
||||||
function (row) {
|
|
||||||
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
|
||||||
@ -106,19 +88,9 @@ export async function collection(
|
|||||||
(?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) {
|
||||||
[
|
rows.push(row);
|
||||||
JSON.stringify(ids),
|
});
|
||||||
max_rowid ?? -1,
|
|
||||||
rowid,
|
|
||||||
kind,
|
|
||||||
parent,
|
|
||||||
include_private ? true : false,
|
|
||||||
],
|
|
||||||
function (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)) {
|
||||||
|
98
core/app.js
98
core/app.js
@ -28,23 +28,23 @@ function App() {
|
|||||||
* TODOC
|
* TODOC
|
||||||
* @param {*} callback
|
* @param {*} callback
|
||||||
*/
|
*/
|
||||||
App.prototype.readOutput = function (callback) {
|
App.prototype.readOutput = function(callback) {
|
||||||
this._on_output = callback;
|
this._on_output = callback;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODOC
|
* TODOC
|
||||||
* @param {*} api
|
* @param {*} api
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
App.prototype.makeFunction = function (api) {
|
App.prototype.makeFunction = function(api) {
|
||||||
let self = this;
|
let self = this;
|
||||||
let result = function () {
|
let result = function() {
|
||||||
let id = g_next_id++;
|
let id = g_next_id++;
|
||||||
while (!id || g_calls[id]) {
|
while (!id || g_calls[id]) {
|
||||||
id = g_next_id++;
|
id = g_next_id++;
|
||||||
}
|
}
|
||||||
let promise = new Promise(function (resolve, reject) {
|
let promise = new Promise(function(resolve, reject) {
|
||||||
g_calls[id] = {resolve: resolve, reject: reject};
|
g_calls[id] = {resolve: resolve, reject: reject};
|
||||||
});
|
});
|
||||||
let message = {
|
let message = {
|
||||||
@ -58,16 +58,16 @@ 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;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODOC
|
* TODOC
|
||||||
* @param {*} message
|
* @param {*} message
|
||||||
*/
|
*/
|
||||||
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);
|
||||||
@ -76,7 +76,7 @@ App.prototype.send = function (message) {
|
|||||||
if (message && this._on_output) {
|
if (message && this._on_output) {
|
||||||
this._on_output(message);
|
this._on_output(message);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODOC
|
* TODOC
|
||||||
@ -90,48 +90,43 @@ function socket(request, response, client) {
|
|||||||
let credentials = auth.query(request.headers);
|
let credentials = auth.query(request.headers);
|
||||||
let refresh = auth.makeRefresh(credentials);
|
let refresh = auth.makeRefresh(credentials);
|
||||||
|
|
||||||
response.onClose = async function () {
|
response.onClose = async function() {
|
||||||
if (process && process.task) {
|
if (process && process.task) {
|
||||||
process.task.kill();
|
process.task.kill();
|
||||||
}
|
}
|
||||||
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) {
|
||||||
let message;
|
let message;
|
||||||
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 (
|
if (match = /^\/([&%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(message.path)) {
|
||||||
(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(
|
response.send(JSON.stringify({
|
||||||
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') {
|
||||||
@ -142,15 +137,12 @@ function socket(request, response, client) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response.send(
|
response.send(JSON.stringify({
|
||||||
JSON.stringify({
|
action: "session",
|
||||||
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;
|
||||||
@ -160,26 +152,19 @@ 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(
|
response.send(JSON.stringify({action: 'ready', edit_only: true}), 0x1);
|
||||||
JSON.stringify({action: 'ready', edit_only: true}),
|
|
||||||
0x1
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
process = await core.getSessionProcessBlob(
|
process = await core.getSessionProcessBlob(blobId, sessionId, options);
|
||||||
blobId,
|
|
||||||
sessionId,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (process) {
|
if (process) {
|
||||||
process.app.readOutput(function (message) {
|
process.app.readOutput(function(message) {
|
||||||
response.send(JSON.stringify(message), 0x1);
|
response.send(JSON.stringify(message), 0x1);
|
||||||
});
|
});
|
||||||
process.app.send();
|
process.app.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
let ping = function () {
|
let ping = function() {
|
||||||
let now = Date.now();
|
let now = Date.now();
|
||||||
let again = true;
|
let again = true;
|
||||||
if (now - process.lastActive < process.timeout) {
|
if (now - process.lastActive < process.timeout) {
|
||||||
@ -192,14 +177,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);
|
||||||
@ -239,16 +224,11 @@ function socket(request, response, client) {
|
|||||||
if (process) {
|
if (process) {
|
||||||
process.lastActive = Date.now();
|
process.lastActive = Date.now();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
response.upgrade(
|
response.upgrade(100, refresh ? {
|
||||||
100,
|
'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`,
|
||||||
refresh
|
} : {});
|
||||||
? {
|
|
||||||
'Set-Cookie': `session=${refresh.token}; path=/; Max-Age=${refresh.interval}; Secure; SameSite=Strict`,
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {socket, App};
|
export { socket, App };
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
<!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>
|
<script>window.litDisableBundleWarning = true;</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;
|
||||||
|
174
core/auth.js
174
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;
|
||||||
|
|
||||||
@ -57,17 +57,27 @@ function makeJwt(payload) {
|
|||||||
const final_payload = b64url(
|
const final_payload = b64url(
|
||||||
base64Encode(
|
base64Encode(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
Object.assign({}, payload, {
|
Object.assign({}, payload, {exp: (new Date().valueOf()) + kRefreshInterval}
|
||||||
exp: new Date().valueOf() + kRefreshInterval,
|
)
|
||||||
})
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const jwt = [
|
const jwt = [
|
||||||
b64url(base64Encode(JSON.stringify({alg: 'HS256', typ: 'JWT'}))),
|
b64url(
|
||||||
|
base64Encode(
|
||||||
|
JSON.stringify({
|
||||||
|
alg: 'HS256',
|
||||||
|
typ: 'JWT'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
final_payload,
|
final_payload,
|
||||||
b64url(ssb.hmacsha256sign(final_payload, ':auth', id)),
|
b64url(
|
||||||
|
ssb.hmacsha256sign(final_payload, ':auth', id)
|
||||||
|
)
|
||||||
].join('.');
|
].join('.');
|
||||||
|
|
||||||
return jwt;
|
return jwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +99,7 @@ function readSession(session) {
|
|||||||
|
|
||||||
if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) {
|
if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) {
|
||||||
const result = JSON.parse(utf8Decode(base64Decode(unb64url(payload))));
|
const result = JSON.parse(utf8Decode(base64Decode(unb64url(payload))));
|
||||||
const now = new Date().valueOf();
|
const 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.`);
|
||||||
@ -131,15 +141,11 @@ function hashPassword(password) {
|
|||||||
* @returns TODOC
|
* @returns TODOC
|
||||||
*/
|
*/
|
||||||
function noAdministrator() {
|
function noAdministrator() {
|
||||||
return (
|
return !core.globalSettings ||
|
||||||
!core.globalSettings ||
|
!core.globalSettings.permissions ||
|
||||||
!core.globalSettings.permissions ||
|
!Object.keys(core.globalSettings.permissions).some(function(name) {
|
||||||
!Object.keys(core.globalSettings.permissions).some(function (name) {
|
return core.globalSettings.permissions[name].indexOf("administration") != -1;
|
||||||
return (
|
});
|
||||||
core.globalSettings.permissions[name].indexOf('administration') != -1
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -153,8 +159,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);
|
||||||
@ -171,7 +177,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;
|
||||||
@ -189,17 +195,7 @@ function getCookies(headers) {
|
|||||||
function isNameValid(name) {
|
function isNameValid(name) {
|
||||||
// TODO(tasiaiso): convert this into a regex
|
// TODO(tasiaiso): convert this into a regex
|
||||||
let c = name.charAt(0);
|
let c = name.charAt(0);
|
||||||
return (
|
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'));
|
||||||
((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) &&
|
|
||||||
name
|
|
||||||
.split()
|
|
||||||
.map(
|
|
||||||
(x) =>
|
|
||||||
x >= ('a' && x <= 'z') ||
|
|
||||||
x >= ('A' && x <= 'Z') ||
|
|
||||||
x >= ('0' && x <= '9')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -211,19 +207,14 @@ function isNameValid(name) {
|
|||||||
function handler(request, response) {
|
function handler(request, response) {
|
||||||
// TODO(tasiaiso): split this function
|
// TODO(tasiaiso): split this function
|
||||||
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, {
|
response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + '/', "Content-Length": "0"});
|
||||||
Location:
|
|
||||||
(request.client.tls ? 'https://' : 'http://') +
|
|
||||||
request.headers.host +
|
|
||||||
'/',
|
|
||||||
'Content-Length': '0',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
response.end();
|
response.end();
|
||||||
return;
|
return;
|
||||||
@ -232,23 +223,22 @@ 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 (
|
if (!account &&
|
||||||
!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);
|
||||||
}
|
}
|
||||||
@ -266,12 +256,10 @@ function handler(request, response) {
|
|||||||
loginError = 'Error registering account.';
|
loginError = 'Error registering account.';
|
||||||
}
|
}
|
||||||
} else if (formData.change == '1') {
|
} else if (formData.change == '1') {
|
||||||
if (
|
if (account &&
|
||||||
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));
|
||||||
@ -279,11 +267,9 @@ function handler(request, response) {
|
|||||||
loginError = 'Error changing password.';
|
loginError = 'Error changing password.';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (account &&
|
||||||
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);
|
||||||
@ -301,52 +287,32 @@ 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, {
|
response.writeHead(303, {"Location": formData.return, "Set-Cookie": cookie});
|
||||||
Location: formData.return,
|
|
||||||
'Set-Cookie': cookie,
|
|
||||||
});
|
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
File.readFile('core/auth.html')
|
File.readFile("core/auth.html").then(function(data) {
|
||||||
.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,
|
name: entry?.name,
|
||||||
name: entry?.name,
|
error: loginError,
|
||||||
error: loginError,
|
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.end(html);
|
||||||
);
|
}).catch(function(error) {
|
||||||
response.writeHead(200, {
|
response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
||||||
'Content-Type': 'text/html; charset=utf-8',
|
response.end("404 File not found");
|
||||||
'Set-Cookie': cookie,
|
});
|
||||||
'Content-Length': html.length,
|
|
||||||
});
|
|
||||||
response.end(html);
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
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, {
|
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 : "")});
|
||||||
'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, {
|
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
||||||
'Content-Type': 'text/plain; charset=utf-8',
|
response.end("Hello, " + request.client.peerName + ".");
|
||||||
Connection: 'close',
|
|
||||||
});
|
|
||||||
response.end('Hello, ' + request.client.peerName + '.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,7 +326,7 @@ 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 || {};
|
||||||
}
|
}
|
||||||
@ -372,11 +338,7 @@ function getPermissions(session) {
|
|||||||
*/
|
*/
|
||||||
function getPermissionsForUser(userName) {
|
function getPermissionsForUser(userName) {
|
||||||
let permissions = {};
|
let permissions = {};
|
||||||
if (
|
if (core.globalSettings && core.globalSettings.permissions && core.globalSettings.permissions[userName]) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
@ -393,12 +355,10 @@ 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
|
permissions: autologin ? getPermissionsForUser(autologin) : getPermissions(session),
|
||||||
? getPermissionsForUser(autologin)
|
|
||||||
: getPermissions(session),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -417,4 +377,4 @@ function makeRefresh(credentials) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {handler, query, makeRefresh};
|
export { handler, query, makeRefresh };
|
||||||
|
800
core/client.js
800
core/client.js
File diff suppressed because it is too large
Load Diff
908
core/core.js
908
core/core.js
File diff suppressed because it is too large
Load Diff
10
core/form.js
10
core/form.js
@ -4,12 +4,12 @@
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
@ -41,4 +41,4 @@ function decodeForm(encoded, initial) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {decodeForm};
|
export { decodeForm };
|
||||||
|
101
core/http.js
101
core/http.js
@ -6,12 +6,12 @@
|
|||||||
*/
|
*/
|
||||||
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +32,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -48,66 +48,57 @@ function parseResponse(data) {
|
|||||||
*/
|
*/
|
||||||
export function fetch(url, options, allowed_hosts) {
|
export function fetch(url, options, allowed_hosts) {
|
||||||
let parsed = parseUrl(url);
|
let parsed = parseUrl(url);
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
if ((allowed_hosts ?? []).indexOf(parsed.host) == -1) {
|
if ((allowed_hosts ?? []).indexOf(parsed.host) == -1) {
|
||||||
throw new Error(`fetch() request to host ${parsed.host} is not allowed.`);
|
throw new Error(`fetch() request to host ${parsed.host} is not allowed.`);
|
||||||
}
|
}
|
||||||
let socket = new Socket();
|
let socket = new Socket();
|
||||||
let buffer = new Uint8Array(0);
|
let buffer = new Uint8Array(0);
|
||||||
|
|
||||||
return socket
|
return socket.connect(parsed.host, parsed.port).then(function() {
|
||||||
.connect(parsed.host, parsed.port)
|
socket.read(function(data) {
|
||||||
.then(function () {
|
if (data && data.length) {
|
||||||
socket.read(function (data) {
|
let newBuffer = new Uint8Array(buffer.length + data.length);
|
||||||
if (data && data.length) {
|
newBuffer.set(buffer, 0);
|
||||||
let newBuffer = new Uint8Array(buffer.length + data.length);
|
newBuffer.set(data, buffer.length);
|
||||||
newBuffer.set(buffer, 0);
|
buffer = newBuffer;
|
||||||
newBuffer.set(data, buffer.length);
|
} else {
|
||||||
buffer = newBuffer;
|
let result = parseHttpResponse(buffer);
|
||||||
} else {
|
if (!result) {
|
||||||
let result = parseHttpResponse(buffer);
|
reject(new Exception('Parse failed.'));
|
||||||
if (!result) {
|
|
||||||
reject(new Exception('Parse failed.'));
|
|
||||||
}
|
|
||||||
if (typeof result == 'number') {
|
|
||||||
if (result == -2) {
|
|
||||||
reject('Incomplete request.');
|
|
||||||
} else {
|
|
||||||
reject('Bad request.');
|
|
||||||
}
|
|
||||||
} else if (typeof result == 'object') {
|
|
||||||
resolve({
|
|
||||||
body: buffer.slice(result.bytes_parsed),
|
|
||||||
status: result.status,
|
|
||||||
message: result.message,
|
|
||||||
headers: result.headers,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reject(new Exception('Unexpected parse result.'));
|
|
||||||
}
|
|
||||||
resolve(parseResponse(utf8Decode(buffer)));
|
|
||||||
}
|
}
|
||||||
});
|
if (typeof result == 'number') {
|
||||||
|
if (result == -2) {
|
||||||
if (parsed.port == 443) {
|
reject('Incomplete request.');
|
||||||
return socket.startTls();
|
} else {
|
||||||
|
reject('Bad request.');
|
||||||
|
}
|
||||||
|
} else if (typeof result == 'object') {
|
||||||
|
resolve({
|
||||||
|
body: buffer.slice(result.bytes_parsed),
|
||||||
|
status: result.status,
|
||||||
|
message: result.message,
|
||||||
|
headers: result.headers,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(new Exception('Unexpected parse result.'));
|
||||||
|
}
|
||||||
|
resolve(parseResponse(utf8Decode(buffer)));
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
let body =
|
|
||||||
typeof options?.body == 'string'
|
|
||||||
? utf8Encode(options.body)
|
|
||||||
: options.body || new Uint8Array(0);
|
|
||||||
let headers = utf8Encode(
|
|
||||||
`${options?.method ?? 'GET'} ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\nContent-Length: ${body.length}\r\n\r\n`
|
|
||||||
);
|
|
||||||
let fullRequest = new Uint8Array(headers.length + body.length);
|
|
||||||
fullRequest.set(headers, 0);
|
|
||||||
fullRequest.set(body, headers.length);
|
|
||||||
socket.write(fullRequest);
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
reject(error);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (parsed.port == 443) {
|
||||||
|
return socket.startTls();
|
||||||
|
}
|
||||||
|
}).then(function() {
|
||||||
|
let body = typeof options?.body == 'string' ? utf8Encode(options.body) : (options.body || new Uint8Array(0));
|
||||||
|
let headers = utf8Encode(`${options?.method ?? 'GET'} ${parsed.path} HTTP/1.0\r\nHost: ${parsed.host}\r\nConnection: close\r\nContent-Length: ${body.length}\r\n\r\n`);
|
||||||
|
let fullRequest = new Uint8Array(headers.length + body.length);
|
||||||
|
fullRequest.set(headers, 0);
|
||||||
|
fullRequest.set(body, headers.length);
|
||||||
|
socket.write(fullRequest);
|
||||||
|
}).catch(function(error) {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -102,54 +102,22 @@ a:active {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Solarized Color Scheme Colors */
|
/* Solarized Color Scheme Colors */
|
||||||
.base03 {
|
.base03 { color: #002b36; }
|
||||||
color: #002b36;
|
.base02 { color: #073642; }
|
||||||
}
|
.base01 { color: #586e75; }
|
||||||
.base02 {
|
.base00 { color: #657b83; }
|
||||||
color: #073642;
|
.base0 { color: #839496; }
|
||||||
}
|
.base1 { color: #93a1a1; }
|
||||||
.base01 {
|
.base2 { color: #eee8d5; }
|
||||||
color: #586e75;
|
.base3 { color: #fdf6e3; }
|
||||||
}
|
.yellow { color: #b58900; }
|
||||||
.base00 {
|
.orange { color: #cb4b16; }
|
||||||
color: #657b83;
|
.red { color: #dc322f; }
|
||||||
}
|
.magenta { color: #d33682; }
|
||||||
.base0 {
|
.violet { color: #6c71c4; }
|
||||||
color: #839496;
|
.blue { color: #268bd2; }
|
||||||
}
|
.cyan { color: #2aa198; }
|
||||||
.base1 {
|
.green { color: #859900; }
|
||||||
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;
|
||||||
|
@ -8,11 +8,7 @@ let g_calls = {};
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function get_is_browser() {
|
function get_is_browser() {
|
||||||
try {
|
try { return window !== undefined && console !== undefined; } catch { return false; }
|
||||||
return window !== undefined && console !== undefined;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (k_is_browser) {
|
if (k_is_browser) {
|
||||||
@ -27,31 +23,21 @@ if (k_is_browser) {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function make_rpc(target, prop, receiver) {
|
function make_rpc(target, prop, receiver) {
|
||||||
return function () {
|
return function() {
|
||||||
let id = g_next_id++;
|
let id = g_next_id++;
|
||||||
while (!id || g_calls[id] !== undefined) {
|
while (!id || g_calls[id] !== undefined) {
|
||||||
id = g_next_id++;
|
id = g_next_id++;
|
||||||
}
|
}
|
||||||
let promise = new Promise(function (resolve, reject) {
|
let promise = new Promise(function(resolve, reject) {
|
||||||
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(
|
window.parent.postMessage({message: 'tfrpc', method: prop, params: [...arguments], id: id}, '*');
|
||||||
{message: 'tfrpc', method: prop, params: [...arguments], id: id},
|
|
||||||
'*'
|
|
||||||
);
|
|
||||||
return promise;
|
return promise;
|
||||||
} else {
|
} else {
|
||||||
return app
|
return app.postMessage({message: 'tfrpc', method: prop, params: [...arguments], id: id}).then(x => promise);
|
||||||
.postMessage({
|
|
||||||
message: 'tfrpc',
|
|
||||||
method: prop,
|
|
||||||
params: [...arguments],
|
|
||||||
id: id,
|
|
||||||
})
|
|
||||||
.then((x) => promise);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,22 +63,16 @@ 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))
|
Promise.resolve(method(...message.params)).then(function(result) {
|
||||||
.then(function (result) {
|
send({message: 'tfrpc', id: id, result: result});
|
||||||
send({message: 'tfrpc', id: id, result: result});
|
}).catch(function(error) {
|
||||||
})
|
send({message: 'tfrpc', id: id, error: error});
|
||||||
.catch(function (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({
|
send({message: 'tfrpc', id: id, error: `Method '${message.method}' not found.`});
|
||||||
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]) {
|
||||||
@ -113,11 +93,11 @@ function call_rpc(message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (k_is_browser) {
|
if (k_is_browser) {
|
||||||
window.addEventListener('message', function (event) {
|
window.addEventListener('message', function(event) {
|
||||||
call_rpc(event.data);
|
call_rpc(event.data);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
core.register('message', function (message) {
|
core.register('message', function(message) {
|
||||||
call_rpc(message?.message);
|
call_rpc(message?.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
109
docs/guide.md
109
docs/guide.md
@ -5,7 +5,7 @@ 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
|
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
|
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.
|
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
|
means that unlike client-side JavaScript, it can have the ability to read and
|
||||||
@ -21,7 +21,7 @@ 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
|
restricted access to filesystem, network, and other system resources. The core
|
||||||
process runs a core set of scripts that implement a web server, typically
|
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
|
starting a new process for each visitor's session which runs scripts for the
|
||||||
active application and stopping it when the visitor leaves.
|
active application and stopping it when the visitor leaves.
|
||||||
@ -42,38 +42,38 @@ 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
|
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.
|
a basic JavaScript to C++ transition and are relatively fast and immediate.
|
||||||
|
|
||||||
// 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
|
exported and called across process boundaries. When this is done, any
|
||||||
arguments are serialized to a network protocol, deserialized by the other
|
arguments are serialized to a network protocol, deserialized by the other
|
||||||
process, the function called, and finally any return value is passed back in
|
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
|
the same way. Any functions referenced by the arguments or return value are
|
||||||
also exported and can be subsequently called across process boundaries.
|
also exported and can be subsequently called across process boundaries.
|
||||||
Functions called across process boundaries are always asynchronous, returning a
|
Functions called across process boundaries are always asynchronous, returning a
|
||||||
Promise. Care must be taken for security reasons to not pass dangerous
|
Promise. Care must be taken for security reasons to not pass dangerous
|
||||||
functions ("deleteAllMydata()") to untrusted processes, and it is best for
|
functions ("deleteAllMydata()") to untrusted processes, and it is best for
|
||||||
performance reasons to minimize the data size transferred between processes.
|
performance reasons to minimize the data size transferred between processes.
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
@ -89,96 +89,80 @@ 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
|
||||||
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
|
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
|
||||||
Print to the terminal. Arguments and lists are recursively expanded. Numerous
|
|
||||||
special values are supported as implemented in client.cs.
|
special values are supported as implemented in client.cs.
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
||||||
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
|
Sends a message to the iframe that was created with the given name, using the
|
||||||
browser's window.postMessage.
|
browser's window.postMessage.
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
|
Tilde Friends uses lmdb as a basic key value store. Keys and values are all
|
||||||
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
|
||||||
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
|
||||||
@ -186,24 +170,19 @@ administration app.
|
|||||||
|
|
||||||
Apps that require network access must declare it like this:
|
Apps that require network access must declare it like this:
|
||||||
|
|
||||||
//! { "permissions": ["network"] }
|
//! { "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.
|
||||||
|
27
package-lock.json
generated
27
package-lock.json
generated
@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "tildefriends",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "tildefriends",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prettier": "^3.2.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prettier": {
|
|
||||||
"version": "3.2.5",
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"prettier": "bin/prettier.cjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
11
package.json
11
package.json
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "tildefriends",
|
|
||||||
"scripts": {
|
|
||||||
"prettier": "prettier . --check --cache --write"
|
|
||||||
},
|
|
||||||
"author": "Cory McWilliams",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"prettier": "^3.2.5"
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,16 +11,12 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
static void _write_file(const char* path, const void* blob, size_t size, bool force_add_trailing_newline)
|
static void _write_file(const char* path, const void* blob, size_t size)
|
||||||
{
|
{
|
||||||
FILE* file = fopen(path, "wb");
|
FILE* file = fopen(path, "wb");
|
||||||
if (file)
|
if (file)
|
||||||
{
|
{
|
||||||
fwrite(blob, 1, size, file);
|
fwrite(blob, 1, size, file);
|
||||||
if (force_add_trailing_newline)
|
|
||||||
{
|
|
||||||
fputc('\n', file);
|
|
||||||
}
|
|
||||||
fclose(file);
|
fclose(file);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -151,7 +147,7 @@ void tf_ssb_export(tf_ssb_t* ssb, const char* key)
|
|||||||
if (tf_ssb_db_blob_get(ssb, blob_id, &file_blob, &file_size))
|
if (tf_ssb_db_blob_get(ssb, blob_id, &file_blob, &file_size))
|
||||||
{
|
{
|
||||||
snprintf(file_path, sizeof(file_path), "apps/%s/%s", path, file_name);
|
snprintf(file_path, sizeof(file_path), "apps/%s/%s", path, file_name);
|
||||||
_write_file(file_path, file_blob, file_size, false);
|
_write_file(file_path, file_blob, file_size);
|
||||||
tf_free(file_blob);
|
tf_free(file_blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +177,7 @@ void tf_ssb_export(tf_ssb_t* ssb, const char* key)
|
|||||||
size_t length = 0;
|
size_t length = 0;
|
||||||
const char* string = JS_ToCStringLen(context, &length, json);
|
const char* string = JS_ToCStringLen(context, &length, json);
|
||||||
snprintf(file_path, sizeof(file_path), "apps/%s.json", path);
|
snprintf(file_path, sizeof(file_path), "apps/%s.json", path);
|
||||||
_write_file(file_path, string, length, true);
|
_write_file(file_path, string, length);
|
||||||
JS_FreeCString(context, string);
|
JS_FreeCString(context, string);
|
||||||
JS_FreeValue(context, json);
|
JS_FreeValue(context, json);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user