Remove the old SSB client interface.

git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@4042 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
Cory McWilliams 2022-11-16 00:18:54 +00:00
parent efdecc6017
commit 9b696503de
18 changed files with 0 additions and 28731 deletions

View File

@ -1 +0,0 @@
{"type":"tildefriends-app","files":{"app.js":"&7dCNJFk5RMHTWZC2qR8/UzjS22bXn7ZsPS6L/LIgUOg=.sha256","index.html":"&c+LsaIXIVMFXYjv5PkbgwjzxQ967QYRER5yy0YgpnZo=.sha256","vue-material.js":"&K5cdLqXYCENPak/TCINHQhyJhpS4G9DlZHGwoh/LF2g=.sha256","tf-user.js":"&cI/JLy83mOngcqYCEP8Vej8urDvAQAV1WxFsL67/K3M=.sha256","tf-message.js":"&qFKclMumLUufurbQyh2MrjwBG6E9w3L7HLfpelzmxzc=.sha256","tf.js":"&U4OySOFb2gqZJBgm+dUT44+OigJEgeNnJk3aYilnKNg=.sha256","commonmark.min.js":"&EP0OeR9zyLwZannz+0ga4s9AGES2RLvvIIQYHqqV6+k=.sha256","vue.js":"&g1wvA+yHl1sVC+eufTsg9If7ZeVyMTBU+h0tks7ZNzE=.sha256","vue-material-theme-default-dark.css":"&RP2nr+2CR18BpHHw5ST9a5GJUCOG9n0G2kuGkcQioWE=.sha256","vue-material.min.css":"&kGbUM2QgFSyHZRzqQb0b+0S3EVIlZ0AXpdiAVjIhou8=.sha256","roboto.css":"&jJv43Om673mQO5JK0jj7714s5E+5Yrf82H6LcDx7wUs=.sha256","material-icons.css":"&a28PdcVvgq/DxyIvJAx/e+ZOEtOuHnr3kjLWKyzH11M=.sha256","tf-shared.js":"&LXyUSm6zSakN/ghJlZ1Qg2VJfV5alhN0gl8F7txIIOU=.sha256","style.css":"&qegBNCrVUihxffRUxGFuG/6u+0Y6d18zHtfNHBZtZ04=.sha256","emojis.json":"&h3P4pez+AI4aYdsN0dJ3pbUEFR0276t9AM20caj/W/s=.sha256","emojis.js":"&zgD9eGT2WzOpHkQyraApePUZ7w5NdUFG+ip8KnSVGqw=.sha256"}}

View File

@ -1,625 +0,0 @@
import * as tfrpc from '/tfrpc.js';
const k_posts_max = 40;
const k_votes_max = 20;
var g_ready = false;
var g_selected = null;
let g_whoami = null;
var g_blocking_cache = {};
var g_following_cache = {};
var g_following_deep_cache = {};
var g_sequence = {};
async function following(db, id) {
if (g_following_cache[id]) {
return g_following_cache[id];
}
var o = await db.get(id + ":following");
const k_version = 5;
var f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {users: [], sequence: 0, version: k_version};
}
f.users = new Set(f.users);
await ssb.sqlStream(
"SELECT "+
" sequence, "+
" json_extract(content, '$.contact') AS contact, "+
" json_extract(content, '$.following') AS following "+
"FROM messages "+
"WHERE "+
" author = ?1 AND "+
" sequence > ?2 AND "+
" json_extract(content, '$.type') = 'contact' "+
"UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
"ORDER BY sequence",
[id, f.sequence],
function(row) {
if (row.following) {
f.users.add(row.contact);
} else {
f.users.delete(row.contact);
}
if (row.sequence) {
f.sequence = row.sequence;
}
});
g_sequence[id] = f.sequence;
var as_set = f.users;
f.users = Array.from(f.users).sort();
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":following", j);
}
f.users = as_set;
g_following_cache[id] = f.users;
return f.users;
}
async function followingDeep(db, seed_ids, depth, blocked) {
if (depth <= 0) {
return seed_ids;
}
var key = JSON.stringify([seed_ids, depth, blocked]);
if (g_following_deep_cache[key]) {
return g_following_deep_cache[key];
}
var f = await Promise.all(seed_ids.map(x => following(db, x).then(x => [...x])));
var ids = [].concat(...f);
if (blocked) {
ids = ids.filter(x => !blocked.has(x));
}
var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1, blocked);
x = [...new Set([].concat(...x, ...seed_ids))].sort();
g_following_deep_cache[key] = x;
return x;
}
async function blocking(db, id) {
if (g_blocking_cache[id]) {
return g_blocking_cache[id];
}
var o = await db.get(id + ":blocking");
const k_version = 5;
var f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {users: [], sequence: 0, version: k_version};
}
f.users = new Set(f.users);
if (!g_sequence[id] || g_sequence[id] > f.sequence) {
await ssb.sqlStream(
"SELECT "+
" sequence, "+
" json_extract(content, '$.contact') AS contact, "+
" json_extract(content, '$.blocking') AS blocking "+
"FROM messages "+
"WHERE "+
" author = ?1 AND "+
" sequence > ?2 AND "+
" json_extract(content, '$.type') = 'contact' "+
"UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+
"ORDER BY sequence",
[id, f.sequence],
function(row) {
if (row.blocking) {
f.users.add(row.contact);
} else {
f.users.delete(row.contact);
}
if (row.sequence) {
f.sequence = row.sequence;
}
});
g_sequence[id] = f.sequence;
}
var as_set = f.users;
f.users = Array.from(f.users).sort();
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":blocking", j);
}
f.users = as_set;
g_blocking_cache[id] = f.users;
return f.users;
}
async function getAbout(db, id) {
var o = await db.get(id + ":about");
const k_version = 5;
var f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {about: {}, sequence: 0, version: k_version};
}
if (g_sequence[id] === undefined || g_sequence[id] > f.sequence) {
await ssb.sqlStream(
"SELECT "+
" sequence, "+
" content "+
"FROM messages "+
"WHERE "+
" author = ?1 AND "+
" sequence > ?2 AND "+
" json_extract(content, '$.type') = 'about' AND "+
" json_extract(content, '$.about') = ?1 "+
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+
"ORDER BY sequence",
[id, f.sequence],
function(row) {
if (row.content) {
var about = {};
try {
about = JSON.parse(row.content);
} catch {
}
delete about.about;
delete about.type;
f.about = Object.assign(f.about, about);
}
if (row.sequence) {
f.sequence = Math.max(f.sequence, row.sequence);
}
});
g_sequence[id] = f.sequence;
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":about", j);
}
}
return f.about;
}
function fnv32a(value)
{
var result = 0x811c9dc5;
for (var i = 0; i < value.length; i++) {
result ^= value.charCodeAt(i);
result += (result << 1) + (result << 4) + (result << 7) + (result << 8) + (result << 24);
}
return result >>> 0;
}
async function getRecentPostsSingleId(db, id, limit) {
var recent = [];
await ssb.sqlStream(
"SELECT "+
" rowid, "+
" id, "+
" timestamp "+
"FROM messages "+
"WHERE "+
" author = ? AND "+
" json_extract(content, '$.type') = 'post' "+
"ORDER BY sequence DESC LIMIT ?",
[id, limit],
function(row) {
if (row.id) {
recent.push({id: row.id, timestamp: row.timestamp});
}
});
recent.sort((x, y) => y.timestamp - x.timestamp);
return recent.map(x => x.id);
}
async function getRecentPostIds(db, id, ids, limit) {
if (ids.length == 1) {
return await getRecentPostsSingleId(db, ids[0], limit);
}
const k_version = 11;
const k_batch_max = 32;
var o = await db.get(id + ':recent_posts');
var recent = [];
var f = o ? JSON.parse(o) : o;
var ids_hash = fnv32a(JSON.stringify(ids));
if (!f || f.version != k_version || f.ids_hash != ids_hash) {
f = {recent: [], rowid: 0, version: k_version, ids_hash: ids_hash};
}
var row_id_max = 0;
await ssb.sqlStream(
"SELECT MAX(rowid) as rowid FROM messages",
[],
function(row) {
row_id_max = row.rowid;
});
for (var i = 0; i < ids.length; i += k_batch_max) {
var ids_batch = ids.slice(i, Math.min(i + k_batch_max, ids.length));
await ssb.sqlStream(
"SELECT "+
" rowid, "+
" id, "+
" timestamp "+
"FROM messages "+
"WHERE "+
" rowid > ? AND "+
" rowid <= ? AND "+
" author IN (" + ids_batch.map(x => '?').join(", ") + ") "+
"ORDER BY timestamp DESC LIMIT ?",
[].concat([f.rowid, row_id_max], ids_batch, [limit]),
function(row) {
if (row.id) {
recent.push({id: row.id, timestamp: row.timestamp});
}
});
}
f.rowid = row_id_max;
f.recent = [].concat(recent, f.recent);
var have = {};
f.recent = f.recent.filter(function(x) {
if (!have[x.id]) {
have[x.id] = true;
return true;
}
});
f.recent.sort((x, y) => y.timestamp - x.timestamp);
f.recent = f.recent.slice(0, limit);
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":recent_posts", j);
}
return f.recent.map(x => x.id);
}
async function getRecentPostIds2(db, id, ids, start_time) {
if (ids.length == 1) {
return getRecentPostsSingleId(db, ids[0], 20);
}
const k_batch_max = 32;
var row_id_max = 0;
await ssb.sqlStream(
"SELECT MAX(rowid) as rowid FROM messages",
[],
function(row) {
row_id_max = row.rowid;
});
var posts_by_author = {};
for (var i = 0; i < ids.length; i += k_batch_max) {
var ids_batch = ids.slice(i, Math.min(i + k_batch_max, ids.length));
await ssb.sqlStream(
"SELECT "+
" author, "+
" id "+
"FROM messages "+
"WHERE "+
" author IN (" + ids_batch.map(x => '?').join(", ") + ") AND "+
" timestamp > ? AND "+
" rowid <= ? AND "+
" json_extract(content, '$.type') = 'post' "+
"ORDER BY timestamp DESC",
[].concat(ids_batch, [start_time, row_id_max]),
function(row) {
if (row.id) {
if (!posts_by_author[row.author]) {
posts_by_author[row.author] = [];
}
posts_by_author[row.author].push(row.id);
}
});
}
return Object.values(posts_by_author).map(x => x[0]);
}
async function getRelatedPostIds(db, message, ids, limit) {
const k_batch_max = 16;
var recent = [];
var row_id_max = 0;
await ssb.sqlStream(
"SELECT MAX(rowid) as rowid FROM messages",
[],
function(row) {
row_id_max = row.rowid;
});
var id = message.id;
try {
id = JSON.parse(message.content).root || id;
} catch {
}
for (var i = 0; i < ids.length; i += k_batch_max) {
var ids_batch = ids.slice(i, Math.min(i + k_batch_max, ids.length));
await ssb.sqlStream(
"SELECT "+
" rowid, "+
" id, "+
" timestamp "+
"FROM messages "+
"WHERE "+
" timestamp >= ? AND "+
" rowid <= ? AND "+
" author IN (" + ids_batch.map(x => '?').join(", ") + ") AND "+
" json_extract(content, '$.type') = 'post' AND "+
" (id = ? OR json_extract(content, '$.root') = ?) "+
"ORDER BY timestamp DESC LIMIT ?",
[].concat([message.timestamp || 0, row_id_max], ids_batch, [message.id, id, limit]),
function(row) {
if (row.id) {
recent.push({id: row.id, timestamp: row.timestamp});
}
});
}
recent.sort((x, y) => y.timestamp - x.timestamp);
recent = recent.slice(0, limit);
return recent.map(x => x.id);
}
async function getVotes(db, id) {
var o = await db.get(id + ":votes");
const k_version = 7;
var f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {votes: [], sequence: 0, version: k_version};
}
if (g_sequence[id] === undefined || g_sequence[id] > f.sequence) {
var votes = [];
await ssb.sqlStream(
"SELECT "+
" author, "+
" id, "+
" sequence, "+
" timestamp, "+
" content "+
"FROM messages "+
"WHERE "+
" author = ? AND "+
" sequence > ? AND "+
" json_extract(content, '$.type') = 'vote' "+
"UNION SELECT NULL, NULL, MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ? "+
"ORDER BY sequence DESC LIMIT ?",
[id, f.sequence, id, k_votes_max],
function(row) {
if (row.id) {
votes.push(row);
}
if (row.sequence) {
f.sequence = Math.max(f.sequence, row.sequence);
}
});
g_sequence[id] = f.sequence;
f.votes = [].concat(votes, f.votes).slice(0, k_votes_max);
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":votes", j);
}
}
return f.votes;
}
async function getPosts(db, ids) {
var posts = [];
if (ids.length) {
await ssb.sqlStream(
"SELECT rowid, * FROM messages WHERE id IN (" + ids.map(x => "?").join(", ") + ") ORDER BY timestamp DESC",
ids,
row => posts.push(row));
}
return posts;
}
tfrpc.register(async function ready() {
let identities = await ssb.getIdentities();
let whoami = await app.localStorageGet('whoami');
await tfrpc.rpc.set_identities(identities);
g_ready = true;
refresh_internal(whoami, g_selected, true);
});
tfrpc.register(async function store_blob(blob) {
if (Array.isArray(blob)) {
blob = Uint8Array.from(blob);
}
return await ssb.blobStore(blob);
});
ssb.addEventListener('broadcasts', async function() {
await tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts());
});
core.register('onConnectionsChanged', async function() {
var connections = await ssb.connections();
await tfrpc.rpc.set('connections', connections);
});
async function updateSequences(db) {
var k_batch_max = 100;
var changes = {};
var keys = Object.keys(g_sequence);
for (var i = 0; i < keys.length; i += k_batch_max) {
var ids_batch = keys.slice(i, Math.min(i + k_batch_max, keys.length));
await ssb.sqlStream(
"SELECT "+
" author, "+
" MAX(sequence) AS sequence "+
"FROM messages "+
"WHERE "+
" author IN (" + ids_batch.map(x => '?').join(", ") + ")",
ids_batch,
function(row) {
if (g_sequence[row.author] != row.sequence) {
g_sequence[row.author] = row.sequence;
changes[row.author] = row.sequence;
}
});
}
return changes;
}
async function refresh_internal(whoami, selected, force) {
if (whoami !== g_whoami || selected !== g_selected || force) {
if (g_whoami !== whoami && whoami) {
await app.localStorageSet('whoami', whoami);
}
g_whoami = whoami;
g_selected = selected;
} else {
return;
}
if (typeof(whoami) !== 'string') {
return;
}
if (!g_ready) {
return;
}
var timing = [];
timing.push({name: 'start', time: new Date()});
g_following_cache = {};
g_following_deep_cache = {};
await tfrpc.rpc.clear();
await tfrpc.rpc.set_identities(await ssb.getIdentities());
await tfrpc.rpc.set('all_identities', await ssb.getAllIdentities());
await tfrpc.rpc.set('selected', selected);
var db = await database("ssb");
g_sequence = {};
try {
g_sequence = JSON.parse(await db.get('sequence'));
} catch (e) {
}
await updateSequences(db);
timing.push({name: 'init', time: new Date()});
var blocked = await blocking(db, whoami);
timing.push({name: 'blocked', time: new Date()});
var all_followed = await followingDeep(db, [whoami], 2, blocked);
timing.push({name: 'all_followed', time: new Date()});
let actual_selected = (selected ? [selected] : all_followed) ?? [];
await Promise.all([
tfrpc.rpc.set('whoami', whoami),
tfrpc.rpc.set('broadcasts', await ssb.getBroadcasts()),
tfrpc.rpc.set('connections', await ssb.connections()),
tfrpc.rpc.set('apps', await core.apps()),
]);
timing.push({name: 'core', time: new Date()});
var ids;
if (selected && selected.startsWith('%')) {
var m = await getPosts(db, [selected]);
m = m.length ? m[0] : {id: selected};
ids = await getRelatedPostIds(db, m, all_followed, k_posts_max);
} else {
ids = await getRecentPostIds2(db, whoami, actual_selected, (new Date()).valueOf() - (24 * 60 * 60 * 1000));
}
timing.push({name: 'get_post_ids', time: new Date()});
var posts = await getPosts(db, ids);
timing.push({name: 'get_posts', time: new Date()});
var roots = posts.map(function(x) {
try {
return JSON.parse(x.content).root;
} catch {
return null;
}
});
var have = new Set(posts.map(x => x.id));
roots = [...new Set(roots)].filter(x => x && !have.has(x));
var all_posts = [].concat(posts, await getPosts(db, roots));
timing.push({name: 'get_root_posts', time: new Date()});
await tfrpc.rpc.push_posts(all_posts);
timing.push({name: 'send_posts', time: new Date()});
let all_users = {};
await Promise.all(all_followed.map(id => getAbout(db, id).then(function(results) {
if (Object.keys(results).length) {
all_users[id] = results;
}
})));
await tfrpc.rpc.push_users(all_users);
timing.push({name: 'about', time: new Date()});
var all_votes = [];
for (let id of all_followed) {
var results = await getVotes(db, id);
if (results.length) {
all_votes.push(results);
}
}
all_votes = all_votes.flat();
await tfrpc.rpc.push_votes(all_votes);
timing.push({name: 'votes', time: new Date()});
if (selected && selected.length == 1 && selected[0].startsWith('@')) {
let size = 0;
await ssb.sqlStream(
'SELECT SUM(LENGTH(content)) AS length FROM messages WHERE author = ?1',
selected,
function(row) {
size = row.length;
});
let users = {};
users[selected[0]] = {size: size};
await tfrpc.rpc.push_users(users);
}
await tfrpc.rpc.push_following(Object.fromEntries(all_followed.map(id => [id, [...(g_following_cache[id] || [])]])));
timing.push({name: 'following', time: new Date()});
await tfrpc.rpc.push_blocking(whoami, [...(g_blocking_cache[whoami] || [])]);
timing.push({name: 'send_blocking', time: new Date()});
await db.set('sequence', JSON.stringify(g_sequence));
var times = {};
var previous = null;
for (let t of timing) {
times[t.name] = (t.time - (previous || t).time) / 1000.0 + ' s';
previous = t;
}times.total = (new Date() - timing[0].time) / 1000.0 + ' s';
await tfrpc.rpc.ready(times);
}
ssb.addEventListener('message', async function(id) {
var db = await database("ssb");
var posts = await getPosts(db, [id]);
for (let post of posts) {
if (post.author == g_whoami ||
JSON.parse(post.content).type != 'post') {
await tfrpc.rpc.push_posts([post]);
} else {
await tfrpc.rpc.add_unread(1);
}
}
});
tfrpc.register(async function refresh(whoami, selected, force) {
return refresh_internal(whoami, selected, force);
});
tfrpc.register(async function createIdentity() {
return ssb.createIdentity();
});
tfrpc.register(async function appendMessage(message) {
await addAppSources(message);
return ssb.appendMessageWithIdentity(g_whoami, message);
});
async function addAppSources(message) {
if (message.mentions) {
for (let mention of message.mentions) {
if (mention.type == 'application/tildefriends') {
var blob = await ssb.blobGet(mention.link);
var json = JSON.parse(utf8Decode(blob));
for (let file of Object.keys(json.files)) {
message.mentions.push({
name: file,
link: json.files[file],
});
}
}
}
}
}
core.register('message', async function(m) {
if (m.message) {
if (m.message.connect) {
await ssb.connect(m.message.connect);
}
} else if (m.event == 'hashChange') {
let hash = m.hash.length > 1 ? m.hash.substring(1) : null;
let changed = g_selected !== hash;
await refresh_internal(g_whoami, hash, changed);
} else if (m.event == 'focus' || m.event == 'blur') {
/* Shh. */
} else {
print(JSON.stringify(m));
}
});
async function main() {
if (core.user?.credentials?.permissions?.authenticated) {
await app.setDocument(utf8Decode(await getFile("index.html")));
} else {
await app.setDocument('<div style="color: #f00">You must be signed in to use this app at this time. Login at the top right.</div>');
}
}
main();

File diff suppressed because one or more lines are too long

View File

@ -1,95 +0,0 @@
let g_emojis;
function get_emojis() {
if (g_emojis) {
return Promise.resolve(g_emojis);
}
return fetch('emojis.json').then(function(result) {
g_emojis = result.json();
return g_emojis;
});
}
export function picker(callback, anchor) {
get_emojis().then(function(json) {
let existing = document.getElementById('emoji_picker');
if (existing) {
existing.parentElement.removeChild(existing);
return;
}
let div = document.createElement('div');
div.id = 'emoji_picker';
div.style.color = '#000';
div.style.background = '#fff';
div.style.border = '1px solid #000';
div.style.display = 'block';
div.style.position = 'absolute';
div.style.maxWidth = '16em';
div.style.maxHeight = '16em';
div.style.overflow = 'scroll';
div.style.fontWeight = 'bold';
let input = document.createElement('input');
input.type = 'text';
div.appendChild(input);
let list = document.createElement('div');
div.appendChild(list);
function refresh() {
while (list.firstChild) {
list.removeChild(list.firstChild);
}
let search = input.value;
console.log('refrsh', search);
Object.entries(json).forEach(function(row) {
let header = document.createElement('div');
header.appendChild(document.createTextNode(row[0]));
list.appendChild(header);
let any = false;
for (let entry of row[1]) {
if (search &&
search.length &&
entry.name.indexOf(search) == -1) {
continue;
}
let emoji = document.createElement('span');
const k_size = '1.25em';
emoji.style.width = k_size;
emoji.style.maxWidth = k_size;
emoji.style.minWidth = k_size;
emoji.style.height = k_size;
emoji.style.maxHeight = k_size;
emoji.style.minHeight = k_size;
emoji.style.display = 'inline-block';
emoji.style.overflow = 'hidden';
emoji.style.cursor = 'pointer';
emoji.onclick = function() {
callback(entry);
div.parentElement.removeChild(div);
}
emoji.title = entry.name;
emoji.appendChild(document.createTextNode(entry.emoji));
list.appendChild(emoji);
any = true;
}
if (!any) {
list.removeChild(header);
}
});
}
refresh();
input.oninput = refresh;
document.body.appendChild(div);
if (anchor) {
let rect = anchor.getBoundingClientRect();
if (rect.top < window.innerHeight / 2) {
div.style.top = rect.bottom + 'px';
} else {
div.style.top = (rect.top - div.clientHeight) + 'px';
}
if (rect.left < window.clientWidth / 2) {
div.style.left = rect.left + 'px';
} else {
div.style.left = (rect.left - div.clientWidth) + 'px';
}
}
});
}

File diff suppressed because it is too large Load Diff

View File

@ -1,201 +0,0 @@
<html>
<head>
<meta content="width=device-width,initial-scale=1,minimal-ui" name="viewport">
<link rel="stylesheet" href="roboto.css">
<link rel="stylesheet" href="material-icons.css">
<link rel="stylesheet" href="vue-material.min.css">
<link rel="stylesheet" href="vue-material-theme-default-dark.css">
<link rel="stylesheet" href="style.css">
<script src="vue.js"></script>
<script src="vue-material.js"></script>
<script src="commonmark.min.js"></script>
<script src="tf-shared.js" type="module"></script>
<script src="tf-user.js" type="module"></script>
<script src="tf-message.js" type="module"></script>
<script src="tf.js" type="module"></script>
<base target="_top">
</head>
<body style="color: #fff">
<div id="app">
<md-dialog :md-active.sync="show_connect_dialog">
<md-dialog-title>Connect</md-dialog-title>
<md-dialog-content>
<md-field>
<label>net:127.0.0.1:8008~shs:id</label>
<md-input v-model="connect"></md-input>
</md-field>
</md-dialog-content>
<md-dialog-actions>
<md-button class="md-primary" @click="ssb_connect(connect); connect = null; show_connect_dialog = false">Connect</md-button>
<md-button @click="connect = null; show_connect_dialog = false">Cancel</md-button>
</md-dialog-actions>
</md-dialog>
<md-app style="position: absolute; height: 100%; width: 100%">
<md-app-toolbar class="md-primary">
<span class="md-title">Tilde Scuttlebutt</span>
</md-app-toolbar>
<md-app-content>
<md-tabs>
<md-tab md-label="News">
<md-app-toolbar class="md-secondary" v-show="unread > 0">
<md-button @click="refresh()" class="md-raised md-primary">Refresh</md-button>
<span class="md-title">{{unread}} unread item{{unread == 1 ? '' : 's'}}</span>
</md-app-toolbar>
<md-app class="md-elevation-8">
<md-app-toolbar>
<div>Welcome, <tf-user :id="whoami"></tf-user></div>
<div>
<md-field>
<md-select v-model="whoami" name="whoami" id="whoami">
<md-option v-for="identity in identities" v-bind:key="identity" :value="identity">{{identity}}</md-option>
</md-select>
</md-field>
</div>
<div>
<md-button @click="create_identity()">
Create Identity
</md-button>
</div>
</md-app-toolbar>
<md-app-content>
<span v-if="times?.total">
Loaded in {{times.total}}.
<md-tooltip v-if="Object.keys(times).length" style="height: auto">
<div v-for="key in Object.keys(times)">{{key}}: {{times[key]}}</div>
</md-tooltip>
</span>
<span v-else>
Loading...
</span>
</md-app-content>
</md-app>
<md-card class="md-elevation-8" style="margin: 1em">
<md-card-header>
<div class="md-title">What's up?</div>
</md-card-header>
<md-card-content>
<md-chip v-if="reply_root || reply_branch" md-deletable @md-delete="reply_root = null; reply_branch = null">Replying in thread {{reply_root}} to message {{reply_branch}}</md-chip>
<md-chip v-for="link in Object.keys(mentions)" v-bind:key="link" md-deletable @md-delete="remove_from_mentions(link)">
{{mentions[link].name}}: {{link}}
</md-chip>
<div class="md-layout">
<md-field class="md-layout-item">
<label>Post a message</label>
<md-textarea id="post_text" v-model="post_text" v-on:paste="paste" md-autogrow></md-textarea>
</md-field>
<md-card class="md-layout-item" v-if="post_text && post_text.length">
<md-card-content v-html="markdown(post_text)"></md-card-content>
<md-card-content>
<md-card-media v-for="link in Object.keys(mentions)" v-bind:key="link">
<img v-if="(mentions[link].type || '').startsWith('image/')" :src="'/' + link + '/view'" class="md-elevation-4" style="margin: 4px; max-width: 320px; max-height: 240px">
</md-card-media>
</md-card-content>
</md-card>
</div>
</md-card-content>
<md-card-actions>
<md-button class="md-icon-button" @click="attach">
<md-icon>attach_file</md-icon>
</md-button>
<md-menu>
<md-button md-menu-trigger>Share App</md-button>
<md-menu-content>
<md-menu-item v-for="app in Object.keys(apps)" v-bind:key="app" @click="add_app_to_mentions(app)">
{{app}}
</md-menu-item>
</md-menu-content>
</md-menu>
<md-button class="md-raised md-primary" v-on:click="post_message()">Submit Post</md-button>
</md-card-actions>
</md-card>
<md-button v-if="selected !== undefined" class="md-raised md-primary" style="margin: 1em" @click="selected = null">
<md-icon>home</md-icon> Home
</md-button>
<md-card v-if="selected && selected.charAt(0) == '@'" class="md-raised md-elevation-8" style="margin: 1em">
<md-card-header>
<md-card-header-text>
<div class="md-title">{{users[selected] &amp;&amp; users[selected].name ? users[selected].name : selected}}</div>
<div class="md-subhead" v-if="users[selected] && users[selected].name">{{selected}}</div>
</md-card-header-text>
<md-card-media v-if="users[selected] && users[selected].image" class="md-medium">
<div><img :src="'/' + (typeof(users[selected].image) == 'string' ? users[selected].image : users[selected].image.link) + '/view'"></div>
</md-card-media>
</md-card-header>
<md-card-content>
<div v-if="selected == whoami">
<img v-if="edit_profile_image" :src="'/' + edit_profile_image + '/view'">
<md-field>
<label>Name</label>
<md-input v-model="edit_profile_name"></md-input>
</md-field>
<md-field>
<label>Description</label>
<md-textarea v-model="edit_profile_description"></md-textarea>
</md-field>
</div>
<template v-if="users[selected]">
<div>Total Message Size: {{human_size(users[selected].size)}}</div>
<div v-if="users[selected].name">{{selected}}</div>
<div v-html="markdown(users[selected].description)"></div>
</template>
<md-card-actions>
<md-button class="md-icon-button" @click="attach('profile')">
<md-icon>attach_file</md-icon>
</md-button>
<md-menu md-size="small" v-if="users[selected] && users[selected].followers">
<md-button md-menu-trigger>{{Object.keys(users[selected].followers).length}} followers</md-button>
<md-menu-content>
<md-menu-item v-for="id of Object.keys(users[selected].followers)" v-bind:key="id"><tf-user :id="id"></tf-user></md-menu-item>
</md-menu-content>
</md-menu>
<md-menu md-size="small" v-if="users[selected] && users[selected].following">
<md-button md-menu-trigger>{{Object.keys(users[selected].following).length}} following</md-button>
<md-menu-content>
<md-menu-item v-for="id of Object.keys(users[selected].following)" v-bind:key="id"><tf-user :id="id"></tf-user></md-menu-item>
</md-menu-content>
</md-menu>
<template v-if="selected != whoami && users[whoami] && users[whoami].blocking">
<md-button @click="block(selected)" v-if="!users[whoami].blocking[selected]" class="md-raised md-secondary">Block</md-button>
<md-button @click="unblock(selected)" v-else class="md-raised md-secondary">Unblock</md-button>
</template>
<template v-if="selected != whoami && users[whoami] && users[whoami].following">
<md-button @click="follow(selected)" v-if="!users[whoami].following[selected]" class="md-raised md-secondary">Follow</md-button>
<md-button @click="unfollow(selected)" v-else class="md-raised md-secondary">Unfollow</md-button>
</template>
<md-button @click="save_profile" v-if="selected == whoami" class="md-primary md-raised">Save Profile</md-button>
</md-card-actions>
</md-card-content>
</md-card>
<template v-if="messages.length">
<tf-message
v-for="message in messages"
v-bind:message="message"
v-bind:messages="messages"
v-bind:key="message.id"
v-bind:votes="votes"></tf-message>
</template>
<md-empty-state v-else-if="loading" md-label="Loading...">
<md-progress-spinner md-mode="indeterminate"></md-progress-spinner>
</md-empty-state>
<md-empty-state v-else md-label="Nothing to see here."></md-empty-state>
</md-tab>
<md-tab md-label="Network">
<md-list>
<md-subheader>Broadcasts</md-subheader>
<md-list-item v-for="broadcast in broadcasts" v-bind:key="JSON.stringify(broadcast)" @click="ssb_connect(broadcast)">{{broadcast.address}}:{{broadcast.port}} <tf-user :id="broadcast.pubkey"></tf-user></md-list-item>
<md-subheader>Connections</md-subheader>
<md-list-item v-for="connection in connections" v-bind:key="'connection-' + JSON.stringify(connection)"><tf-user :id="connection"></tf-user></md-list-item>
<md-list-item @click="show_connect_dialog = true">Connect</md-list-item>
<md-subheader>Local Accounts</md-subheader>
<md-list-item v-for="identity in all_identities" v-bind:key="'connection-identity-' + identity"><tf-user :id="identity"></tf-user></md-list-item>
</md-list>
</md-tab>
</md-tabs>
</md-app-content>
</md-app>
</div>
</body>
</html>

View File

@ -1,20 +0,0 @@
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/materialicons/v118/flUhRq6tzZclQEJ-Vdg-IuiaDsNZ.ttf) format('truetype');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
}

View File

@ -1,44 +0,0 @@
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/materialicons/v118/flUhRq6tzZclQEJ-Vdg-IuiaDsNZ.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v29/KFOkCnqEu92Fr1Mu52xP.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/roboto/v29/KFOmCnqEu92Fr1Me5Q.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmEU9vAw.ttf) format('truetype');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmWUlvAw.ttf) format('truetype');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
}

View File

@ -1,4 +0,0 @@
img {
max-width: 33%;
max-height: 33%;
}

View File

@ -1,181 +0,0 @@
import * as tfrpc from '/static/tfrpc.js';
import * as tfshared from './tf-shared.js';
import * as tf from './tf.js';
import * as emojis from './emojis.js';
Vue.component('tf-message', {
props: ['message', 'messages', 'votes'],
data: function() { return { showRaw: false } },
computed: {
content_json: function() {
try {
return JSON.parse(this.message.content);
} catch {
return undefined;
}
},
content_raw: function() {
try {
return JSON.stringify(JSON.parse(this.message.content), null, 2);
} catch {
return this.message.content;
}
},
timestamp_relative: function() {
var units = [
{value: 1, name: 'milliseconds'},
{value: 1000, name: 'seconds'},
{value: 1000 * 60, name: 'minutes'},
{value: 1000 * 60 * 60, name: 'hours'},
{value: 1000 * 60 * 60 * 24, name: 'days'},
{value: 1000 * 60 * 60 * 24 * 7, name: 'weeks'},
{value: 1000 * 60 * 60 * 24 * 30, name: 'months'},
{value: 1000 * 60 * 60 * 24 * 365, name: 'years'},
];
var v = new Date().valueOf() - this.message.timestamp;
var result = null;
for (let unit of units) {
if (v >= unit.value) {
result = Math.round(v / unit.value) + ' ' + unit.name + ' ago';
}
}
return result;
},
},
methods: {
markdown: tfshared.markdown,
set_reply: function() {
tf.g_data.reply_root = this.content_json.root || this.message.id;
tf.g_data.reply_branch = this.message.id;
},
vote: function(emoji) {
let reaction = emoji.emoji;
var message = this.message.id;
if (confirm('Are you sure you want to react with ' + reaction + ' to ' + message + '?')) {
tfrpc.rpc.appendMessage({
type: 'vote',
vote: {
link: message,
value: 1,
expression: reaction,
},
}).catch(function(error) {
alert(error?.message);
});
}
},
show_emoji_picker: function(event) {
let self = this;
emojis.picker(x => self.vote(x), event.srcElement);
},
show_message: function() {
window.parent.postMessage({
action: 'setHash',
hash: this.message.id,
}, '*');
},
expand_image: function(event) {
var div = document.createElement('div');
div.style.left = 0;
div.style.top = 0;
div.style.width = '100%';
div.style.height = '100%';
div.style.position = 'fixed';
div.style.background = '#000';
div.style.zIndex = 100;
div.style.display = 'grid';
var img = document.createElement('img');
img.src = event.srcElement.src;
img.style.maxWidth = '100%';
img.style.maxHeight = '100%';
img.style.display = 'block';
img.style.margin = 'auto';
img.style.objectFit = 'contain';
img.style.width = '100%';
div.appendChild(img);
function image_close(event) {
document.body.removeChild(div);
window.removeEventListener('keydown', image_close);
}
div.onclick = image_close;
window.addEventListener('keydown', image_close);
document.body.appendChild(div);
},
},
template: `<md-app class="md-elevation-8" style="margin: 1em" v-if="!content_json || ['pub', 'vote'].indexOf(content_json.type) == -1">
<md-app-toolbar>
<h3>
<md-button class="md-icon-button md-dense" @click="show_message">
<md-icon>percent</md-icon>
</md-button>
<tf-user :id="message.author" v-if="message.author"></tf-user>
</h3>
<template v-if="message.author">
<div style="font-size: x-small">
{{timestamp_relative}}
<md-tooltip style="height: auto">
<div>{{new Date(message.timestamp)}}</div>
<div>{{message.id}}</div>
</md-tooltip>
</div>
<div class="md-toolbar-section-end">
<md-menu>
<md-switch v-model="showRaw"></md-switch>
<md-tooltip>Show Raw Message</md-tooltip>
</md-menu>
</div>
</template>
<template v-else>
<h3>Missing</h3>
</template>
</md-app-toolbar>
<md-app-content>
<template v-if="message.author">
<div v-if="showRaw">
<h1>{{message.id}}</h1>
<pre style="word-wrap: break-all; white-space: pre-wrap">{{content_raw}}</pre>
</div>
<div v-else>
<div v-if="content_json && content_json.type == 'post'">
<div v-html="this.markdown(content_json.text)"></div>
<span v-for="mention in content_json.mentions" v-if="mention.link && typeof(mention.link) == 'string' && mention.link.startsWith('&')">
<a v-if="mention.type == 'application/tildefriends'" :href="'/' + mention.link + '/'" target="_top">{{mention.name}}</a>
<div v-else-if="mention.name && mention.name.startsWith('audio:')">
<audio controls style="height: 32px">
<source :src="'/' + mention.link + '/view'"></source>
</audio>
</div>
<img v-else class="md-elevation-4" style="margin: 4px; max-width: 320px; max-height: 240px" :src="'/' + mention.link + '/view'" v-on:click="expand_image"></img>
</span>
</div>
<div v-else-if="content_json && content_json.type == 'tildefriends-app'">
<div v-html="this.markdown(content_json.text)"></div>
<md-button target="_top" :href="'/' + message.id + '/'">{{content_json.name || 'tildefriends-app'}}</md-button>
</div>
<div v-else-if="content_json && content_json.type == 'contact'"><tf-user :id="message.author"></tf-user> {{content_json.following ? '==&gt;' : '=/=&gt;'}} <tf-user :id="content_json.contact"></tf-user></div>
<div v-else>{{message.content}}</div>
</div>
</template>
<template v-else>
{{message.id}}
</template>
<tf-message v-for="sub_message in (message.children || [])" v-bind:message="sub_message" v-bind:messages="messages" v-bind:votes="votes" v-bind:key="sub_message.id"></tf-message>
<md-chip md-clickable v-for="v in Object.keys(votes[message.id] || {})" v-bind:key="v" @click="vote">
{{v + (votes[message.id][v].length > 1 ? ' (' + votes[message.id][v].length + ')' : '')}}
<md-tooltip style="height: auto; background-color: #000; border: 1px solid #fff">
<tf-user v-for="vote in votes[message.id][v]" :id="vote.author" :key="vote.author"></tf-user>
</md-tooltip>
</md-chip>
<md-card-actions v-if="message.author">
<md-button class="md-icon-button" @click="set_reply">
<md-icon>reply</md-icon>
</md-button>
<md-menu>
<md-button class="md-icon-button" @click="show_emoji_picker">
<md-icon>thumb_up</md-icon>
</md-button>
</md-menu>
</md-card-actions>
</md-app-content>
</md-app>`,
});

View File

@ -1,31 +0,0 @@
import * as tf from './tf.js';
export function markdown(md) {
var reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer();
var parsed = reader.parse(md || '');
var walker = parsed.walker();
var event, node;
while ((event = walker.next())) {
node = event.node;
if (event.entering) {
if (node.type == 'link') {
if (node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')) {
node.destination = '#' + node.destination;
} else if (node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')) {
node.destination = '#' + node.destination;
} else if (node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')) {
node.destination = '/' + node.destination + '/view';
}
} else if (node.type == 'image') {
if (node.destination.startsWith('&')) {
node.destination = '/' + node.destination + '/view';
}
}
}
}
return writer.render(parsed);
}

View File

@ -1,26 +0,0 @@
import * as tf from './tf.js';
Vue.component('tf-user', {
props: ['id'],
computed: {
following: {
get: function() {
return tf.g_data.users?.[tf.g_data.whoami]?.following?.[this.id];
},
},
whoami: { get: function() { return tf.g_data.whoami; } },
users: { get: function() { return tf.g_data.users; } },
},
methods: {
show_user: function() {
window.parent.postMessage({
action: 'setHash',
hash: this.id,
}, '*');
},
},
template: `<md-chip :class="following ? 'md-accent' : ''">
<a :href="'#' + id" style="color: #fff">{{users[id] && users[id].name ? users[id].name : id}}</a>
<md-tooltip v-if="users[id] && users[id].name">{{id}}</md-tooltip>
</md-chip>`,
});

View File

@ -1,356 +0,0 @@
import * as tfrpc from '/static/tfrpc.js';
import * as tfshared from './tf-shared.js';
export var g_data = {
whoami: null,
selected: null,
all_identities: [],
identities: [],
connections: [],
messages: [],
messages_by_id: {},
users: {},
broadcasts: [],
show_connect_dialog: false,
show_user_dialog: null,
connect: null,
pubs: [],
votes: {},
apps: {},
reply_root: null,
reply_branch: null,
mentions: {},
unread: 0,
loading: true,
edit_profile_name: null,
edit_profile_description: null,
edit_profile_image: null,
post_text: null,
times: {},
};
var g_load_start = new Date();
var g_data_initial = JSON.parse(JSON.stringify(g_data));
function updateEditUser() {
g_data.edit_profile_name = g_data.users[g_data.whoami] ? g_data.users[g_data.whoami].name : null;
g_data.edit_profile_description = g_data.users[g_data.whoami] ? g_data.users[g_data.whoami].description : null;
g_data.edit_profile_image = g_data.users[g_data.whoami] ? g_data.users[g_data.whoami].image : null;
}
tfrpc.register(function clear() {
g_load_start = new Date();
g_data.loading = true;
Object.keys(g_data_initial).forEach(function(key) {
if (key != 'identities' && key != 'whoami' && key != 'selected') {
Vue.set(g_data, key, JSON.parse(JSON.stringify(g_data_initial[key])));
}
});
});
tfrpc.register(function set(key, value) {
g_data[key] = value;
});
tfrpc.register(function set_identities(value) {
if (JSON.stringify(g_data.identities) != JSON.stringify(value)) {
g_data.identities = value.map(x => x);
}
});
tfrpc.register(function push_users(users) {
for (let [id, user] of Object.entries(users)) {
Vue.set(g_data.users, id, Object.assign({}, g_data.users[id] || {}, user));
if (id == g_data.whoami) {
updateEditUser();
}
}
});
tfrpc.register(function push_posts(posts) {
for (let new_message of posts) {
new_message.children = [];
var found = false;
var root = JSON.parse(new_message.content).root;
/* If we had inserted a fake root, replace it if we see the real message. */
if (g_data.messages_by_id[new_message.id]) {
var old_message = g_data.messages_by_id[new_message.id];
new_message.children = old_message.children;
for (let child of new_message.children) {
child.parent = new_message;
}
if (old_message.parent) {
old_message.parent.children = old_message.parent.children.filter(x => x != old_message);
} else {
g_data.messages = g_data.messages.filter(x => x != old_message);
}
}
Vue.set(g_data.messages_by_id, new_message.id, new_message);
if (root) {
/* If we don't know of the message's root, add it. */
if (!g_data.messages_by_id[root]) {
var fake_root = {
id: root,
children: [],
timestamp: new_message.timestamp,
content: '{}',
};
Vue.set(g_data.messages_by_id, root, fake_root);
g_data.messages.push(fake_root);
g_data.messages.sort((x, y) => y.timestamp - x.timestamp);
found = true;
}
var message = g_data.messages_by_id[root];
new_message.parent = message;
message.children.push(new_message);
message.children.sort((x, y) => x.timestamp - y.timestamp);
} else {
/* This is just a new message with no root. Add it. */
g_data.messages.push(new_message);
g_data.messages.sort((x, y) => y.timestamp - x.timestamp);
g_data.messages = g_data.messages.slice(0, 32);
}
}
});
tfrpc.register(function push_votes(votes) {
Vue.set(g_data, 'votes', {});
votes.forEach(function(vote) {
var content = JSON.parse(vote.content);
var link = content.vote.link;
if (!g_data.votes[link]) {
Vue.set(g_data.votes, link, {});
}
if (!g_data.votes[link][content.vote.expression]) {
Vue.set(g_data.votes[link], content.vote.expression, []);
}
g_data.votes[link][content.vote.expression].push({author: vote.author, value: content.vote.value});
});
});
tfrpc.register(function push_following(following) {
for (let [id, users] of Object.entries(following)) {
if (!g_data.users[id]) {
Vue.set(g_data.users, id, {});
}
if (!g_data.users[id].following) {
Vue.set(g_data.users[id], 'following', {});
}
for (let user of users) {
Vue.set(g_data.users[id].following, user, true);
if (!g_data.users[user]) {
Vue.set(g_data.users, user, {});
}
if (!g_data.users[user].followers) {
Vue.set(g_data.users[user], 'followers', {});
}
Vue.set(g_data.users[user].followers, id, true);
}
}
});
tfrpc.register(function push_blocking(id, blocking) {
if (!g_data.users[id]) {
Vue.set(g_data.users, id, {});
}
if (!g_data.users[id].blocking) {
Vue.set(g_data.users[id], 'blocking', {});
}
for (let user of blocking) {
Vue.set(g_data.users[id].blocking, user, true);
}
});
tfrpc.register(function add_unread(unread) {
g_data.unread += unread;
});
tfrpc.register(function ready(times) {
g_data.loading = false;
g_data.times = times;
});
window.addEventListener('load', function() {
Vue.use(VueMaterial.default);
var vue = new Vue({
el: '#app',
data: g_data,
watch: {
whoami: function(newValue, oldValue) {
tfrpc.rpc.refresh(newValue, this.selected, oldValue !== undefined);
},
selected: function(newValue, oldValue) {
let self = this;
setTimeout(function() { self.set_hash(); }, 100);
},
},
methods: {
post_message: function() {
var message = {
type: 'post',
text: document.getElementById('post_text').value,
};
if (g_data.reply_root || g_data.reply_branch) {
message.root = g_data.reply_root;
message.branch = g_data.reply_branch;
}
if (Object.keys(g_data.mentions).length) {
message.mentions = Object.values(g_data.mentions);
}
tfrpc.rpc.appendMessage(message).then(function() {
g_data.post_text = null;
Vue.set(g_data, 'mentions', {});
g_data.reply_root = null;
g_data.reply_branch = null;
}).catch(function(error) {
alert(error?.message);
});
},
ssb_connect: function(connection) {
window.parent.postMessage({connect: connection}, '*');
},
content_json: function(message) {
try {
return JSON.parse(message.content);
} catch {
return undefined;
}
},
markdown: tfshared.markdown,
refresh: function() {
tfrpc.rpc.refresh(this.whoami, this.selected, true);
},
add_app_to_mentions: function(app) {
Vue.set(g_data.mentions, g_data.apps[app], {
link: g_data.apps[app],
name: app,
type: 'application/tildefriends',
});
},
remove_from_mentions: function(link) {
Vue.delete(g_data.mentions, link);
},
save_profile: function() {
tfrpc.rpc.appendMessage({
type: 'about',
about: g_data.selected,
name: g_data.edit_profile_name,
description: g_data.edit_profile_description,
image: g_data.edit_profile_image,
}).catch(function(error) {
alert(error?.message);
});
},
follow: function(id) {
if (confirm('Are you sure you want to follow ' + id + '?')) {
tfrpc.rpc.appendMessage({type: "contact", following: true, contact: id}).catch(function(error) {
alert(error?.message);
});
}
},
unfollow: function(id) {
if (confirm('Are you sure you want to unfollow ' + id + '?')) {
tfrpc.rpc.appendMessage({type: "contact", following: false, contact: id}).catch(function(error) {
alert(error?.message);
});
}
},
block: function(id) {
if (confirm('Are you sure you want to block ' + id + '?')) {
tfrpc.rpc.appendMessage({type: "contact", blocking: true, contact: id}).catch(function(error) {
alert(error?.message);
});
}
},
unblock: function(id) {
if (confirm('Are you sure you want to unblock ' + id + '?')) {
tfrpc.rpc.appendMessage({type: "contact", blocking: false, contact: id}).catch(function(error) {
alert(error?.message);
});
}
},
set_hash() {
let hash = this.selected ?? '#';
window.parent.postMessage({
action: 'setHash',
hash: hash,
}, '*');
},
attach(context) {
var input = document.createElement('input');
input.type = 'file';
input.onchange = function(event) {
var file = event.target.files[0];
file.arrayBuffer().then(function(buffer) {
let bin = Array.from(new Uint8Array(buffer));
return tfrpc.rpc.store_blob(bin);
}).then(function(id) {
if (context == 'profile') {
g_data.edit_profile_image = id;
} else {
g_data.post_text = `${g_data.post_text || ''}\n![${file.name}](${id})`;
Vue.set(g_data.mentions, id, {
link: id,
name: file.name,
type: file.type,
});
}
}).catch(function(e) {
console.log('error', e);
});
};
input.click();
},
paste(event) {
var items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (let item of items) {
var file = item.getAsFile();
if (file) {
file.arrayBuffer().then(function(buffer) {
let bin = Array.from(new Uint8Array(buffer));
return tfrpc.rpc.store_blob(bin);
}).then(function(id) {
g_data.post_text = `${g_data.post_text || ''}\n![${file.name}](${id})`;
Vue.set(g_data.mentions, id, {
link: id,
name: file.name,
type: file.type,
});
});
event.preventDefault();
break;
}
}
},
human_size(bytes) {
if (typeof bytes == 'number') {
let value = bytes;
let unit = 'B';
const k_units = ['kB', 'MB', 'GB', 'TB'];
for (let u of k_units) {
if (value > 1024) {
value /= 1024;
unit = u;
}
}
return Math.round(value * 10) / 10 + ' ' + unit;
} else {
return bytes;
}
},
create_identity() {
if (confirm("Are you sure you would like to create a new identity?")) {
tfrpc.rpc.createIdentity().then(function(id) {
alert(`Identity '${id}' created.`);
}).catch(function(e) {
alert('Error creating a new identity: ' + e);
});
}
},
}
});
tfrpc.rpc.ready();
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -298,7 +298,6 @@ static int _tf_command_export(const char* file, int argc, char* argv[])
"/~cory/db", "/~cory/db",
"/~cory/docs", "/~cory/docs",
"/~cory/follow", "/~cory/follow",
"/~cory/ssb",
"/~cory/ssblit", "/~cory/ssblit",
}; };
for (int i = 0; i < (int)_countof(k_export); i++) for (int i = 0; i < (int)_countof(k_export); i++)