forked from lavender/watch-party
		
	lotsa frontend changes
This commit is contained in:
		
							parent
							
								
									1e73e0df72
								
							
						
					
					
						commit
						558617f644
					
				
					 7 changed files with 185 additions and 59 deletions
				
			
		|  | @ -39,12 +39,12 @@ | ||||||
|           placeholder="English" |           placeholder="English" | ||||||
|         /> |         /> | ||||||
|         <button>Create</button> |         <button>Create</button> | ||||||
|       </form> |  | ||||||
| 
 | 
 | ||||||
|         <p> |         <p> | ||||||
|           Already have a session? |           Already have a session? | ||||||
|           <a href="/">Join your session</a> instead. |           <a href="/">Join your session</a> instead. | ||||||
|         </p> |         </p> | ||||||
|  |       </form> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <script type="module" src="/create.mjs?v=9"></script> |     <script type="module" src="/create.mjs?v=9"></script> | ||||||
|  |  | ||||||
|  | @ -41,11 +41,11 @@ | ||||||
|           required |           required | ||||||
|         /> |         /> | ||||||
|         <button id="join-session-button">Join</button> |         <button id="join-session-button">Join</button> | ||||||
|       </form> |  | ||||||
| 
 | 
 | ||||||
|         <p> |         <p> | ||||||
|           No session to join? <a href="/create.html">Create a session</a> instead. |           No session to join? <a href="/create.html">Create a session</a> instead. | ||||||
|         </p> |         </p> | ||||||
|  |       </form> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <div id="video-container"></div> |     <div id="video-container"></div> | ||||||
|  |  | ||||||
|  | @ -29,6 +29,7 @@ const setupChatboxEvents = (socket) => { | ||||||
| 	autocompleting = false; | 	autocompleting = false; | ||||||
|   } |   } | ||||||
|   messageInput.addEventListener("input", autocomplete) |   messageInput.addEventListener("input", autocomplete) | ||||||
|  |   messageInput.addEventListener("selectionchange", autocomplete); | ||||||
| 
 | 
 | ||||||
|   chatForm.addEventListener("submit", async (e) => { |   chatForm.addEventListener("submit", async (e) => { | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|  |  | ||||||
|  | @ -72,13 +72,18 @@ export const setupJoinSessionForm = () => { | ||||||
|     sessionId.value = window.location.hash.substring(1); |     sessionId.value = window.location.hash.substring(1); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   form.addEventListener("submit", (event) => { |   form.addEventListener("submit", async (event) => { | ||||||
|     event.preventDefault(); |     event.preventDefault(); | ||||||
| 
 | 
 | ||||||
|     button.disabled = true; |     button.disabled = true; | ||||||
| 
 | 
 | ||||||
|     saveNickname(nickname); |     saveNickname(nickname); | ||||||
|     saveColour(colour); |     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; | ||||||
|  |     } | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
							
								
								
									
										59
									
								
								frontend/lib/reconnecting-web-socket.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								frontend/lib/reconnecting-web-socket.mjs
									
									
									
									
									
										Normal file
									
								
							|  | @ -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) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -1,10 +1,11 @@ | ||||||
| import { setupVideo } from "./video.mjs?v=9"; | import { setupVideo } from "./video.mjs?v=9"; | ||||||
| import { setupChat, logEventToChat, updateViewerList } from "./chat.mjs?v=9"; | import { setupChat, logEventToChat, updateViewerList } from "./chat.mjs?v=9"; | ||||||
|  | import ReconnectingWebSocket from "./reconnecting-web-socket.mjs" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @param {string} sessionId |  * @param {string} sessionId | ||||||
|  * @param {string} nickname |  * @param {string} nickname | ||||||
|  * @returns {WebSocket} |  * @returns {ReconnectingWebSocket} | ||||||
|  */ |  */ | ||||||
| const createWebSocket = (sessionId, nickname, colour) => { | const createWebSocket = (sessionId, nickname, colour) => { | ||||||
|   const wsUrl = new URL( |   const wsUrl = new URL( | ||||||
|  | @ -13,8 +14,8 @@ const createWebSocket = (sessionId, nickname, colour) => { | ||||||
|       `&colour=${encodeURIComponent(colour)}`, |       `&colour=${encodeURIComponent(colour)}`, | ||||||
|     window.location.href |     window.location.href | ||||||
|   ); |   ); | ||||||
|   wsUrl.protocol = { "http:": "ws:", "https:": "wss:" }[wsUrl.protocol]; |   wsUrl.protocol = "ws" + window.location.protocol.slice(4); | ||||||
|   const socket = new WebSocket(wsUrl.toString()); |   const socket = new ReconnectingWebSocket(wsUrl); | ||||||
| 
 | 
 | ||||||
|   return socket; |   return socket; | ||||||
| }; | }; | ||||||
|  | @ -60,7 +61,7 @@ export const setPlaying = async (playing, video = null) => { | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @param {HTMLVideoElement} video |  * @param {HTMLVideoElement} video | ||||||
|  * @param {WebSocket} socket |  * @param {ReconnectingWebSocket} socket | ||||||
|  */ |  */ | ||||||
| const setupIncomingEvents = (video, socket) => { | const setupIncomingEvents = (video, socket) => { | ||||||
|   socket.addEventListener("message", async (messageEvent) => { |   socket.addEventListener("message", async (messageEvent) => { | ||||||
|  | @ -97,7 +98,7 @@ const setupIncomingEvents = (video, socket) => { | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @param {HTMLVideoElement} video |  * @param {HTMLVideoElement} video | ||||||
|  * @param {WebSocket} socket |  * @param {ReconnectingWebSocket} socket | ||||||
|  */ |  */ | ||||||
| const setupOutgoingEvents = (video, socket) => { | const setupOutgoingEvents = (video, socket) => { | ||||||
|   const currentVideoTime = () => (video.currentTime * 1000) | 0; |   const currentVideoTime = () => (video.currentTime * 1000) | 0; | ||||||
|  | @ -167,11 +168,33 @@ const setupOutgoingEvents = (video, socket) => { | ||||||
|  * @param {string} sessionId |  * @param {string} sessionId | ||||||
|  */ |  */ | ||||||
| export const joinSession = async (nickname, sessionId, colour) => { | export const joinSession = async (nickname, sessionId, colour) => { | ||||||
|   try { |   // 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; |   window.location.hash = sessionId; | ||||||
| 
 |   let response, video_url, subtitle_tracks, current_time_ms, is_playing; | ||||||
|     const { video_url, subtitle_tracks, current_time_ms, is_playing } = |   try { | ||||||
|       await fetch(`/sess/${sessionId}`).then((r) => r.json()); |     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); |   const socket = createWebSocket(sessionId, nickname, colour); | ||||||
|   socket.addEventListener("open", async () => { |   socket.addEventListener("open", async () => { | ||||||
|  | @ -193,11 +216,15 @@ export const joinSession = async (nickname, sessionId, colour) => { | ||||||
|     setupIncomingEvents(video, socket); |     setupIncomingEvents(video, socket); | ||||||
|     setupChat(socket); |     setupChat(socket); | ||||||
|   }); |   }); | ||||||
|     // TODO: Close listener ?
 |   socket.addEventListener("reconnecting", e => { | ||||||
|   } catch (err) { |     console.log("Reconnecting..."); | ||||||
|     // TODO: Show an error on the screen
 |   }); | ||||||
|     console.error(err); |   socket.addEventListener("reconnected", e => { | ||||||
|   } |     console.log("Reconnected."); | ||||||
|  |   }); | ||||||
|  |   //} catch (e) {
 | ||||||
|  |   //  alert(e.message)
 | ||||||
|  |   //}
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  | ||||||
|  | @ -1,3 +1,7 @@ | ||||||
|  | * { | ||||||
|  |   box-sizing: border-box; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| :root { | :root { | ||||||
|   --bg-rgb: 28, 23, 36; |   --bg-rgb: 28, 23, 36; | ||||||
|   --fg-rgb: 234, 234, 248; |   --fg-rgb: 234, 234, 248; | ||||||
|  | @ -57,8 +61,6 @@ label { | ||||||
| 
 | 
 | ||||||
| input[type="url"], | input[type="url"], | ||||||
| input[type="text"] { | input[type="text"] { | ||||||
|   box-sizing: border-box; |  | ||||||
| 
 |  | ||||||
|   background: #fff; |   background: #fff; | ||||||
|   background-clip: padding-box; |   background-clip: padding-box; | ||||||
|   border: 1px solid rgba(0, 0, 0, 0.12); |   border: 1px solid rgba(0, 0, 0, 0.12); | ||||||
|  | @ -72,8 +74,7 @@ input[type="text"] { | ||||||
| 
 | 
 | ||||||
|   font-family: sans-serif; |   font-family: sans-serif; | ||||||
|   font-size: 1em; |   font-size: 1em; | ||||||
|   width: 500px; |   width: 100%; | ||||||
|   max-width: 100%; |  | ||||||
| 
 | 
 | ||||||
|   resize: none; |   resize: none; | ||||||
|   overflow-x: wrap; |   overflow-x: wrap; | ||||||
|  | @ -84,7 +85,7 @@ button { | ||||||
|   background-color: var(--accent); |   background-color: var(--accent); | ||||||
|   border: var(--accent); |   border: var(--accent); | ||||||
|   border-radius: 6px; |   border-radius: 6px; | ||||||
|   color: var(--fg); |   color: #fff; | ||||||
|   padding: 0.5em 1em; |   padding: 0.5em 1em; | ||||||
|   display: inline-block; |   display: inline-block; | ||||||
|   font-weight: 400; |   font-weight: 400; | ||||||
|  | @ -94,13 +95,19 @@ button { | ||||||
| 
 | 
 | ||||||
|   font-family: sans-serif; |   font-family: sans-serif; | ||||||
|   font-size: 1em; |   font-size: 1em; | ||||||
|   width: 500px; |   width: 100%; | ||||||
|   max-width: 100%; |  | ||||||
| 
 | 
 | ||||||
|   user-select: none; |   user-select: none; | ||||||
|   border: 1px solid rgba(0, 0, 0, 0); |   border: 1px solid rgba(0, 0, 0, 0); | ||||||
|   line-height: 1.5; |   line-height: 1.5; | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|  |   margin: 0.5em 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | button:disabled { | ||||||
|  |   filter: saturate(0.75); | ||||||
|  |   opacity: 0.75; | ||||||
|  |   cursor: default; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| button.small-button { | button.small-button { | ||||||
|  | @ -121,20 +128,25 @@ button.small-button { | ||||||
| 
 | 
 | ||||||
| #pre-join-controls, | #pre-join-controls, | ||||||
| #create-controls { | #create-controls { | ||||||
|   width: 60%; |   margin: 0; | ||||||
|   margin: 0 auto; |   flex-grow: 1; | ||||||
|   margin-top: 4em; |   overflow-y: auto; | ||||||
|  |   display: flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |   align-items: center; | ||||||
|  |   justify-content: center; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #join-session-form, | #join-session-form, | ||||||
| #create-session-form { | #create-session-form { | ||||||
|   margin-bottom: 4em; |   width: 500px; | ||||||
|  |   max-width: 100%; | ||||||
|  |   padding: 1rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #post-create-message { | #post-create-message { | ||||||
|   display: none; |   display: none; | ||||||
|   width: 500px; |   width: 100%; | ||||||
|   max-width: 100%; |  | ||||||
|   font-size: 0.85em; |   font-size: 0.85em; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -245,13 +257,35 @@ button.small-button { | ||||||
|   border-radius: 4px; |   border-radius: 4px; | ||||||
|   width: 100%; |   width: 100%; | ||||||
| } | } | ||||||
|  | 
 | ||||||
| .emoji-option:hover, .emoji-option:focus { | .emoji-option:hover, .emoji-option:focus { | ||||||
|   background: var(--fg-transparent); |   background: var(--fg-transparent); | ||||||
| } | } | ||||||
|  | 
 | ||||||
| .emoji-option:last-child { | .emoji-option:last-child { | ||||||
|   margin: 0; |   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) { | @media (min-aspect-ratio: 4/3) { | ||||||
|   body { |   body { | ||||||
|     flex-direction: row; |     flex-direction: row; | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue