vybe/client/thread.js

463 lines
15 KiB
JavaScript

import { render, html } from '/uhtml.js';
import loadMessages from '/message.js';
import loadSpace from '/space.js';
import loadCall from '/call.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() {
if (!document.getElementById('removemember').classList.contains('hidden'))
document.getElementById('member').classList.add('hidden');
const edit = document.getElementById('edit');
let thread = this.thread;
let url = new URL(location);
if (window.currentThread) {
window.currentThread.div.classList.remove('active');
let editform = document.getElementById('editthread');
if (editform) {
editform.remove();
edit.textContent = 'edit';
}
url.searchParams.delete('tab');
if (window.currentThread.id === thread.id)
url.searchParams.delete('thread');
window.history.pushState(null, '', url.toString());
if (window.currentThread.id === thread.id) {
document.getElementById('thread').classList.add('hidden');
window.currentThread = null;
return;
}
}
else
document.getElementById('thread').classList.remove('hidden');
if (thread.permissions.admin) {
edit.classList.remove('hidden');
document.getElementById('membersadd').classList.remove('hidden');
} else {
edit.classList.add('hidden');
document.getElementById('membersadd').classList.add('hidden');
}
document.getElementById('threadname').textContent = thread.name;
this.classList.add('active');
window.currentThread = thread;
window.currentInstance = thread.instance;
let div = this;
window.currentInstance.emit('get_thread', { thread: thread.id }, async msg => {
if (!msg.success) {
console.log('get_thread failed:', msg.message);
return;
}
Object.assign(thread, msg.thread);
setVisibility();
document.getElementById('memberlist').replaceChildren(
...await Promise.all(msg.thread.members.map(async member => {
let p = document.createElement('p');
if (member.name)
p.append(window.makeUser(member, window.currentInstance.url, 'member'));
else
try {
let user = await window.getUser(member.id);
user.id = member.id;
user.permissions = member.permissions;
p.append(window.makeUser(user, user.id.split?.('@')[1], 'member'));
}
catch (e) {
console.log(`error getting user ${member.id}: ${e}`);
p.append(html.node`<span>${member.id}</span>`);
}
return p;
}))
);
loadCall();
let tab = url.searchParams.get('tab');
if (!['message', 'space', 'call'].includes(tab))
tab = null;
if (tab || this.tab) {
if (tab)
this.tab = tab + 'tab';
switchTab(document.getElementById(this.tab));
if (this.tab === 'messagetab')
loadMessages();
else if (this.tab === 'spacetab')
loadSpace();
}
else // load first tab that has any content
await new Promise(resolve => {
loadMessages(messages => {
if (messages.length) {
switchTab(document.getElementById(div.tab = 'messagetab'));
resolve();
} else
loadSpace(spans => {
if (spans.length)
switchTab(document.getElementById(div.tab = 'spacetab'));
else if (window.currentThread.streams.length)
switchTab(document.getElementById(div.tab = 'calltab'));
else
switchTab(document.getElementById(
div.tab = spans.length ? 'spacetab' :
window.currentThread.streams.length ? 'calltab' : 'messagetab'));
resolve();
});
});
});
if (window.currentInstance === window.instancelist[0]) {
url.searchParams.set('thread', thread.id);
url.searchParams.set('tab', div.tab.substring(0, div.tab.length - 3));
} else {
url.searchParams.delete('thread');
url.searchParams.delete('tab');
}
window.history.pushState(null, '', url.toString());
});
}
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tabcontent').forEach(tab => tab.classList.add('hidden'));
tab.classList.add('active');
document.getElementById(tab.id.slice(0, -3)).classList.remove('hidden');
}
function newThread(instance) {
let form = document.getElementById('createthread');
if (form) {
form.remove();
for (let create of document.querySelectorAll('#newthread')) {
create.textContent = 'create';
create.classList.remove('hidden');
}
return;
}
let members = [{
id: String(window.id),
name: window.name,
permissions: { admin: true }
}];
if (instance !== window.instancelist[0]) {
members[0].id += '@' + location.host;
members[0].name += '@' + location.host;
}
async function addMember() {
const name = document.getElementById('newmembername');
if (!name.value)
return;
let at = name.value.split('@');
let url = at[1] || instance.url;
let error = document.getElementById('newmembererror');
let user = await window.getUser('@' + url, at[0]);
if (!user) {
error.innerText = 'user not found';
return;
}
error.innerText = '';
user.id = String(user.id);
if (instance.url !== url) {
user.id += '@' + url;
user.name += '@' + url;
}
if (members.find(member => member.name === user.value))
return;
members.push(user);
document.getElementById('newmembers')
.append(html.node`<p class='member'>${user.name}</p>`);
name.value = '';
}
document.querySelector('#instance + .separator').insertAdjacentElement('afterend', html.node`
<form id='createthread' class='column' onsubmit=${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 };
// todo: generate key and encrypt
}
instance.emit('create_thread',
{ name, permissions, members },
msg => {
if (!msg.success) {
console.log('create_thread error:', msg.message);
return;
}
// since the form exists, this will perform cleanup
newThread();
chooseThread.call(this.parentElement.parentElement.children['threadlist']
.children['thread' + msg.id]);
}
);
}}>
<h4>create thread</h4>
<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='newmembername'>members</label>
<input type='text' id='newmembername' placeholder='username' onkeydown=${event => {
if (event.key === 'Enter') {
event.preventDefault();
addMember();
}
}} />
<button type='button' onclick=${addMember}>add</button>
<p id='newmembererror'></p>
<div id='newmembers'>
<p class='member'>${members[0].name}</p>
</div>
<br>
<button id='submitthread' type='submit'>create</button>
</form>`);
for (let create of document.querySelectorAll('#newthread')) {
if (create === this)
create.textContent = 'cancel';
else
create.classList.add('hidden');
}
}
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=${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.currentInstance.emit('edit_thread',
{ id: window.currentThread.id, name, permissions },
msg => {
if (!msg.success) {
console.log('edit_thread failed: ', msg.message);
return;
}
editThread();
});
}}>
<h4>edit thread</h4>
<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.getElementById('thread').append(form);
document.getElementById('edit').textContent = 'cancel';
}
function clickedTab(event) {
switchTab(this);
document.getElementById(`thread${window.currentThread.id}`).tab = this.id;
if (this.id === 'messagetab')
loadMessages();
else if (this.id === 'spacetab')
loadSpace();
let url = new URL(location);
url.searchParams.set('tab', this.id.substring(0, this.id.length - 3));
window.history.pushState(null, '', url.toString());
}
async function addMember() {
let name = document.getElementById('addmembername');
let error = document.getElementById('membererror');
function close() {
error.innerText = '';
name.value = '';
document.getElementById('addmember').classList.add('hidden');
}
if (!name.value)
return close();
let at = name.value.split('@');
let url = at[1] || window.currentInstance.url;
let user = await window.getUser('@' + url, at[0]);
if (!user) {
error.innerText = 'user not found';
return;
}
error.innerText = '';
if (window.currentInstance.url !== url)
user.id += '@' + url;
for (let p of document.getElementById('memberlist').children)
if (p.children[0].user.id == user.id)
return close();
window.currentInstance.emit('add_member', {
id: String(user.id),
thread: window.currentThread.id
}, msg => {
if (!msg.success) {
console.log('add_member failed:', msg.message);
error.textContent = 'error: ' + msg.message;
return;
}
close();
let p = document.createElement('p');
p.append(window.makeUser(user, url, 'member'));
document.getElementById('memberlist').append(p);
});
}
async function loadThreads(instancediv, select) {
function makeThread(thread) {
thread.instance = instancediv.instance;
let node = html.node`
<div class='thread' onclick=${chooseThread}>${
thread.name
}</div>`;
node.id = 'thread' + thread.id;
node.thread = thread;
thread.div = node;
return node;
}
let thread = document.getElementById('thread');
if (!thread.hasChildNodes())
thread.append(html.node`
<div id='content' class='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='calltab' class='tab' onclick=${clickedTab}>call / stream</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='call' class='tabcontent hidden'></div>
</div>
<hr class='separator' color='#505050'>
<div id='members' class='column hidden'>
<p id='visibility'></p>
<div id='membershead'>
<strong>members</strong>
<button id='membersadd' onclick=${e => {
document.getElementById('addmember').classList.remove('hidden');
document.getElementById('addmembername').focus();
}}>add</button>
</div>
<div id='addmember' class='hidden'>
<input id='addmembername' onblur=${addMember} onkeydown=${event => {
if (event.key === 'Enter')
addMember();
}}>
<p id='membererror'></p>
</div>
<div id='memberlist'>
</div>
</div>
<hr class='separator' color='#505050'>`);
if (!instancediv.children['threads']) {
instancediv.append(html.node`
<div id='threads'>
<div id='threadshead'>
<strong>threads</strong>
<button id='newthread' onclick=${function(e) {
newThread.call(this, instancediv.instance);
}}>create</button>
</div>
<div id='threadlist'>loading...</div>
</div>`);
const threadlist = instancediv.children['threads'].children['threadlist'];
instancediv.instance.emit('list_threads', {}, msg => {
threadlist.replaceChildren(...msg.threads.map(makeThread));
let thread = msg.threads.find(thread =>
thread.id == (new URLSearchParams(location.search)).get('thread'))?.div;
if (!window.currentThread && thread)
chooseThread.call(thread);
else if (select && msg.threads.length)
chooseThread.call(threadlist.firstChild);
});
instancediv.instance.socket.on('thread', thread => {
let el = threadlist.children['thread' + thread.id];
if (el) {
Object.assign(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;
}
threadlist.prepend(makeThread(thread));
});
}
}
export default loadThreads;