forked from cory/tildefriends
Cory McWilliams
ab1f47ee9a
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3984 ed5197a5-7fde-0310-b194-c3ffbd925b24
309 lines
8.9 KiB
JavaScript
309 lines
8.9 KiB
JavaScript
import {LitElement, html, css, guard, until} from './lit-all.min.js';
|
|
import * as tfrpc from '/static/tfrpc.js';
|
|
import {styles} from './tf-styles.js';
|
|
|
|
class TfElement extends LitElement {
|
|
static get properties() {
|
|
return {
|
|
whoami: {type: String},
|
|
hash: {type: String},
|
|
unread: {type: Array},
|
|
tab: {type: String},
|
|
broadcasts: {type: Array},
|
|
connections: {type: Array},
|
|
loading: {type: Boolean},
|
|
loaded: {type: Boolean},
|
|
following: {type: Array},
|
|
users: {type: Object},
|
|
};
|
|
}
|
|
|
|
static styles = styles;
|
|
|
|
constructor() {
|
|
super();
|
|
let self = this;
|
|
this.hash = '#';
|
|
this.unread = [];
|
|
this.tab = 'news';
|
|
this.broadcasts = [];
|
|
this.connections = [];
|
|
this.following = [];
|
|
this.users = {};
|
|
this.loaded = false;
|
|
tfrpc.rpc.getBroadcasts().then(b => { self.broadcasts = b || [] });
|
|
tfrpc.rpc.getConnections().then(c => { self.connections = c || [] });
|
|
tfrpc.rpc.getHash().then(hash => self.set_hash(hash));
|
|
tfrpc.register(function hashChanged(hash) {
|
|
self.set_hash(hash);
|
|
});
|
|
tfrpc.register(async function notifyNewMessage(id) {
|
|
await self.fetch_new_message(id);
|
|
});
|
|
tfrpc.register(function set(name, value) {
|
|
if (name === 'broadcasts') {
|
|
self.broadcasts = value;
|
|
} else if (name === 'connections') {
|
|
self.connections = value;
|
|
}
|
|
});
|
|
tfrpc.rpc.localStorageGet('whoami').then(whoami => self.whoami = whoami);
|
|
}
|
|
|
|
set_hash(hash) {
|
|
this.hash = hash || '#';
|
|
if (this.hash.startsWith('#q=')) {
|
|
this.tab = 'search';
|
|
} else {
|
|
this.tab = 'news';
|
|
}
|
|
}
|
|
|
|
async contacts_internal(id, last_row_id, following, max_row_id) {
|
|
let result = Object.assign({}, following[id] || {});
|
|
result.following = result.following || {};
|
|
result.blocking = result.blocking || {};
|
|
let contacts = await tfrpc.rpc.query(
|
|
`
|
|
SELECT content FROM messages
|
|
WHERE author = ? AND
|
|
rowid > ? AND
|
|
rowid <= ? AND
|
|
json_extract(content, "$.type") = "contact"
|
|
ORDER BY sequence
|
|
`,
|
|
[id, last_row_id, max_row_id]);
|
|
for (let row of contacts) {
|
|
let contact = JSON.parse(row.content);
|
|
if (contact.following === true) {
|
|
result.following[contact.contact] = true;
|
|
} else if (contact.following === false) {
|
|
delete result.following[contact.contact];
|
|
} else if (contact.blocking === true) {
|
|
result.blocking[contact.contact] = true;
|
|
} else if (contact.blocking === false) {
|
|
delete result.blocking[contact.contact];
|
|
}
|
|
}
|
|
following[id] = result;
|
|
return result;
|
|
}
|
|
|
|
async contact(id, last_row_id, following, max_row_id) {
|
|
return await this.contacts_internal(id, last_row_id, following, max_row_id);
|
|
}
|
|
|
|
async following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) {
|
|
let contacts = await Promise.all([...new Set(ids)].map(x => this.contact(x, last_row_id, following, max_row_id)));
|
|
let result = {};
|
|
for (let i = 0; i < ids.length; i++) {
|
|
let id = ids[i];
|
|
let contact = contacts[i];
|
|
let found = Object.keys(contact.following).filter(y => !contact.blocking[y]);
|
|
let deeper = depth > 1 ? await this.following_deep_internal(found, depth - 1, Object.assign({}, contact.blocking, blocking), last_row_id, following, max_row_id) : [];
|
|
result[id] = [id, ...found, ...deeper];
|
|
}
|
|
return [...new Set(Object.values(result).flat())];
|
|
}
|
|
|
|
async following_deep(ids, depth, blocking) {
|
|
const k_cache_version = 5;
|
|
let cache = await tfrpc.rpc.databaseGet('following');
|
|
cache = cache ? JSON.parse(cache) : {};
|
|
if (cache.version !== k_cache_version) {
|
|
cache = {
|
|
version: k_cache_version,
|
|
following: {},
|
|
last_row_id: 0,
|
|
};
|
|
}
|
|
let max_row_id = (await tfrpc.rpc.query(`
|
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
|
`, []))[0].max_row_id;
|
|
let result = await this.following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id);
|
|
cache.last_row_id = max_row_id;
|
|
await tfrpc.rpc.databaseSet('following', JSON.stringify(cache));
|
|
console.log(cache);
|
|
return [result, cache.following];
|
|
}
|
|
|
|
async fetch_about(ids, users) {
|
|
const k_cache_version = 1;
|
|
let cache = await tfrpc.rpc.databaseGet('about');
|
|
cache = cache ? JSON.parse(cache) : {};
|
|
if (cache.version !== k_cache_version) {
|
|
cache = {
|
|
version: k_cache_version,
|
|
about: {},
|
|
last_row_id: 0,
|
|
};
|
|
}
|
|
let max_row_id = (await tfrpc.rpc.query(`
|
|
SELECT MAX(rowid) AS max_row_id FROM messages
|
|
`, []))[0].max_row_id;
|
|
for (let id of Object.keys(cache.about)) {
|
|
if (ids.indexOf(id) == -1) {
|
|
delete cache.about[id];
|
|
}
|
|
}
|
|
|
|
let abouts = await tfrpc.rpc.query(
|
|
`
|
|
SELECT
|
|
messages.*
|
|
FROM
|
|
messages,
|
|
json_each(?1) AS following
|
|
WHERE
|
|
messages.author = following.value AND
|
|
messages.rowid > ?3 AND
|
|
messages.rowid <= ?4 AND
|
|
json_extract(messages.content, '$.type') = 'about'
|
|
UNION
|
|
SELECT
|
|
messages.*
|
|
FROM
|
|
messages,
|
|
json_each(?2) AS following
|
|
WHERE
|
|
messages.author = following.value AND
|
|
messages.rowid <= ?4 AND
|
|
json_extract(messages.content, '$.type') = 'about'
|
|
ORDER BY messages.author, messages.sequence
|
|
`,
|
|
[
|
|
JSON.stringify(ids.filter(id => cache.about[id])),
|
|
JSON.stringify(ids.filter(id => !cache.about[id])),
|
|
cache.last_row_id,
|
|
max_row_id,
|
|
]);
|
|
for (let about of abouts) {
|
|
let content = JSON.parse(about.content);
|
|
if (content.about === about.author) {
|
|
delete content.type;
|
|
delete content.about;
|
|
cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content);
|
|
}
|
|
}
|
|
cache.last_row_id = max_row_id;
|
|
await tfrpc.rpc.databaseSet('about', JSON.stringify(cache));
|
|
users = users || {};
|
|
for (let id of Object.keys(cache.about)) {
|
|
users[id] = Object.assign(users[id] || {}, cache.about[id]);
|
|
}
|
|
return Object.assign({}, users);
|
|
}
|
|
|
|
async fetch_new_message(id) {
|
|
let messages = await tfrpc.rpc.query(
|
|
`
|
|
SELECT messages.*
|
|
FROM messages
|
|
JOIN json_each(?) AS following ON messages.author = following.value
|
|
WHERE messages.id = ?
|
|
`,
|
|
[
|
|
JSON.stringify(this.following),
|
|
id,
|
|
]);
|
|
if (messages && messages.length) {
|
|
this.unread = [...this.unread, ...messages];
|
|
}
|
|
}
|
|
|
|
_handle_whoami_changed(event) {
|
|
if (this.whoami !== event.srcElement.selected) {
|
|
this.whoami = event.srcElement.selected;
|
|
}
|
|
}
|
|
|
|
async create_identity() {
|
|
if (confirm("Are you sure you want to create a new identity?")) {
|
|
await tfrpc.rpc.createIdentity();
|
|
this.requestUpdate();
|
|
}
|
|
}
|
|
|
|
async render_id_picker() {
|
|
this._ids = this._ids || (await tfrpc.rpc.getIdentities()) || [];
|
|
return html`
|
|
<tf-id-picker id="picker" selected=${this.whoami} .ids=${this._ids} @change=${this._handle_whoami_changed}></tf-id-picker>
|
|
<button @click=${this.create_identity}>Create Identity</button>
|
|
`;
|
|
}
|
|
|
|
async load() {
|
|
let whoami = this.whoami;
|
|
let [following, users] = await this.following_deep([whoami], 2, {});
|
|
users = await this.fetch_about(following.sort(), users);
|
|
this.following = following;
|
|
this.users = users;
|
|
this.whoami = whoami;
|
|
this.loaded = whoami;
|
|
}
|
|
|
|
render_tab() {
|
|
let following = this.following;
|
|
let users = this.users;
|
|
if (this.tab === 'news') {
|
|
return html`
|
|
<tf-tab-news .following=${this.following} whoami=${this.whoami} .users=${this.users} hash=${this.hash} .unread=${this.unread} @refresh=${() => this.unread = []}></tf-tab-news>
|
|
`;
|
|
} else if (this.tab === 'connections') {
|
|
return html`
|
|
<tf-tab-connections .users=${this.users} .connections=${this.connections} .broadcasts=${this.broadcasts}></tf-tab-connections>
|
|
`;
|
|
} else if (this.tab === 'search') {
|
|
return html`
|
|
<tf-tab-search .following=${this.following} whoami=${this.whoami} .users=${this.users} query=${this.hash?.startsWith('#q=') ? decodeURIComponent(this.hash.substring(3)) : null}></tf-tab-search>
|
|
`;
|
|
}
|
|
}
|
|
|
|
add_fake_news() {
|
|
this.unread = [{
|
|
author: this.whoami,
|
|
placeholder: true,
|
|
id: '%fake_id',
|
|
text: 'text',
|
|
content: 'hello',
|
|
}, ...this.unread];
|
|
}
|
|
|
|
render() {
|
|
let self = this;
|
|
|
|
if (!this.loading && this.whoami && this.loaded !== this.whoami) {
|
|
console.log('loading', this.whoami);
|
|
this.loading = true;
|
|
this.following = [];
|
|
this.users = {};
|
|
this.load().finally(function() {
|
|
self.loading = false;
|
|
console.log('loaded');
|
|
});
|
|
}
|
|
|
|
let id_picker = html`
|
|
${guard([this.whoami], () => until(this.render_id_picker(), html`<div>Loading...</div>`))}
|
|
`;
|
|
let tabs = html`
|
|
<div>
|
|
<input type="button" value="News" ?disabled=${self.tab == 'news'} @click=${event => self.tab = 'news'}></input>
|
|
<input type="button" value="Connections" ?disabled=${self.tab == 'connections'} @click=${event => self.tab = 'connections'}></input>
|
|
<input type="button" value="Search" ?disabled=${self.tab == 'search'} @click=${event => self.tab = 'search'}></input>
|
|
</div>
|
|
`;
|
|
let contents = !this.loaded ?
|
|
html`<div>Loading...</div>` :
|
|
this.render_tab();
|
|
return html`
|
|
${id_picker}
|
|
${tabs}
|
|
<!-- <input type="button" value="Fake News" @click=${this.add_fake_news}></input> -->
|
|
${contents}
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define('tf-app', TfElement); |