add and remove thread members

main
jerl 2024-06-17 21:44:11 -05:00
parent 043a68874b
commit c87c193118
6 changed files with 250 additions and 58 deletions

View File

@ -69,7 +69,8 @@ window.getUser = async (id, name) => {
); );
}; };
window.makeUser = (user, url) => { window.makeUser = (user, url, removable) => {
removable &&= window.currentThread.permissions.admin && !user.permissions?.admin;
let span = html.node` let span = html.node`
<span class='user' onclick=${function(e) { <span class='user' onclick=${function(e) {
let member = document.getElementById('member'); let member = document.getElementById('member');
@ -80,6 +81,10 @@ window.makeUser = (user, url) => {
member.user = user; member.user = user;
document.getElementById('membername').textContent = user.displayname; document.getElementById('membername').textContent = user.displayname;
document.getElementById('memberusername').textContent = `${user.name}@${url}`; document.getElementById('memberusername').textContent = `${user.name}@${url}`;
if (removable)
document.getElementById('removemember').classList.remove('hidden');
else
document.getElementById('removemember').classList.add('hidden');
}}>${user.displayname}</span>`; }}>${user.displayname}</span>`;
span.user = user; span.user = user;
return span; return span;
@ -224,12 +229,14 @@ document.body.append(html.node`
<div id='userlist'> <div id='userlist'>
</div> </div>
</div> </div>
<p>
<button id='disconnect' onclick=${function(event) { <button id='disconnect' onclick=${function(event) {
document.getElementById('instance' + this.parentElement.instance.id).remove(); document.getElementById('instance' + this.parentElement.instance.id).remove();
window.instancelist.splice(window.instancelist.indexOf(this.parentElement.instance), 1); window.instancelist.splice(window.instancelist.indexOf(this.parentElement.instance), 1);
saveInstances(); saveInstances();
document.getElementById('instance').classList.add('hidden'); document.getElementById('instance').classList.add('hidden');
}}>disconnect</button> }}>disconnect</button>
</p>
</div> </div>
<hr class='separator' color='#505050'> <hr class='separator' color='#505050'>
<!-- create thread column goes here --> <!-- create thread column goes here -->
@ -237,6 +244,7 @@ document.body.append(html.node`
<div id='thread' class='column hidden'></div> <div id='thread' class='column hidden'></div>
<hr class='separator' color='#505050'> <hr class='separator' color='#505050'>
<div id='member' class='column hidden'> <div id='member' class='column hidden'>
<div class='content'>
<p> <p>
<button onclick=${e => <button onclick=${e =>
document.getElementById('member').classList.add('hidden') document.getElementById('member').classList.add('hidden')
@ -245,6 +253,24 @@ document.body.append(html.node`
<p>user: <strong id='membername'></strong></p> <p>user: <strong id='membername'></strong></p>
<p id='memberusername'></p> <p id='memberusername'></p>
</div> </div>
<p>
<button id='removemember' onclick=${e => {
let member = document.getElementById('member');
window.currentInstance.emit('remove_member', {
thread: window.currentThread.id,
id: member.user.id
}, msg => {
if (!msg.success) {
console.log('remove_member failed:', msg.message);
return;
}
Array.from(document.getElementById('memberlist').children).find(p =>
p.children[0].user.id == member.user.id).remove();
member.classList.add('hidden');
})
}}>remove from thread</button>
</p>
</div>
`); `);
for (let i = 0; i < instancelist.length; ++i) { for (let i = 0; i < instancelist.length; ++i) {

View File

@ -153,7 +153,7 @@
margin: 3px; margin: 3px;
} }
} }
#instances { #instances, #membershead {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
@ -180,15 +180,12 @@
#profile { #profile {
max-width: 250px; max-width: 250px;
} }
#instance { #instance, #member {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
max-width: 250px; max-width: 250px;
} }
#disconnect {
width: fit-content;
}
.thread { .thread {
padding: 2px 3px; padding: 2px 3px;
white-space: pre; white-space: pre;

View File

@ -53,27 +53,29 @@ function loadMessages(callback) {
earliestMessage = msg.messages[msg.messages.length - 1].id; earliestMessage = msg.messages[msg.messages.length - 1].id;
let users = {}; let users = {};
for (let message of msg.messages) { for (let message of msg.messages) {
let span; let user;
if (message.name) if (message.user.name)
span = window.makeUser({ user = message.user;
displayname: message.displayname,
name: message.name,
id: message.user
}, instance.url);
else { else {
user = users[message.user.id];
if (user === undefined)
try { try {
span = window.makeUser(users[message.userid] || ( user = users[message.user.id] = await window.getUser(message.user.id);
users[message.userid] = await window.getUser(message.userid)
), message.userid.split('@')[1]);
} }
catch (e) { catch (e) {
console.log(`error getting user ${message.userid}`, e); console.log(`error getting user ${message.user.id}:`, e);
span = html.node`<span>${message.userid}</span>`; users[message.user.id] = false;
}
if (user) {
user.id = message.user.id;
user.permissions = message.user.permissions;
} }
} }
message = html.node`<div class='message'>: ${message.content}</div>`; let div = html.node`<div class='message'>: ${message.content}</div>`;
message.prepend(span); div.prepend(user ? window.makeUser(user,
messages.prepend(message); user.id.split?.('@')[1] || instance.url, true)
: html.node`<span>${message.user.id}</span>`);
messages.prepend(div);
} }
} }
if (msg.more) if (msg.more)
@ -90,11 +92,8 @@ function loadMessages(callback) {
const messages = document.getElementById('messages'); const messages = document.getElementById('messages');
let scroll = messages.scrollTop + 10 >= messages.scrollHeight - messages.clientHeight; let scroll = messages.scrollTop + 10 >= messages.scrollHeight - messages.clientHeight;
let div = html.node`<div class='message'>: ${message.content}</div>`; let div = html.node`<div class='message'>: ${message.content}</div>`;
div.prepend(window.makeUser({ div.prepend(window.makeUser(message.user,
name: window.name, message.user.id.split?.('@')[1] || instance.url, true));
displayname: window.displayname,
id: window.id
}, location.host));
messages.append(div); messages.append(div);
if (scroll) if (scroll)
messages.scroll(0, messages.scrollHeight - messages.clientHeight); messages.scroll(0, messages.scrollHeight - messages.clientHeight);

View File

@ -15,6 +15,8 @@ function setVisibility() {
} }
function chooseThread() { function chooseThread() {
if (!document.getElementById('removemember').classList.contains('hidden'))
document.getElementById('member').classList.add('hidden');
const edit = document.getElementById('edit'); const edit = document.getElementById('edit');
let thread = this.thread; let thread = this.thread;
let url = new URL(location); let url = new URL(location);
@ -46,7 +48,7 @@ function chooseThread() {
window.currentThread = thread; window.currentThread = thread;
window.currentInstance = thread.instance; window.currentInstance = thread.instance;
let tab = url.searchParams.get('tab'); let tab = url.searchParams.get('tab');
if (['message', 'space', 'stream'].indexOf(tab) === -1) if (!['message', 'space', 'stream'].includes(tab))
tab = null; tab = null;
if (tab || this.tab) { if (tab || this.tab) {
if (tab) if (tab)
@ -68,8 +70,19 @@ function chooseThread() {
document.getElementById('memberlist').replaceChildren( document.getElementById('memberlist').replaceChildren(
...await Promise.all(msg.thread.members.map(async member => { ...await Promise.all(msg.thread.members.map(async member => {
let p = document.createElement('p'); let p = document.createElement('p');
p.append(member.name ? window.makeUser(member, window.currentInstance.url) if (member.name)
: makeUser(await window.getUser(member.id), member.id.split('@')[1])); 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; return p;
})) }))
); );
@ -137,7 +150,7 @@ function newThread() {
return; return;
let at = name.value.split('@'); let at = name.value.split('@');
let url = at[1] || instancediv.instance.url; let url = at[1] || instancediv.instance.url;
let error = document.getElementById('membererror'); let error = document.getElementById('newmembererror');
let user = await window.getUser('@' + url, at[0]); let user = await window.getUser('@' + url, at[0]);
if (!user) { if (!user) {
error.innerText = 'user not found'; error.innerText = 'user not found';
@ -213,8 +226,8 @@ function newThread() {
addMember(); addMember();
} }
}} /> }} />
<button id='addmember' type='button' onclick=${addMember}>add</button> <button type='button' onclick=${addMember}>add</button>
<p id='membererror'></p> <p id='newmembererror'></p>
<div id='newmembers'> <div id='newmembers'>
<p class='member'>${members[0].name}</p> <p class='member'>${members[0].name}</p>
</div> </div>
@ -306,6 +319,51 @@ function clickedTab(event) {
window.history.pushState(null, '', url.toString()); 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) { async function loadThreads(instancediv, select) {
function makeThread(thread) { function makeThread(thread) {
thread.instance = instancediv.instance; thread.instance = instancediv.instance;
@ -344,7 +402,20 @@ async function loadThreads(instancediv, select) {
<hr class='separator' color='#505050'> <hr class='separator' color='#505050'>
<div id='members' class='column hidden'> <div id='members' class='column hidden'>
<p id='visibility'></p> <p id='visibility'></p>
<h4>members</h4> <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 id='memberlist'>
</div> </div>
</div> </div>

View File

@ -25,17 +25,26 @@ async function send_message(msg, respond) {
await db.query('select user from member where member.thread = ?', msg.thread) await db.query('select user from member where member.thread = ?', msg.thread)
).rows.map(i => i.user); ).rows.map(i => i.user);
// get perms // get perms
const permissions = await db.query( const perms = (await db.query(
`select * from permission `select * from permission where thread = ?`,
where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'`, msg.thread)).rows;
msg.thread); let userperms = {};
for (let p of perms)
if (p.type === 'user' && p.user == msg.auth_user.id)
userperms[p.permission] = ['admin', 'post', 'view'].includes(p.permission)
? p.value === 'true' : p.value;
for (let id in vybe.users) { for (let id in vybe.users) {
if (permissions.rows.length > 0 || members.includes(id)) { if (perms.find(p => p.type === 'everyone' && p.permission === 'view' && p.value === 'true')
|| members.includes(id)) {
for (let s of vybe.users[id].sockets) { for (let s of vybe.users[id].sockets) {
s.emit('new_message', { s.emit('new_message', {
id, id,
userid: msg.auth_user.id, user: {
name: msg.auth_user.displayname, id: msg.auth_user.id,
name: msg.auth_user.name,
displayname: msg.auth_user.displayname,
permissions: userperms
},
content: msg.message, content: msg.message,
thread: msg.thread thread: msg.thread
}); });
@ -67,8 +76,8 @@ async function get_history(msg, respond) {
message: "you can't view this thread" message: "you can't view this thread"
}); });
} }
const messages = (await db.query( const messages = (await db.query(`
`select coalesce(displayname, name) as displayname, name, user as userid, post.id, content select coalesce(displayname, name) as displayname, name, user as userid, post.id, content
from post from post
left join user on post.user = user.id left join user on post.user = user.id
where ${msg.before ? 'post.id < ? and' : ''} where ${msg.before ? 'post.id < ? and' : ''}
@ -77,9 +86,25 @@ async function get_history(msg, respond) {
limit 101`, limit 101`,
msg.before ? [msg.before, msg.thread] : [msg.thread] msg.before ? [msg.before, msg.thread] : [msg.thread]
)).rows; )).rows;
let perms = {};
for (let p of (await db.query(
`select * from permission where type = 'user' and thread = ?`,
msg.thread)).rows)
(perms[p.user] || (perms[p.user] = {}))[p.permission] =
['admin', 'post', 'view'].includes(p.permission)
? p.value === 'true' : p.value;
return respond({ return respond({
success: true, success: true,
messages: messages.slice(0, 100), messages: messages.slice(0, 100).map(message => ({
id: message.id,
user: {
id: message.userid,
name: message.name,
displayname: message.displayname,
permissions: perms[message.userid]
},
content: message.content
})),
more: messages.length > 100 more: messages.length > 100
}); });
} }

View File

@ -373,9 +373,83 @@ async function edit_thread(msg, respond) {
}); });
} }
async function add_member(msg, respond) {
if (!msg.thread || !msg.id) {
return respond({
success: false,
message: 'invalid msg'
});
}
if (!(await check_permission(msg.auth_user.id, msg.thread)).admin)
return respond({
success: false,
message: "user doesn't have permission"
});
if ((await db.query(
`select user from thread
join member on thread.id = member.thread
where thread.id = ? and user = ?`,
[msg.thread, msg.id]
)).rows.length)
return respond({
success: false,
message: 'user is already a member of this thread'
});
await db.query(`
insert into member (thread, user) values (?, ?)`,
[msg.thread, msg.id]);
return respond({
success: true
});
}
async function remove_member(msg, respond) {
if (!msg.thread || !msg.id) {
return respond({
success: false,
message: 'invalid msg'
});
}
if (!(await check_permission(msg.auth_user.id, msg.thread)).admin)
return respond({
success: false,
message: "user doesn't have permission"
});
let members = (await db.query(
`select member.user, p.value from thread
join member on thread.id = member.thread
left join permission p on p.user = member.user
and p.type = 'user' and p.permission = 'admin' and p.value = 'true'
where thread.id = ?`,
msg.thread
)).rows;
if (!members.find(member => member.user == msg.id))
return respond({
success: false,
message: "user isn't a member of this thread"
});
members = members.filter(member => member.value);
if (members.length <= 1 && members[0]?.user == msg.id)
return respond({
success: false,
message: "can't remove the only admin of the thread"
});
await db.query(`
delete from member where thread = ? and user = ?`,
[msg.thread, msg.id]);
await db.query(`
delete from permission where type = 'user' and thread = ? and user = ?`,
[msg.thread, msg.id]);
return respond({
success: true
});
}
module.exports = { module.exports = {
create_thread: authwrap(create_thread), create_thread: authwrap(create_thread),
list_threads: authwrap(list_threads), list_threads: authwrap(list_threads),
get_thread: authwrap(get_thread), get_thread: authwrap(get_thread),
edit_thread: authwrap(edit_thread) edit_thread: authwrap(edit_thread),
add_member: authwrap(add_member),
remove_member: authwrap(remove_member)
}; };