forked from lavender/watch-party
		
	Refactor frontend to use ES modules
This commit is contained in:
		
							parent
							
								
									caf96d1d04
								
							
						
					
					
						commit
						8da286fad9
					
				
					 7 changed files with 388 additions and 333 deletions
				
			
		|  | @ -47,6 +47,6 @@ | |||
|       </form> | ||||
|     </div> | ||||
| 
 | ||||
|     <script src="/main.js?v=2"></script> | ||||
|     <script type="module" src="/main.mjs?v=1"></script> | ||||
|   </body> | ||||
| </html> | ||||
|  |  | |||
							
								
								
									
										102
									
								
								frontend/lib/chat.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								frontend/lib/chat.mjs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,102 @@ | |||
| const setupChatboxEvents = (socket) => { | ||||
|   // clear events by just reconstructing the form
 | ||||
|   const oldChatForm = document.querySelector("#chatbox-send"); | ||||
|   const chatForm = oldChatForm.cloneNode(true); | ||||
|   oldChatForm.replaceWith(chatForm); | ||||
| 
 | ||||
|   chatForm.addEventListener("submit", (e) => { | ||||
|     e.preventDefault(); | ||||
| 
 | ||||
|     const input = chatForm.querySelector("input"); | ||||
|     const content = input.value; | ||||
|     if (content.trim().length) { | ||||
|       input.value = ""; | ||||
| 
 | ||||
|       socket.send( | ||||
|         JSON.stringify({ | ||||
|           op: "ChatMessage", | ||||
|           data: { | ||||
|             message: content, | ||||
|           }, | ||||
|         }) | ||||
|       ); | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| const fixChatSize = () => { | ||||
|   const video = document.querySelector("video"); | ||||
|   const chatbox = document.querySelector("#chatbox"); | ||||
|   const chatboxContainer = document.querySelector("#chatbox-container"); | ||||
| 
 | ||||
|   if (video && chatbox && chatboxContainer) { | ||||
|     const delta = chatboxContainer.clientHeight - chatbox.clientHeight; | ||||
| 
 | ||||
|     chatbox.style["height"] = `calc(${ | ||||
|       window.innerHeight - video.clientHeight | ||||
|     }px - ${delta}px - 1em)`;
 | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @param {WebSocket} socket | ||||
|  */ | ||||
| export const setupChat = async (socket) => { | ||||
|   document.querySelector("#chatbox-container").style["display"] = "block"; | ||||
|   setupChatboxEvents(socket); | ||||
| 
 | ||||
|   fixChatSize(); | ||||
|   window.addEventListener("resize", () => { | ||||
|     fixChatSize(); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| const printToChat = (elem) => { | ||||
|   const chatbox = document.querySelector("#chatbox"); | ||||
|   chatbox.appendChild(elem); | ||||
|   chatbox.scrollTop = chatbox.scrollHeight; | ||||
| }; | ||||
| 
 | ||||
| export const handleChatEvent = (event) => { | ||||
|   switch (event.op) { | ||||
|     case "UserJoin": { | ||||
|       // print something to the chat
 | ||||
|       const chatMessage = document.createElement("div"); | ||||
|       chatMessage.classList.add("chat-message"); | ||||
|       chatMessage.classList.add("user-join"); | ||||
|       const userName = document.createElement("strong"); | ||||
|       userName.textContent = event.data; | ||||
|       chatMessage.appendChild(userName); | ||||
|       chatMessage.appendChild(document.createTextNode(" joined")); | ||||
|       printToChat(chatMessage); | ||||
| 
 | ||||
|       break; | ||||
|     } | ||||
|     case "UserLeave": { | ||||
|       const chatMessage = document.createElement("div"); | ||||
|       chatMessage.classList.add("chat-message"); | ||||
|       chatMessage.classList.add("user-leave"); | ||||
|       const userName = document.createElement("strong"); | ||||
|       userName.textContent = event.data; | ||||
|       chatMessage.appendChild(userName); | ||||
|       chatMessage.appendChild(document.createTextNode(" left")); | ||||
|       printToChat(chatMessage); | ||||
| 
 | ||||
|       break; | ||||
|     } | ||||
|     case "ChatMessage": { | ||||
|       const chatMessage = document.createElement("div"); | ||||
|       chatMessage.classList.add("chat-message"); | ||||
|       const userName = document.createElement("strong"); | ||||
|       userName.innerText = event.data.user; | ||||
|       chatMessage.appendChild(userName); | ||||
|       chatMessage.appendChild(document.createTextNode(" ")); | ||||
|       const messageContent = document.createElement("span"); | ||||
|       messageContent.classList.add("message-content"); | ||||
|       messageContent.textContent = event.data.message; | ||||
|       chatMessage.appendChild(messageContent); | ||||
|       printToChat(chatMessage); | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										45
									
								
								frontend/lib/join-session.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								frontend/lib/join-session.mjs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | |||
| import { joinSession } from "./watch-session.mjs"; | ||||
| 
 | ||||
| /** | ||||
|  * @param {HTMLInputElement} field | ||||
|  */ | ||||
| const loadNickname = (field) => { | ||||
|   try { | ||||
|     const savedNickname = localStorage.getItem("watch-party-nickname"); | ||||
|     field.value = savedNickname; | ||||
|   } catch (_err) { | ||||
|     // Sometimes localStorage is blocked from use
 | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @param {HTMLInputElement} field | ||||
|  */ | ||||
| const saveNickname = (field) => { | ||||
|   try { | ||||
|     localStorage.setItem("watch-party-nickname", field.value); | ||||
|   } catch (_err) { | ||||
|     // see loadNickname
 | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| export const setupJoinSessionForm = () => { | ||||
|   const form = document.querySelector("#join-session-form"); | ||||
|   const nickname = form.querySelector("#join-session-nickname"); | ||||
|   const sessionId = form.querySelector("#join-session-id"); | ||||
| 
 | ||||
|   loadNickname(nickname); | ||||
| 
 | ||||
|   if (window.location.hash.match(/#[0-9a-f\-]+/)) { | ||||
|     sessionId.value = window.location.hash.substring(1); | ||||
|   } | ||||
| 
 | ||||
|   document | ||||
|     .querySelector("#join-session-form") | ||||
|     .addEventListener("submit", (event) => { | ||||
|       event.preventDefault(); | ||||
| 
 | ||||
|       saveNickname(nickname); | ||||
|       joinSession(nickname.value, sessionId.value); | ||||
|     }); | ||||
| }; | ||||
							
								
								
									
										58
									
								
								frontend/lib/video.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								frontend/lib/video.mjs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,58 @@ | |||
| /** | ||||
|  * @param {string} videoUrl | ||||
|  * @param {{name: string, url: string}[]} subtitles | ||||
|  */ | ||||
| const createVideoElement = (videoUrl, subtitles) => { | ||||
|   const video = document.createElement("video"); | ||||
|   video.controls = true; | ||||
|   video.autoplay = false; | ||||
|   video.crossOrigin = "anonymous"; | ||||
| 
 | ||||
|   const source = document.createElement("source"); | ||||
|   source.src = videoUrl; | ||||
| 
 | ||||
|   video.appendChild(source); | ||||
| 
 | ||||
|   let first = true; | ||||
|   for (const { name, url } of subtitles) { | ||||
|     const track = document.createElement("track"); | ||||
|     track.label = name; | ||||
|     track.src = url; | ||||
|     track.kind = "captions"; | ||||
| 
 | ||||
|     if (first) { | ||||
|       track.default = true; | ||||
|       first = false; | ||||
|     } | ||||
| 
 | ||||
|     video.appendChild(track); | ||||
|   } | ||||
| 
 | ||||
|   return video; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} videoUrl | ||||
|  * @param {{name: string, url: string}[]} subtitles | ||||
|  * @param {number} currentTime | ||||
|  * @param {boolean} playing | ||||
|  */ | ||||
| export const setupVideo = async (videoUrl, subtitles, currentTime, playing) => { | ||||
|   document.querySelector("#pre-join-controls").style["display"] = "none"; | ||||
|   const video = createVideoElement(videoUrl, subtitles); | ||||
|   document.querySelector("#video-container").appendChild(video); | ||||
| 
 | ||||
|   video.currentTime = currentTime / 1000.0; | ||||
| 
 | ||||
|   try { | ||||
|     if (playing) { | ||||
|       await video.play(); | ||||
|     } else { | ||||
|       video.pause(); | ||||
|     } | ||||
|   } catch (err) { | ||||
|     // Auto-play is probably disabled, we should uhhhhhhh do something about it
 | ||||
|   } | ||||
| 
 | ||||
|   return video; | ||||
| }; | ||||
							
								
								
									
										171
									
								
								frontend/lib/watch-session.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								frontend/lib/watch-session.mjs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,171 @@ | |||
| import { setupVideo } from "./video.mjs"; | ||||
| import { setupChat, handleChatEvent } from "./chat.mjs"; | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} sessionId | ||||
|  * @param {string} nickname | ||||
|  * @returns {WebSocket} | ||||
|  */ | ||||
| const createWebSocket = (sessionId, nickname) => { | ||||
|   const wsUrl = new URL( | ||||
|     `/sess/${sessionId}/subscribe` + | ||||
|       `?nickname=${encodeURIComponent(nickname)}`, | ||||
|     window.location.href | ||||
|   ); | ||||
|   wsUrl.protocol = { "http:": "ws:", "https:": "wss:" }[wsUrl.protocol]; | ||||
|   const socket = new WebSocket(wsUrl.toString()); | ||||
| 
 | ||||
|   return socket; | ||||
| }; | ||||
| 
 | ||||
| let outgoingDebounce = false; | ||||
| let outgoingDebounceCallbackId = null; | ||||
| 
 | ||||
| const setDebounce = () => { | ||||
|   outgoingDebounce = true; | ||||
| 
 | ||||
|   if (outgoingDebounceCallbackId) { | ||||
|     cancelIdleCallback(outgoingDebounceCallbackId); | ||||
|     outgoingDebounceCallbackId = null; | ||||
|   } | ||||
| 
 | ||||
|   outgoingDebounceCallbackId = setTimeout(() => { | ||||
|     outgoingDebounce = false; | ||||
|   }, 500); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @param {HTMLVideoElement} video | ||||
|  * @param {WebSocket} socket | ||||
|  */ | ||||
| const setupIncomingEvents = (video, socket) => { | ||||
|   const setVideoTime = (time) => { | ||||
|     const timeSecs = time / 1000.0; | ||||
| 
 | ||||
|     if (Math.abs(video.currentTime - timeSecs) > 0.5) { | ||||
|       video.currentTime = timeSecs; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   socket.addEventListener("message", async (messageEvent) => { | ||||
|     try { | ||||
|       const event = JSON.parse(messageEvent.data); | ||||
|       // console.log(event);
 | ||||
| 
 | ||||
|       switch (event.op) { | ||||
|         case "SetPlaying": | ||||
|           setDebounce(); | ||||
| 
 | ||||
|           if (event.data.playing) { | ||||
|             await video.play(); | ||||
|           } else { | ||||
|             video.pause(); | ||||
|           } | ||||
| 
 | ||||
|           setVideoTime(event.data.time); | ||||
| 
 | ||||
|           break; | ||||
|         case "SetTime": | ||||
|           setDebounce(); | ||||
|           setVideoTime(event.data); | ||||
|           break; | ||||
|         case "UserJoin": | ||||
|         case "UserLeave": | ||||
|         case "ChatMessage": | ||||
|           handleChatEvent(event); | ||||
|           break; | ||||
|       } | ||||
|     } catch (_err) {} | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @param {HTMLVideoElement} video | ||||
|  * @param {WebSocket} socket | ||||
|  */ | ||||
| const setupOutgoingEvents = (video, socket) => { | ||||
|   const currentVideoTime = () => (video.currentTime * 1000) | 0; | ||||
| 
 | ||||
|   video.addEventListener("pause", async (event) => { | ||||
|     if (outgoingDebounce) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     socket.send( | ||||
|       JSON.stringify({ | ||||
|         op: "SetPlaying", | ||||
|         data: { | ||||
|           playing: false, | ||||
|           time: currentVideoTime(), | ||||
|         }, | ||||
|       }) | ||||
|     ); | ||||
|   }); | ||||
| 
 | ||||
|   video.addEventListener("play", (event) => { | ||||
|     if (outgoingDebounce) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     socket.send( | ||||
|       JSON.stringify({ | ||||
|         op: "SetPlaying", | ||||
|         data: { | ||||
|           playing: true, | ||||
|           time: currentVideoTime(), | ||||
|         }, | ||||
|       }) | ||||
|     ); | ||||
|   }); | ||||
| 
 | ||||
|   let firstSeekComplete = false; | ||||
|   video.addEventListener("seeked", async (event) => { | ||||
|     if (!firstSeekComplete) { | ||||
|       // The first seeked event is performed by the browser when the video is loading
 | ||||
|       firstSeekComplete = true; | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (outgoingDebounce) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     socket.send( | ||||
|       JSON.stringify({ | ||||
|         op: "SetTime", | ||||
|         data: currentVideoTime(), | ||||
|       }) | ||||
|     ); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} nickname | ||||
|  * @param {string} sessionId | ||||
|  */ | ||||
| export const joinSession = async (nickname, sessionId) => { | ||||
|   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); | ||||
|     socket.addEventListener("open", async () => { | ||||
|       const video = await setupVideo( | ||||
|         video_url, | ||||
|         subtitle_tracks, | ||||
|         current_time_ms, | ||||
|         is_playing | ||||
|       ); | ||||
| 
 | ||||
|       setupOutgoingEvents(video, socket); | ||||
|       setupIncomingEvents(video, socket); | ||||
|       setupChat(socket); | ||||
|     }); | ||||
|     // TODO: Close listener ?
 | ||||
|   } catch (err) { | ||||
|     // TODO: Show an error on the screen
 | ||||
|     console.error(err); | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										332
									
								
								frontend/main.js
									
									
									
									
									
								
							
							
						
						
									
										332
									
								
								frontend/main.js
									
									
									
									
									
								
							|  | @ -1,332 +0,0 @@ | |||
| /** | ||||
|  * @param {string} videoUrl | ||||
|  * @param {{name: string, url: string}[]} subtitles | ||||
|  */ | ||||
| const createVideoElement = (videoUrl, subtitles) => { | ||||
|   const video = document.createElement("video"); | ||||
|   video.controls = true; | ||||
|   video.autoplay = false; | ||||
|   video.crossOrigin = "anonymous"; | ||||
| 
 | ||||
|   const source = document.createElement("source"); | ||||
|   source.src = videoUrl; | ||||
| 
 | ||||
|   video.appendChild(source); | ||||
| 
 | ||||
|   let first = true; | ||||
|   for (const { name, url } of subtitles) { | ||||
|     const track = document.createElement("track"); | ||||
|     track.label = name; | ||||
|     track.src = url; | ||||
|     track.kind = "captions"; | ||||
| 
 | ||||
|     if (first) { | ||||
|       track.default = true; | ||||
|       first = false; | ||||
|     } | ||||
| 
 | ||||
|     video.appendChild(track); | ||||
|   } | ||||
| 
 | ||||
|   return video; | ||||
| } | ||||
| 
 | ||||
| let outgoingDebounce = false; | ||||
| let outgoingDebounceCallbackId = null; | ||||
| 
 | ||||
| const setDebounce = () => { | ||||
|   outgoingDebounce = true; | ||||
| 
 | ||||
|   if (outgoingDebounceCallbackId) { | ||||
|     cancelIdleCallback(outgoingDebounceCallbackId); | ||||
|     outgoingDebounceCallbackId = null; | ||||
|   } | ||||
| 
 | ||||
|   outgoingDebounceCallbackId = setTimeout(() => { | ||||
|     outgoingDebounce = false; | ||||
|   }, 500); | ||||
| } | ||||
| 
 | ||||
| const clearChat = () => { | ||||
|   document.querySelector("#chatbox").innerHTML = ""; | ||||
| } | ||||
| 
 | ||||
| const printToChat = (elem) => { | ||||
|   const chatbox = document.querySelector("#chatbox"); | ||||
|   chatbox.appendChild(elem); | ||||
|   chatbox.scrollTop = chatbox.scrollHeight; | ||||
| } | ||||
| 
 | ||||
| const handleChatEvent = (event) => { | ||||
|   switch (event.op) { | ||||
|     case "UserJoin": { | ||||
|       // print something to the chat
 | ||||
|       const chatMessage = document.createElement("div"); | ||||
|       chatMessage.classList.add("chat-message"); | ||||
|       chatMessage.classList.add("user-join"); | ||||
|       const userName = document.createElement("strong"); | ||||
|       userName.textContent = event.data; | ||||
|       chatMessage.appendChild(userName); | ||||
|       chatMessage.appendChild(document.createTextNode(" joined")); | ||||
|       printToChat(chatMessage); | ||||
| 
 | ||||
|       break; | ||||
|     } | ||||
|     case "UserLeave": { | ||||
|       const chatMessage = document.createElement("div"); | ||||
|       chatMessage.classList.add("chat-message"); | ||||
|       chatMessage.classList.add("user-leave"); | ||||
|       const userName = document.createElement("strong"); | ||||
|       userName.textContent = event.data; | ||||
|       chatMessage.appendChild(userName); | ||||
|       chatMessage.appendChild(document.createTextNode(" left")); | ||||
|       printToChat(chatMessage); | ||||
| 
 | ||||
|       break; | ||||
|     } | ||||
|     case "ChatMessage": { | ||||
|       const chatMessage = document.createElement("div"); | ||||
|       chatMessage.classList.add("chat-message"); | ||||
|       const userName = document.createElement("strong"); | ||||
|       userName.innerText = event.data.user; | ||||
|       chatMessage.appendChild(userName); | ||||
|       chatMessage.appendChild(document.createTextNode(" ")); | ||||
|       const messageContent = document.createElement("span"); | ||||
|       messageContent.classList.add("message-content"); | ||||
|       messageContent.textContent = event.data.message; | ||||
|       chatMessage.appendChild(messageContent); | ||||
|       printToChat(chatMessage); | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @param {WebSocket} socket | ||||
|  * @param {HTMLVideoElement} video | ||||
|  */ | ||||
| const setupSocketEvents = (socket, video) => { | ||||
|   const setVideoTime = time => { | ||||
|     const timeSecs = time / 1000.0; | ||||
| 
 | ||||
|     if (Math.abs(video.currentTime - timeSecs) > 0.5) { | ||||
|       video.currentTime = timeSecs; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   socket.addEventListener("message", async messageEvent => { | ||||
|     try { | ||||
|       const event = JSON.parse(messageEvent.data); | ||||
|       console.log(event); | ||||
| 
 | ||||
|       switch (event.op) { | ||||
|         case "SetPlaying": | ||||
|           setDebounce(); | ||||
| 
 | ||||
|           if (event.data.playing) { | ||||
|             await video.play(); | ||||
|           } else { | ||||
|             video.pause(); | ||||
|           } | ||||
| 
 | ||||
|           setVideoTime(event.data.time); | ||||
| 
 | ||||
|           break; | ||||
|         case "SetTime": | ||||
|           setDebounce(); | ||||
|           setVideoTime(event.data); | ||||
|           break; | ||||
|         case "UserJoin": | ||||
|         case "UserLeave": | ||||
|         case "ChatMessage": | ||||
|           handleChatEvent(event); | ||||
|           break; | ||||
|       } | ||||
|     } catch (_err) { | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} sessionId | ||||
|  * @param {HTMLVideoElement} video | ||||
|  * @param {WebSocket} socket | ||||
|  */ | ||||
| const setupVideoEvents = (sessionId, video, socket) => { | ||||
|   const currentVideoTime = () => (video.currentTime * 1000) | 0; | ||||
| 
 | ||||
|   video.addEventListener("pause", async event => { | ||||
|     if (outgoingDebounce) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     socket.send(JSON.stringify({ | ||||
|       "op": "SetPlaying", | ||||
|       "data": { | ||||
|         "playing": false, | ||||
|         "time": currentVideoTime(), | ||||
|       } | ||||
|     })); | ||||
|   }); | ||||
| 
 | ||||
|   video.addEventListener("play", event => { | ||||
|     if (outgoingDebounce) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     socket.send(JSON.stringify({ | ||||
|       "op": "SetPlaying", | ||||
|       "data": { | ||||
|         "playing": true, | ||||
|         "time": currentVideoTime(), | ||||
|       } | ||||
|     })); | ||||
|   }); | ||||
| 
 | ||||
|   let firstSeekComplete = false; | ||||
|   video.addEventListener("seeked", async event => { | ||||
|     if (!firstSeekComplete) { | ||||
|       // The first seeked event is performed by the browser when the video is loading
 | ||||
|       firstSeekComplete = true; | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (outgoingDebounce) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     socket.send(JSON.stringify({ | ||||
|       "op": "SetTime", | ||||
|       "data": currentVideoTime(), | ||||
|     })); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| /**  | ||||
|  * @param {string} videoUrl | ||||
|  * @param {{name: string, url: string}[]} subtitles | ||||
|  * @param {number} currentTime | ||||
|  * @param {boolean} playing | ||||
|  * @param {WebSocket} socket | ||||
|  */ | ||||
| const setupVideo = async (sessionId, videoUrl, subtitles, currentTime, playing, socket) => { | ||||
|   document.querySelector("#pre-join-controls").style["display"] = "none"; | ||||
|   const video = createVideoElement(videoUrl, subtitles); | ||||
|   document.querySelector("#video-container").appendChild(video); | ||||
| 
 | ||||
|   video.currentTime = (currentTime / 1000.0); | ||||
| 
 | ||||
|   try { | ||||
|     if (playing) { | ||||
|       await video.play() | ||||
|     } else { | ||||
|       video.pause() | ||||
|     } | ||||
|   } catch (err) { | ||||
|     // Auto-play is probably disabled, we should uhhhhhhh do something about it
 | ||||
|   } | ||||
| 
 | ||||
|   setupSocketEvents(socket, video); | ||||
|   setupVideoEvents(sessionId, video, socket); | ||||
| } | ||||
| 
 | ||||
| const fixChatSize = () => { | ||||
|   const video = document.querySelector("video"); | ||||
|   const chatbox = document.querySelector("#chatbox"); | ||||
|   const chatboxContainer = document.querySelector("#chatbox-container"); | ||||
| 
 | ||||
|   if (video && chatbox && chatboxContainer) { | ||||
|     const delta = chatboxContainer.clientHeight - chatbox.clientHeight; | ||||
| 
 | ||||
|     chatbox.style["height"] = `calc(${(window.innerHeight - video.clientHeight)}px - ${delta}px - 1em)`; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const setupChatboxEvents = (socket) => { | ||||
|   // clear events by just reconstructing the form
 | ||||
|   const oldChatForm = document.querySelector("#chatbox-send"); | ||||
|   const chatForm = oldChatForm.cloneNode(true); | ||||
|   oldChatForm.replaceWith(chatForm); | ||||
| 
 | ||||
|   chatForm.addEventListener("submit", e => { | ||||
|     e.preventDefault(); | ||||
| 
 | ||||
|     const input = chatForm.querySelector("input"); | ||||
|     const content = input.value; | ||||
|     if (content.trim().length) { | ||||
|       input.value = ""; | ||||
| 
 | ||||
|       socket.send(JSON.stringify({ | ||||
|         "op": "ChatMessage", | ||||
|         "data": { | ||||
|           "message": content, | ||||
|         } | ||||
|       })); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} sessionId | ||||
|  * @param {WebSocket} socket | ||||
|  */ | ||||
| const setupChat = async (sessionId, socket) => { | ||||
|   document.querySelector("#chatbox-container").style["display"] = "block"; | ||||
|   setupChatboxEvents(socket); | ||||
|   fixChatSize(); | ||||
| } | ||||
| 
 | ||||
| /**  | ||||
|  * @param {string} nickname | ||||
|  * @param {string} sessionId | ||||
|  */ | ||||
| const joinSession = async (nickname, sessionId) => { | ||||
|   try { | ||||
|     window.location.hash = sessionId; | ||||
| 
 | ||||
|     const { | ||||
|       video_url, subtitle_tracks, | ||||
|       current_time_ms, is_playing | ||||
|     } = await fetch(`/sess/${sessionId}`).then(r => r.json()); | ||||
| 
 | ||||
|     const wsUrl = new URL(`/sess/${sessionId}/subscribe?nickname=${encodeURIComponent(nickname)}`, window.location.href); | ||||
|     wsUrl.protocol = { "http:": "ws:", "https:": "wss:" }[wsUrl.protocol]; | ||||
|     const socket = new WebSocket(wsUrl.toString()); | ||||
| 
 | ||||
|     socket.addEventListener("open", () => { | ||||
|       setupVideo(sessionId, video_url, subtitle_tracks, current_time_ms, is_playing, socket); | ||||
|       setupChat(sessionId, socket); | ||||
|     }); | ||||
|   } catch (err) { | ||||
|     // TODO: Show an error on the screen
 | ||||
|     console.error(err); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const main = () => { | ||||
|   document.querySelector("#join-session-nickname").value = localStorage.getItem("watch-party-nickname"); | ||||
| 
 | ||||
|   document.querySelector("#join-session-form").addEventListener("submit", event => { | ||||
|     event.preventDefault(); | ||||
| 
 | ||||
|     const nickname = document.querySelector("#join-session-nickname").value; | ||||
|     const sessionId = document.querySelector("#join-session-id").value; | ||||
| 
 | ||||
|     localStorage.setItem("watch-party-nickname", nickname); | ||||
|     joinSession(nickname, sessionId); | ||||
|   }); | ||||
| 
 | ||||
|   if (window.location.hash.match(/#[0-9a-f\-]+/)) { | ||||
|     document.querySelector("#join-session-id").value = window.location.hash.substring(1); | ||||
|   } | ||||
| 
 | ||||
|   window.addEventListener("resize", event => { | ||||
|     fixChatSize(); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| if (document.readyState === "complete") { | ||||
|   main(); | ||||
| } else { | ||||
|   document.addEventListener("DOMContentLoaded", main); | ||||
| } | ||||
							
								
								
									
										11
									
								
								frontend/main.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/main.mjs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| import { setupJoinSessionForm } from "./lib/join-session.mjs"; | ||||
| 
 | ||||
| const main = () => { | ||||
|   setupJoinSessionForm(); | ||||
| }; | ||||
| 
 | ||||
| if (document.readyState === "complete") { | ||||
|   main(); | ||||
| } else { | ||||
|   document.addEventListener("DOMContentLoaded", main); | ||||
| } | ||||
		Loading…
	
		Reference in a new issue