diff --git a/pages/index.html b/pages/index.html index 339c6a5..7f67c46 100644 --- a/pages/index.html +++ b/pages/index.html @@ -22,23 +22,24 @@ .log { margin: auto; font-style: italic; - } - .log::before { - content: "«"; - } - .log::after { - content: "»"; + &::before { + content: "«"; + } + &::after { + content: "»"; + } } #input-area > * { display: flex; flex-direction: row; - } - #input-area > * > label { - margin: auto; - } - #input-area > * > input { - flex: 1; + & > label { + margin: auto; + } + & > input, + & > select { + flex: 1; + } } @@ -53,17 +54,24 @@ -
- + - +
+
+ + + + + + +
diff --git a/pages/main.js b/pages/main.js index 850f3e0..c27cb1a 100644 --- a/pages/main.js +++ b/pages/main.js @@ -1,12 +1,13 @@ const msgFlow = document.querySelector('#msg-flow'); 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 regenKeyBtn = document.querySelector('#regen-key'); -const joinRoomBtn = document.querySelector('#join-room'); -let roomUrl = ''; -let roomUuid = null; +let serverUrl = null; +let curRoom = null; let ws = null; let keypair = null; let defaultConfig = {}; @@ -65,9 +66,7 @@ async function loadKeypair() { async function generateKeypair() { log('generating keypair'); - regenKeyBtn.disabled = true; - chatInput.disabled = true; - joinRoomBtn.disabled = true; + document.querySelectorAll('input, button, select').forEach((el) => el.disabled = true); try { keypair = await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']); } catch (e) { @@ -84,10 +83,7 @@ async function generateKeypair() { } log('keypair generated'); - - regenKeyBtn.disabled = false; - chatInput.disabled = false; - joinRoomBtn.disabled = false; + document.querySelectorAll('input, button, select').forEach((el) => el.disabled = false); try { const serialize = (k) => crypto.subtle.exportKey('jwk', k); @@ -165,23 +161,21 @@ function escapeHtml(text) { .replaceAll("'", '''); } -async function connectRoom(url) { - if (url === '' || url == roomUrl || keypair === null) 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})\/?/); - if (match === null) { - log('invalid room url'); - return; - } +async function genAuthHeader() { + return { + headers: { + 'Authorization': await signData({ typ: 'auth' }), + }, + }; +} - roomUrl = url; - roomUuid = match[1]; +async function enterRoom(ruuid) { + log(`loading room: ${ruuid}`); + curRoom = ruuid; - log(`fetching room: ${url}`); - - const genFetchOpts = async () => ({ headers: { 'Authorization': await signData({ typ: 'auth' }) } }); - genFetchOpts() - .then(opts => fetch(url, opts)) - .then(async (resp) => { return [resp.status, await resp.json()]; }) + genAuthHeader() + .then(opts => fetch(`${serverUrl}/room/${ruuid}`, 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}` @@ -190,8 +184,8 @@ async function connectRoom(url) { log(`failed to get room metadata: ${e}`); }); - genFetchOpts() - .then(opts => fetch(`${url}/item`, opts)) + genAuthHeader() + .then(opts => fetch(`${serverUrl}/room/${ruuid}/item`, opts)) .then(async (resp) => { return [resp.status, await resp.json()]; }) .then(async ([status, json]) => { if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`); @@ -205,24 +199,31 @@ async function connectRoom(url) { .catch((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) { ws.close(); } - const wsUrl = new URL(roomUrl); + + log('connecting server'); wsUrl.protocol = wsUrl.protocol == 'http:' ? 'ws:' : 'wss:'; wsUrl.pathname = '/ws'; ws = new WebSocket(wsUrl); ws.onopen = async (_) => { const auth = await signData({ typ: 'auth' }); await ws.send(auth); - log('listening on events'); + log(`listening events on server: ${serverUrl}`); } ws.onclose = (e) => { console.error(e); @@ -236,32 +237,69 @@ async function connectWs() { console.log('ws event', e.data); const msg = JSON.parse(e.data); 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) { log('some events are dropped because of queue overflow') } else { 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 { + targetEl.replaceChildren(); + 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 { - joinRoomBtn.disabled = true; - await signAndPost(`${roomUrl}/admin`, { + joinNewRoomInput.disabled = true; + await signAndPost(`${serverUrl}/room/${ruuid}/admin`, { // sorted fields. permission: 1, // POST_CHAT - room: roomUuid, + room: ruuid, typ: 'add_member', user: await getUserPubkey(), }); log('joined room'); - await connectWs(); + await loadRoomList(false) + await enterRoom(ruuid); } catch (e) { console.error(e); log(`failed to join room: ${e}`); } finally { - joinRoomBtn.disabled = false; + joinNewRoomInput.disabled = false; } } @@ -302,7 +340,7 @@ async function signData(payload) { async function postChat(text) { text = text.trim(); - if (keypair === null || roomUuid === null || text === '') return; + if (keypair === null || curRoom === null || text === '') return; chatInput.disabled = true; @@ -313,10 +351,10 @@ async function postChat(text) { } else { richText = [text]; } - await signAndPost(`${roomUrl}/item`, { + await signAndPost(`${serverUrl}/room/${curRoom}/item`, { // sorted fields. rich_text: richText, - room: roomUuid, + room: curRoom, typ: 'chat', }); chatInput.value = ''; @@ -342,13 +380,15 @@ window.onload = async (_) => { if (keypair !== null) { userPubkeyDisplay.value = await getUserPubkey(); } - if (roomUrlInput.value === '' && defaultConfig.room_url) { - roomUrlInput.value = defaultConfig.room_url; + if (serverUrlInput.value === '' && defaultConfig.server_url) { + serverUrlInput.value = defaultConfig.server_url; + } + if (serverUrlInput.value !== '') { + await connectServer(serverUrlInput.value); } - await connectRoom(roomUrlInput.value); }; -roomUrlInput.onchange = async (e) => { - await connectRoom(e.target.value); +serverUrlInput.onchange = async (e) => { + await connectServer(e.target.value); }; chatInput.onkeypress = async (e) => { if (e.key === 'Enter') { @@ -358,6 +398,12 @@ chatInput.onkeypress = async (e) => { regenKeyBtn.onclick = async (_) => { await generateKeypair(); }; -joinRoomBtn.onclick = async (_) => { - await joinRoom(); +roomsInput.onchange = async (_) => { + await enterRoom(roomsInput.value); +}; +joinNewRoomInput.onchange = async (_) => { + await joinRoom(joinNewRoomInput.value); +}; +document.querySelector('#refresh-rooms').onclick = async (_) => { + await loadRoomList(true); };