forked from cory/tildefriends
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:
1
apps/cory/docs.json
Normal file
1
apps/cory/docs.json
Normal 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
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
16
apps/cory/docs/guide.md
Normal 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
11
apps/cory/docs/index.md
Normal 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
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
24
apps/cory/docs/purpose.md
Normal 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.
|
67
apps/cory/docs/structure.md
Normal file
67
apps/cory/docs/structure.md
Normal 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
44
apps/cory/docs/todo.md
Normal 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
1
apps/cory/index.json
Normal 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
378
apps/cory/index/app.js
Normal 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
278
apps/cory/index/index.html
Normal 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 ? '==>' : '=/=>'}} <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>
|
6
apps/cory/index/vue-material.js
Normal file
6
apps/cory/index/vue-material.js
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user