diff options
Diffstat (limited to 'srv/src/http')
-rw-r--r-- | srv/src/http/tpl/chat.html | 251 |
1 files changed, 251 insertions, 0 deletions
diff --git a/srv/src/http/tpl/chat.html b/srv/src/http/tpl/chat.html new file mode 100644 index 0000000..b2038e2 --- /dev/null +++ b/srv/src/http/tpl/chat.html @@ -0,0 +1,251 @@ +{{ define "body" }} + +<script async type="module" src="/assets/api.js"></script> + +<style> + #messages { + max-height: 65vh; + overflow: auto; + padding-right: 2rem; + } + + #messages .message { + border: 1px solid #AAA; + border-radius: 10px; + margin-bottom: 1rem; + padding: 2rem; + overflow: auto; + } + + #messages .message .title { + font-weight: bold; + font-size: 120%; + } + + #messages .message .secondaryTitle { + font-family: monospace; + color: #CCC; + } + + #messages .message p { + font-family: monospace; + margin: 1rem 0 0 0; + } + +</style> + +<div id="messages"></div> + +<span id="fail" style="color: red;"></span> + +<script> + +const messagesEl = document.getElementById("messages"); + +let messagesScrolledToBottom = true; +messagesEl.onscroll = () => { + const el = messagesEl; + messagesScrolledToBottom = el.scrollHeight == el.scrollTop + el.clientHeight; +}; + +function renderMessages(msgs) { + + messagesEl.innerHTML = ''; + + msgs.forEach((msg) => { + const el = document.createElement("div"); + el.className = "row message" + + const elWithTextContents = (tag, body) => { + const el = document.createElement(tag); + el.appendChild(document.createTextNode(body)); + return el; + }; + + const titleEl = document.createElement("div"); + titleEl.className = "title"; + el.appendChild(titleEl); + + const userNameEl = elWithTextContents("span", msg.userID.name); + titleEl.appendChild(userNameEl); + + const secondaryTitleEl = document.createElement("div"); + secondaryTitleEl.className = "secondaryTitle"; + el.appendChild(secondaryTitleEl); + + const dt = new Date(msg.createdAt*1000); + const dtStr + = `${dt.getFullYear()}-${dt.getMonth()+1}-${dt.getDate()}` + + ` ${dt.getHours()}:${dt.getMinutes()}:${dt.getSeconds()}`; + + const userIDEl = elWithTextContents("span", `userID:${msg.userID.id} @ ${dtStr}`); + secondaryTitleEl.appendChild(userIDEl); + + const bodyEl = document.createElement("p"); + + const bodyParts = msg.body.split("\n"); + for (const i in bodyParts) { + if (i > 0) bodyEl.appendChild(document.createElement("br")); + bodyEl.appendChild(document.createTextNode(bodyParts[i])); + } + + el.appendChild(bodyEl); + + messagesEl.appendChild(el); + }); +} + + +(async () => { + + const failEl = document.getElementById("fail"); + setErr = (msg) => failEl.innerHTML = `${msg} (please refresh the page to retry)`; + + try { + + const api = await import("/assets/api.js"); + + const history = await api.call("/api/chat/global/history"); + const msgs = history.messages; + + // history returns msgs in time descending, but we display them in time + // ascending. + msgs.reverse() + + const sinceID = (msgs.length > 0) ? msgs[msgs.length-1].id : ""; + + const ws = await api.ws("/api/chat/global/listen", { + params: { sinceID }, + }); + + while (true) { + renderMessages(msgs); + + // If the user was previously scrolled to the bottom then keep them + // there. + if (messagesScrolledToBottom) { + messagesEl.scrollTop = messagesEl.scrollHeight; + } + + const msg = await ws.next(); + msgs.push(msg.message); + renderMessages(msgs); + } + + + } catch (e) { + e = `Failed to fetch message history: ${e}` + setErr(e); + console.error(e); + return; + } + +})() + +</script> + +<style> +#append { + border: 1px dashed #AAA; + border-radius: 10px; + padding: 2rem; +} + +#append #appendBody { + font-family: monospace; +} + +#append #appendStatus { + color: red; +} + +</style> + +<form id="append"> + <h5>New Message</h5> + <div class="row"> + <div class="columns four"> + <input class="u-full-width" placeholder="Name" id="appendName" type="text" /> + <input class="u-full-width" placeholder="Secret" id="appendSecret" type="password" /> + </div> + <div class="columns eight"> + <p> + Your name is displayed alongside your message. + + Your name+secret is used to generate your userID, which is also + displayed alongside your message. + + Other users can validate two messages are from the same person + by comparing the messages' userID. + </p> + </div> + </div> + <div class="row"> + <div class="columns twelve"> + <textarea + style="font-family: monospace" + id="appendBody" + class="u-full-width" + placeholder="Well thought out statement goes here..." + ></textarea> + </div> + </div> + <div class="row"> + <div class="columns four"> + <input class="u-full-width button-primary" id="appendSubmit" type="button" value="Submit" /> + </div> + </div> + <span id="appendStatus"></span> +</form> + +<script> + +const append = document.getElementById("append"); +const appendName = document.getElementById("appendName"); +const appendSecret = document.getElementById("appendSecret"); +const appendBody = document.getElementById("appendBody"); +const appendSubmit = document.getElementById("appendSubmit"); +const appendStatus = document.getElementById("appendStatus"); + +appendSubmit.onclick = async () => { + + const appendSubmitOrigValue = appendSubmit.value; + + appendSubmit.disabled = true; + appendSubmit.className = ""; + appendSubmit.value = "Please hold..."; + + appendStatus.innerHTML = ''; + + try { + + const api = await import("/assets/api.js"); + + await api.call('/api/chat/global/append', { + body: { + name: appendName.value, + password: appendSecret.value, + body: appendBody.value, + }, + requiresPow: true, + }); + + appendBody.value = ''; + + } catch (e) { + + appendStatus.innerHTML = e; + + } finally { + appendSubmit.disabled = false; + appendSubmit.className = "button-primary"; + appendSubmit.value = appendSubmitOrigValue; + } +}; + +</script> + +{{ end }} + +{{ template "base.html" . }} + |