mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 00:31:09 +00:00
frontend: improve layout and registration
This commit is contained in:
parent
76a9e501c5
commit
364e517b7d
2 changed files with 150 additions and 69 deletions
|
@ -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>
|
||||||
|
|
|
@ -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 (_) => {
|
||||||
|
|
Loading…
Add table
Reference in a new issue