jerl 2024-02-19 23:42:44 -08:00
commit f347e659e9
5 changed files with 247 additions and 138 deletions

View File

@ -4,20 +4,30 @@ 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)
@ -31,28 +41,25 @@ function loadMessages() {
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=${() => { <div class='thread' onclick=${() => chooseThread(thread)}>${
chooseThread(thread); thread.name
if (!thread.permissions.post) { }</div>`;
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 = "";
} }
@ -65,11 +72,14 @@ async function createThread(e) {
} }
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 = {
@ -101,19 +111,24 @@ 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) {
@ -128,18 +143,17 @@ function sendMessage(e) {
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('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">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,10 +179,11 @@ 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';
} }
} }
@ -178,7 +193,8 @@ function switchTab(event){
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
.getElementById(event.target.id.substring(0, event.target.id.length - 3))
.classList.remove('hidden'); .classList.remove('hidden');
} }
@ -194,10 +210,14 @@ render(document.body, html`
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}>messages</button><button id='spacetab' class='tab' onclick=${switchTab}>space</button> <button id='messagetab' class='tab active' onclick=${switchTab}>
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}>load more messages</button> <button id="loadmore" class="hidden" onclick=${loadMessages}>
load more messages
</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" />
@ -219,7 +239,7 @@ 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", 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 = "";

View File

@ -16,18 +16,25 @@ 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(
"authenticate",
{ name: window.name, message: sig },
msg => { 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(
document.body,
html`
<div id="register" class='hidden'> <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>
@ -36,7 +43,8 @@ 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)
@ -46,10 +54,16 @@ render(document.body, html`
}); });
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 +71,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) {
@ -87,4 +115,10 @@ window.onload = async () => {
} }
else else
document.getElementById('register').classList.remove('hidden'); document.getElementById('register').classList.remove('hidden');
window.socket.io.on("reconnect", async attempt => {
await gensession();
if (localStorage.getItem("keys"))
await auth();
});
}; };

View File

@ -18,23 +18,60 @@
body, body,
button, button,
input { input {
background: #020202;
color: #eaeaea; color: #eaeaea;
border: none;
outline: none;
}
html {
height: 100%;
} }
body { body {
height: 100%;
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: #303030;
}
input {
background: #171717;
border-bottom: 2px solid transparent;
padding-bottom: 3px;
}
input:focus {
border-bottom: 2px solid #4f4f4f;
}
input::placeholder {
color: #aaa;
}
.thread:hover,
.tab:hover {
background-color: #3b3b3b;
}
.tab.active,
.thread.active,
button:hover {
background-color: #4f4f4f;
color: #fff;
}
label.heading {
margin-bottom: 5px;
display: block;
}
h3,
h4 {
margin: 10px 0;
} }
.hidden { .hidden {
display: none; display: none !important;
} }
.column { .column {
flex: 1; flex: 1;
@ -44,11 +81,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;
@ -59,28 +95,46 @@
#permissions { #permissions {
margin-bottom: 5px; margin-bottom: 5px;
} }
#thread {
display: flex;
flex-direction: column;
}
#title { #title {
margin: 4px 2px; margin: 4px 2px;
} }
#tabs {
margin-block: 2px;
}
.tab { .tab {
padding: 5px 7px;
background-color: #1f1f1f;
border: 0; border: 0;
margin-top: 2px; margin-top: 2px;
padding: 5px 7px; color: #ddd;
color: #ccc;
font-weight: 500; font-weight: 500;
} }
.tab.active { .tabcontent {
background-color: #4f4f4f; flex-grow: 1;
color: #fff;
} }
.tab:hover { #message {
background-color:#3b3b3b; display: flex;
flex-direction: column;
} }
#messages { #messages {
margin: 4px 2px; margin: 4px 2px;
flex-grow: 1;
} }
#msginput { #msginput {
margin-top: 15px; display: flex;
flex-direction: row;
}
#msg {
flex-grow: 1;
margin: 2px;
padding: 5px;
}
#sendmsg {
margin: 2px 3px;
} }
.message { .message {
margin-bottom: 5px; margin-bottom: 5px;
@ -97,6 +151,5 @@
} }
</style> </style>
</head> </head>
<body> <body></body>
</body>
</html> </html>

View File

@ -3,7 +3,8 @@ 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', 'create_user',
'get_history', 'get_history',
'send_message', 'send_message',
@ -11,7 +12,8 @@ const events = Object.fromEntries([
'create_thread', 'create_thread',
'list_threads', 'list_threads',
'get_keys', 'get_keys',
].map(event => [event, require('./src/' + event)])); ].map(event => [event, require('./src/' + event)])
);
const app = express(); const app = express();
app.use(compression()); app.use(compression());

View File

@ -85,7 +85,7 @@ const create_thread = async (msg, respond, socket, io) => {
} }
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,