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`
<span class='user' onclick=${function(e) {
let member = document.getElementById('member');
@ -80,6 +81,10 @@ window.makeUser = (user, url) => {
member.user = user;
document.getElementById('membername').textContent = user.displayname;
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>`;
span.user = user;
return span;
@ -224,12 +229,14 @@ document.body.append(html.node`
<div id='userlist'>
</div>
</div>
<p>
<button id='disconnect' onclick=${function(event) {
document.getElementById('instance' + this.parentElement.instance.id).remove();
window.instancelist.splice(window.instancelist.indexOf(this.parentElement.instance), 1);
saveInstances();
document.getElementById('instance').classList.add('hidden');
}}>disconnect</button>
</p>
</div>
<hr class='separator' color='#505050'>
<!-- create thread column goes here -->
@ -237,6 +244,7 @@ document.body.append(html.node`
<div id='thread' class='column hidden'></div>
<hr class='separator' color='#505050'>
<div id='member' class='column hidden'>
<div class='content'>
<p>
<button onclick=${e =>
document.getElementById('member').classList.add('hidden')
@ -245,6 +253,24 @@ document.body.append(html.node`
<p>user: <strong id='membername'></strong></p>
<p id='memberusername'></p>
</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) {

View File

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

View File

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

View File

@ -15,6 +15,8 @@ function setVisibility() {
}
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);
@ -46,7 +48,7 @@ function chooseThread() {
window.currentThread = thread;
window.currentInstance = thread.instance;
let tab = url.searchParams.get('tab');
if (['message', 'space', 'stream'].indexOf(tab) === -1)
if (!['message', 'space', 'stream'].includes(tab))
tab = null;
if (tab || this.tab) {
if (tab)
@ -68,8 +70,19 @@ function chooseThread() {
document.getElementById('memberlist').replaceChildren(
...await Promise.all(msg.thread.members.map(async member => {
let p = document.createElement('p');
p.append(member.name ? window.makeUser(member, window.currentInstance.url)
: makeUser(await window.getUser(member.id), member.id.split('@')[1]));
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;
}))
);
@ -137,7 +150,7 @@ function newThread() {
return;
let at = name.value.split('@');
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]);
if (!user) {
error.innerText = 'user not found';
@ -213,8 +226,8 @@ function newThread() {
addMember();
}
}} />
<button id='addmember' type='button' onclick=${addMember}>add</button>
<p id='membererror'></p>
<button type='button' onclick=${addMember}>add</button>
<p id='newmembererror'></p>
<div id='newmembers'>
<p class='member'>${members[0].name}</p>
</div>
@ -306,6 +319,51 @@ function clickedTab(event) {
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;
@ -344,7 +402,20 @@ async function loadThreads(instancediv, select) {
<hr class='separator' color='#505050'>
<div id='members' class='column hidden'>
<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>
</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)
).rows.map(i => i.user);
// get perms
const permissions = await db.query(
`select * from permission
where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'`,
msg.thread);
const perms = (await db.query(
`select * from permission where thread = ?`,
msg.thread)).rows;
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) {
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) {
s.emit('new_message', {
id,
userid: msg.auth_user.id,
name: msg.auth_user.displayname,
user: {
id: msg.auth_user.id,
name: msg.auth_user.name,
displayname: msg.auth_user.displayname,
permissions: userperms
},
content: msg.message,
thread: msg.thread
});
@ -67,8 +76,8 @@ async function get_history(msg, respond) {
message: "you can't view this thread"
});
}
const messages = (await db.query(
`select coalesce(displayname, name) as displayname, name, user as userid, post.id, content
const messages = (await db.query(`
select coalesce(displayname, name) as displayname, name, user as userid, post.id, content
from post
left join user on post.user = user.id
where ${msg.before ? 'post.id < ? and' : ''}
@ -77,9 +86,25 @@ async function get_history(msg, respond) {
limit 101`,
msg.before ? [msg.before, msg.thread] : [msg.thread]
)).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({
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
});
}

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 = {
create_thread: authwrap(create_thread),
list_threads: authwrap(list_threads),
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)
};