Impl room listing for testing frontend

This commit is contained in:
oxalica 2024-09-03 04:03:49 -04:00
parent 6831c3d25a
commit 57b17547ca
2 changed files with 122 additions and 68 deletions

View file

@ -22,24 +22,25 @@
.log { .log {
margin: auto; margin: auto;
font-style: italic; font-style: italic;
} &::before {
.log::before {
content: "«"; content: "«";
} }
.log::after { &::after {
content: "»"; content: "»";
} }
}
#input-area > * { #input-area > * {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} & > label {
#input-area > * > label {
margin: auto; margin: auto;
} }
#input-area > * > input { & > input,
& > select {
flex: 1; flex: 1;
} }
}
</style> </style>
</head> </head>
@ -53,17 +54,24 @@
<input type="text" id="user-pubkey" placeholder="-" readonly /> <input type="text" id="user-pubkey" placeholder="-" readonly />
<button id="regen-key">regenerate</button> <button id="regen-key">regenerate</button>
</div> </div>
<div> <div>
<label for="room-url">room url:</label> <label for="server-url">server url:</label>
<input <input
type="url" type="url"
id="room-url" id="server-url"
placeholder="https://example.com" placeholder="https://example.com"
pattern="https://.*" pattern="https://.*"
required required
/> />
<button id="join-room">join room</button> </div>
<div>
<label for="rooms">joined rooms:</label>
<select id="rooms"></select>
<label for="join-new-room">join public room:</label>
<select id="join-new-room"></select>
<button id="refresh-rooms">refresh room list</select>
</div> </div>
<div> <div>
<label for="chat">chat:</label> <label for="chat">chat:</label>

View file

@ -1,12 +1,13 @@
const msgFlow = document.querySelector('#msg-flow'); const msgFlow = document.querySelector('#msg-flow');
const userPubkeyDisplay = document.querySelector('#user-pubkey'); const userPubkeyDisplay = document.querySelector('#user-pubkey');
const roomUrlInput = document.querySelector('#room-url'); const serverUrlInput = document.querySelector('#server-url');
const roomsInput = document.querySelector('#rooms');
const joinNewRoomInput = document.querySelector('#join-new-room');
const chatInput = document.querySelector('#chat'); const chatInput = document.querySelector('#chat');
const regenKeyBtn = document.querySelector('#regen-key'); const regenKeyBtn = document.querySelector('#regen-key');
const joinRoomBtn = document.querySelector('#join-room');
let roomUrl = ''; let serverUrl = null;
let roomUuid = null; let curRoom = null;
let ws = null; let ws = null;
let keypair = null; let keypair = null;
let defaultConfig = {}; let defaultConfig = {};
@ -65,9 +66,7 @@ async function loadKeypair() {
async function generateKeypair() { async function generateKeypair() {
log('generating keypair'); log('generating keypair');
regenKeyBtn.disabled = true; document.querySelectorAll('input, button, select').forEach((el) => el.disabled = true);
chatInput.disabled = true;
joinRoomBtn.disabled = true;
try { try {
keypair = await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']); keypair = await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']);
} catch (e) { } catch (e) {
@ -84,10 +83,7 @@ async function generateKeypair() {
} }
log('keypair generated'); log('keypair generated');
document.querySelectorAll('input, button, select').forEach((el) => el.disabled = false);
regenKeyBtn.disabled = false;
chatInput.disabled = false;
joinRoomBtn.disabled = false;
try { try {
const serialize = (k) => crypto.subtle.exportKey('jwk', k); const serialize = (k) => crypto.subtle.exportKey('jwk', k);
@ -165,23 +161,21 @@ function escapeHtml(text) {
.replaceAll("'", '&#039;'); .replaceAll("'", '&#039;');
} }
async function connectRoom(url) { async function genAuthHeader() {
if (url === '' || url == roomUrl || keypair === null) return; return {
const match = url.match(/^https?:\/\/.*\/([a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})\/?/); headers: {
if (match === null) { 'Authorization': await signData({ typ: 'auth' }),
log('invalid room url'); },
return; };
} }
roomUrl = url; async function enterRoom(ruuid) {
roomUuid = match[1]; log(`loading room: ${ruuid}`);
curRoom = ruuid;
log(`fetching room: ${url}`); genAuthHeader()
.then(opts => fetch(`${serverUrl}/room/${ruuid}`, opts))
const genFetchOpts = async () => ({ headers: { 'Authorization': await signData({ typ: 'auth' }) } }); .then(async (resp) => [resp.status, await resp.json()])
genFetchOpts()
.then(opts => fetch(url, opts))
.then(async (resp) => { return [resp.status, await resp.json()]; })
.then(async ([status, json]) => { .then(async ([status, json]) => {
if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`); if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`);
document.title = `room: ${json.title}` document.title = `room: ${json.title}`
@ -190,8 +184,8 @@ async function connectRoom(url) {
log(`failed to get room metadata: ${e}`); log(`failed to get room metadata: ${e}`);
}); });
genFetchOpts() genAuthHeader()
.then(opts => fetch(`${url}/item`, opts)) .then(opts => fetch(`${serverUrl}/room/${ruuid}/item`, opts))
.then(async (resp) => { return [resp.status, await resp.json()]; }) .then(async (resp) => { return [resp.status, await resp.json()]; })
.then(async ([status, json]) => { .then(async ([status, json]) => {
if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`); if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`);
@ -205,24 +199,31 @@ async function connectRoom(url) {
.catch((e) => { .catch((e) => {
log(`failed to fetch history: ${e}`); log(`failed to fetch history: ${e}`);
}); });
// TODO: There is a time window where events would be lost.
await connectWs();
} }
async function connectWs() { async function connectServer(newServerUrl) {
if (newServerUrl === '' || keypair === null) return;
let wsUrl
try {
wsUrl = new URL(newServerUrl);
} catch (e) {
log(`invalid url: ${e}`);
return;
}
serverUrl = newServerUrl;
if (ws !== null) { if (ws !== null) {
ws.close(); ws.close();
} }
const wsUrl = new URL(roomUrl);
log('connecting server');
wsUrl.protocol = wsUrl.protocol == 'http:' ? 'ws:' : 'wss:'; wsUrl.protocol = wsUrl.protocol == 'http:' ? 'ws:' : 'wss:';
wsUrl.pathname = '/ws'; wsUrl.pathname = '/ws';
ws = new WebSocket(wsUrl); ws = new WebSocket(wsUrl);
ws.onopen = async (_) => { ws.onopen = async (_) => {
const auth = await signData({ typ: 'auth' }); const auth = await signData({ typ: 'auth' });
await ws.send(auth); await ws.send(auth);
log('listening on events'); log(`listening events on server: ${serverUrl}`);
} }
ws.onclose = (e) => { ws.onclose = (e) => {
console.error(e); console.error(e);
@ -236,32 +237,69 @@ async function connectWs() {
console.log('ws event', e.data); console.log('ws event', e.data);
const msg = JSON.parse(e.data); const msg = JSON.parse(e.data);
if (msg.chat !== undefined) { if (msg.chat !== undefined) {
showChatMsg(msg.chat); if (msg.chat.signee.payload.room === curRoom) {
await showChatMsg(msg.chat);
} else {
console.log('ignore background room item');
}
} else if (msg.lagged !== undefined) { } else if (msg.lagged !== undefined) {
log('some events are dropped because of queue overflow') log('some events are dropped because of queue overflow')
} else { } else {
log(`unknown ws message: ${e.data}`); log(`unknown ws message: ${e.data}`);
} }
}; };
loadRoomList(true);
} }
async function joinRoom() { async function loadRoomList(autoJoin) {
log('loading room list');
async function loadInto(targetEl, filter) {
try { try {
joinRoomBtn.disabled = true; targetEl.replaceChildren();
await signAndPost(`${roomUrl}/admin`, { const resp = await fetch(`${serverUrl}/room?filter=${filter}`, await genAuthHeader())
const json = await resp.json()
if (resp.status !== 200) throw new Error(`status ${resp.status}: ${json.error.message}`);
for (const { ruuid, title, attrs } of json.rooms) {
const el = document.createElement('option');
el.value = ruuid;
el.innerText = `${title} (uuid=${ruuid}, attrs=${attrs})`;
targetEl.appendChild(el);
}
} catch (e) {
log(`failed to load room list: ${err}`)
}
}
loadInto(roomsInput, 'joined')
.then(async (_) => {
if (autoJoin && roomsInput.value !== '') {
await enterRoom(roomsInput.value);
}
});
loadInto(joinNewRoomInput, 'public')
}
async function joinRoom(ruuid) {
try {
joinNewRoomInput.disabled = true;
await signAndPost(`${serverUrl}/room/${ruuid}/admin`, {
// sorted fields. // sorted fields.
permission: 1, // POST_CHAT permission: 1, // POST_CHAT
room: roomUuid, room: ruuid,
typ: 'add_member', typ: 'add_member',
user: await getUserPubkey(), user: await getUserPubkey(),
}); });
log('joined room'); log('joined room');
await connectWs(); await loadRoomList(false)
await enterRoom(ruuid);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
log(`failed to join room: ${e}`); log(`failed to join room: ${e}`);
} finally { } finally {
joinRoomBtn.disabled = false; joinNewRoomInput.disabled = false;
} }
} }
@ -302,7 +340,7 @@ async function signData(payload) {
async function postChat(text) { async function postChat(text) {
text = text.trim(); text = text.trim();
if (keypair === null || roomUuid === null || text === '') return; if (keypair === null || curRoom === null || text === '') return;
chatInput.disabled = true; chatInput.disabled = true;
@ -313,10 +351,10 @@ async function postChat(text) {
} else { } else {
richText = [text]; richText = [text];
} }
await signAndPost(`${roomUrl}/item`, { await signAndPost(`${serverUrl}/room/${curRoom}/item`, {
// sorted fields. // sorted fields.
rich_text: richText, rich_text: richText,
room: roomUuid, room: curRoom,
typ: 'chat', typ: 'chat',
}); });
chatInput.value = ''; chatInput.value = '';
@ -342,13 +380,15 @@ window.onload = async (_) => {
if (keypair !== null) { if (keypair !== null) {
userPubkeyDisplay.value = await getUserPubkey(); userPubkeyDisplay.value = await getUserPubkey();
} }
if (roomUrlInput.value === '' && defaultConfig.room_url) { if (serverUrlInput.value === '' && defaultConfig.server_url) {
roomUrlInput.value = defaultConfig.room_url; serverUrlInput.value = defaultConfig.server_url;
}
if (serverUrlInput.value !== '') {
await connectServer(serverUrlInput.value);
} }
await connectRoom(roomUrlInput.value);
}; };
roomUrlInput.onchange = async (e) => { serverUrlInput.onchange = async (e) => {
await connectRoom(e.target.value); await connectServer(e.target.value);
}; };
chatInput.onkeypress = async (e) => { chatInput.onkeypress = async (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
@ -358,6 +398,12 @@ chatInput.onkeypress = async (e) => {
regenKeyBtn.onclick = async (_) => { regenKeyBtn.onclick = async (_) => {
await generateKeypair(); await generateKeypair();
}; };
joinRoomBtn.onclick = async (_) => { roomsInput.onchange = async (_) => {
await joinRoom(); await enterRoom(roomsInput.value);
};
joinNewRoomInput.onchange = async (_) => {
await joinRoom(joinNewRoomInput.value);
};
document.querySelector('#refresh-rooms').onclick = async (_) => {
await loadRoomList(true);
}; };