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" | ||||
|         /> | ||||
|         <button>Create</button> | ||||
|       </form> | ||||
| 
 | ||||
|       <p> | ||||
|         Already have a session? | ||||
|         <a href="/">Join your session</a> instead. | ||||
|       </p> | ||||
|         <p> | ||||
|           Already have a session? | ||||
|           <a href="/">Join your session</a> instead. | ||||
|         </p> | ||||
|       </form> | ||||
|     </div> | ||||
| 
 | ||||
|     <script type="module" src="/create.mjs?v=9"></script> | ||||
|  |  | |||
|  | @ -41,11 +41,11 @@ | |||
|           required | ||||
|         /> | ||||
|         <button id="join-session-button">Join</button> | ||||
|       </form> | ||||
| 
 | ||||
|       <p> | ||||
|         No session to join? <a href="/create.html">Create a session</a> instead. | ||||
|       </p> | ||||
|         <p> | ||||
|           No session to join? <a href="/create.html">Create a session</a> instead. | ||||
|         </p> | ||||
|       </form> | ||||
|     </div> | ||||
| 
 | ||||
|     <div id="video-container"></div> | ||||
|  | @ -54,7 +54,7 @@ | |||
|       <div id="chatbox"></div> | ||||
|       <form id="chatbox-send"> | ||||
|         <input type="text" placeholder="Message... (/help for commands)" list="emoji-autocomplete" /> | ||||
| 		<div id="emoji-autocomplete"></div> <!-- DO NOT ADD SPACING INSIDE THE TAG IT WILL BREAK THE CSS kthxbye --> | ||||
|         <div id="emoji-autocomplete"></div> <!-- DO NOT ADD SPACING INSIDE THE TAG IT WILL BREAK THE CSS kthxbye --> | ||||
|       </form> | ||||
|     </div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -29,6 +29,7 @@ const setupChatboxEvents = (socket) => { | |||
| 	autocompleting = false; | ||||
|   } | ||||
|   messageInput.addEventListener("input", autocomplete) | ||||
|   messageInput.addEventListener("selectionchange", autocomplete); | ||||
| 
 | ||||
|   chatForm.addEventListener("submit", async (e) => { | ||||
|     e.preventDefault(); | ||||
|  |  | |||
|  | @ -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; | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
|  |  | |||
							
								
								
									
										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 { 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)
 | ||||
|   //}
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue