tildefriends/apps/ssb/tf-profile.js

251 lines
7.5 KiB
JavaScript

import {LitElement, html, unsafeHTML} from './lit-all.min.js';
import * as tfrpc from '/static/tfrpc.js';
import * as tfutils from './tf-utils.js';
import {styles} from './tf-styles.js';
class TfProfileElement extends LitElement {
static get properties() {
return {
editing: {type: Object},
whoami: {type: String},
id: {type: String},
users: {type: Object},
size: {type: Number},
export_progress: {type: Object},
};
}
static styles = styles;
constructor() {
super();
let self = this;
this.editing = null;
this.whoami = null;
this.id = null;
this.users = {};
this.size = 0;
this.export_progress = null;
}
modify(change) {
tfrpc.rpc.appendMessage(this.whoami,
Object.assign({
type: 'contact',
contact: this.id,
}, change)).catch(function(error) {
alert(error?.message);
});
}
follow() {
this.modify({following: true});
}
unfollow() {
this.modify({following: false});
}
block() {
this.modify({blocking: true});
}
unblock() {
this.modify({blocking: false});
}
edit() {
let original = this.users[this.id];
this.editing = {
name: original.name,
description: original.description,
image: original.image,
};
console.log(this.editing);
}
save_edits() {
let self = this;
let message = {
type: 'about',
about: this.whoami,
};
for (let key of Object.keys(this.editing)) {
if (this.editing[key] !== this.users[this.id][key]) {
message[key] = this.editing[key];
}
}
tfrpc.rpc.appendMessage(this.whoami, message).then(function() {
self.editing = null;
}).catch(function(error) {
alert(error?.message);
});
}
discard_edits() {
this.editing = null;
}
attach_image() {
let self = this;
let input = document.createElement('input');
input.type = 'file';
input.onchange = function(event) {
let 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) {
self.editing = Object.assign({}, self.editing, {image: id});
console.log(self.editing);
}).catch(function(e) {
alert(e.message);
});
};
input.click();
}
format_message(message) {
let out = {
previous: message.previous ?? null,
};
if (message.sequence_before_author) {
out.sequence = message.sequence;
out.author = message.author;
} else {
out.author = message.author;
out.sequence = message.sequence;
}
out.timestamp = message.timestamp;
out.hash = message.hash;
out.content = JSON.parse(message.content);
out.signature = message.signature;
return out;
}
async export() {
let all_messages = [];
let sequence = -1;
let messages_max = (await tfrpc.rpc.query('SELECT MAX(sequence) FROM messages WHERE author = ?', [this.id]))[0].sequence;
while (true) {
let messages = await tfrpc.rpc.query(
'SELECT * FROM messages WHERE author = ? AND SEQUENCE > ? ORDER BY sequence LIMIT 100',
[this.id, sequence]
);
if (messages?.length) {
all_messages = [].concat(all_messages, messages.map(x => this.format_message(x)));
sequence = messages[messages.length - 1].sequence;
this.export_progress = {name: 'messages', value: all_messages.length, max: messages_max};
} else {
break;
}
}
let zip = new JSZip();
zip.file('messages.txt', JSON.stringify(all_messages, null, 2));
let blobs = await tfrpc.rpc.query(
`SELECT blobs.id
FROM messages
JOIN messages_refs ON messages.id = messages_refs.message
JOIN blobs ON messages_refs.ref = blobs.id
WHERE messages.author = ?`,
[this.id]);
let blobs_done = 0;
for (let row of blobs) {
this.export_progress = {name: 'blobs', value: blobs_done, max: blobs.length};
let blob = await tfrpc.rpc.get_blob(row.id);
zip.folder('blobs').file(row.id.replaceAll('/', '_').replaceAll('+', '-'), blob);
blobs_done++;
}
this.export_progress = {name: 'saving'};
let blob = await zip.generateAsync({type: 'blob'});
saveAs(blob, `${this.id.replaceAll('/', '_').replaceAll('+', '-')}.zip`);
this.export_progress = null;
}
render() {
let self = this;
let profile = this.users[this.id] || {};
tfrpc.rpc.query(
`SELECT SUM(LENGTH(content)) AS size FROM messages WHERE author = ?`,
[this.id]).then(function(result) {
self.size = result[0].size;
});
let edit;
let follow;
let block;
if (this.id === this.whoami) {
if (this.editing) {
edit = html`
<input type="button" value="Save Profile" @click=${this.save_edits}></input>
<input type="button" value="Discard" @click=${this.discard_edits}></input>
`;
} else {
edit = html`<input type="button" value="Edit Profile" @click=${this.edit}></input>`;
}
}
if (this.id !== this.whoami &&
this.users[this.whoami]?.following) {
follow =
this.users[this.whoami].following[this.id] ?
html`<input type="button" value="Unfollow" @click=${this.unfollow}></input>` :
html`<input type="button" value="Follow" @click=${this.follow}></input>`;
}
if (this.id !== this.whoami &&
this.users[this.whoami]?.blocking) {
block =
this.users[this.whoami].blocking[this.id] ?
html`<input type="button" value="Unblock" @click=${this.unblock}></input>` :
html`<input type="button" value="Block" @click=${this.block}></input>`;
}
let edit_profile = this.editing ? html`
<div style="flex: 1 0 50%">
<div>
<label for="name">Name:</label>
<input type="text" id="name" value=${this.editing.name} @input=${event => this.editing = Object.assign({}, this.editing, {name: event.srcElement.value})}></input>
</div>
<div>
<div><label for="description">Description:</label></div>
<textarea id="description" @input=${event => this.editing = Object.assign({}, this.editing, {description: event.srcElement.value})}>${this.editing.description}</textarea>
</div>
<input type="button" value="Attach Image" @click=${this.attach_image}></input>
</div>` : null;
let export_state = html`<input type="button" value="Export" @click=${this.export}></input>`;
if (this.export_progress) {
if (this.export_progress.max) {
export_state = html`<span>${this.export_progress.name}</span><progress value=${this.export_progress.value} max=${this.export_progress.max}></progress>`;
} else {
export_state = html`<span>${this.export_progress.name}</span>`;
}
}
let image = typeof(profile.image) == 'string' ? profile.image : profile.image?.link;
image = this.editing?.image ?? image;
let description = this.editing?.description ?? profile.description;
return html`<div style="border: 2px solid black; background-color: rgba(255, 255, 255, 0.2); padding: 16px">
<tf-user id=${this.id} .users=${this.users}></tf-user> (${tfutils.human_readable_size(this.size)})
<div style="display: flex; flex-direction: row">
${edit_profile}
<div style="flex: 1 0 50%">
<div><img src=${'/' + image + '/view'} style="width: 256px; height: auto"></img></div>
<div>${unsafeHTML(tfutils.markdown(description))}</div>
</div>
</div>
<div>
Following ${Object.keys(profile.following || {}).length} identities.
Followed by ${Object.values(self.users).filter(x => (x.following || {})[self.id]).length} identities.
Blocking ${Object.keys(profile.blocking || {}).length} identities.
Blocked by ${Object.values(self.users).filter(x => (x.blocking || {})[self.id]).length} identities.
</div>
<div>
${edit}
${follow}
${block}
${export_state}
</div>
</div>`;
}
}
customElements.define('tf-profile', TfProfileElement);