From 558617f64417d30bbf50a0bd0716c78094d01417 Mon Sep 17 00:00:00 2001 From: easrng Date: Tue, 15 Feb 2022 17:19:48 -0500 Subject: [PATCH] lotsa frontend changes --- frontend/create.html | 10 +-- frontend/index.html | 10 +-- frontend/lib/chat.mjs | 1 + frontend/lib/join-session.mjs | 9 ++- frontend/lib/reconnecting-web-socket.mjs | 59 +++++++++++++++ frontend/lib/watch-session.mjs | 95 +++++++++++++++--------- frontend/styles.css | 60 +++++++++++---- 7 files changed, 185 insertions(+), 59 deletions(-) create mode 100644 frontend/lib/reconnecting-web-socket.mjs diff --git a/frontend/create.html b/frontend/create.html index 914f54d..8d8eb3a 100644 --- a/frontend/create.html +++ b/frontend/create.html @@ -39,12 +39,12 @@ placeholder="English" /> - -

- Already have a session? - Join your session instead. -

+

+ Already have a session? + Join your session instead. +

+ diff --git a/frontend/index.html b/frontend/index.html index b6489a4..25871e4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -41,11 +41,11 @@ required /> - -

- No session to join? Create a session instead. -

+

+ No session to join? Create a session instead. +

+
@@ -54,7 +54,7 @@
-
+
diff --git a/frontend/lib/chat.mjs b/frontend/lib/chat.mjs index 4aed5cd..9136a0b 100644 --- a/frontend/lib/chat.mjs +++ b/frontend/lib/chat.mjs @@ -29,6 +29,7 @@ const setupChatboxEvents = (socket) => { autocompleting = false; } messageInput.addEventListener("input", autocomplete) + messageInput.addEventListener("selectionchange", autocomplete); chatForm.addEventListener("submit", async (e) => { e.preventDefault(); diff --git a/frontend/lib/join-session.mjs b/frontend/lib/join-session.mjs index 8d9f04b..ae3778e 100644 --- a/frontend/lib/join-session.mjs +++ b/frontend/lib/join-session.mjs @@ -72,13 +72,18 @@ export const setupJoinSessionForm = () => { sessionId.value = window.location.hash.substring(1); } - form.addEventListener("submit", (event) => { + form.addEventListener("submit", async (event) => { event.preventDefault(); button.disabled = true; saveNickname(nickname); saveColour(colour); - joinSession(nickname.value, sessionId.value, colour.value.replace(/^#/, "")); + try { + await joinSession(nickname.value, sessionId.value, colour.value.replace(/^#/, "")); + } catch (e) { + alert(e.message) + button.disabled = false; + } }); }; diff --git a/frontend/lib/reconnecting-web-socket.mjs b/frontend/lib/reconnecting-web-socket.mjs new file mode 100644 index 0000000..2c66b93 --- /dev/null +++ b/frontend/lib/reconnecting-web-socket.mjs @@ -0,0 +1,59 @@ +export default class ReconnectingWebSocket { + constructor(url){ + if(url instanceof URL) { + this.url = url; + } else { + this.url = new URL(url) + } + this.connected = false; + this._eventTarget = new EventTarget(); + this._backoff = 250; // milliseconds, doubled before use + this._lastConnect = 0; + this._socket = null; + this._unsent = []; + this._connect(true); + } + _connect(first) { + if(this._socket) try { this._socket.close() } catch (e) {}; + try { + this._socket = new WebSocket(this.url.href); + } catch (e) { + this._reconnecting = false; + return this._reconnect() + } + this._socket.addEventListener("close", () => this._reconnect()) + this._socket.addEventListener("error", () => this._reconnect()) + this._socket.addEventListener("message", ({data}) => this._eventTarget.dispatchEvent(new MessageEvent("message", {data}))) + this._socket.addEventListener("open", e => { + if(first) this._eventTarget.dispatchEvent(new Event("open")); + if(this._reconnecting) this._eventTarget.dispatchEvent(new Event("reconnected")); + this._reconnecting = false; + this._backoff = 250; + this.connected = true; + while(this._unsent.length > 0) this._socket.send(this._unsent.shift()) + }) + } + _reconnect(){ + if(this._reconnecting) return; + this._eventTarget.dispatchEvent(new Event("reconnecting")); + this._reconnecting = true; + this.connected = false; + this._backoff *= 2; // exponential backoff + setTimeout(() => { + this._connect(); + }, Math.floor(this._backoff+(Math.random()*this._backoff*0.25)-(this._backoff*0.125))) + } + send(message) { + if(this.connected) { + this._socket.send(message); + } else { + this._unsent.push(message); + } + } + addEventListener(...a) { + return this._eventTarget.addEventListener(...a) + } + removeEventListener(...a) { + return this._eventTarget.removeEventListener(...a) + } +} \ No newline at end of file diff --git a/frontend/lib/watch-session.mjs b/frontend/lib/watch-session.mjs index e2cb661..9010131 100644 --- a/frontend/lib/watch-session.mjs +++ b/frontend/lib/watch-session.mjs @@ -1,10 +1,11 @@ import { setupVideo } from "./video.mjs?v=9"; import { setupChat, logEventToChat, updateViewerList } from "./chat.mjs?v=9"; +import ReconnectingWebSocket from "./reconnecting-web-socket.mjs" /** * @param {string} sessionId * @param {string} nickname - * @returns {WebSocket} + * @returns {ReconnectingWebSocket} */ const createWebSocket = (sessionId, nickname, colour) => { const wsUrl = new URL( @@ -13,8 +14,8 @@ const createWebSocket = (sessionId, nickname, colour) => { `&colour=${encodeURIComponent(colour)}`, window.location.href ); - wsUrl.protocol = { "http:": "ws:", "https:": "wss:" }[wsUrl.protocol]; - const socket = new WebSocket(wsUrl.toString()); + wsUrl.protocol = "ws" + window.location.protocol.slice(4); + const socket = new ReconnectingWebSocket(wsUrl); return socket; }; @@ -60,7 +61,7 @@ export const setPlaying = async (playing, video = null) => { /** * @param {HTMLVideoElement} video - * @param {WebSocket} socket + * @param {ReconnectingWebSocket} socket */ const setupIncomingEvents = (video, socket) => { socket.addEventListener("message", async (messageEvent) => { @@ -97,7 +98,7 @@ const setupIncomingEvents = (video, socket) => { /** * @param {HTMLVideoElement} video - * @param {WebSocket} socket + * @param {ReconnectingWebSocket} socket */ const setupOutgoingEvents = (video, socket) => { const currentVideoTime = () => (video.currentTime * 1000) | 0; @@ -167,37 +168,63 @@ const setupOutgoingEvents = (video, socket) => { * @param {string} sessionId */ export const joinSession = async (nickname, sessionId, colour) => { + // try { // we are handling errors in the join form. + const genericConnectionError = new Error("There was an issue getting the session information."); + window.location.hash = sessionId; + let response, video_url, subtitle_tracks, current_time_ms, is_playing; try { - window.location.hash = sessionId; - - const { video_url, subtitle_tracks, current_time_ms, is_playing } = - await fetch(`/sess/${sessionId}`).then((r) => r.json()); - - const socket = createWebSocket(sessionId, nickname, colour); - socket.addEventListener("open", async () => { - const video = await setupVideo( - video_url, - subtitle_tracks, - current_time_ms, - is_playing - ); - - // By default, we should disable video controls if the video is already playing. - // This solves an issue where Safari users join and seek to 00:00:00 because of - // outgoing events. - if (current_time_ms != 0) { - video.controls = false; - } - - setupOutgoingEvents(video, socket); - setupIncomingEvents(video, socket); - setupChat(socket); - }); - // TODO: Close listener ? - } catch (err) { - // TODO: Show an error on the screen - console.error(err); + response = await fetch(`/sess/${sessionId}`); + } catch (e) { + console.error(e); + throw genericConnectionError; } + if(!response.ok) { + let error; + try { + ({ error } = await response.json()); + if(!error) throw new Error(); + } catch (e) { + console.error(e); + throw genericConnectionError; + } + throw new Error(error) + } + try { + ({ video_url, subtitle_tracks, current_time_ms, is_playing } = await response.json()); + } catch (e) { + console.error(e); + throw genericConnectionError; + } + + const socket = createWebSocket(sessionId, nickname, colour); + socket.addEventListener("open", async () => { + const video = await setupVideo( + video_url, + subtitle_tracks, + current_time_ms, + is_playing + ); + + // By default, we should disable video controls if the video is already playing. + // This solves an issue where Safari users join and seek to 00:00:00 because of + // outgoing events. + if (current_time_ms != 0) { + video.controls = false; + } + + setupOutgoingEvents(video, socket); + setupIncomingEvents(video, socket); + setupChat(socket); + }); + socket.addEventListener("reconnecting", e => { + console.log("Reconnecting..."); + }); + socket.addEventListener("reconnected", e => { + console.log("Reconnected."); + }); + //} catch (e) { + // alert(e.message) + //} }; /** diff --git a/frontend/styles.css b/frontend/styles.css index 8f9a91e..8193498 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -1,3 +1,7 @@ +* { + box-sizing: border-box; +} + :root { --bg-rgb: 28, 23, 36; --fg-rgb: 234, 234, 248; @@ -57,8 +61,6 @@ label { input[type="url"], input[type="text"] { - box-sizing: border-box; - background: #fff; background-clip: padding-box; border: 1px solid rgba(0, 0, 0, 0.12); @@ -72,8 +74,7 @@ input[type="text"] { font-family: sans-serif; font-size: 1em; - width: 500px; - max-width: 100%; + width: 100%; resize: none; overflow-x: wrap; @@ -84,7 +85,7 @@ button { background-color: var(--accent); border: var(--accent); border-radius: 6px; - color: var(--fg); + color: #fff; padding: 0.5em 1em; display: inline-block; font-weight: 400; @@ -94,13 +95,19 @@ button { font-family: sans-serif; font-size: 1em; - width: 500px; - max-width: 100%; + width: 100%; user-select: none; border: 1px solid rgba(0, 0, 0, 0); line-height: 1.5; cursor: pointer; + margin: 0.5em 0; +} + +button:disabled { + filter: saturate(0.75); + opacity: 0.75; + cursor: default; } button.small-button { @@ -121,20 +128,25 @@ button.small-button { #pre-join-controls, #create-controls { - width: 60%; - margin: 0 auto; - margin-top: 4em; + margin: 0; + flex-grow: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; } #join-session-form, #create-session-form { - margin-bottom: 4em; + width: 500px; + max-width: 100%; + padding: 1rem; } #post-create-message { display: none; - width: 500px; - max-width: 100%; + width: 100%; font-size: 0.85em; } @@ -245,13 +257,35 @@ button.small-button { border-radius: 4px; width: 100%; } + .emoji-option:hover, .emoji-option:focus { background: var(--fg-transparent); } + .emoji-option:last-child { margin: 0; } +#join-session-colour { + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + border: none; + padding: 0; + border-radius: 6px; + overflow: hidden; + margin: 0.5em 0; + height: 2rem; + width: 2.5rem; + cursor: pointer; +} + +input[type="color"]::-moz-color-swatch, input[type="color"]::-webkit-color-swatch, input[type="color"]::-webkit-color-swatch-wrapper { /* This *should* be working in Chrome, but it doesn't for reasons that are beyond me. */ + border: none; + margin: 0; + padding: 0; +} + @media (min-aspect-ratio: 4/3) { body { flex-direction: row;