From 59d0b9eab2849bc33739800692c490b42a15bc68 Mon Sep 17 00:00:00 2001 From: jerl Date: Mon, 15 Apr 2024 14:27:21 -0700 Subject: [PATCH] spaces working real --- client/app.js | 169 +++++++++++++++++--------------- client/index.html | 6 +- client/space.js | 72 +++++++++----- db/1-init.sql | 10 +- index.js | 24 ++--- src/get_space.js | 28 ++++++ src/helpers/check_permission.js | 2 +- src/save_span.js | 67 +++++++++++++ src/send_message.js | 12 +-- 9 files changed, 257 insertions(+), 133 deletions(-) create mode 100644 src/get_space.js create mode 100644 src/save_span.js diff --git a/client/app.js b/client/app.js index b58985f..bdf795f 100644 --- a/client/app.js +++ b/client/app.js @@ -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`
${message.name}: ${message.message}
`); } 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 }`; 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`

${name}

`); - 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', `

name cannot be empty

`); 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` -
+

create thread

- - + +

thread permissions

- -
- +
+ -
- anyone can view, only members can post
+ - only members can view and post

- - { - if (e.key == "Enter") { + + { + if (e.key == 'Enter') { e.preventDefault(); addMember(); } }}/> - -
+ +

${window.name}


- + ` ); - 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` -
+

vybe

threads

-
loading...
+
loading...
-
+
+
- thread: meow + thread: meow
- +
-
-
- - + + +
`); -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`
${msg.name}: ${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'); diff --git a/client/index.html b/client/index.html index 1ec5c0f..400bcd8 100644 --- a/client/index.html +++ b/client/index.html @@ -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; diff --git a/client/space.js b/client/space.js index 30ff5cf..2bb8a33 100644 --- a/client/space.js +++ b/client/space.js @@ -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 => { - if (dragging) { - dragging.onmouseup(event); - return; +export default function loadSpace() { + if (!space) { + space = document.getElementById('space'); + 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 (!editing.innerText) - remove(editing); - if (event.target !== editing) - editing = null; + if (window.space === window.currentThreadId) return; - } - editing = add(event.offsetX + space.scrollLeft, event.offsetY + space.scrollTop); - editing.focus(); + 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; + } + }); }; diff --git a/db/1-init.sql b/db/1-init.sql index 5fae6c9..1832aa3 100644 --- a/db/1-init.sql +++ b/db/1-init.sql @@ -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) ); @@ -70,4 +68,4 @@ insert into permission (1, "everyone", "view", "true"); insert into permission (thread, type, permission, value) values - (1, "everyone", "post", "true"); \ No newline at end of file + (1, "everyone", "post", "true"); diff --git a/index.js b/index.js index 3bbd481..9ffbf80 100644 --- a/index.js +++ b/index.js @@ -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')); diff --git a/src/get_space.js b/src/get_space.js new file mode 100644 index 0000000..db4eae8 --- /dev/null +++ b/src/get_space.js @@ -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); diff --git a/src/helpers/check_permission.js b/src/helpers/check_permission.js index c4ed9bf..8618383 100644 --- a/src/helpers/check_permission.js +++ b/src/helpers/check_permission.js @@ -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") }; }; diff --git a/src/save_span.js b/src/save_span.js new file mode 100644 index 0000000..a9112d4 --- /dev/null +++ b/src/save_span.js @@ -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); diff --git a/src/send_message.js b/src/send_message.js index 4816e3a..d144010 100644 --- a/src/send_message.js +++ b/src/send_message.js @@ -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", {