bugfixes and style improvements

main
june moretz 2023-12-28 14:59:17 -06:00
parent 0b830174d3
commit 05bbdc0d60
5 changed files with 524 additions and 455 deletions

View File

@ -1,23 +1,33 @@
import { render, html } from '/uhtml.js'; import { render, html } from "/uhtml.js";
window.currentThreadId = 1; window.currentThreadId = 1;
function chooseThread(thread) { function chooseThread(thread) {
if (window.currentThreadId) if (window.currentThreadId)
document.getElementById(`thread${window.currentThreadId}`).classList.remove('selected'); document
document.getElementById(`thread${thread.id}`).classList.add('selected'); .getElementById(`thread${window.currentThreadId}`)
.classList.remove("active");
document.getElementById(`thread${thread.id}`).classList.add("active");
window.currentThreadId = thread.id; window.currentThreadId = thread.id;
window.earliestMessage = null; window.earliestMessage = null;
document.getElementById("messages").innerHTML = ""; document.getElementById("messages").innerHTML = "";
document.getElementById("threadname").textContent = thread.name; document.getElementById("threadname").textContent = thread.name;
if (!thread.permissions.post) {
document.getElementById("msginput").classList.add("hidden");
} else {
document.getElementById("msginput").classList.remove("hidden");
}
loadMessages(); loadMessages();
} }
function loadMessages() { function loadMessages() {
window.emit("get_history", { window.emit(
"get_history",
{
before: window.earliestMessage, before: window.earliestMessage,
thread: window.currentThreadId, thread: window.currentThreadId,
}, msg => { },
(msg) => {
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)
@ -29,30 +39,26 @@ function loadMessages() {
} }
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"); }
}); );
} }
function addThread(thread) { function addThread(thread, top) {
let node = html.node` let node = html.node`
<div class='thread' onclick=${() => { <button class='thread tab' onclick=${() => chooseThread(thread)}>${
chooseThread(thread); thread.name
if (!thread.permissions.post) { }</button>`;
document.getElementById("msginput").classList.add("hidden");
} else {
document.getElementById("msginput").classList.remove("hidden");
}
}}>${thread.name}</div>`;
node.id = `thread${thread.id}`; node.id = `thread${thread.id}`;
document.getElementById("threadlist").appendChild(node); document.getElementById("threadlist")[top ? "prepend" : "appendChild"](node);
} }
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.getElementById("memberlist").appendChild( document
html.node`<p class='member'>${name}</p>`); .getElementById("memberlist")
.appendChild(html.node`<p class='member'>${name}</p>`);
document.getElementById("membername").value = ""; document.getElementById("membername").value = "";
} }
@ -60,31 +66,34 @@ 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;
} }
let members = window.threadmembers.map(name => ({ name })); let members = window.threadmembers.map((name) => ({ name }));
const perms = document.querySelector( const perms = document.querySelector(
'input[name="permissions"]:checked').value; 'input[name="permissions"]:checked'
).value;
if (perms === "private_view") if (perms === "private_view")
members = (await new Promise(resolve => members = (
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,
}; };
// generate key // generate key
/* wip /* wip
@ -101,45 +110,50 @@ async function createThread(e) {
} }
*/ */
} }
window.emit("create_thread", { window.emit(
"create_thread",
{
name: name.value, name: name.value,
permissions, permissions,
members members,
}, msg => { },
(msg) => {
chooseThread({ chooseThread({
name: name.value, name: name.value,
id: msg.id id: msg.id,
}); });
document.getElementById('createthread').remove(); // since the form exists, this will perform cleanup
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(e) { function newThread() {
let form = document.getElementById('createthread'); let form = document.getElementById("createthread");
if (form) { if (form) {
form.remove(); form.remove();
e.target.textContent = 'create'; document.getElementById("newthread").textContent = "create";
} } else {
else { window.threadmembers = [window.name];
window.threadmembers = [ window.name ]; document.getElementById("threads").insertAdjacentElement(
document.getElementById('threads').insertAdjacentElement('afterend', html.node` "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">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 />
@ -153,7 +167,7 @@ function newThread(e) {
/> />
<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 />
<span>members</span><br /> <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();
@ -165,65 +179,68 @@ function newThread(e) {
<p class='member'>${window.name}</p> <p class='member'>${window.name}</p>
</div> </div>
<br /> <br />
<input id="submitthread" type="submit" value="create" /> <button id="submitthread" type="submit">create</button>
</form> </form>
`); `
e.target.textContent = 'cancel'; );
document.getElementById("newthread").textContent = "cancel";
} }
} }
function switchTab(event){ function switchTab(event) {
for (let tab of document.querySelectorAll('.tab')) for (let tab of document.querySelectorAll(".tab"))
tab.classList.remove('active'); tab.classList.remove("active");
for (let tab of document.querySelectorAll('.tabcontent')) for (let tab of document.querySelectorAll(".tabcontent"))
tab.classList.add('hidden'); tab.classList.add("hidden");
event.target.classList.add('active'); event.target.classList.add("active");
document.getElementById(event.target.id.substring(0, event.target.id.length - 3)) document
.classList.remove('hidden'); .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"> <div id="thread" class="column">
<div id='title'> <div id="title">thread: <strong id="threadname">meow</strong></div>
thread: <strong id="threadname">meow</strong> <button id="messagetab" class="tab active" onclick=${switchTab}>
</div> messages
<button id='messagetab' class='tab active' onclick=${switchTab}>messages</button> </button>
<button id='spacetab' class='tab' onclick=${switchTab}>space</button> <button id="spacetab" class="tab" onclick=${switchTab}>space</button>
<button id="loadmore" class="hidden" onclick=${loadMessages}>load more messages</button> <button id="loadmore" class="hidden" onclick=${loadMessages}>
<div id='message' class='tabcontent'> load more messages
<div id='messages'> </button>
</div> <div id="message" class="tabcontent">
<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" class="hidden" id="sendmsg"></button> <button type="submit" class="hidden" id="sendmsg"></button>
</form> </form>
</div> </div>
<div id='space' class='tabcontent'></div> <div id="space" class="tabcontent"></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}
</div>`); </div>`);
if (!window.earliestMessage) if (!window.earliestMessage) window.earliestMessage = msg.id;
window.earliestMessage = msg.id;
}); });
window.socket.on("new_thread", addThread); 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]);
}); });

View File

@ -1,4 +1,4 @@
import { render, html } from '/uhtml.js'; import { render, html } from "/uhtml.js";
function rand() { function rand() {
let str = ""; let str = "";
@ -16,19 +16,26 @@ async function auth() {
message: new openpgp.CleartextMessage("vybe_auth " + window.session, ""), message: new openpgp.CleartextMessage("vybe_auth " + window.session, ""),
signingKeys: window.keys.priv, signingKeys: window.keys.priv,
}); });
window.socket.emit("authenticate", { name: window.name, message: sig }, window.socket.emit(
msg => { "authenticate",
{ name: window.name, message: sig },
(msg) => {
if (!msg.success) { if (!msg.success) {
console.log('authenticate failed'); console.log("authenticate failed", msg);
return; return;
} }
if (document.getElementById("register")) {
document.getElementById("register").remove(); document.getElementById("register").remove();
import('/app.js'); import("/app.js");
}); }
}
);
} }
render(document.body, html` render(
<div id="register" class='hidden'> document.body,
html`
<div id="register" class="hidden">
<h1>welcome to vybe</h1> <h1>welcome to vybe</h1>
<h3>a communication network (beta)</h3> <h3>a communication network (beta)</h3>
<p> <p>
@ -36,20 +43,26 @@ render(document.body, html`
for security, rather than passwords. your keys are stored in your for security, rather than passwords. your keys are stored in your
browser storage only, so do this on a browser you can access again. browser storage only, so do this on a browser you can access again.
</p> </p>
<form onsubmit=${async e => { <form
onsubmit=${async (e) => {
e.preventDefault(); e.preventDefault();
const name = document.getElementById("name").value; const name = document.getElementById("name").value;
if (!name) if (!name) return;
return;
const keys = await openpgp.generateKey({ const keys = await openpgp.generateKey({
userIDs: [{ name }], userIDs: [{ name }],
}); });
const priv = await openpgp.readKey({ armoredKey: keys.privateKey }); const priv = await openpgp.readKey({ armoredKey: keys.privateKey });
const pub = await openpgp.readKey({ armoredKey: keys.publicKey }); const pub = await openpgp.readKey({ armoredKey: keys.publicKey });
window.emit("create_user", { name, pubkey: keys.publicKey }, msg => { window.emit(
"create_user",
{ name, pubkey: keys.publicKey },
(msg) => {
if (!msg.success) { if (!msg.success) {
document.querySelector('#registerform').insertAdjacentHTML('afterend', ` document.querySelector("#registerform").insertAdjacentHTML(
<p>${msg.message}</p>`); "afterend",
`
<p>${msg.message}</p>`
);
return; return;
} }
window.keys = { priv, pub }; window.keys = { priv, pub };
@ -57,23 +70,37 @@ render(document.body, html`
localStorage.setItem("name", name); localStorage.setItem("name", name);
window.name = name; window.name = name;
auth(); auth();
}); }
}} id="registerform"> );
}}
id="registerform"
>
<label for="name">username: </label> <label for="name">username: </label>
<input id="name" type="text" /> <input id="name" type="text" />
<input id="submit" type="submit" value='generate keys & register'> <input id="submit" type="submit" value="generate keys & register" />
</form> </form>
</div> </div>
`); `
);
const gensession = async () => {
window.session = rand();
window.emit = (type, data, callback) =>
window.socket.emit(
type,
{
...data,
__session: window.session,
},
callback
);
};
window.onload = async () => { window.onload = async () => {
window.socket = io(); window.socket = io();
window.session = rand(); await gensession();
window.emit = (type, data, callback) => window.socket.emit(type, {
...data,
__session: window.session,
}, callback);
let keys = localStorage.getItem("keys"); let keys = localStorage.getItem("keys");
if (keys) { if (keys) {
@ -81,10 +108,13 @@ window.onload = async () => {
keys = JSON.parse(keys); keys = JSON.parse(keys);
window.keys = { window.keys = {
priv: await openpgp.readKey({ armoredKey: keys.privateKey }), priv: await openpgp.readKey({ armoredKey: keys.privateKey }),
pub: await openpgp.readKey({ armoredKey: keys.publicKey }) pub: await openpgp.readKey({ armoredKey: keys.publicKey }),
}; };
await auth(); await auth();
} } else document.getElementById("register").classList.remove("hidden");
else
document.getElementById('register').classList.remove('hidden'); window.socket.io.on("reconnect", async (attempt) => {
await gensession();
if (localStorage.getItem("keys")) await auth();
});
}; };

View File

@ -18,20 +18,47 @@
body, body,
button, button,
input { input {
background: #020202;
color: #eaeaea; color: #eaeaea;
border: none;
outline: none;
} }
body { body {
background: #020202;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
margin: 0; margin: 0;
min-width: min-content; min-width: min-content;
} }
h3, h4 { button,
margin: 10px 0; input,
.tab {
padding: 5px 7px;
} }
button { button {
border-color: #767676; background: #4f4f4f;
}
input {
background: #2f2f2f;
border-bottom: 2px solid transparent;
padding-bottom: 3px;
}
input:focus {
border-bottom: 2px solid #4f4f4f;
}
input::placeholder {
color: #aaa;
}
button:hover,
.tab:hover {
background-color: #3b3b3b;
}
label.heading {
margin-bottom: 5px;
display: block;
}
h3,
h4 {
margin: 10px 0;
} }
.hidden { .hidden {
display: none; display: none;
@ -44,11 +71,10 @@
#threads { #threads {
max-width: 250px; max-width: 250px;
} }
.thread.selected { .thread {
background-color: #4f4f4f; display: block;
} width: 100%;
.thread:hover { text-align: left;
background-color: #3b3b3b;
} }
#newthread { #newthread {
margin-top: 5px; margin-top: 5px;
@ -63,9 +89,9 @@
margin: 4px 2px; margin: 4px 2px;
} }
.tab { .tab {
background: transparent;
border: 0; border: 0;
margin-top: 2px; margin-top: 2px;
padding: 5px 7px;
color: #ccc; color: #ccc;
font-weight: 500; font-weight: 500;
} }
@ -73,9 +99,6 @@
background-color: #4f4f4f; background-color: #4f4f4f;
color: #fff; color: #fff;
} }
.tab:hover {
background-color:#3b3b3b;
}
#messages { #messages {
margin: 4px 2px; margin: 4px 2px;
} }
@ -97,6 +120,5 @@
} }
</style> </style>
</head> </head>
<body> <body></body>
</body>
</html> </html>

View File

@ -3,15 +3,17 @@ 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', "create_user",
'send_message', "get_history",
'authenticate', "send_message",
'create_thread', "authenticate",
'list_threads', "create_thread",
'get_keys', "list_threads",
].map(event => [event, require('./src/' + event)])); "get_keys",
].map((event) => [event, require("./src/" + event)])
);
const app = express(); const app = express();
app.use(compression()); app.use(compression());
@ -32,11 +34,10 @@ io.on("connection", (socket) => {
events[event](msg, callback, socket, io) events[event](msg, callback, socket, io)
); );
} }
socket.on('disconnect', reason => { socket.on("disconnect", (reason) => {
let sockets = io.cache[socket.username]; let sockets = io.cache[socket.username];
if (sockets) if (sockets) sockets.splice(sockets.indexOf(socket.id), 1);
sockets.splice(sockets.indexOf(socket.id), 1); });
})
}); });
server.listen(PORT, () => { server.listen(PORT, () => {

View File

@ -3,7 +3,7 @@ const authwrap = require("./authwrap");
const create_thread = async (msg, respond, socket, io) => { const create_thread = async (msg, respond, socket, io) => {
// validate inputs // validate inputs
if (typeof msg.name !== 'string') { if (typeof msg.name !== "string") {
return respond({ return respond({
success: false, success: false,
message: "thread name required", message: "thread name required",
@ -77,24 +77,23 @@ const create_thread = async (msg, respond, socket, io) => {
permissions: { permissions: {
is_member: false, is_member: false,
view: true, view: true,
post: !msg.permissions || !msg.permissions.post_limited post: !msg.permissions || !msg.permissions.post_limited,
}, },
}); });
} }
} }
} } else {
else {
for (let member of msg.members) { for (let member of msg.members) {
for (let socket of io.cache[member.user]) { for (let socket of io.cache[member.name]) {
io.to(socket).emit("new_thread", { io.to(socket).emit("new_thread", {
name: msg.name, name: msg.name,
id: insert.rows[0].id, id: insert.rows[0].id,
permissions: { permissions: {
is_member: true, is_member: true,
view: true, view: true,
post: true post: true,
}, },
key: member.key key: member.key,
}); });
} }
} }