frontend: improve layout and registration

This commit is contained in:
oxalica 2024-09-30 19:34:22 -04:00
parent 76a9e501c5
commit 364e517b7d
2 changed files with 150 additions and 69 deletions

View file

@ -2,23 +2,82 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>poc</title> <title>Blah testing frontend</title>
<script src="./main.js" defer></script> <script src="./main.js" defer></script>
<style> <style>
@media screen and (max-width: 50em) {
body {
flex-direction: column;
}
#sidebar {
width: auto !important;
border-bottom: 1px solid grey;
border-right: none !important;
flex: .5;
}
}
input, select {
min-width: 0;
}
body { body {
display: flex; display: flex;
flex-direction: column;
height: 100vh; height: 100vh;
margin: 0; margin: 0;
font-family: monospace; font-family: monospace;
} }
#msg-flow {
flex: 1; input {
overflow: scroll; text-overflow: ellipsis;
} }
#msg-flow > * {
#sidebar {
padding: .5em;
width: 25em;
display: flex;
flex-direction: column;
border-right: 1px solid grey;
& label {
margin-left: 0;
}
& > #rooms {
flex: 1;
}
}
.label-input {
display: flex;
flex-direction: row;
& > label {
margin: auto .5em;
}
& > input, & > select {
flex: 1;
}
}
#mainbar {
flex: 1;
display: flex;
flex-direction: column;
& > .flow {
padding: .5em;
overflow: scroll;
border-bottom: 1px solid grey;
& > * {
display: block; display: block;
} }
}
}
#log-flow {
flex: 1;
}
#msg-flow {
flex: 2;
}
.log { .log {
margin: auto; margin: auto;
font-style: italic; font-style: italic;
@ -29,59 +88,61 @@
content: "»"; content: "»";
} }
} }
#input-area > * {
display: flex;
flex-direction: row;
& > label {
margin: auto;
}
& > input,
& > select {
flex: 1;
}
}
</style> </style>
</head> </head>
<body> <body>
<div id="msg-flow"> <div id="sidebar">
<span class="log">please enter room url below and press ENTER</span> <div class="label-input">
<label for="id-url">id_url:</label>
<input type="url" id="id-url" placeholder="https://example.com" />
<button id="register">register</button>
</div> </div>
<div id="input-area"> <div class="label-input">
<div>
<label for="id-pubkey">id_pubkey :</label> <label for="id-pubkey">id_pubkey :</label>
<input type="text" id="id-pubkey" placeholder="-" /> <input type="text" id="id-pubkey" placeholder="-" />
</div>
<div class="label-input">
<label for="act-pubkey">act_pubkey:</label> <label for="act-pubkey">act_pubkey:</label>
<input type="text" id="act-pubkey" placeholder="-" readonly /> <input type="text" id="act-pubkey" placeholder="-" readonly disabled />
<button id="regen-key">regenerate</button> <button id="regen-key">regenerate</button>
</div> </div>
<div>
<div class="label-input">
<label for="server-url">server url:</label> <label for="server-url">server url:</label>
<input <input
type="url" type="url"
id="server-url" id="server-url"
placeholder="https://example.com" placeholder="https://example.com"
pattern="https://.*" pattern="https:\/\/[^\/]*\/?"
required required
/> />
<button id="register">register</button>
</div> </div>
<div>
<label for="rooms">joined rooms:</label>
<select id="rooms"></select>
<label for="join-new-room">join public room:</label> <div>
<label for="rooms">rooms:</label>
<button id="refresh-rooms">refresh room list</button>
</div>
<select id="rooms" size="2"></select>
<div class="label-input">
<label for="join-new-room">join new room:</label>
<select id="join-new-room"></select> <select id="join-new-room"></select>
<button id="leave-room">leave room</select>
<button id="refresh-rooms">refresh room list</select>
</div> </div>
<div> </div>
<label for="chat">chat:</label>
<input type="text" id="chat" placeholder="message" />
<div id="mainbar">
<div id="log-flow" class="flow">
<noscript>
<span class="log">javascript is required</span>
</noscript>
</div>
<div id="msg-flow" class="flow">
</div>
<div class="label-input">
<label for="chat">chat:</label>
<input type="text" id="chat" placeholder="message or raw JSON" />
<button id="mark-seen">mark history seen</button> <button id="mark-seen">mark history seen</button>
<button id="leave-room">leave room</button>
</div> </div>
</div> </div>
</body> </body>

View file

@ -1,3 +1,5 @@
const idUrlInput = document.querySelector('#id-url');
const logFlow = document.querySelector('#log-flow');
const msgFlow = document.querySelector('#msg-flow'); const msgFlow = document.querySelector('#msg-flow');
const idPubkeyInput = document.querySelector('#id-pubkey'); const idPubkeyInput = document.querySelector('#id-pubkey');
const actPubkeyDisplay = document.querySelector('#act-pubkey'); const actPubkeyDisplay = document.querySelector('#act-pubkey');
@ -36,10 +38,10 @@ async function getActPubkey() {
return bufToHex(await crypto.subtle.exportKey('raw', keypair.publicKey)); return bufToHex(await crypto.subtle.exportKey('raw', keypair.publicKey));
} }
function appendMsg(el) { function appendMsg(parent, el) {
msgFlow.append(el); parent.append(el);
msgFlow.scrollTo({ parent.scrollTo({
top: msgFlow.scrollTopMax, top: parent.scrollTopMax,
behavior: 'instant', behavior: 'instant',
}) })
} }
@ -52,7 +54,7 @@ function log(msg, isHtml) {
} else { } else {
el.innerText = msg; el.innerText = msg;
} }
appendMsg(el) appendMsg(logFlow, el)
} }
async function loadKeypair() { async function loadKeypair() {
@ -114,8 +116,18 @@ async function register() {
} }
try { try {
const idUrl = prompt('id_url:', defaultConfig.id_url || ''); const idUrl = idUrlInput.value.trim();
if (idUrl === null) return; if (idUrl === '') return;
const wellKnownUrl = (new URL(idUrl)) + '.well-known/blah/identity.json';
log(`fetching ${wellKnownUrl}`);
const idDescResp = await fetch(wellKnownUrl, { redirect: 'error' });
if (idDescResp.status !== 200) throw new Error(`status ${idDescResp.status}`);
const idDescJson = await idDescResp.json()
if (typeof idDescJson.id_key !== 'string' || idDescJson.id_key.length !== 64) {
throw new Error('invalid id_key from identity description response');
}
idPubkeyInput.value = idDescJson.id_key;
const getResp = await fetch(`${apiUrl}/user/me`, { const getResp = await fetch(`${apiUrl}/user/me`, {
cache: 'no-store' cache: 'no-store'
@ -124,6 +136,7 @@ async function register() {
const difficulty = parseInt(getResp.headers.get('x-blah-difficulty')); const difficulty = parseInt(getResp.headers.get('x-blah-difficulty'));
if (challenge === null) throw new Error('cannot get challenge nonce'); if (challenge === null) throw new Error('cannot get challenge nonce');
log('solving challenge')
const postResp = await signAndPost(`${apiUrl}/user/me`, { const postResp = await signAndPost(`${apiUrl}/user/me`, {
// sorted fields. // sorted fields.
challenge_nonce: challenge, challenge_nonce: challenge,
@ -170,7 +183,7 @@ async function showChatMsg(chat) {
elContent.innerHTML = richTextToHtml(chat.signee.payload.rich_text); elContent.innerHTML = richTextToHtml(chat.signee.payload.rich_text);
el.appendChild(elHeader); el.appendChild(elHeader);
el.appendChild(elContent); el.appendChild(elContent);
appendMsg(el) appendMsg(msgFlow, el)
} }
function richTextToHtml(richText) { function richTextToHtml(richText) {
@ -218,33 +231,36 @@ async function enterRoom(rid) {
curRoom = rid; curRoom = rid;
roomsInput.value = rid; roomsInput.value = rid;
genAuthHeader() msgFlow.replaceChildren();
.then(opts => fetch(`${apiUrl}/room/${rid}`, opts))
.then(async (resp) => [resp.status, await resp.json()])
.then(async ([status, json]) => {
if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`);
document.title = `room: ${json.title}`
})
.catch((e) => {
log(`failed to get room metadata: ${e}`);
});
genAuthHeader() let roomMetadata;
.then(opts => fetch(`${apiUrl}/room/${rid}/msg`, opts)) try {
.then(async (resp) => { return [resp.status, await resp.json()]; }) const resp = await fetch(`${apiUrl}/room/${rid}`, await genAuthHeader());
.then(async ([status, json]) => { roomMetadata = await resp.json();
if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`); if (resp.status !== 200) throw new Error(`status ${resp.status}: ${roomMetadata.error.message}`);
document.title = `Blah: ${roomMetadata.title}`;
} catch (err) {
log(`failed to get room metadata: ${err}`);
}
try {
const resp = await fetch(`${apiUrl}/room/${rid}/msg`, await genAuthHeader());
const json = await resp.json();
if (resp.status !== 200) throw new Error(`status ${resp.status}: ${json.error.message}`);
const { msgs } = json const { msgs } = json
msgs.reverse(); msgs.reverse();
for (const msg of msgs) { for (const msg of msgs) {
lastCid = msg.cid; lastCid = msg.cid;
await showChatMsg(msg); await showChatMsg(msg);
if (msg.cid === roomMetadata.last_seen_cid) {
const el = document.createElement('span');
el.innerText = '---last seen---';
appendMsg(msgFlow, el)
}
}
} catch (err) {
log(`failed to fetch history: ${err}`);
} }
log('---history---');
})
.catch((e) => {
log(`failed to fetch history: ${e}`);
});
} }
async function connectServer(newServerUrl) { async function connectServer(newServerUrl) {
@ -366,7 +382,7 @@ async function leaveRoom() {
await signAndPost(`${apiUrl}/room/${curRoom}/admin`, { await signAndPost(`${apiUrl}/room/${curRoom}/admin`, {
room: curRoom, room: curRoom,
typ: 'remove_member', typ: 'remove_member',
user: await getActPubkey(), user: await getIdPubkey(),
}); });
log('left room'); log('left room');
await loadRoomList(true); await loadRoomList(true);
@ -486,6 +502,9 @@ window.onload = async (_) => {
if (keypair !== null) { if (keypair !== null) {
actPubkeyDisplay.value = await getActPubkey(); actPubkeyDisplay.value = await getActPubkey();
} }
if (idUrlInput.value === '' && defaultConfig.id_url) {
idUrlInput.value = defaultConfig.id_url;
}
if (idPubkeyInput.value === '' && defaultConfig.id_key) { if (idPubkeyInput.value === '' && defaultConfig.id_key) {
idPubkeyInput.value = defaultConfig.id_key; idPubkeyInput.value = defaultConfig.id_key;
} }
@ -520,6 +539,7 @@ serverUrlInput.onchange = async (e) => {
chatInput.onkeypress = async (e) => { chatInput.onkeypress = async (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
await postChat(chatInput.value); await postChat(chatInput.value);
chatInput.focus();
} }
}; };
roomsInput.onchange = async (_) => { roomsInput.onchange = async (_) => {