Merge branches/quickjs to trunk. This is the way.

git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3621 ed5197a5-7fde-0310-b194-c3ffbd925b24
This commit is contained in:
2021-01-02 18:10:00 +00:00
parent d293637741
commit 79022e1e1f
703 changed files with 419987 additions and 30640 deletions

1
apps/cory/docs.json Normal file
View File

@ -0,0 +1 @@
{"type":"tildefriends-app","files":{"app.js":"&WCq6ssQedT5denXPXlz2BswPD6hmt++EmWIMIDUMurA=.sha256","index.md":"&Lr7IXs8osbmWz6SDsGTQCiybbxkbWSK2MrUcXMzgqTs=.sha256","todo.md":"&/vzp5PrF93TPAuS/97fbj6+mzyNOxYFjBGQJmZYe4As=.sha256","structure.md":"&xRhQ4Mpom1Idskum07osbBQYcYWroH0sELQBkQHrOMg=.sha256","purpose.md":"&c0/YqFhXC0X3DqiEo55NqzI5wq0VTw6cVZTf/gAWS3w=.sha256","guide.md":"&SgnGL0+rjetY2o9A2+lVRbNvHIkqKwMnZr9gXWneIlc=.sha256"}}

28
apps/cory/docs/app.js Normal file

File diff suppressed because one or more lines are too long

16
apps/cory/docs/guide.md Normal file
View File

@ -0,0 +1,16 @@
# Tilde Friends Developer's Guide
[Back to index](#index)
A Tilde Friends application runs on the server. To make an interesting
application that interacts with the client, it's necessary to understand
how the parts work together.
## Hello, world!
A simple starting point. Presents `Hello, world!` in the browser when
visited.
**app.js**:
```
app.setDocument('<h1>Hello, world!</h1>');
```

11
apps/cory/docs/index.md Normal file
View File

@ -0,0 +1,11 @@
# Tilde Friends Documentation
Tilde Friends is a participating member of a greater social
network, [Secure Scuttlebutt](https://scuttlebutt.nz/),
augmenting it with a way to safely and securely write, share,
and run code.
- [Purpose](#purpose)
- [Structure](#structure)
- [Guide](#guide)
- [TODO](#todo)

4
apps/cory/docs/markdeep.min.js vendored Normal file

File diff suppressed because one or more lines are too long

24
apps/cory/docs/purpose.md Normal file
View File

@ -0,0 +1,24 @@
# Tilde Friends Purpose
[Back to index](#index)
## Beliefs
1. The web is the universal virtual machine.
- It is here, ready to be used from your desktop, laptop, smart phone,
tablet, game console, and smart TV.
- It is not ideal, but it is the best we have right now,
and all signs point to it continuing to improve, at least
in terms of features, security, and device support.
2. Distributed is superior to centralized.
- Distributed services don't need ads.
- Distributed services can't be acquired by evil corporations.
- Distributed services respect the user's privacy.
- Distributed services respect the user.
3. Offline-first is superior to online-only.
- The internet goes down sometimes. Applications should continue
to work.
3. Making and sharing code should be easy.
- Cloning your repository, installing dev tools, running a
docker image, or fighting with dependencies is *not* easy.
- If you see a thing in a web browser, you should be able to click
`edit`, make a change, save, and see the result.
[Wikipedia](https://www.wikipedia.org/) is easy.

View File

@ -0,0 +1,67 @@
# Tilde Friends Structure
[Back to index](#index)
Tilde Friends is a mostly-self-contained executable written in C.
In combines the following key components:
- A Secure Scuttlebutt (SSB) client/server. This talks with other SSB
instances, storing messages and blobs for anyone visible to local
users as they are encountered and sharing anything published locally
as appropriate.
- An sqlite database. This is where the SSB instance stores its data.
The general schema involves a `messages` table, storing mostly JSON,
a `blobs` table storing arbitrary blob data, and a `properties` table,
storing arbitrary state gleaned from `messages` and `blobs`, generally
updated on demand and incrementally.
- A QuickJS runtime. The core process runs stock scripts and has access
and permission to use all resources. All other processes, which
includes everything which runs untrusted code created by Tilde Friends
users, are strictly sandboxed in ways similar to how web browsers run
untrusted code. All attempts to access potentially sensitive resources
are mediated through the core process.
When run with no arguments, it starts a web server on
[http://localhost:12345/](http://localhost:12345/) and an SSB server.
## Web Interface
The Tilde Friends web server provides access to Tilde Friends applications,
which are arbitrary user-defined web applications.
At the top left, in addition to some basic navigation links, is an `edit`
link. Anyone can view, modify, and run in-place the code to any Tilde
Friends application by using the in-browser editor.
At the top right, one can `login` (to save work in their own space)
or `logout` (proceeding as a guest).
The rest of the page is an iframe belonging to the application.
## Special Paths
- `/~user/app/` - Tilde Friends application paths take the form `/~user/app/`, where `user`
is a username of a Tilde Friends account, and `app` is an arbitrary name
of an application saved by the given user.
- `/~user/app/file` - A raw file in an app.
- `/&blobid.ed25519` - A raw blob. Content-Type is inferred for at least
a few common image types.
## Communication Channels
Web Browser <-> Core <-> Sandbox
Visiting an application path delivers stock HTML and JavaScript which
establishes a WebSocket connection back to the server.
At this point, a new sandbox process is started in Tilde Friends, much
as a new sandboxed process might be started for a new tab in a web
browser. This process has a custom RPC connection to the core process
which holds the WebSocket connection to the browser.
The custom RPC communication between the sandbox process and the core
process facilitates calling functions asynchronously. Calling a remote
function (ie. a function in another process) returns a `Promise`. In
addition, any functions passed in either direction are serialized in
such a way that they can be called remotely.
An application will typically call `app.setDocument()` at startup to
populate the app's iframe in the web browser with its own client web
application resources.

44
apps/cory/docs/todo.md Normal file
View File

@ -0,0 +1,44 @@
# Tilde Friends TODO
[Back to index](#index)
## MVP
- release
- blog
- update COPYING
- update README
- auto-populate data on initial launch
- audit + document API exposed to apps
- ssb core
- good refresh
- live updates
- apps
- app messages
- installable apps
- web interface
- live updates
- strip out unnecessary things?
- more raw views until it's more functional?
## Done
- likely classes of script errors
- tf core
- good error feedback
- markdeep demo
- send blobs
## Later
- DB migration
- stop using CDNs
- collect loads of stats
- faster save - parallel / don't save unmodified
- test likely denials of service
- package standalone executable
- ideas
- visualizations / analysis of gps data
- good web interface for managing connections
- identity
- multiple identities
- tie identities to TF login accounts
- tf account timeout why
- make some demo apps
- rock paper scissors, somehow

1
apps/cory/index.json Normal file
View File

@ -0,0 +1 @@
{"type":"tildefriends-app","files":{"app.js":"&GOWmWDTHuNMz2XpxNxspAqsITn/RwvlB4Um6euxHFbU=.sha256","index.html":"&/noD6q/rH4VgszVb6W0D3mnK9t0GbDYYZtBGM/xs2kY=.sha256","vue-material.js":"&K5cdLqXYCENPak/TCINHQhyJhpS4G9DlZHGwoh/LF2g=.sha256"}}

378
apps/cory/index/app.js Normal file
View File

@ -0,0 +1,378 @@
"use strict";
const k_posts_max = 20;
const k_votes_max = 100;
async function following(db, id) {
var o = await db.get(id + ":following");
const k_version = 4;
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],
async function(row) {
if (row.following) {
f.users.add(row.contact);
} else {
f.users.delete(row.contact);
}
f.sequence = row.sequence;
});
f.users = Array.from(f.users);
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":following", j);
}
return f.users;
}
async function followingDeep(db, seed_ids, depth) {
if (depth <= 0) {
return seed_ids;
}
var f = await Promise.all(seed_ids.map(x => following(db, x)));
var ids = [].concat(...f);
var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1);
x = [].concat(...x, ...seed_ids);
return x;
}
async function followers(db, id) {
var o = await db.get(id + ":followers");
const k_version = 2;
var f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {users: [], rowid: 0, version: k_version};
}
f.users = new Set(f.users);
await ssb.sqlStream(
"SELECT "+
" rowid, "+
" author AS contact, "+
" json_extract(content, '$.following') AS following "+
"FROM messages "+
"WHERE "+
" rowid > $1 AND "+
" json_extract(content, '$.type') = 'contact' AND "+
" json_extract(content, '$.contact') = $2 "+
"UNION SELECT MAX(rowid) as rowid, NULL, NULL FROM messages "+
"ORDER BY rowid",
[f.rowid, id],
async function(row) {
if (row.following) {
f.users.add(row.contact);
} else {
f.users.delete(row.contact);
}
f.rowid = row.rowid;
});
f.users = Array.from(f.users);
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":followers", j);
}
return f.users;
}
async function sendUser(db, id) {
return Promise.all([
following(db, id).then(async function(following) {
return app.postMessage({following: {id: id, users: following}});
}),
followers(db, id).then(async function(followers) {
return app.postMessage({followers: {id: id, users: followers}});
}),
]);
}
async function pubsByUser(db, id) {
var o = await db.get(id + ":pubs");
const k_version = 2;
var f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {pubs: [], sequence: 0, version: k_version};
}
f.pubs = Object.fromEntries(f.pubs.map(x => [JSON.stringify(x), x]));
await ssb.sqlStream(
"SELECT "+
" sequence, "+
" json_extract(content, '$.address.host') AS host, "+
" json_extract(content, '$.address.port') AS port, "+
" json_extract(content, '$.address.key') AS key "+
"FROM messages "+
"WHERE "+
" sequence > ?1 AND "+
" author = ?2 AND "+
" json_extract(content, '$.type') = 'pub' "+
"UNION SELECT MAX(sequence) as sequence, NULL, NULL, NULL FROM messages WHERE author = ?2 "+
"ORDER BY sequence",
[f.sequence, id],
async function(row) {
f.sequence = row.sequence;
if (row.host) {
row = {host: row.host, port: row.port, key: row.key};
f.pubs[JSON.stringify(row)] = row;
}
});
f.pubs = Object.values(f.pubs);
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":pubs", j);
}
return f.pubs;
}
async function visiblePubs(db, id) {
var ids = [id].concat(await following(db, id));
var pubs = {};
for (var follow of ids) {
var followPubs = await pubsByUser(db, follow);
for (var pub of followPubs) {
pubs[JSON.stringify(pub)] = pub;
}
}
return Object.values(pubs);
}
async function getAbout(db, id) {
var o = await db.get(id + ":about");
const k_version = 3;
var f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {about: {}, sequence: 0, version: k_version};
}
await ssb.sqlStream(
"SELECT "+
" sequence, "+
" content "+
"FROM messages "+
"WHERE "+
" sequence > ?1 AND "+
" author = ?2 AND "+
" json_extract(content, '$.type') = 'about' AND "+
" json_extract(content, '$.about') = author "+
"UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?2 "+
"ORDER BY sequence",
[f.sequence, id],
async function(row) {
f.sequence = row.sequence;
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);
}
});
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 getRecentPostIds(db, id, ids, limit) {
const k_version = 6;
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};
}
await ssb.sqlStream(
"SELECT "+
" rowid, "+
" id "+
"FROM messages "+
"WHERE "+
" rowid > ? AND "+
" author IN (" + ids.map(x => '?').join(", ") + ") AND "+
" json_extract(content, '$.type') = 'post' "+
"UNION SELECT MAX(rowid) as rowid, NULL FROM messages "+
"ORDER BY rowid DESC LIMIT ?",
[].concat([f.rowid], ids, [limit + 1]),
function(row) {
if (row.id) {
recent.push(row.id);
}
if (row.rowid) {
f.rowid = row.rowid;
}
});
f.recent = [].concat(recent, f.recent).slice(0, limit);
var j = JSON.stringify(f);
if (o != j) {
await db.set(id + ":recent_posts", j);
}
return f.recent;
}
async function getVotes(db, id) {
var o = await db.get(id + ":votes");
const k_version = 2;
var votes = [];
var f = o ? JSON.parse(o) : o;
if (!f || f.version != k_version) {
f = {votes: [], rowid: 0, version: k_version};
}
await ssb.sqlStream(
"SELECT "+
" rowid, "+
" author, "+
" id, "+
" sequence, "+
" timestamp, "+
" content "+
"FROM messages "+
"WHERE "+
" rowid > ? AND "+
" author = ? AND "+
" json_extract(content, '$.type') = 'vote' "+
"UNION SELECT MAX(rowid) as rowid, NULL, NULL AS id, NULL, NULL, NULL FROM messages "+
"ORDER BY rowid DESC LIMIT ?",
[f.rowid, id, k_votes_max],
async function(row) {
if (row.id) {
votes.push(row);
} else {
f.rowid = row.rowid;
}
});
f.votes = [].concat(votes.reverse(), 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 * FROM messages WHERE id IN (" + ids.map(x => "?").join(", ") + ")",
ids,
async function(row) {
try {
posts.push(row);
} catch {
}
});
}
return posts;
}
async function ready() {
var whoami = await ssb.whoami();
var db = await database("ssb");
await Promise.all([
app.postMessage({whoami: whoami}),
app.postMessage({pubs: await visiblePubs(db, whoami)}),
app.postMessage({broadcasts: await ssb.getBroadcasts()}),
app.postMessage({connections: await ssb.connections()}),
followingDeep(db, [whoami], 2).then(function(f) {
getRecentPostIds(db, whoami, [].concat([whoami], f), k_posts_max).then(async function(ids) {
return getPosts(db, ids);
}).then(async function(posts) {
var roots = posts.map(function(x) {
try {
return JSON.parse(x.content).root;
} catch {
return null;
}
});
roots = roots.filter(function(root) {
return root && posts.every(post => post.id != root);
});
return [].concat(posts, await getPosts(db, roots));
}).then(async function(posts) {
posts.forEach(async function(post) {
await app.postMessage({message: post});
});
});
f.forEach(async function(id) {
await Promise.all([
getVotes(db, id).then(async function(votes) {
return Promise.all(votes.map(vote => app.postMessage({vote: vote})));
}),
getAbout(db, id).then(async function(user) {
return app.postMessage({user: {user: id, about: user}});
}),
]);
});
}),
sendUser(db, whoami),
]);
}
core.register('onBroadcastsChanged', async function() {
await app.postMessage({broadcasts: await ssb.getBroadcasts()});
});
core.register('onConnectionsChanged', async function() {
var connections = await ssb.connections();
await app.postMessage({connections: connections});
});
async function refresh() {
var db = await database("ssb");
var whoami = await ssb.whoami();
var ids = await followingDeep(db, [whoami], 2);
for (var id of ids) {
await ssb.createHistoryStream(id);
}
}
core.register('message', async function(m) {
if (m.message == 'ready') {
await ready();
} else if (m.message) {
if (m.message.connect) {
await ssb.connect(m.message.connect);
} else if (m.message.post) {
await ssb.post(m.message.post);
} else if (m.message.appendMessage) {
await ssb.appendMessage(m.message.appendMessage);
} else if (m.message.user) {
await sendUser(await database("ssb"), m.message.user);
} else if (m.message.refresh) {
await refresh();
}
} else {
print(JSON.stringify(m));
}
});
async function main() {
await app.setDocument(utf8Decode(await getFile("index.html")));
}
main();

278
apps/cory/index/index.html Normal file
View File

@ -0,0 +1,278 @@
<html>
<head>
<meta content="width=device-width,initial-scale=1,minimal-ui" name="viewport">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:400,500,700,400italic|Material+Icons">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/vue-material/dist/vue-material.min.css">
<link rel="stylesheet" href="https://unpkg.com/vue-material/dist/theme/default-dark.css">
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="vue-material.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/commonmark/0.29.1/commonmark.min.js"></script>
<script>
var g_data = {
whoami: null,
connections: [],
messages: [],
users: {},
broadcasts: [],
showUsers: false,
show_connect_dialog: false,
show_user_dialog: null,
connect: null,
pubs: [],
votes: [],
};
window.addEventListener('message', function(event) {
var key = Object.keys(event.data)[0];
if (key + 's' in g_data && Array.isArray(g_data[key + 's'])) {
g_data[key + 's'].push(event.data[key]);
} else if (key == 'user') {
Vue.set(g_data.users, event.data.user.user, Object.assign({}, g_data.users[event.data.user.user] || {}, event.data.user.about));
} else if (key == 'followers') {
if (!g_data.users[event.data.followers.id]) {
Vue.set(g_data.users, event.data.followers.id, {});
}
Vue.set(g_data.users[event.data.followers.id], 'followers', event.data.followers.users);
} else if (key == 'following') {
if (!g_data.users[event.data.following.id]) {
Vue.set(g_data.users, event.data.following.id, {});
}
Vue.set(g_data.users[event.data.following.id], 'following', event.data.following.users);
} else if (key == 'broadcasts') {
g_data.broadcasts = event.data.broadcasts;
} else if (key == 'pubs') {
g_data.pubs = event.data.pubs;
} else {
g_data[key] = event.data[key];
}
});
window.addEventListener('load', function() {
Vue.use(VueMaterial.default);
Vue.component('tf-user', {
data: function() { return {users: g_data.users, show_user_dialog: false, show_follow_dialog: false} },
props: ['id'],
mounted: function() {
window.parent.postMessage({user: this.id}, '*');
},
computed: {
following: {
get: function() {
return g_data.users[g_data.whoami] &&
g_data.users[g_data.whoami].following &&
g_data.users[g_data.whoami].following.indexOf(this.id) != -1;
},
set: function(newValue) {
if (g_data.users[g_data.whoami] &&
g_data.users[g_data.whoami].following) {
if (newValue && g_data.users[g_data.whoami].following.indexOf(this.id) == -1) {
window.parent.postMessage({appendMessage: {type: "contact", following: true, contact: this.id}}, '*');
} else if (!newValue) {
window.parent.postMessage({appendMessage: {type: "contact", following: false, contact: this.id}}, '*');
}
}
},
},
},
template: `<span @click="show_user_dialog = true">
{{users[id] && users[id].name ? users[id].name : id}}
<md-tooltip v-if="users[id] && users[id].name">{{id}}</md-tooltip>
<md-dialog :md-active.sync="show_user_dialog">
<md-dialog-title>{{users[id] && users[id].name ? users[id].name : id}}</md-dialog-title>
<md-dialog-content v-if="users[id]">
<div v-if="users[id].image"><img :src="'/' + users[id].image + '/view'"></div>
<div v-if="users[id].name">{{id}}</div>
<div>{{users[id].description}}</div>
<div><md-switch v-model="following">Following</md-switch></div>
<md-list>
<md-subheader>Followers</md-subheader>
<md-list-item v-for="follower in (users[id] || []).followers" v-bind:key="'follower-' + follower">
<tf-user :id="follower"></tf-user>
</md-list-item>
<md-subheader>Following</md-subheader>
<md-list-item v-for="user in (users[id] || []).following" v-bind:key="'following-' + user">
<tf-user :id="user"></tf-user>
</md-list-item>
</md-list>
</md-dialog-content>
<md-dialog-actions>
<md-button @click="show_user_dialog = false">Close</md-button>
</md-dialog-actions>
</md-dialog>
</span>`,
});
Vue.component('tf-message', {
props: ['message', 'messages'],
data: function() { return { showRaw: false } },
computed: {
content_json: function() {
try {
return JSON.parse(this.message.content);
} catch {
return undefined;
}
},
sub_messages: function() {
var id = this.message.id;
return this.messages.filter(function (x) {
try {
return JSON.parse(x.content).root == id;
} catch {}
});
},
votes: function() {
return [];
var id = this.message.id;
return this.votes.filter(function (x) {
try {
var j = JSON.parse(x.content);
return j.type == 'vote' && j.vote.link == id;
} catch {}
}).reduce(function (accum, value) {
var expression = JSON.parse(value.content).vote.expression;
if (!accum[expression]) {
accum[expression] = [];
}
accum[expression].push(value);
return accum;
}, {});
}
},
methods: {
markdown: function(md) {
var reader = new commonmark.Parser({safe: true});
var writer = new commonmark.HtmlRenderer();
return writer.render(reader.parse(md));
},
json: function(message) {
try {
return JSON.parse(message.content);
} catch {
return undefined;
}
},
},
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>
<tf-user :id="message.author"></tf-user>
</h3>
<div style="font-size: x-small">
{{new Date(message.timestamp)}}
</div>
<div class="md-toolbar-section-end">
<md-menu>
<md-button md-menu-trigger class="md-icon-button"><md-icon>more_vert</md-icon></md-button>
<md-menu-content>
<md-menu-item v-if="!showRaw" v-on:click="showRaw = true">View Raw</md-menu-item>
<md-menu-item v-else v-on:click="showRaw = false">View Message</md-menu-item>
</md-menu-content>
</md-menu>
</div>
</md-app-toolbar>
<md-app-content>
<div v-if="showRaw">{{message.content}}</div>
<div v-else>
<div v-if="content_json && content_json.type == 'post'">
<div v-html="this.markdown(content_json.text)"></div>
<img v-for="mention in content_json.mentions" v-if="mention.link && typeof(mention.link) == 'string' && mention.link.startsWith('&')" :src="'/' + mention.link + '/view'"></img>
</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>
<tf-message v-for="sub_message in sub_messages" v-bind:message="sub_message" v-bind:messages="messages" v-bind:key="sub_message.id"></tf-message>
<md-chip v-for="vote in Object.keys(votes)" v-bind:key="vote">
{{vote + (votes[vote].length > 1 ? ' (' + votes[vote].length + ')' : '')}}
</md-chip>
</md-app-content>
</md-app>`,
});
function markdown(d) { return d; }
Vue.config.performance = true;
var vue = new Vue({
el: '#app',
data: g_data,
methods: {
post_message: function() {
window.parent.postMessage({post: document.getElementById('post_text').value}, '*');
},
ssb_connect: function(connection) {
window.parent.postMessage({connect: connection}, '*');
},
content_json: function(message) {
try {
return JSON.parse(message.content);
} catch {
return undefined;
}
},
refresh: function() {
window.parent.postMessage({refresh: true}, '*');
},
}
});
});
window.parent.postMessage('ready', '*');
</script>
</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">
<md-button class="md-icon-button" @click="showUsers = !showUsers">
<md-icon>menu</md-icon>
</md-button>
<span class="md-title">Tilde Friends Secure Scuttlebutt Test</span>
</md-app-toolbar>
<md-app-drawer :md-active.sync="showUsers" md-persistent="full">
<md-list>
<md-subheader>Followers</md-subheader>
<md-list-item v-for="follower in (users[whoami] || []).followers" v-bind:key="'follower-' + follower"><tf-user :id="follower"></tf-user></md-list-item>
<md-subheader>Following</md-subheader>
<md-list-item v-for="user in (users[whoami] || []).following" v-bind:key="'following-' + user"><tf-user :id="user"></tf-user></md-list-item>
<md-subheader>Network</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>Pubs</md-subheader>
<md-list-item v-for="pub in pubs" v-bind:key="JSON.stringify(pub)" @click="ssb_connect({address: pub.host, port: pub.port, pubkey: pub.key})">{{pub.host}}:{{pub.port}} <tf-user :id="pub.key"></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-list>
</md-app-drawer>
<md-app-content>
<md-button @click="refresh()" class="md-icon-button md-dense md-raised md-primary">
<md-icon>cached</md-icon>
</md-button>
Welcome, <tf-user :id="whoami"></tf-user>.
<md-card class="md-elevation-8">
<md-card-header>
<div class="md-title">What's up?</div>
</md-card-header>
<md-card-content>
<md-field>
<label>Post a message</label>
<md-textarea id="post_text"></md-textarea>
</md-field>
</md-card-content>
<md-card-actions>
<md-button class="md-raised md-primary" v-on:click="post_message()">Submit Post</md-button>
</md-card-actions>
</md-card>
<tf-message v-for="message in messages" v-if="!content_json(message).root" v-bind:message="message" v-bind:messages="messages" v-bind:key="message.id"></tf-message>
</md-app-content>
</md-app>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long