forked from lavender/watch-party
		
	allow users to pick their username colour
This commit is contained in:
		
							parent
							
								
									727e72d89f
								
							
						
					
					
						commit
						152d51f4fc
					
				
					 11 changed files with 88 additions and 30 deletions
				
			
		|  | @ -47,6 +47,6 @@ | ||||||
|       </p> |       </p> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <script type="module" src="/create.mjs?v=5"></script> |     <script type="module" src="/create.mjs?v=7"></script> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { setupCreateSessionForm } from "./lib/create-session.mjs?v=5"; | import { setupCreateSessionForm } from "./lib/create-session.mjs?v=7"; | ||||||
| 
 | 
 | ||||||
| const main = () => { | const main = () => { | ||||||
|   setupCreateSessionForm(); |   setupCreateSessionForm(); | ||||||
|  |  | ||||||
|  | @ -28,6 +28,16 @@ | ||||||
|           placeholder="Nickname" |           placeholder="Nickname" | ||||||
|           required |           required | ||||||
|         /> |         /> | ||||||
|  |          | ||||||
|  |         <label for="join-session-colour">Colour:</label> | ||||||
|  |         <input | ||||||
|  |           type="text" | ||||||
|  |           id="join-session-colour" | ||||||
|  |           placeholder="7ed0ff" | ||||||
|  |           value="7ed0ff" | ||||||
|  |           pattern="[a-fA-F\d]{6}" | ||||||
|  |           required | ||||||
|  |         /> | ||||||
| 
 | 
 | ||||||
|         <label for="join-session-id">Session ID:</label> |         <label for="join-session-id">Session ID:</label> | ||||||
|         <input |         <input | ||||||
|  | @ -52,6 +62,6 @@ | ||||||
|       </form> |       </form> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <script type="module" src="/main.mjs?v=6"></script> |     <script type="module" src="/main.mjs?v=7"></script> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
|  | @ -43,13 +43,12 @@ export const setupChat = async (socket) => { | ||||||
|   document.querySelector("#chatbox-container").style["display"] = "block"; |   document.querySelector("#chatbox-container").style["display"] = "block"; | ||||||
|   setupChatboxEvents(socket); |   setupChatboxEvents(socket); | ||||||
| 
 | 
 | ||||||
|   window.addEventListener("keydown", event => { |   window.addEventListener("keydown", (event) => { | ||||||
|     try { |     try { | ||||||
|       const isSelectionEmpty = window.getSelection().toString().length === 0; |       const isSelectionEmpty = window.getSelection().toString().length === 0; | ||||||
|       if (event.code.match(/Key\w/) && isSelectionEmpty) |       if (event.code.match(/Key\w/) && isSelectionEmpty) | ||||||
|         document.querySelector("#chatbox-send > input").focus() |         document.querySelector("#chatbox-send > input").focus(); | ||||||
|     } catch (_err) { |     } catch (_err) {} | ||||||
|     } |  | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   fixChatSize(); |   fixChatSize(); | ||||||
|  | @ -101,7 +100,9 @@ const checkDebounce = (event) => { | ||||||
|  */ |  */ | ||||||
| const getCurrentTimestamp = () => { | const getCurrentTimestamp = () => { | ||||||
|   const t = new Date(); |   const t = new Date(); | ||||||
|   return `${matpad(t.getHours())}:${matpad(t.getMinutes())}:${matpad(t.getSeconds())}`; |   return `${matpad(t.getHours())}:${matpad(t.getMinutes())}:${matpad( | ||||||
|  |     t.getSeconds() | ||||||
|  |   )}`;
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -116,7 +117,7 @@ const matpad = (n) => { | ||||||
|  * @param {string?} user |  * @param {string?} user | ||||||
|  * @param {Node?} content |  * @param {Node?} content | ||||||
|  */ |  */ | ||||||
| const printChatMessage = (eventType, user, content) => { | const printChatMessage = (eventType, user, colour, content) => { | ||||||
|   const chatMessage = document.createElement("div"); |   const chatMessage = document.createElement("div"); | ||||||
|   chatMessage.classList.add("chat-message"); |   chatMessage.classList.add("chat-message"); | ||||||
|   chatMessage.classList.add(eventType); |   chatMessage.classList.add(eventType); | ||||||
|  | @ -124,6 +125,7 @@ const printChatMessage = (eventType, user, content) => { | ||||||
| 
 | 
 | ||||||
|   if (user != null) { |   if (user != null) { | ||||||
|     const userName = document.createElement("strong"); |     const userName = document.createElement("strong"); | ||||||
|  |     userName.style = `color: #${colour}`; | ||||||
|     userName.textContent = user; |     userName.textContent = user; | ||||||
|     chatMessage.appendChild(userName); |     chatMessage.appendChild(userName); | ||||||
|   } |   } | ||||||
|  | @ -158,6 +160,7 @@ export const logEventToChat = (event) => { | ||||||
|       printChatMessage( |       printChatMessage( | ||||||
|         "user-join", |         "user-join", | ||||||
|         event.user, |         event.user, | ||||||
|  |         event.colour, | ||||||
|         document.createTextNode("joined") |         document.createTextNode("joined") | ||||||
|       ); |       ); | ||||||
|       break; |       break; | ||||||
|  | @ -166,6 +169,7 @@ export const logEventToChat = (event) => { | ||||||
|       printChatMessage( |       printChatMessage( | ||||||
|         "user-leave", |         "user-leave", | ||||||
|         event.user, |         event.user, | ||||||
|  |         event.colour, | ||||||
|         document.createTextNode("left") |         document.createTextNode("left") | ||||||
|       ); |       ); | ||||||
|       break; |       break; | ||||||
|  | @ -174,7 +178,12 @@ export const logEventToChat = (event) => { | ||||||
|       const messageContent = document.createElement("span"); |       const messageContent = document.createElement("span"); | ||||||
|       messageContent.classList.add("message-content"); |       messageContent.classList.add("message-content"); | ||||||
|       messageContent.textContent = event.data; |       messageContent.textContent = event.data; | ||||||
|       printChatMessage("chat-message", event.user, messageContent); |       printChatMessage( | ||||||
|  |         "chat-message", | ||||||
|  |         event.user, | ||||||
|  |         event.colour, | ||||||
|  |         messageContent | ||||||
|  |       ); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case "SetTime": { |     case "SetTime": { | ||||||
|  | @ -185,7 +194,7 @@ export const logEventToChat = (event) => { | ||||||
|         document.createTextNode(formatTime(event.data)) |         document.createTextNode(formatTime(event.data)) | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|       printChatMessage("set-time", event.user, messageContent); |       printChatMessage("set-time", event.user, event.colour, messageContent); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case "SetPlaying": { |     case "SetPlaying": { | ||||||
|  | @ -200,8 +209,7 @@ export const logEventToChat = (event) => { | ||||||
|         document.createTextNode(formatTime(event.data.time)) |         document.createTextNode(formatTime(event.data.time)) | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|       printChatMessage("set-playing", event.user, messageContent); |       printChatMessage("set-playing", event.user, event.colour, messageContent); | ||||||
| 
 |  | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { createSession } from "./watch-session.mjs?v=6"; | import { createSession } from "./watch-session.mjs?v=7"; | ||||||
| 
 | 
 | ||||||
| export const setupCreateSessionForm = () => { | export const setupCreateSessionForm = () => { | ||||||
|   const form = document.querySelector("#create-session-form"); |   const form = document.querySelector("#create-session-form"); | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { joinSession } from "./watch-session.mjs?v=6"; | import { joinSession } from "./watch-session.mjs?v=7"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @param {HTMLInputElement} field |  * @param {HTMLInputElement} field | ||||||
|  | @ -23,6 +23,31 @@ const saveNickname = (field) => { | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * @param {HTMLInputElement} field | ||||||
|  |  */ | ||||||
|  | const loadColour = (field) => { | ||||||
|  |   try { | ||||||
|  |     const savedColour = localStorage.getItem("watch-party-colour"); | ||||||
|  |     if (savedColour != null && savedColour != "") { | ||||||
|  |       field.value = savedColour; | ||||||
|  |     } | ||||||
|  |   } catch (_err) { | ||||||
|  |     // Sometimes localStorage is blocked from use
 | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @param {HTMLInputElement} field | ||||||
|  |  */ | ||||||
|  | const saveColour = (field) => { | ||||||
|  |   try { | ||||||
|  |     localStorage.setItem("watch-party-colour", field.value); | ||||||
|  |   } catch (_err) { | ||||||
|  |     // see loadColour
 | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| const displayPostCreateMessage = () => { | const displayPostCreateMessage = () => { | ||||||
|   const params = new URLSearchParams(window.location.search); |   const params = new URLSearchParams(window.location.search); | ||||||
|   if (params.get("created") == "true") { |   if (params.get("created") == "true") { | ||||||
|  | @ -36,10 +61,12 @@ export const setupJoinSessionForm = () => { | ||||||
| 
 | 
 | ||||||
|   const form = document.querySelector("#join-session-form"); |   const form = document.querySelector("#join-session-form"); | ||||||
|   const nickname = form.querySelector("#join-session-nickname"); |   const nickname = form.querySelector("#join-session-nickname"); | ||||||
|  |   const colour = form.querySelector("#join-session-colour"); | ||||||
|   const sessionId = form.querySelector("#join-session-id"); |   const sessionId = form.querySelector("#join-session-id"); | ||||||
|   const button = form.querySelector("#join-session-button"); |   const button = form.querySelector("#join-session-button"); | ||||||
| 
 | 
 | ||||||
|   loadNickname(nickname); |   loadNickname(nickname); | ||||||
|  |   loadColour(colour); | ||||||
| 
 | 
 | ||||||
|   if (window.location.hash.match(/#[0-9a-f\-]+/)) { |   if (window.location.hash.match(/#[0-9a-f\-]+/)) { | ||||||
|     sessionId.value = window.location.hash.substring(1); |     sessionId.value = window.location.hash.substring(1); | ||||||
|  | @ -51,6 +78,7 @@ export const setupJoinSessionForm = () => { | ||||||
|     button.disabled = true; |     button.disabled = true; | ||||||
| 
 | 
 | ||||||
|     saveNickname(nickname); |     saveNickname(nickname); | ||||||
|     joinSession(nickname.value, sessionId.value); |     saveColour(colour); | ||||||
|  |     joinSession(nickname.value, sessionId.value, colour.value); | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,15 +1,16 @@ | ||||||
| import { setupVideo } from "./video.mjs?v=5"; | import { setupVideo } from "./video.mjs?v=7"; | ||||||
| import { setupChat, logEventToChat } from "./chat.mjs?v=6"; | import { setupChat, logEventToChat } from "./chat.mjs?v=7"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @param {string} sessionId |  * @param {string} sessionId | ||||||
|  * @param {string} nickname |  * @param {string} nickname | ||||||
|  * @returns {WebSocket} |  * @returns {WebSocket} | ||||||
|  */ |  */ | ||||||
| const createWebSocket = (sessionId, nickname) => { | const createWebSocket = (sessionId, nickname, colour) => { | ||||||
|   const wsUrl = new URL( |   const wsUrl = new URL( | ||||||
|     `/sess/${sessionId}/subscribe` + |     `/sess/${sessionId}/subscribe` + | ||||||
|     `?nickname=${encodeURIComponent(nickname)}`, |       `?nickname=${encodeURIComponent(nickname)}` + | ||||||
|  |       `&colour=${encodeURIComponent(colour)}`, | ||||||
|     window.location.href |     window.location.href | ||||||
|   ); |   ); | ||||||
|   wsUrl.protocol = { "http:": "ws:", "https:": "wss:" }[wsUrl.protocol]; |   wsUrl.protocol = { "http:": "ws:", "https:": "wss:" }[wsUrl.protocol]; | ||||||
|  | @ -73,7 +74,7 @@ const setupIncomingEvents = (video, socket) => { | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       logEventToChat(event); |       logEventToChat(event); | ||||||
|     } catch (_err) { } |     } catch (_err) {} | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -146,14 +147,14 @@ const setupOutgoingEvents = (video, socket) => { | ||||||
|  * @param {string} nickname |  * @param {string} nickname | ||||||
|  * @param {string} sessionId |  * @param {string} sessionId | ||||||
|  */ |  */ | ||||||
| export const joinSession = async (nickname, sessionId) => { | export const joinSession = async (nickname, sessionId, colour) => { | ||||||
|   try { |   try { | ||||||
|     window.location.hash = sessionId; |     window.location.hash = sessionId; | ||||||
| 
 | 
 | ||||||
|     const { video_url, subtitle_tracks, current_time_ms, is_playing } = |     const { video_url, subtitle_tracks, current_time_ms, is_playing } = | ||||||
|       await fetch(`/sess/${sessionId}`).then((r) => r.json()); |       await fetch(`/sess/${sessionId}`).then((r) => r.json()); | ||||||
| 
 | 
 | ||||||
|     const socket = createWebSocket(sessionId, nickname); |     const socket = createWebSocket(sessionId, nickname, colour); | ||||||
|     socket.addEventListener("open", async () => { |     socket.addEventListener("open", async () => { | ||||||
|       const video = await setupVideo( |       const video = await setupVideo( | ||||||
|         video_url, |         video_url, | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { setupJoinSessionForm } from "./lib/join-session.mjs?v=6"; | import { setupJoinSessionForm } from "./lib/join-session.mjs?v=7"; | ||||||
| 
 | 
 | ||||||
| const main = () => { | const main = () => { | ||||||
|   setupJoinSessionForm(); |   setupJoinSessionForm(); | ||||||
|  |  | ||||||
|  | @ -15,6 +15,8 @@ pub enum WatchEventData { | ||||||
| pub struct WatchEvent { | pub struct WatchEvent { | ||||||
|     #[serde(default, skip_serializing_if = "Option::is_none")] |     #[serde(default, skip_serializing_if = "Option::is_none")] | ||||||
|     pub user: Option<String>, |     pub user: Option<String>, | ||||||
|  |     #[serde(default, skip_serializing_if = "Option::is_none")] | ||||||
|  |     pub colour: Option<String>, | ||||||
|     #[serde(flatten)] |     #[serde(flatten)] | ||||||
|     pub data: WatchEventData, |     pub data: WatchEventData, | ||||||
|     #[serde(default)] |     #[serde(default)] | ||||||
|  | @ -22,9 +24,10 @@ pub struct WatchEvent { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl WatchEvent { | impl WatchEvent { | ||||||
|     pub fn new(user: String, data: WatchEventData) -> Self { |     pub fn new(user: String, colour: String, data: WatchEventData) -> Self { | ||||||
|         WatchEvent { |         WatchEvent { | ||||||
|             user: Some(user), |             user: Some(user), | ||||||
|  |             colour: Some(colour), | ||||||
|             data, |             data, | ||||||
|             reflected: false, |             reflected: false, | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -26,6 +26,7 @@ struct StartSessionBody { | ||||||
| #[derive(Deserialize)] | #[derive(Deserialize)] | ||||||
| struct SubscribeQuery { | struct SubscribeQuery { | ||||||
|     nickname: String, |     nickname: String, | ||||||
|  |     colour: String, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[tokio::main] | #[tokio::main] | ||||||
|  | @ -85,7 +86,7 @@ async fn main() { | ||||||
|         .map( |         .map( | ||||||
|             |requested_session, query: SubscribeQuery, ws: warb::ws::Ws| match requested_session { |             |requested_session, query: SubscribeQuery, ws: warb::ws::Ws| match requested_session { | ||||||
|                 RequestedSession::Session(uuid, _) => ws |                 RequestedSession::Session(uuid, _) => ws | ||||||
|                     .on_upgrade(move |ws| ws_subscribe(uuid, query.nickname, ws)) |                     .on_upgrade(move |ws| ws_subscribe(uuid, query.nickname, query.colour, ws)) | ||||||
|                     .into_response(), |                     .into_response(), | ||||||
|                 RequestedSession::Error(error_response) => error_response.into_response(), |                 RequestedSession::Error(error_response) => error_response.into_response(), | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  | @ -28,9 +28,10 @@ pub struct ConnectedViewer { | ||||||
|     pub viewer_id: usize, |     pub viewer_id: usize, | ||||||
|     pub tx: UnboundedSender<WatchEvent>, |     pub tx: UnboundedSender<WatchEvent>, | ||||||
|     pub nickname: Option<String>, |     pub nickname: Option<String>, | ||||||
|  |     pub colour: Option<String>, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, ws: WebSocket) { | pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, colour: String, ws: WebSocket) { | ||||||
|     let viewer_id = NEXT_VIEWER_ID.fetch_add(1, Ordering::Relaxed); |     let viewer_id = NEXT_VIEWER_ID.fetch_add(1, Ordering::Relaxed); | ||||||
|     let (mut viewer_ws_tx, mut viewer_ws_rx) = ws.split(); |     let (mut viewer_ws_tx, mut viewer_ws_rx) = ws.split(); | ||||||
| 
 | 
 | ||||||
|  | @ -48,6 +49,11 @@ pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, ws: WebSocket) { | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     let mut colour = colour; | ||||||
|  |     if !colour.len() == 6 || !colour.chars().all(|x| x.is_ascii_hexdigit()) { | ||||||
|  |         colour = String::from("7ed0ff"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     CONNECTED_VIEWERS.write().await.insert( |     CONNECTED_VIEWERS.write().await.insert( | ||||||
|         viewer_id, |         viewer_id, | ||||||
|         ConnectedViewer { |         ConnectedViewer { | ||||||
|  | @ -55,13 +61,14 @@ pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, ws: WebSocket) { | ||||||
|             session: session_uuid, |             session: session_uuid, | ||||||
|             tx, |             tx, | ||||||
|             nickname: Some(nickname.clone()), |             nickname: Some(nickname.clone()), | ||||||
|  |             colour: Some(colour.clone()), | ||||||
|         }, |         }, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     ws_publish( |     ws_publish( | ||||||
|         session_uuid, |         session_uuid, | ||||||
|         None, |         None, | ||||||
|         WatchEvent::new(nickname.clone(), WatchEventData::UserJoin), |         WatchEvent::new(nickname.clone(), colour.clone(), WatchEventData::UserJoin), | ||||||
|     ) |     ) | ||||||
|     .await; |     .await; | ||||||
| 
 | 
 | ||||||
|  | @ -84,7 +91,7 @@ pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, ws: WebSocket) { | ||||||
|         ws_publish( |         ws_publish( | ||||||
|             session_uuid, |             session_uuid, | ||||||
|             Some(viewer_id), |             Some(viewer_id), | ||||||
|             WatchEvent::new(nickname.clone(), event), |             WatchEvent::new(nickname.clone(), colour.clone(), event), | ||||||
|         ) |         ) | ||||||
|         .await; |         .await; | ||||||
|     } |     } | ||||||
|  | @ -92,7 +99,7 @@ pub async fn ws_subscribe(session_uuid: Uuid, nickname: String, ws: WebSocket) { | ||||||
|     ws_publish( |     ws_publish( | ||||||
|         session_uuid, |         session_uuid, | ||||||
|         None, |         None, | ||||||
|         WatchEvent::new(nickname.clone(), WatchEventData::UserLeave), |         WatchEvent::new(nickname.clone(), colour.clone(), WatchEventData::UserLeave), | ||||||
|     ) |     ) | ||||||
|     .await; |     .await; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue