vybe/client/app.js

386 lines
12 KiB
JavaScript

import { render, html } from '/uhtml.js';
import loadMessages from '/message.js';
import loadSpace from '/space.js';
import loadStreams from '/stream.js';
function setVisibility() {
document.getElementById('visibility').innerText = `${
window.currentThread.permissions.everyone.view.value ?
'this thread is visible to everyone' :
'members can view this thread'}
${window.currentThread.permissions.everyone.post.value ?
'anyone can post' :
window.currentThread.permissions.members.post.value ?
'only members can post' : 'some members can post'}`;
}
function chooseThread() {
const edit = document.getElementById('edit');
if (window.currentThread) {
document.getElementById(`thread${window.currentThread.id}`).classList.remove('active');
let editform = document.getElementById('editthread');
if (editform) {
editform.remove();
edit.textContent = 'edit';
}
if (window.currentThread.id === this.thread.id) {
document.getElementById('thread').classList.add('hidden');
window.currentThread = null;
return;
}
}
else
document.getElementById('thread').classList.remove('hidden');
if (this.thread.permissions.admin)
edit.classList.remove('hidden');
else
edit.classList.add('hidden');
document.getElementById('threadname').textContent = this.thread.name;
this.classList.add('active');
window.currentThread = this.thread;
loadStreams();
if (this.tab)
switchTab(document.getElementById(this.tab));
else // load first tab that has any content
loadMessages(true, messages => {
if (messages.length)
switchTab(document.getElementById(this.tab = 'messagetab'));
else
loadSpace(spans => {
if (spans.length)
switchTab(document.getElementById(this.tab = 'spacetab'));
else if (window.currentThread.streams.length)
switchTab(document.getElementById(this.tab = 'streamtab'));
else
switchTab(document.getElementById(
this.tab = spans.length ? 'spacetab' :
window.currentThread.streams.length ? 'streamtab' : 'messagetab'));
});
});
window.emit('get_thread', { thread: this.thread.id }, msg => {
window.currentThread = msg.thread;
setVisibility();
document.getElementById('memberlist').replaceChildren(
...msg.thread.members.map(member =>
html.node`<p class='member'>${member.name}</p>`)
);
});
}
function switchTab(tab) {
for (let tab of document.querySelectorAll('.tab'))
tab.classList.remove('active');
for (let tab of document.querySelectorAll('.tabcontent'))
tab.classList.add('hidden');
tab.classList.add('active');
document
.getElementById(tab.id.slice(0, -3))
.classList.remove('hidden');
if (tab.id === 'messagetab')
loadMessages(true);
else if (tab.id === 'spacetab')
loadSpace();
}
async function createThread(event) {
event.preventDefault();
let name = document.getElementById('newthreadname').value;
if (!name) {
document.getElementById('newnameempty').classList.remove('hidden');
return;
}
const perms = document.querySelector(
'input[name="newpermissions"]:checked'
).value;
let permissions;
if (perms === 'public')
permissions = { view_limited: false, post_limited: false };
else if (perms === 'private_post')
permissions = { view_limited: false, post_limited: true };
else if (perms === 'private_view') {
permissions = { view_limited: true, post_limited: true };
// generate key
/* wip
var buf = new Uint8Array(32);
crypto.getRandomValues(buf);
const key = aesjs.utils.hex.fromBytes(Array.from(buf));
// sign it to each of the members
for (let i = 0; i < newmembers.length; i++) {
const member = newmembers[i];
const sig = await openpgp.encrypt({
message: await openpgp.createMessage({ text: key }),
signingKeys: window.keys.priv
});
}
*/
}
window.emit('create_thread',
{ name, permissions, members: window.threadmembers },
msg => {
chooseThread.call(document.getElementById('thread' + msg.id));
// since the form exists, this will perform cleanup
newThread();
document.getElementById('loadmore').classList.add('hidden');
}
);
}
function addMember() {
const name = document.getElementById('membername');
if (!name.value)
return;
window.threadmembers.push({ name: name.value });
document.getElementById('newmembers')
.appendChild(html.node`<p class='member'>${name.value}</p>`);
name.value = '';
}
function newThread() {
let form = document.getElementById('createthread');
if (form) {
form.remove();
document.getElementById('newthread').textContent = 'create';
return;
}
window.threadmembers = [{
name: window.name,
permissions: { admin: 'true' }
}];
document.querySelector('#home + .separator').insertAdjacentElement('afterend', html.node`
<form id='createthread' class='column' onsubmit=${createThread}>
<h3>create thread</h3>
<label for='newthreadname' class='heading'>thread name</label>
<p id='newnameempty' class='hidden'>name cannot be empty</p>
<input type='text' id='newthreadname' />
<p id='permissions'>thread permissions</p>
<input type='radio' name='newpermissions'
id='public' value='public' checked />
<label for='public'>anyone can view and post</label><br>
<input type='radio' name='newpermissions'
id='private_post' value='private_post' />
<label for='private_post'>anyone can view, only members can post</label><br>
<input type='radio' name='newpermissions'
id='private_view' value='private_view' />
<label for='private_view'>only members can view and post</label><br>
<label class='heading' for='membername'>members</label>
<input type='text' id='membername' placeholder='username' onkeydown=${event => {
if (event.key === 'Enter') {
event.preventDefault();
addMember();
}
}} />
<button id='addmember' type='button' onclick=${addMember}>add</button>
<div id='newmembers'>
<p class='member'>${window.name}</p>
</div>
<br>
<button id='submitthread' type='submit'>create</button>
</form>`);
document.getElementById('newthread').textContent = 'cancel';
}
async function saveThread(event) {
event.preventDefault();
let name = document.getElementById('editthreadname').value;
if (!name) {
document.getElementById('nameempty').classList.remove('hidden');
return;
}
const perms = document.querySelector(
'input[name="permissions"]:checked'
).value;
let permissions;
if (perms === 'public')
permissions = { view_limited: false, post_limited: false };
else if (perms === 'private_post')
permissions = { view_limited: false, post_limited: true };
else if (perms === 'private_view') {
permissions = { view_limited: true, post_limited: true };
// todo: generate key and encrypt
}
window.emit('edit_thread',
{ id: window.currentThread.id, name, permissions },
msg => {
if (!msg.success) {
console.log('edit_thread failed: ', msg.message);
return;
}
editThread();
});
}
function editThread() {
let form = document.getElementById('editthread');
if (form) {
form.remove();
document.getElementById('edit').textContent = 'edit';
return;
}
form = html.node`
<form id='editthread' class='column' onsubmit=${saveThread}>
<label for='editthreadname' class='heading'>thread name</label>
<input type='text' id='editthreadname' />
<p id='nameempty' class='hidden'>name cannot be empty</p>
<p id='permissions'>thread permissions</p>
<input type='radio' name='permissions'
id='public' value='public' />
<label for='public'>anyone can view and post</label><br>
<input type='radio' name='permissions'
id='private_post' value='private_post' />
<label for='private_post'>anyone can view, only members can post</label><br>
<input type='radio' name='permissions'
id='private_view' value='private_view' />
<label for='private_view'>only members can view and post</label><br>
<br>
<button id='savethread'>save</button>
</form>`;
form['editthreadname'].value = window.currentThread.name;
if (window.currentThread.permissions.everyone.post.value)
form['public'].checked = true;
else if (window.currentThread.permissions.everyone.view.value)
form['private_post'].checked = true;
else
form['private_view'].checked = true;
document.querySelector('#thread').append(form);
document.getElementById('edit').textContent = 'cancel';
}
function clickedTab(event) {
switchTab(event.target);
document.getElementById(`thread${window.currentThread.id}`).tab = event.target.id;
}
function changeName(e) {
let displayname = document.getElementById('newname').value;
window.emit('update_user', {
displayname
}, msg => {
if (!msg.success) {
console.log('update_user error: ', msg.message);
document.getElementById('savename').classList.remove('hidden');
return;
}
window.displayname = displayname;
document.getElementById('savename').classList.add('hidden');
})
}
// main app html
document.body.append(html.node`
<div id='home' class='column'>
<div id='threads'>
<h3>vybe</h3>
<h4>threads</h4>
<div id='threadlist'>loading...</div>
<button id='newthread' onclick=${newThread}>create</button>
</div>
<div id='user' onclick=${() =>
document.getElementById('profile').classList.toggle('hidden')
}>${window.name}</div>
</div>
<hr class='separator' color='#505050'>
<!-- create thread column goes here -->
<hr class='separator' color='#505050'>
<div id='profile' class='column hidden'>
<label class='heading'>display name</label>
<input id='newname' onkeyup=${function(event) {
if (window.displayname === this.value)
document.getElementById('savename').classList.add('hidden');
else if (event.key === 'Enter')
changeName();
else
document.getElementById('savename').classList.remove('hidden');
}}>
<button class='hidden' id='savename' onclick=${changeName}>save</button>
<label class='heading'>authentication requests</label>
<div id='authrequests'></div>
</div>
<hr class='separator' color='#505050'>
<div id='thread' class='column'>
<div id='content'>
<div id='titlebar'>
<span id='title'>thread: <strong id='threadname'>meow</strong></span>
<button id='edit' class='hidden' onclick=${editThread}>edit</button>
</div>
<div id='buttonbar'>
<div id='tabs'>
<button id='messagetab' class='tab active' onclick=${clickedTab}>messages
</button><button id='spacetab' class='tab' onclick=${clickedTab}>space
</button><button id='streamtab' class='tab' onclick=${clickedTab}>streams</button>
</div>
<button id='showmembers' onclick=${() =>
document.getElementById('members').classList.toggle('hidden')
}>members</button>
</div>
<div id='message' class='tabcontent'></div>
<div id='space' class='tabcontent hidden'></div>
<div id='stream' class='tabcontent hidden'></div>
</div>
<hr class='separator' color='#505050'>
<div id='members' class='hidden'>
<p id='visibility'></p>
<p><strong>members</strong></p>
<div id='memberlist'>
</div>
</div>
<hr class='separator' color='#505050'>
</div>
`);
document.getElementById('newname').value = window.displayname;
function makeThread(thread) {
let node = html.node`
<div class='thread' onclick=${chooseThread}>${
thread.name
}</div>`;
node.id = 'thread' + thread.id;
node.thread = thread;
return node;
}
window.socket.on('thread', thread => {
let el = document.getElementById('thread' + thread.id);
if (el) {
el.thread = thread;
el.textContent = thread.name;
if (window.currentThread?.id === thread.id) {
Object.assign(window.currentThread, thread);
document.getElementById('threadname').textContent = thread.name;
setVisibility();
}
return;
}
document.getElementById('threadlist').prepend(makeThread(thread));
});
function authRequest(authrequest) {
const p = html.node`
<p>
<button onclick=${() => {
window.emit('authorize_key', {
id: authrequest.id
}, msg => {
if (!msg.success)
console.log('authorize_key failed: ', msg.message);
});
p.remove();
}}>approve</button>
session <strong>${authrequest.id}</strong>
</p>`;
document.getElementById('authrequests').append(p);
setTimeout(() => p.remove(), Date.now() - authrequest.time + 60000 * 5);
}
window.socket.on('authrequest', authRequest);
window.emit('list_threads', {}, msg => {
const threadlist = document.getElementById('threadlist');
threadlist.replaceChildren(...msg.threads.map(makeThread));
chooseThread.call(threadlist.firstChild);
});
export {
authRequest
};