Merge branch 'main' of http://git.jerl.zone/jerl/vybe
commit
f347e659e9
102
client/app.js
102
client/app.js
|
@ -4,20 +4,30 @@ window.currentThreadId = 1;
|
|||
|
||||
function chooseThread(thread) {
|
||||
if (window.currentThreadId)
|
||||
document.getElementById(`thread${window.currentThreadId}`).classList.remove('selected');
|
||||
document.getElementById(`thread${thread.id}`).classList.add('selected');
|
||||
document
|
||||
.getElementById(`thread${window.currentThreadId}`)
|
||||
.classList.remove("active");
|
||||
document.getElementById(`thread${thread.id}`).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");
|
||||
}
|
||||
loadMessages();
|
||||
}
|
||||
|
||||
function loadMessages() {
|
||||
window.emit("get_history", {
|
||||
window.emit(
|
||||
"get_history",
|
||||
{
|
||||
before: window.earliestMessage,
|
||||
thread: window.currentThreadId,
|
||||
}, msg => {
|
||||
},
|
||||
msg => {
|
||||
if (msg.messages.length > 0) {
|
||||
window.earliestMessage = msg.messages[msg.messages.length - 1].id;
|
||||
for (let message of msg.messages)
|
||||
|
@ -31,28 +41,25 @@ function loadMessages() {
|
|||
document.getElementById("loadmore").classList.add("hidden");
|
||||
else
|
||||
document.getElementById("loadmore").classList.remove("hidden");
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function addThread(thread) {
|
||||
function addThread(thread, top) {
|
||||
let node = html.node`
|
||||
<div class='thread' onclick=${() => {
|
||||
chooseThread(thread);
|
||||
if (!thread.permissions.post) {
|
||||
document.getElementById("msginput").classList.add("hidden");
|
||||
} else {
|
||||
document.getElementById("msginput").classList.remove("hidden");
|
||||
}
|
||||
}}>${thread.name}</div>`;
|
||||
<div class='thread' onclick=${() => chooseThread(thread)}>${
|
||||
thread.name
|
||||
}</div>`;
|
||||
node.id = `thread${thread.id}`;
|
||||
document.getElementById("threadlist").appendChild(node);
|
||||
document.getElementById("threadlist")[top ? "prepend" : "appendChild"](node);
|
||||
}
|
||||
|
||||
function addMember() {
|
||||
const name = document.getElementById("membername").value;
|
||||
window.threadmembers.push(name);
|
||||
document.getElementById("memberlist").appendChild(
|
||||
html.node`<p class='member'>${name}</p>`);
|
||||
document
|
||||
.getElementById("memberlist")
|
||||
.appendChild(html.node`<p class='member'>${name}</p>`);
|
||||
document.getElementById("membername").value = "";
|
||||
}
|
||||
|
||||
|
@ -65,11 +72,14 @@ async function createThread(e) {
|
|||
}
|
||||
let members = window.threadmembers.map(name => ({ name }));
|
||||
const perms = document.querySelector(
|
||||
'input[name="permissions"]:checked').value;
|
||||
'input[name="permissions"]:checked'
|
||||
).value;
|
||||
if (perms === "private_view")
|
||||
members = (await new Promise(resolve =>
|
||||
members = (
|
||||
await new Promise(resolve =>
|
||||
window.emit("get_keys", { names: window.threadmembers }, resolve)
|
||||
)).keys;
|
||||
)
|
||||
).keys;
|
||||
let permissions;
|
||||
if (perms === "public") {
|
||||
permissions = {
|
||||
|
@ -101,19 +111,24 @@ async function createThread(e) {
|
|||
}
|
||||
*/
|
||||
}
|
||||
window.emit("create_thread", {
|
||||
window.emit(
|
||||
"create_thread",
|
||||
{
|
||||
name: name.value,
|
||||
permissions,
|
||||
members
|
||||
}, msg => {
|
||||
members,
|
||||
},
|
||||
msg => {
|
||||
chooseThread({
|
||||
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("msginput").classList.remove("hidden");
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function sendMessage(e) {
|
||||
|
@ -128,18 +143,17 @@ function sendMessage(e) {
|
|||
document.getElementById("msg").value = "";
|
||||
}
|
||||
|
||||
function newThread(e) {
|
||||
function newThread() {
|
||||
let form = document.getElementById('createthread');
|
||||
if (form) {
|
||||
form.remove();
|
||||
e.target.textContent = 'create';
|
||||
}
|
||||
else {
|
||||
window.threadmembers = [ window.name ];
|
||||
document.getElementById("newthread").textContent = 'create';
|
||||
} else {
|
||||
window.threadmembers = [window.name];
|
||||
document.getElementById('threads').insertAdjacentElement('afterend', html.node`
|
||||
<form id="createthread" class='column' onsubmit=${createThread}>
|
||||
<h3>create thread</h3>
|
||||
<label for="newthreadname">thread name</label>
|
||||
<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 />
|
||||
|
@ -153,7 +167,7 @@ function newThread(e) {
|
|||
/>
|
||||
<label for="private_view">only members can view and post</label
|
||||
><br /><br />
|
||||
<span>members</span><br />
|
||||
<label class="heading" for="membername">members</label>
|
||||
<input type="text" id="membername" placeholder="username" onkeydown=${(e) => {
|
||||
if (e.key == "Enter") {
|
||||
e.preventDefault();
|
||||
|
@ -165,20 +179,22 @@ function newThread(e) {
|
|||
<p class='member'>${window.name}</p>
|
||||
</div>
|
||||
<br />
|
||||
<input id="submitthread" type="submit" value="create" />
|
||||
<button id="submitthread" type="submit">create</button>
|
||||
</form>
|
||||
`);
|
||||
e.target.textContent = 'cancel';
|
||||
`
|
||||
);
|
||||
document.getElementById("newthread").textContent = 'cancel';
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(event){
|
||||
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))
|
||||
document
|
||||
.getElementById(event.target.id.substring(0, event.target.id.length - 3))
|
||||
.classList.remove('hidden');
|
||||
}
|
||||
|
||||
|
@ -194,10 +210,14 @@ render(document.body, html`
|
|||
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=${switchTab}>
|
||||
messages
|
||||
</button><button id='spacetab' class='tab' onclick=${switchTab}>space</button>
|
||||
</div>
|
||||
<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>
|
||||
<form id="msginput" onsubmit=${sendMessage}>
|
||||
<input type="text" placeholder="write a message..." id="msg" />
|
||||
|
@ -219,7 +239,7 @@ window.socket.on("new_message", (msg) => {
|
|||
if (!window.earliestMessage)
|
||||
window.earliestMessage = msg.id;
|
||||
});
|
||||
window.socket.on("new_thread", addThread);
|
||||
window.socket.on("new_thread", thread => addThread(thread, true));
|
||||
|
||||
window.emit("list_threads", {}, msg => {
|
||||
document.getElementById("threadlist").innerHTML = "";
|
||||
|
|
|
@ -16,18 +16,25 @@ async function auth() {
|
|||
message: new openpgp.CleartextMessage("vybe_auth " + window.session, ""),
|
||||
signingKeys: window.keys.priv,
|
||||
});
|
||||
window.socket.emit("authenticate", { name: window.name, message: sig },
|
||||
window.socket.emit(
|
||||
"authenticate",
|
||||
{ name: window.name, message: sig },
|
||||
msg => {
|
||||
if (!msg.success) {
|
||||
console.log('authenticate failed');
|
||||
console.log('authenticate failed', msg);
|
||||
return;
|
||||
}
|
||||
if (document.getElementById("register")) {
|
||||
document.getElementById("register").remove();
|
||||
import('/app.js');
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render(document.body, html`
|
||||
render(
|
||||
document.body,
|
||||
html`
|
||||
<div id="register" class='hidden'>
|
||||
<h1>welcome to vybe</h1>
|
||||
<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
|
||||
browser storage only, so do this on a browser you can access again.
|
||||
</p>
|
||||
<form onsubmit=${async e => {
|
||||
<form
|
||||
onsubmit=${async e => {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById("name").value;
|
||||
if (!name)
|
||||
|
@ -46,10 +54,16 @@ render(document.body, html`
|
|||
});
|
||||
const priv = await openpgp.readKey({ armoredKey: keys.privateKey });
|
||||
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) {
|
||||
document.querySelector('#registerform').insertAdjacentHTML('afterend', `
|
||||
<p>${msg.message}</p>`);
|
||||
document.querySelector('#registerform').insertAdjacentHTML(
|
||||
'afterend',
|
||||
`
|
||||
<p>${msg.message}</p>`
|
||||
);
|
||||
return;
|
||||
}
|
||||
window.keys = { priv, pub };
|
||||
|
@ -57,23 +71,37 @@ render(document.body, html`
|
|||
localStorage.setItem("name", name);
|
||||
window.name = name;
|
||||
auth();
|
||||
});
|
||||
}} id="registerform">
|
||||
}
|
||||
);
|
||||
}}
|
||||
id="registerform"
|
||||
>
|
||||
<label for="name">username: </label>
|
||||
<input id="name" type="text" />
|
||||
<input id="submit" type="submit" value='generate keys & register'>
|
||||
<input id="submit" type="submit" value='generate keys & register' />
|
||||
</form>
|
||||
</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.socket = io();
|
||||
|
||||
window.session = rand();
|
||||
window.emit = (type, data, callback) => window.socket.emit(type, {
|
||||
...data,
|
||||
__session: window.session,
|
||||
}, callback);
|
||||
await gensession();
|
||||
|
||||
let keys = localStorage.getItem("keys");
|
||||
if (keys) {
|
||||
|
@ -87,4 +115,10 @@ window.onload = async () => {
|
|||
}
|
||||
else
|
||||
document.getElementById('register').classList.remove('hidden');
|
||||
|
||||
window.socket.io.on("reconnect", async attempt => {
|
||||
await gensession();
|
||||
if (localStorage.getItem("keys"))
|
||||
await auth();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -18,23 +18,60 @@
|
|||
body,
|
||||
button,
|
||||
input {
|
||||
background: #020202;
|
||||
color: #eaeaea;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
height: 100%;
|
||||
background: #020202;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
margin: 0;
|
||||
min-width: min-content;
|
||||
}
|
||||
h3, h4 {
|
||||
margin: 10px 0;
|
||||
button,
|
||||
input,
|
||||
.tab {
|
||||
padding: 5px 7px;
|
||||
}
|
||||
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 {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
.column {
|
||||
flex: 1;
|
||||
|
@ -44,11 +81,10 @@
|
|||
#threads {
|
||||
max-width: 250px;
|
||||
}
|
||||
.thread.selected {
|
||||
background-color: #4f4f4f;
|
||||
}
|
||||
.thread:hover {
|
||||
background-color: #3b3b3b;
|
||||
.thread {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
#newthread {
|
||||
margin-top: 5px;
|
||||
|
@ -59,28 +95,46 @@
|
|||
#permissions {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#thread {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#title {
|
||||
margin: 4px 2px;
|
||||
}
|
||||
#tabs {
|
||||
margin-block: 2px;
|
||||
}
|
||||
.tab {
|
||||
padding: 5px 7px;
|
||||
background-color: #1f1f1f;
|
||||
border: 0;
|
||||
margin-top: 2px;
|
||||
padding: 5px 7px;
|
||||
color: #ccc;
|
||||
color: #ddd;
|
||||
font-weight: 500;
|
||||
}
|
||||
.tab.active {
|
||||
background-color: #4f4f4f;
|
||||
color: #fff;
|
||||
.tabcontent {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.tab:hover {
|
||||
background-color:#3b3b3b;
|
||||
#message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#messages {
|
||||
margin: 4px 2px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
#msginput {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
#msg {
|
||||
flex-grow: 1;
|
||||
margin: 2px;
|
||||
padding: 5px;
|
||||
}
|
||||
#sendmsg {
|
||||
margin: 2px 3px;
|
||||
}
|
||||
.message {
|
||||
margin-bottom: 5px;
|
||||
|
@ -97,6 +151,5 @@
|
|||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
6
index.js
6
index.js
|
@ -3,7 +3,8 @@ const http = require("http");
|
|||
const { Server } = require("socket.io");
|
||||
const compression = require("compression");
|
||||
|
||||
const events = Object.fromEntries([
|
||||
const events = Object.fromEntries(
|
||||
[
|
||||
'create_user',
|
||||
'get_history',
|
||||
'send_message',
|
||||
|
@ -11,7 +12,8 @@ const events = Object.fromEntries([
|
|||
'create_thread',
|
||||
'list_threads',
|
||||
'get_keys',
|
||||
].map(event => [event, require('./src/' + event)]));
|
||||
].map(event => [event, require('./src/' + event)])
|
||||
);
|
||||
|
||||
const app = express();
|
||||
app.use(compression());
|
||||
|
|
|
@ -85,7 +85,7 @@ const create_thread = async (msg, respond, socket, io) => {
|
|||
}
|
||||
else {
|
||||
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", {
|
||||
name: msg.name,
|
||||
id: insert.rows[0].id,
|
||||
|
|
Loading…
Reference in New Issue