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

View File

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

View File

@ -1,4 +1,4 @@
let space = document.getElementById('space');
let space;
let scale = 1;
@ -13,23 +13,32 @@ function mousemove(event) {
dragging.style.left = `${left < 0 ? 0 : left}px`;
dragging.style.top = `${top < 0 ? 0 : top}px`;
moved = true;
//save
save(dragging);
}
function remove(span) {
//remove
span.remove();
function save(span) {
window.emit('save_span', {
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');
span.classList.add('span');
span.contentEditable = true;
span.spellcheck = false;
span.scale = 1;
span.scale = scale;
span.style.left = `${x}px`;
span.style.top = `${y}px`;
span.style.transform = 'translate(-50%, -50%)';
span.style.transform = `translate(-50%, -50%) scale(${scale})`;
span.onkeydown = function(event) {
if (event.key === 'Enter' && !event.getModifierState('Shift')) {
event.preventDefault();
@ -38,12 +47,13 @@ function add(x, y) {
}
};
span.oninput = function(event) {
this.time = Date.now();
//save
save(this);
};
span.onblur = function(event) {
if (!this.innerText)
remove(this);
if (this.innerText)
return;
save(this);
this.remove();
};
span.onwheel = function(event) {
event.preventDefault();
@ -76,18 +86,32 @@ function add(x, y) {
return span;
}
space.onmouseup = event => {
export default function loadSpace() {
if (!space) {
space = document.getElementById('space');
space.onmouseup = event => {
if (dragging) {
dragging.onmouseup(event);
return;
}
if (editing) {
if (!editing.innerText)
remove(editing);
if (event.target !== editing)
editing = null;
return;
}
editing = add(event.offsetX + space.scrollLeft, event.offsetY + space.scrollTop);
editing.focus();
};
}
if (window.space === window.currentThreadId)
return;
window.space = window.currentThreadId;
space.innerHTML = '';
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 (
id integer primary key asc,
thread integer,
creator integer,
created timestamp default current_timestamp,
deleted bool default false,
content text,
x integer,
y integer,
x decimal,
y decimal,
scale decimal,
foreign key(creator) references user(id),
foreign key(thread) references thread(id)
);

View File

@ -1,17 +1,19 @@
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const compression = require("compression");
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const compression = require('compression');
const events = Object.fromEntries(
[
'create_user',
'get_history',
'send_message',
'authenticate',
'create_thread',
'list_threads',
'create_user',
'get_history',
'get_keys',
'get_space',
'list_threads',
'save_span',
'send_message'
].map(event => [event, require('./src/' + event)])
);
@ -28,7 +30,7 @@ const PORT = process.env.PORT || 3435;
io.cache = {};
io.on("connection", (socket) => {
io.on('connection', (socket) => {
for (let event in events) {
socket.on(event, (msg, callback) =>
events[event](msg, callback, socket, io)
@ -42,7 +44,7 @@ io.on("connection", (socket) => {
});
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 {
is_member,
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]
);
for (let username in io.cache) {
if (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) {
if (permissions.rows.length > 0 || members.includes(username)) {
const sockets = io.cache[username];
for (let s of sockets) {
io.to(s).emit("new_message", {