461 lines
15 KiB
JavaScript
461 lines
15 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() {
|
|
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');
|
|
else
|
|
edit.classList.add('hidden');
|
|
document.getElementById('threadname').textContent = thread.name;
|
|
this.classList.add('active');
|
|
window.currentThread = thread;
|
|
window.currentInstance = thread.instance;
|
|
let tab = url.searchParams.get('tab');
|
|
if (!['message', 'space', 'stream'].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();
|
|
}
|
|
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, true));
|
|
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], true));
|
|
}
|
|
catch (e) {
|
|
console.log(`error getting user ${member.id}: ${e}`);
|
|
p.append(html.node`<span>${member.id}</span>`);
|
|
}
|
|
return p;
|
|
}))
|
|
);
|
|
loadStreams();
|
|
if (!div.tab)
|
|
await new Promise(resolve => {
|
|
// load first tab that has any content
|
|
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 = 'streamtab'));
|
|
else
|
|
switchTab(document.getElementById(
|
|
div.tab = spans.length ? 'spacetab' :
|
|
window.currentThread.streams.length ? 'streamtab' : 'messagetab'));
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
url.searchParams.set('thread', thread.id);
|
|
url.searchParams.set('tab', div.tab.substring(0, div.tab.length - 3));
|
|
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() {
|
|
let form = document.getElementById('createthread');
|
|
if (form) {
|
|
form.remove();
|
|
for (let create of document.querySelectorAll('#newthread')) {
|
|
create.textContent = 'create';
|
|
create.classList.remove('hidden');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const instancediv = this.parentElement.parentElement;
|
|
|
|
let members = [{
|
|
id: String(window.id),
|
|
name: window.name,
|
|
permissions: { admin: 'true' }
|
|
}];
|
|
if (instancediv.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] || instancediv.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 (instancediv.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
|
|
}
|
|
instancediv.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(instancediv.children['threads'].children['threadlist']
|
|
.children['thread' + msg.id]);
|
|
document.getElementById('loadmore').classList.add('hidden');
|
|
}
|
|
);
|
|
}}>
|
|
<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) {
|
|
close();
|
|
return;
|
|
}
|
|
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;
|
|
user.name += '@' + url;
|
|
}
|
|
for (let p of document.getElementById('memberlist').children)
|
|
if (p.children[0].user.id == user.id) {
|
|
close();
|
|
return;
|
|
}
|
|
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, true));
|
|
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='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='column hidden'>
|
|
<p id='visibility'></p>
|
|
<div id='membershead'>
|
|
<strong>members</strong>
|
|
<button 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'>
|
|
<p><strong>threads</strong></p>
|
|
<div id='threadlist'>loading...</div>
|
|
<button id='newthread' onclick=${newThread}>create</button>
|
|
</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;
|