view users

main
jerl 2024-06-14 22:24:32 -07:00
parent b8b7a7edd6
commit 62816d255f
8 changed files with 136 additions and 69 deletions

View File

@ -1,10 +1,11 @@
import { render, html } from '/uhtml.js'; import { render, html } from '/uhtml.js';
import loadThreads from '/thread.js'; import loadThreads from '/thread.js';
function changeName(e) { function saveProfile(e) {
let displayname = document.getElementById('newname').value; let displayname = document.getElementById('newname').value;
window.emit('update_user', { window.emit('update_user', {
displayname displayname,
public: document.getElementById('profilepublic').checked
}, msg => { }, msg => {
if (!msg.success) { if (!msg.success) {
console.log('update_user error: ', msg.message); console.log('update_user error: ', msg.message);
@ -13,7 +14,7 @@ function changeName(e) {
} }
window.displayname = displayname; window.displayname = displayname;
document.getElementById('savename').classList.add('hidden'); document.getElementById('savename').classList.add('hidden');
}) });
} }
window.connectInstance = async url => { window.connectInstance = async url => {
@ -59,12 +60,31 @@ window.getUser = async (id, name) => {
return new Promise((resolve, reject) => return new Promise((resolve, reject) =>
instance.socket.emit('get_user', { id, name }, msg => { instance.socket.emit('get_user', { id, name }, msg => {
if (!msg.success) if (!msg.success)
return reject('get_user failed: ' + msg.message); if (msg.message === 'user not found')
return resolve();
else
return reject(msg.message);
resolve(msg.user); resolve(msg.user);
}) })
); );
}; };
window.makeUser = (user, url) => {
let span = html.node`
<span class='user' onclick=${function(e) {
let member = document.getElementById('member');
if (member.user === user)
member.classList.toggle('hidden');
else
member.classList.remove('hidden');
member.user = user;
document.getElementById('membername').textContent = user.displayname;
document.getElementById('memberusername').textContent = `${user.name}@${url}`;
}}>${user.displayname}</span>`;
span.user = user;
return span;
};
async function authenticateInstance(div, select) { async function authenticateInstance(div, select) {
return new Promise((resolve, reject) => return new Promise((resolve, reject) =>
div.instance.socket.emit('authenticate', { div.instance.socket.emit('authenticate', {
@ -107,8 +127,11 @@ function instanceClicked(event) {
let userlist = document.getElementById('userlist'); let userlist = document.getElementById('userlist');
userlist.innerHTML = ''; userlist.innerHTML = '';
instance.emit('list_users', {}, msg => instance.emit('list_users', {}, msg =>
userlist.innerHTML = msg.users.map(user => userlist.replaceChildren(...msg.users.map(user => {
`<p>${user.displayname}</p>`).join('\n') let p = document.createElement('p');
p.append(window.makeUser(user, instance.url));
return p;
}))
); );
div.instance = instance; div.instance = instance;
} }
@ -155,11 +178,15 @@ document.body.append(html.node`
if (window.displayname === this.value) if (window.displayname === this.value)
document.getElementById('savename').classList.add('hidden'); document.getElementById('savename').classList.add('hidden');
else if (event.key === 'Enter') else if (event.key === 'Enter')
changeName(); saveProfile();
else else
document.getElementById('savename').classList.remove('hidden'); document.getElementById('savename').classList.remove('hidden');
}}> }}>
<button class='hidden' id='savename' onclick=${changeName}>save</button> <button class='hidden' id='savename' onclick=${saveProfile}>save</button>
<p>
<input id='profilepublic' type='checkbox' oninput=${saveProfile}>
<label for='profilepublic'>show profile in instance users</label>
</p>
<label class='heading'>authentication requests</label> <label class='heading'>authentication requests</label>
<div id='authrequests'></div> <div id='authrequests'></div>
</div> </div>
@ -207,6 +234,17 @@ document.body.append(html.node`
<hr class='separator' color='#505050'> <hr class='separator' color='#505050'>
<!-- create thread column goes here --> <!-- create thread column goes here -->
<hr class='separator' color='#505050'> <hr class='separator' color='#505050'>
<div id='thread' class='column hidden'></div>
<hr class='separator' color='#505050'>
<div id='member' class='column hidden'>
<p>
<button onclick=${e =>
document.getElementById('member').classList.add('hidden')
}>close</button>
</p>
<p>user: <strong id='membername'></strong></p>
<p id='memberusername'></p>
</div>
`); `);
for (let i = 0; i < instancelist.length; ++i) { for (let i = 0; i < instancelist.length; ++i) {

View File

@ -32,17 +32,16 @@ async function auth() {
pubkey: window.keys.armored.publicKey pubkey: window.keys.armored.publicKey
}, },
async msg => { async msg => {
let register = document.getElementById('register');
if (!msg.success) { if (!msg.success) {
console.log('authenticate failed:', msg.message); console.log('authenticate failed:', msg.message);
if (document.getElementById('app'))
location.reload();
document.getElementById('result').innerText = msg.message; document.getElementById('result').innerText = msg.message;
register.classList.remove('hidden');
return; return;
} }
localStorage.setItem('keys', JSON.stringify(window.keys.armored)); localStorage.setItem('keys', JSON.stringify(window.keys.armored));
localStorage.setItem('name', window.name = msg.name); localStorage.setItem('name', window.name = msg.name);
localStorage.setItem('id', window.id = msg.id); localStorage.setItem('id', window.id = msg.id);
register.classList.add('hidden');
window.displayname = msg.displayname; window.displayname = msg.displayname;
if (window.instancelist) { if (window.instancelist) {
if (window.instancelist[0].url !== location.host if (window.instancelist[0].url !== location.host
@ -62,8 +61,10 @@ async function auth() {
instancelist[0].socket = window.socket; instancelist[0].socket = window.socket;
instancelist[0].emit = window.emit; instancelist[0].emit = window.emit;
window.instances = Object.fromEntries(instancelist.map(i => [i.url, i])); window.instances = Object.fromEntries(instancelist.map(i => [i.url, i]));
document.getElementById('register')?.remove();
const { authRequest } = await import('/app.js'); const { authRequest } = await import('/app.js');
msg.authrequests.forEach(authRequest); msg.authrequests.forEach(authRequest);
document.getElementById('profilepublic').checked = msg.public;
}); });
} }

View File

@ -61,7 +61,7 @@
&::placeholder { &::placeholder {
color: #aaa; color: #aaa;
} }
&[type='radio'] { &[type='radio'], &[type='checkbox'] {
position: relative; position: relative;
top: 2px; top: 2px;
} }
@ -107,7 +107,7 @@
margin: 8px 2px; margin: 8px 2px;
} }
.separator:has(+ .separator), .separator:has(+ .separator),
*.hidden + .separator, *.hidden:first-child + .separator,
.separator:has(+ *.hidden), .separator:has(+ *.hidden),
.separator:last-child { .separator:last-child {
display: none; display: none;
@ -171,6 +171,12 @@
padding: 6px; padding: 6px;
background-color: #222; background-color: #222;
} }
.user {
white-space: pre-wrap;
&:hover {
background-color: #444;
}
}
#profile { #profile {
max-width: 250px; max-width: 250px;
} }
@ -257,6 +263,9 @@
min-width: 140px; min-width: 140px;
max-width: 250px; max-width: 250px;
} }
#member {
max-width: 250px;
}
#editthread { #editthread {
max-width: fit-content; max-width: fit-content;
} }

View File

@ -53,22 +53,27 @@ 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) {
if (!message.name) { let span;
if (message.name)
span = window.makeUser({
displayname: message.displayname,
name: message.name,
id: message.user
}, instance.url);
else {
try { try {
message.name = (users[message.userid] || ( span = window.makeUser(users[message.userid] || (
users[message.userid] = await window.getUser(message.userid) users[message.userid] = await window.getUser(message.userid)
)).displayname; ), message.userid.split('@')[1]);
} }
catch (e) { catch (e) {
console.log(`error getting user ${message.userid}`, e); console.log(`error getting user ${message.userid}`, e);
message.name = message.userid; span = html.node`<span>${message.userid}</span>`;
} }
} }
messages.prepend(html.node` message = html.node`<div class='message'>: ${message.content}</div>`;
<div class='message'> message.prepend(span);
<strong>${message.name}: </strong> messages.prepend(message);
${message.content}
</div>`);
} }
} }
if (msg.more) if (msg.more)
@ -84,11 +89,13 @@ function loadMessages(callback) {
return; return;
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;
messages.appendChild(html.node` let div = html.node`<div class='message'>: ${message.content}</div>`;
<div class='message'> div.prepend(window.makeUser({
<strong>${message.name}: </strong> name: window.name,
${message.content} displayname: window.displayname,
</div>`); id: window.id
}, location.host));
messages.append(div);
if (scroll) if (scroll)
messages.scroll(0, messages.scrollHeight - messages.clientHeight); messages.scroll(0, messages.scrollHeight - messages.clientHeight);
if (!earliestMessage) if (!earliestMessage)

View File

@ -72,10 +72,12 @@ function chooseThread() {
loadStreams(); loadStreams();
setVisibility(); setVisibility();
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 => {
html.node`<p class='member'>${ let p = document.createElement('p');
member.name || (await window.getUser(member.id)).name p.append(member.name ? window.makeUser(member, window.currentInstance.url)
}</p>`)) : makeUser(await window.getUser(member.id), member.id.split('@')[1]));
return p;
}))
); );
}); });
} }
@ -111,12 +113,19 @@ function newThread() {
} }
async function addMember() { async function addMember() {
const name = document.getElementById('membername'); const name = document.getElementById('newmembername');
if (!name.value) if (!name.value)
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 user = await window.getUser('@' + url, at[0]); 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) { if (instancediv.instance.url !== url) {
user.id += '@' + url; user.id += '@' + url;
user.name += '@' + url; user.name += '@' + url;
@ -178,14 +187,15 @@ function newThread() {
<input type='radio' name='newpermissions' <input type='radio' name='newpermissions'
id='private_view' value='private_view' /> id='private_view' value='private_view' />
<label for='private_view'>only members can view and post</label><br> <label for='private_view'>only members can view and post</label><br>
<label class='heading' for='membername'>members</label> <label class='heading' for='newmembername'>members</label>
<input type='text' id='membername' placeholder='username' onkeydown=${event => { <input type='text' id='newmembername' placeholder='username' onkeydown=${event => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
addMember(); addMember();
} }
}} /> }} />
<button id='addmember' type='button' onclick=${addMember}>add</button> <button id='addmember' type='button' onclick=${addMember}>add</button>
<p id='membererror'></p>
<div id='newmembers'> <div id='newmembers'>
<p class='member'>${members[0].name}</p> <p class='member'>${members[0].name}</p>
</div> </div>
@ -261,7 +271,7 @@ function editThread() {
form['private_post'].checked = true; form['private_post'].checked = true;
else else
form['private_view'].checked = true; form['private_view'].checked = true;
document.body.append(form); document.getElementById('thread').append(form);
document.getElementById('edit').textContent = 'cancel'; document.getElementById('edit').textContent = 'cancel';
} }
@ -287,34 +297,33 @@ async function loadThreads(instancediv, select) {
return node; return node;
} }
if (!document.getElementById('thread')) let thread = document.getElementById('thread');
document.body.append(html.node` if (!thread.hasChildNodes())
<div id='thread' class='column hidden'> thread.append(html.node`
<div id='content' class='content'> <div id='content' class='content'>
<div id='titlebar'> <div id='titlebar'>
<span id='title'>thread: <strong id='threadname'>meow</strong></span> <span id='title'>thread: <strong id='threadname'>meow</strong></span>
<button id='edit' class='hidden' onclick=${editThread}>edit</button> <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> </div>
<hr class='separator' color='#505050'> <div id='buttonbar'>
<div id='members' class='column hidden'> <div id='tabs'>
<p id='visibility'></p> <button id='messagetab' class='tab active' onclick=${clickedTab}>messages
<h4>members</h4> </button><button id='spacetab' class='tab' onclick=${clickedTab}>space
<div id='memberlist'> </button><button id='streamtab' class='tab' onclick=${clickedTab}>streams</button>
</div> </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>
<h4>members</h4>
<div id='memberlist'>
</div> </div>
</div> </div>
<hr class='separator' color='#505050'>`); <hr class='separator' color='#505050'>`);

View File

@ -2,6 +2,7 @@ create table user (
id integer primary key asc, id integer primary key asc,
name text, name text,
displayname text, displayname text,
public boolean default true,
created timestamp default current_timestamp created timestamp default current_timestamp
); );

View File

@ -68,7 +68,7 @@ async function get_history(msg, respond) {
}); });
} }
const messages = (await db.query( const messages = (await db.query(
`select coalesce(displayname, name) as 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' : ''}

View File

@ -39,8 +39,8 @@ async function create_user(msg, respond) {
} }
// add to db // add to db
const insert = await db.query( const insert = await db.query(
'insert into user (name) values (?) returning id', 'insert into user (name, public) values (?, ?) returning id',
[msg.name] [msg.name, true]
); );
await db.query( await db.query(
'insert into key (user, pubkey, active) values (?, ?, true)', 'insert into key (user, pubkey, active) values (?, ?, true)',
@ -55,9 +55,9 @@ async function create_user(msg, respond) {
async function getUser(id, name) { async function getUser(id, name) {
return (id ? await db.query( return (id ? await db.query(
`select name, id, displayname from user where id = ?`, id) `select name, id, displayname, public from user where id = ?`, id)
: await db.query( : await db.query(
`select name, id, displayname from user where name = ?`, name) `select name, id, displayname, public from user where name = ?`, name)
).rows[0]; ).rows[0];
} }
@ -184,6 +184,7 @@ async function authenticate(msg, respond, socket) {
id: user.id, id: user.id,
name: user.name, name: user.name,
displayname: user.displayname, displayname: user.displayname,
public: user.public,
authrequests: Object.entries(user.authrequests).map(authrequest => ({ authrequests: Object.entries(user.authrequests).map(authrequest => ({
id: authrequest[0], id: authrequest[0],
time: authrequest[1].time time: authrequest[1].time
@ -221,8 +222,8 @@ async function update_user(msg, respond) {
message: 'user not found' message: 'user not found'
}); });
await db.query( await db.query(
`update user set displayname = ? where id = ?`, `update user set displayname = ?, public = ? where id = ?`,
[msg.displayname, msg.auth_user.id]); [msg.displayname, !!msg.public, msg.auth_user.id]);
vybe.users[msg.auth_user.id].displayname = msg.displayname; vybe.users[msg.auth_user.id].displayname = msg.displayname;
respond({ respond({
success: true success: true
@ -254,7 +255,8 @@ async function list_users(msg, respond) {
success: true, success: true,
users: (await db.query(` users: (await db.query(`
select id, name, coalesce(displayname, name) as displayname select id, name, coalesce(displayname, name) as displayname
from user`)).rows from user
where public = true`)).rows
}); });
} }