spaces working real

main
jerl 2024-04-15 14:27:21 -07:00
parent 26530690b9
commit 59d0b9eab2
9 changed files with 257 additions and 133 deletions

View File

@ -1,28 +1,34 @@
import { render, html } from '/uhtml.js'; import { render, html } from '/uhtml.js';
import loadSpace from '/space.js';
window.currentThreadId = 1; window.currentThreadId = 1;
function chooseThread(thread) { function chooseThread(thread) {
if (window.currentThreadId) if (window.currentThreadId) {
document if (window.currentThreadId === thread.id)
.getElementById(`thread${window.currentThreadId}`) return;
.classList.remove("active"); document.getElementById(`thread${window.currentThreadId}`)
document.getElementById(`thread${thread.id}`).classList.add("active"); .classList.remove('active');
}
const el = document.getElementById(`thread${thread.id}`);
el.classList.add('active');
window.currentThreadId = thread.id; window.currentThreadId = thread.id;
window.earliestMessage = null; window.earliestMessage = null;
document.getElementById("messages").innerHTML = ""; document.getElementById('threadname').textContent = thread.name;
document.getElementById("threadname").textContent = thread.name; if (thread.permissions.post)
if (!thread.permissions.post) { document.getElementById('msginput').classList.remove('hidden');
document.getElementById("msginput").classList.add("hidden"); else
} else { document.getElementById('msginput').classList.add('hidden');
document.getElementById("msginput").classList.remove("hidden"); switchTab(document.getElementById(el.tab));
}
loadMessages(); loadMessages();
if (el.tab === 'spacetab')
loadSpace();
} }
function loadMessages() { function loadMessages() {
document.getElementById('messages').innerHTML = '';
window.emit( window.emit(
"get_history", 'get_history',
{ {
before: window.earliestMessage, before: window.earliestMessage,
thread: window.currentThreadId, thread: window.currentThreadId,
@ -31,16 +37,16 @@ function loadMessages() {
if (msg.messages.length > 0) { if (msg.messages.length > 0) {
window.earliestMessage = msg.messages[msg.messages.length - 1].id; window.earliestMessage = msg.messages[msg.messages.length - 1].id;
for (let message of msg.messages) for (let message of msg.messages)
document.getElementById("messages").prepend(html.node` document.getElementById('messages').prepend(html.node`
<div class='message'> <div class='message'>
<strong>${message.name}: </strong> <strong>${message.name}: </strong>
${message.message} ${message.message}
</div>`); </div>`);
} }
if (!msg.more) if (!msg.more)
document.getElementById("loadmore").classList.add("hidden"); document.getElementById('loadmore').classList.add('hidden');
else else
document.getElementById("loadmore").classList.remove("hidden"); document.getElementById('loadmore').classList.remove('hidden');
} }
); );
} }
@ -51,21 +57,35 @@ function addThread(thread, top) {
thread.name thread.name
}</div>`; }</div>`;
node.id = `thread${thread.id}`; node.id = `thread${thread.id}`;
document.getElementById("threadlist")[top ? "prepend" : "appendChild"](node); node.tab = 'messagetab';
document.getElementById('threadlist')[top ? 'prepend' : 'appendChild'](node);
}
function switchTab(tab) {
for (let tab of document.querySelectorAll('.tab'))
tab.classList.remove('active');
for (let tab of document.querySelectorAll('.tabcontent'))
tab.classList.add('hidden');
tab.classList.add('active');
document
.getElementById(tab.id.slice(0, -3))
.classList.remove('hidden');
if (tab.id === 'spacetab')
loadSpace();
} }
function addMember() { function addMember() {
const name = document.getElementById("membername").value; const name = document.getElementById('membername').value;
window.threadmembers.push(name); window.threadmembers.push(name);
document document
.getElementById("memberlist") .getElementById('memberlist')
.appendChild(html.node`<p class='member'>${name}</p>`); .appendChild(html.node`<p class='member'>${name}</p>`);
document.getElementById("membername").value = ""; document.getElementById('membername').value = '';
} }
async function createThread(e) { async function createThread(e) {
e.preventDefault(); e.preventDefault();
let name = document.getElementById("newthreadname"); let name = document.getElementById('newthreadname');
if (!name.value) { if (!name.value) {
name.insertAdjacentHTML('afterend', `<p>name cannot be empty</p>`); name.insertAdjacentHTML('afterend', `<p>name cannot be empty</p>`);
return; return;
@ -74,24 +94,24 @@ async function createThread(e) {
const perms = document.querySelector( const perms = document.querySelector(
'input[name="permissions"]:checked' 'input[name="permissions"]:checked'
).value; ).value;
if (perms === "private_view") if (perms === 'private_view')
members = ( members = (
await new Promise(resolve => await new Promise(resolve =>
window.emit("get_keys", { names: window.threadmembers }, resolve) window.emit('get_keys', { names: window.threadmembers }, resolve)
) )
).keys; ).keys;
let permissions; let permissions;
if (perms === "public") { if (perms === 'public') {
permissions = { permissions = {
view_limited: false, view_limited: false,
post_limited: false post_limited: false
}; };
} else if (perms === "private_post") { } else if (perms === 'private_post') {
permissions = { permissions = {
view_limited: false, view_limited: false,
post_limited: true post_limited: true
}; };
} else if (perms === "private_view") { } else if (perms === 'private_view') {
permissions = { permissions = {
view_limited: true, view_limited: true,
post_limited: true post_limited: true
@ -112,11 +132,11 @@ async function createThread(e) {
*/ */
} }
window.emit( window.emit(
"create_thread", 'create_thread',
{ {
name: name.value, name: name.value,
permissions, permissions,
members, members
}, },
msg => { msg => {
chooseThread({ chooseThread({
@ -130,113 +150,106 @@ async function createThread(e) {
}); });
// since the form exists, this will perform cleanup // since the form exists, this will perform cleanup
newThread(); newThread();
document.getElementById("loadmore").classList.add("hidden"); document.getElementById('loadmore').classList.add('hidden');
document.getElementById("msginput").classList.remove("hidden"); document.getElementById('msginput').classList.remove('hidden');
} }
); );
} }
function sendMessage(e) { function sendMessage(e) {
e.preventDefault(); e.preventDefault();
const msg = document.getElementById("msg").value; const msg = document.getElementById('msg').value;
if (!msg) if (!msg)
return; return;
window.emit("send_message", { window.emit('send_message', {
message: msg, message: msg,
thread: window.currentThreadId thread: window.currentThreadId
}); });
document.getElementById("msg").value = ""; document.getElementById('msg').value = '';
} }
function newThread() { function newThread() {
let form = document.getElementById('createthread'); let form = document.getElementById('createthread');
if (form) { if (form) {
form.remove(); form.remove();
document.getElementById("newthread").textContent = 'create'; document.getElementById('newthread').textContent = 'create';
} else { } else {
window.threadmembers = [window.name]; window.threadmembers = [window.name];
document.getElementById('threads').insertAdjacentElement('afterend', html.node` document.getElementById('threads').insertAdjacentElement('afterend', html.node`
<form id="createthread" class='column' onsubmit=${createThread}> <form id='createthread' class='column' onsubmit=${createThread}>
<h3>create thread</h3> <h3>create thread</h3>
<label for="newthreadname" class="heading">thread name</label> <label for='newthreadname' class='heading'>thread name</label>
<input type="text" id="newthreadname" /> <input type='text' id='newthreadname' />
<p id='permissions'>thread permissions</p> <p id='permissions'>thread permissions</p>
<input type="radio" id="public" name="permissions" value="public" checked /> <input type='radio' id='public' name='permissions' value='public' checked />
<label for="public">anyone can view and post</label><br /> <label for='public'>anyone can view and post</label><br />
<input type="radio" id="private_post" <input type='radio' id='private_post'
name="permissions" value="private_post" name='permissions' value='private_post'
/> />
<label for="private_post">anyone can view, only members can post</label><br/> <label for='private_post'>anyone can view, only members can post</label><br/>
<input type="radio" id="private_view" <input type='radio' id='private_view'
name="permissions" value="private_view" name='permissions' value='private_view'
/> />
<label for="private_view">only members can view and post</label <label for='private_view'>only members can view and post</label
><br /><br /> ><br /><br />
<label class="heading" for="membername">members</label> <label class='heading' for='membername'>members</label>
<input type="text" id="membername" placeholder="username" onkeydown=${(e) => { <input type='text' id='membername' placeholder='username' onkeydown=${(e) => {
if (e.key == "Enter") { if (e.key == 'Enter') {
e.preventDefault(); e.preventDefault();
addMember(); addMember();
} }
}}/> }}/>
<button id="addmember" onclick=${addMember}>add</button> <button id='addmember' onclick=${addMember}>add</button>
<div id="memberlist"> <div id='memberlist'>
<p class='member'>${window.name}</p> <p class='member'>${window.name}</p>
</div> </div>
<br /> <br />
<button id="submitthread" type="submit">create</button> <button id='submitthread' type='submit'>create</button>
</form> </form>
` `
); );
document.getElementById("newthread").textContent = 'cancel'; document.getElementById('newthread').textContent = 'cancel';
} }
} }
function switchTab(event) { function clickedTab(event) {
for (let tab of document.querySelectorAll('.tab')) switchTab(event.target);
tab.classList.remove('active'); document.getElementById(`thread${window.currentThreadId}`).tab = event.target.id;
for (let tab of document.querySelectorAll('.tabcontent'))
tab.classList.add('hidden');
event.target.classList.add('active');
document
.getElementById(event.target.id.substring(0, event.target.id.length - 3))
.classList.remove('hidden');
} }
render(document.body, html` render(document.body, html`
<div id="threads" class="column"> <div id='threads' class='column'>
<h3>vybe</h3> <h3>vybe</h3>
<h4>threads</h4> <h4>threads</h4>
<div id="threadlist">loading...</div> <div id='threadlist'>loading...</div>
<button id='newthread' onclick=${newThread}>create</button> <button id='newthread' onclick=${newThread}>create</button>
</div> </div>
<div id="thread" class="column"> <hr class='separator' color='#666'>
<div id='thread' class='column'>
<div id='title'> <div id='title'>
thread: <strong id="threadname">meow</strong> thread: <strong id='threadname'>meow</strong>
</div> </div>
<div id='tabs'> <div id='tabs'>
<button id='messagetab' class='tab active' onclick=${switchTab}> <button id='messagetab' class='tab active' onclick=${clickedTab}>messages</button><button id='spacetab' class='tab' onclick=${clickedTab}>space</button>
messages
</button><button id='spacetab' class='tab' onclick=${switchTab}>space</button>
</div> </div>
<div id='message' class='tabcontent'> <div id='message' class='tabcontent'>
<button id="loadmore" class="hidden" onclick=${loadMessages}> <button id='loadmore' class='hidden' onclick=${loadMessages}>
load more messages load more messages
</button> </button>
<div id='messages'></div> <div id='messages'></div>
<form id="msginput" onsubmit=${sendMessage}> <form id='msginput' onsubmit=${sendMessage}>
<input type="text" placeholder="write a message..." id="msg" /> <input type='text' placeholder='write a message...' id='msg' />
<button type="submit" id="sendmsg">send</button> <button type='submit' id='sendmsg'>send</button>
</form> </form>
</div> </div>
<div id='space' class='tabcontent hidden'></div> <div id='space' class='tabcontent hidden'></div>
</div> </div>
`); `);
window.socket.on("new_message", msg => { window.socket.on('new_message', msg => {
if (msg.thread !== window.currentThreadId) if (msg.thread !== window.currentThreadId)
return; return;
document.getElementById("messages").appendChild(html.node` document.getElementById('messages').appendChild(html.node`
<div class='message'> <div class='message'>
<strong>${msg.name}: </strong> <strong>${msg.name}: </strong>
${msg.message} ${msg.message}
@ -244,13 +257,11 @@ window.socket.on("new_message", msg => {
if (!window.earliestMessage) if (!window.earliestMessage)
window.earliestMessage = msg.id; window.earliestMessage = msg.id;
}); });
window.socket.on("new_thread", thread => addThread(thread, true)); window.socket.on('new_thread', thread => addThread(thread, true));
window.emit("list_threads", {}, msg => { window.emit('list_threads', {}, msg => {
document.getElementById("threadlist").innerHTML = ""; document.getElementById('threadlist').innerHTML = '';
for (let thread of msg.threads) for (let thread of msg.threads)
addThread(thread); addThread(thread);
chooseThread(msg.threads[0]); chooseThread(msg.threads[0]);
}); });
import('/space.js');

View File

@ -91,11 +91,14 @@
overflow: hidden; overflow: hidden;
margin: 5px; margin: 5px;
} }
.separator {
margin: 8px 2px;
}
#threads { #threads {
max-width: 250px; max-width: 250px;
} }
.thread { .thread {
padding: 2px; padding: 2px 4px;
} }
#newthread { #newthread {
margin-top: 5px; margin-top: 5px;
@ -139,6 +142,7 @@
#msginput { #msginput {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin: 2px;
} }
#msg { #msg {
flex-grow: 1; flex-grow: 1;

View File

@ -1,4 +1,4 @@
let space = document.getElementById('space'); let space;
let scale = 1; let scale = 1;
@ -13,23 +13,32 @@ function mousemove(event) {
dragging.style.left = `${left < 0 ? 0 : left}px`; dragging.style.left = `${left < 0 ? 0 : left}px`;
dragging.style.top = `${top < 0 ? 0 : top}px`; dragging.style.top = `${top < 0 ? 0 : top}px`;
moved = true; moved = true;
//save save(dragging);
} }
function remove(span) { function save(span) {
//remove window.emit('save_span', {
span.remove(); thread: window.space,
id: span.id ? span.id.slice(4) : '',
content: span.innerText,
x: span.style.left.slice(0, -2),
y: span.style.top.slice(0, -2),
scale: span.scale
}, msg => {
if (!span.id)
span.id = 'span' + msg.id;
});
} }
function add(x, y) { function add(x, y, scale = 1) {
let span = document.createElement('span'); let span = document.createElement('span');
span.classList.add('span'); span.classList.add('span');
span.contentEditable = true; span.contentEditable = true;
span.spellcheck = false; span.spellcheck = false;
span.scale = 1; span.scale = scale;
span.style.left = `${x}px`; span.style.left = `${x}px`;
span.style.top = `${y}px`; span.style.top = `${y}px`;
span.style.transform = 'translate(-50%, -50%)'; span.style.transform = `translate(-50%, -50%) scale(${scale})`;
span.onkeydown = function(event) { span.onkeydown = function(event) {
if (event.key === 'Enter' && !event.getModifierState('Shift')) { if (event.key === 'Enter' && !event.getModifierState('Shift')) {
event.preventDefault(); event.preventDefault();
@ -38,12 +47,13 @@ function add(x, y) {
} }
}; };
span.oninput = function(event) { span.oninput = function(event) {
this.time = Date.now(); save(this);
//save
}; };
span.onblur = function(event) { span.onblur = function(event) {
if (!this.innerText) if (this.innerText)
remove(this); return;
save(this);
this.remove();
}; };
span.onwheel = function(event) { span.onwheel = function(event) {
event.preventDefault(); event.preventDefault();
@ -76,18 +86,32 @@ function add(x, y) {
return span; return span;
} }
space.onmouseup = event => { export default function loadSpace() {
if (dragging) { if (!space) {
dragging.onmouseup(event); space = document.getElementById('space');
return; space.onmouseup = event => {
if (dragging) {
dragging.onmouseup(event);
return;
}
if (editing) {
if (event.target !== editing)
editing = null;
return;
}
editing = add(event.offsetX + space.scrollLeft, event.offsetY + space.scrollTop);
editing.focus();
};
} }
if (editing) { if (window.space === window.currentThreadId)
if (!editing.innerText)
remove(editing);
if (event.target !== editing)
editing = null;
return; return;
} window.space = window.currentThreadId;
editing = add(event.offsetX + space.scrollLeft, event.offsetY + space.scrollTop); space.innerHTML = '';
editing.focus(); window.emit('get_space', { thread: window.space }, msg => {
for (const span of msg.spans) {
let el = add(span.x, span.y, span.scale);
el.innerText = span.content;
el.id = 'span' + span.id;
}
});
}; };

View File

@ -54,13 +54,11 @@ create table post (
create table span ( create table span (
id integer primary key asc, id integer primary key asc,
thread integer, thread integer,
creator integer, deleted bool default false,
created timestamp default current_timestamp,
content text, content text,
x integer, x decimal,
y integer, y decimal,
scale decimal, scale decimal,
foreign key(creator) references user(id),
foreign key(thread) references thread(id) foreign key(thread) references thread(id)
); );

View File

@ -1,17 +1,19 @@
const express = require("express"); const express = require('express');
const http = require("http"); const http = require('http');
const { Server } = require("socket.io"); const { Server } = require('socket.io');
const compression = require("compression"); const compression = require('compression');
const events = Object.fromEntries( const events = Object.fromEntries(
[ [
'create_user',
'get_history',
'send_message',
'authenticate', 'authenticate',
'create_thread', 'create_thread',
'list_threads', 'create_user',
'get_history',
'get_keys', 'get_keys',
'get_space',
'list_threads',
'save_span',
'send_message'
].map(event => [event, require('./src/' + event)]) ].map(event => [event, require('./src/' + event)])
); );
@ -28,7 +30,7 @@ const PORT = process.env.PORT || 3435;
io.cache = {}; io.cache = {};
io.on("connection", (socket) => { io.on('connection', (socket) => {
for (let event in events) { for (let event in events) {
socket.on(event, (msg, callback) => socket.on(event, (msg, callback) =>
events[event](msg, callback, socket, io) events[event](msg, callback, socket, io)
@ -42,7 +44,7 @@ io.on("connection", (socket) => {
}); });
server.listen(PORT, () => { server.listen(PORT, () => {
console.log("server running on port " + PORT); console.log('server running on port ' + PORT);
}); });
app.use(express.static("client")); app.use(express.static('client'));

28
src/get_space.js Normal file
View File

@ -0,0 +1,28 @@
const db = require('../db');
const authwrap = require('./authwrap');
const check_permission = require('./helpers/check_permission');
const get_space = async (msg, respond) => {
if (!msg.thread) {
return respond({
success: false,
message: 'thread ID required',
});
}
if (!(await check_permission(msg.auth_user.id, msg.thread)).view) {
return respond({
success: false,
message: "you can't view this thread",
});
}
const spans = await db.query(
'select id, content, x, y, scale from span where thread=? and deleted=false',
[msg.thread]
);
return respond({
success: true,
spans: spans.rows
});
};
module.exports = authwrap(get_space);

View File

@ -30,7 +30,7 @@ const check_permission = async (user_id, thread_id) => {
return { return {
is_member, is_member,
view: get_permission("view"), view: get_permission("view"),
post: get_permission("post"), post: get_permission("post")
}; };
}; };

67
src/save_span.js Normal file
View File

@ -0,0 +1,67 @@
const db = require('../db');
const authwrap = require('./authwrap');
const check_permission = require('./helpers/check_permission');
const save_span = async (msg, respond, socket, io) => {
if (!msg.thread) {
return respond({
success: false,
message: 'thread ID required',
});
}
if (!(await check_permission(msg.auth_user.id, msg.thread)).post) {
return respond({
success: false,
message: "you can't post to this thread",
});
}
// save span and send it to everyone
let id;
if (msg.id) {
id = msg.id;
if (msg.content)
await db.query('update span set content=?, x=?, y=?, scale=?, deleted=false where id=?',
[msg.content, msg.x, msg.y, msg.scale, msg.id]);
else
await db.query('update span set deleted=true where id=?', [msg.id]);
}
else {
id = (await db.query(
'insert into span (thread, content, x, y, scale) values (?, ?, ?, ?, ?) returning id',
[msg.thread, msg.content, msg.x, msg.y, msg.scale]
)).rows[0].id;
}
// get thread members
const members = (
await db.query(
'select name from user join member on member.user = user.id where member.thread = ?',
[msg.thread]
)
).rows.map(i => i.name);
// get perms
const permissions = await db.query(
"select * from permission where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'",
[msg.thread]
);
for (let username in io.cache) {
if (permissions.rows.length > 0 || members.includes(username)) {
const sockets = io.cache[username];
for (let s of sockets) {
io.to(s).emit('span', {
id,
thread: msg.thread,
content: msg.content,
x: msg.x,
y: msg.y,
scale: msg.scale
});
}
}
}
return respond({
success: true,
id
});
};
module.exports = authwrap(save_span);

View File

@ -33,17 +33,7 @@ const send_message = async (msg, respond, socket, io) => {
[msg.thread] [msg.thread]
); );
for (let username in io.cache) { for (let username in io.cache) {
if (members.includes(username)) { if (permissions.rows.length > 0 || members.includes(username)) {
const sockets = io.cache[username];
for (let s of sockets) {
io.to(s).emit("new_message", {
id: id.rows[0].id,
name: msg.auth_user.name,
message: msg.message,
thread: msg.thread,
});
}
} else if (permissions.rows.length > 0) {
const sockets = io.cache[username]; const sockets = io.cache[username];
for (let s of sockets) { for (let s of sockets) {
io.to(s).emit("new_message", { io.to(s).emit("new_message", {