forked from lavender/watch-party
		
	ui and emoji changes
This commit is contained in:
		
							parent
							
								
									362c990d22
								
							
						
					
					
						commit
						e6699e05dd
					
				
					 7 changed files with 249 additions and 119 deletions
				
			
		|  | @ -43,7 +43,8 @@ | |||
|         <button id="join-session-button">Join</button> | ||||
| 
 | ||||
|         <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> | ||||
|       </form> | ||||
|     </div> | ||||
|  | @ -53,8 +54,13 @@ | |||
|       <div id="viewer-list"></div> | ||||
|       <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 --> | ||||
|         <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 --> | ||||
|       </form> | ||||
|     </div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,23 @@ | |||
| import { setDebounce, setVideoTime, setPlaying } from "./watch-session.mjs?v=9"; | ||||
| import { emojify, emojis } from "./emojis.mjs?v=9"; | ||||
| 
 | ||||
| function insertAtCursor(input, textToInsert) { | ||||
|   const isSuccess = document.execCommand("insertText", false, textToInsert); | ||||
| 
 | ||||
|   // Firefox (non-standard method)
 | ||||
|   if (!isSuccess && typeof input.setRangeText === "function") { | ||||
|     const start = input.selectionStart; | ||||
|     input.setRangeText(textToInsert); | ||||
|     // update cursor to be at the end of insertion
 | ||||
|     input.selectionStart = input.selectionEnd = start + textToInsert.length; | ||||
| 
 | ||||
|     // Notify any possible listeners of the change
 | ||||
|     const e = document.createEvent("UIEvent"); | ||||
|     e.initEvent("input", true, false); | ||||
|     input.dispatchEvent(e); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const setupChatboxEvents = (socket) => { | ||||
|   // clear events by just reconstructing the form
 | ||||
|   const oldChatForm = document.querySelector("#chatbox-send"); | ||||
|  | @ -11,25 +28,77 @@ const setupChatboxEvents = (socket) => { | |||
| 
 | ||||
|   let autocompleting = false; | ||||
| 
 | ||||
|   const replaceMessage = message => () => { | ||||
|   const replaceMessage = (message) => () => { | ||||
|     messageInput.value = message; | ||||
| 	autocomplete(); | ||||
|   } | ||||
|   async function autocomplete(){ | ||||
|     if(autocompleting) return; | ||||
| 	emojiAutocomplete.textContent = ""; | ||||
|     autocomplete(); | ||||
|   }; | ||||
|   async function autocomplete() { | ||||
|     if (autocompleting) return; | ||||
|     emojiAutocomplete.textContent = ""; | ||||
|     autocompleting = true; | ||||
|     let text = messageInput.value.slice(0, messageInput.selectionStart); | ||||
| 	const match = text.match(/(:[^\s:]+)?:[^\s:]*$/); | ||||
| 	if(!match || match[1]) return autocompleting = false; // We don't need to autocomplete.
 | ||||
| 	const prefix = text.slice(0, match.index); | ||||
| 	const search = text.slice(match.index + 1); | ||||
| 	const suffix = messageInput.value.slice(messageInput.selectionStart); | ||||
| 	emojiAutocomplete.append(...(await emojis).filter(e => e.toLowerCase().startsWith(search.toLowerCase())).map(e => Object.assign(document.createElement("button"), {className: "emoji-option", textContent: e, onclick: replaceMessage(prefix + ":" + e + ":" + suffix)}))) | ||||
| 	autocompleting = false; | ||||
|     const match = text.match(/(:[^\s:]+)?:([^\s:]*)$/); | ||||
|     if (!match || match[1]) return (autocompleting = false); // We don't need to autocomplete.
 | ||||
|     const prefix = text.slice(0, match.index); | ||||
|     const search = text.slice(match.index + 1); | ||||
|     const suffix = messageInput.value.slice(messageInput.selectionStart); | ||||
|     const select = (button) => { | ||||
|       const selected = document.querySelector(".emoji-option.selected"); | ||||
|       if (selected) selected.classList.remove("selected"); | ||||
|       button.classList.add("selected"); | ||||
|     }; | ||||
|     emojiAutocomplete.append( | ||||
|       ...(await emojis) | ||||
|         .filter((e) => e.toLowerCase().startsWith(search.toLowerCase())) | ||||
|         .map((name, i) => { | ||||
|           const button = Object.assign(document.createElement("button"), { | ||||
|             className: "emoji-option" + (i === 0 ? " selected" : ""), | ||||
|             onmousedown: (e) => e.preventDefault(), | ||||
|             onmouseup: () => | ||||
|               insertAtCursor(button, name.slice(match[2].length) + ": "), | ||||
|             onmouseover: () => select(button), | ||||
|             onfocus: () => select(button), | ||||
|           }); | ||||
|           button.append( | ||||
|             Object.assign(new Image(), { | ||||
|               loading: "lazy", | ||||
|               src: `/emojis/${name}.png`, | ||||
|               className: "emoji", | ||||
|             }), | ||||
|             Object.assign(document.createElement("span"), { textContent: name }) | ||||
|           ); | ||||
|           return button; | ||||
|         }) | ||||
|     ); | ||||
|     if (emojiAutocomplete.children[0]) | ||||
|       emojiAutocomplete.children[0].scrollIntoView(); | ||||
|     autocompleting = false; | ||||
|   } | ||||
|   messageInput.addEventListener("input", autocomplete) | ||||
|   messageInput.addEventListener("input", autocomplete); | ||||
|   messageInput.addEventListener("selectionchange", autocomplete); | ||||
|   messageInput.addEventListener("keydown", (event) => { | ||||
|     if (event.key == "ArrowUp" || event.key == "ArrowDown") { | ||||
|       let selected = document.querySelector(".emoji-option.selected"); | ||||
|       if (!selected) return; | ||||
|       event.preventDefault(); | ||||
|       selected.classList.remove("selected"); | ||||
|       selected = | ||||
|         event.key == "ArrowDown" | ||||
|           ? selected.nextElementSibling || selected.parentElement.children[0] | ||||
|           : selected.previousElementSibling || | ||||
|             selected.parentElement.children[ | ||||
|               selected.parentElement.children.length - 1 | ||||
|             ]; | ||||
|       selected.classList.add("selected"); | ||||
|       selected.scrollIntoView({ scrollMode: "if-needed", block: "nearest" }); | ||||
|     } | ||||
|     if (event.key == "Tab") { | ||||
|       let selected = document.querySelector(".emoji-option.selected"); | ||||
|       if (!selected) return; | ||||
|       event.preventDefault(); | ||||
|       selected.onmouseup(); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   chatForm.addEventListener("submit", async (e) => { | ||||
|     e.preventDefault(); | ||||
|  | @ -81,7 +150,12 @@ const setupChatboxEvents = (socket) => { | |||
|               " <code>/ping [message]</code> - ping all viewers<br>" + | ||||
|               " <code>/sync</code> - resyncs you with other viewers"; | ||||
| 
 | ||||
|             printChatMessage("command-message", "/help", "b57fdc", helpMessageContent); | ||||
|             printChatMessage( | ||||
|               "command-message", | ||||
|               "/help", | ||||
|               "b57fdc", | ||||
|               helpMessageContent | ||||
|             ); | ||||
|             handled = true; | ||||
|             break; | ||||
|           default: | ||||
|  | @ -114,8 +188,7 @@ export const setupChat = async (socket) => { | |||
|   window.addEventListener("keydown", (event) => { | ||||
|     try { | ||||
|       const isSelectionEmpty = window.getSelection().toString().length === 0; | ||||
|       if (event.code.match(/Key\w/) && isSelectionEmpty) | ||||
|         messageInput.focus(); | ||||
|       if (event.code.match(/Key\w/) && isSelectionEmpty) messageInput.focus(); | ||||
|     } catch (_err) {} | ||||
|   }); | ||||
| }; | ||||
|  |  | |||
|  | @ -2,11 +2,20 @@ export function emojify(text) { | |||
|   let last = 0; | ||||
|   let nodes = []; | ||||
|   text.replace(/:([^\s:]+):/g, (match, name, index) => { | ||||
|     if(last <= index) nodes.push(document.createTextNode(text.slice(last, index))) | ||||
|     nodes.push(Object.assign(new Image(), {src: `/emojis/${name}.png`, className: "emoji", alt: name})) | ||||
|     last = index + match.length | ||||
|   }) | ||||
|   if(last < text.length) nodes.push(document.createTextNode(text.slice(last))) | ||||
|   return nodes | ||||
|     if (last <= index) | ||||
|       nodes.push(document.createTextNode(text.slice(last, index))); | ||||
|     nodes.push( | ||||
|       Object.assign(new Image(), { | ||||
|         src: `/emojis/${name}.png`, | ||||
|         className: "emoji", | ||||
|         alt: name, | ||||
|       }) | ||||
|     ); | ||||
|     last = index + match.length; | ||||
|   }); | ||||
|   if (last < text.length) nodes.push(document.createTextNode(text.slice(last))); | ||||
|   return nodes; | ||||
| } | ||||
| export const emojis = Promise.resolve(["blobcat", "blobhaj"]) | ||||
| export const emojis = fetch("/emojis") | ||||
|   .then((e) => e.json()) | ||||
|   .then((e) => e.map((e) => e.slice(0, -4))); | ||||
|  |  | |||
|  | @ -80,10 +80,14 @@ export const setupJoinSessionForm = () => { | |||
|     saveNickname(nickname); | ||||
|     saveColour(colour); | ||||
|     try { | ||||
|       await joinSession(nickname.value, sessionId.value, colour.value.replace(/^#/, "")); | ||||
|       await joinSession( | ||||
|         nickname.value, | ||||
|         sessionId.value, | ||||
|         colour.value.replace(/^#/, "") | ||||
|       ); | ||||
|     } catch (e) { | ||||
|       alert(e.message) | ||||
| 	  button.disabled = false; | ||||
|       alert(e.message); | ||||
|       button.disabled = false; | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
|  |  | |||
|  | @ -1,59 +1,65 @@ | |||
| export default class ReconnectingWebSocket { | ||||
|   constructor(url){ | ||||
|     if(url instanceof URL) { | ||||
|   constructor(url) { | ||||
|     if (url instanceof URL) { | ||||
|       this.url = url; | ||||
|     } else { | ||||
|       this.url = new URL(url) | ||||
|       this.url = new URL(url); | ||||
|     } | ||||
|     this.connected = false; | ||||
|     this._eventTarget = new EventTarget(); | ||||
|     this._backoff = 250; // milliseconds, doubled before use
 | ||||
| 	this._lastConnect = 0; | ||||
|     this._lastConnect = 0; | ||||
|     this._socket = null; | ||||
|     this._unsent = []; | ||||
|     this._connect(true); | ||||
|   } | ||||
|   _connect(first) { | ||||
|     if(this._socket) try { this._socket.close() } catch (e) {}; | ||||
|     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() | ||||
|       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._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()) | ||||
|     }) | ||||
|       while (this._unsent.length > 0) this._socket.send(this._unsent.shift()); | ||||
|     }); | ||||
|   } | ||||
|   _reconnect(){ | ||||
|     if(this._reconnecting) return; | ||||
|   _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))) | ||||
|     }, Math.floor(this._backoff + Math.random() * this._backoff * 0.25 - this._backoff * 0.125)); | ||||
|   } | ||||
|   send(message) { | ||||
|     if(this.connected) { | ||||
|     if (this.connected) { | ||||
|       this._socket.send(message); | ||||
|     } else { | ||||
|       this._unsent.push(message); | ||||
|     } | ||||
|   } | ||||
|   addEventListener(...a) { | ||||
|     return this._eventTarget.addEventListener(...a) | ||||
|     return this._eventTarget.addEventListener(...a); | ||||
|   } | ||||
|   removeEventListener(...a) { | ||||
|     return this._eventTarget.removeEventListener(...a) | ||||
|     return this._eventTarget.removeEventListener(...a); | ||||
|   } | ||||
| } | ||||
|  | @ -1,6 +1,6 @@ | |||
| import { setupVideo } from "./video.mjs?v=9"; | ||||
| import { setupChat, logEventToChat, updateViewerList } from "./chat.mjs?v=9"; | ||||
| import ReconnectingWebSocket from "./reconnecting-web-socket.mjs" | ||||
| import ReconnectingWebSocket from "./reconnecting-web-socket.mjs"; | ||||
| 
 | ||||
| /** | ||||
|  * @param {string} sessionId | ||||
|  | @ -169,7 +169,9 @@ const setupOutgoingEvents = (video, socket) => { | |||
|  */ | ||||
| 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."); | ||||
|   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 { | ||||
|  | @ -178,19 +180,20 @@ export const joinSession = async (nickname, sessionId, colour) => { | |||
|     console.error(e); | ||||
|     throw genericConnectionError; | ||||
|   } | ||||
|   if(!response.ok) { | ||||
|   if (!response.ok) { | ||||
|     let error; | ||||
|     try { | ||||
|       ({ error } = await response.json()); | ||||
|       if(!error) throw new Error(); | ||||
|       if (!error) throw new Error(); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
|       throw genericConnectionError; | ||||
|     } | ||||
|     throw new Error(error) | ||||
|     throw new Error(error); | ||||
|   } | ||||
|   try { | ||||
|     ({ video_url, subtitle_tracks, current_time_ms, is_playing } = await response.json()); | ||||
|     ({ video_url, subtitle_tracks, current_time_ms, is_playing } = | ||||
|       await response.json()); | ||||
|   } catch (e) { | ||||
|     console.error(e); | ||||
|     throw genericConnectionError; | ||||
|  | @ -216,10 +219,10 @@ export const joinSession = async (nickname, sessionId, colour) => { | |||
|     setupIncomingEvents(video, socket); | ||||
|     setupChat(socket); | ||||
|   }); | ||||
|   socket.addEventListener("reconnecting", e => { | ||||
|   socket.addEventListener("reconnecting", (e) => { | ||||
|     console.log("Reconnecting..."); | ||||
|   }); | ||||
|   socket.addEventListener("reconnected", e => { | ||||
|   socket.addEventListener("reconnected", (e) => { | ||||
|     console.log("Reconnected."); | ||||
|   }); | ||||
|   //} catch (e) {
 | ||||
|  |  | |||
|  | @ -12,8 +12,14 @@ | |||
|   --accent: rgb(var(--accent-rgb)); | ||||
|   --fg-transparent: rgba(var(--fg-rgb), 0.125); | ||||
|   --bg-transparent: rgba(var(--bg-rgb), 0.125); | ||||
|   --chat-bg: linear-gradient(var(--fg-transparent), var(--fg-transparent)), linear-gradient(var(--bg), var(--bg)); | ||||
|   --autocomplete-bg: linear-gradient(var(--fg-transparent), var(--fg-transparent)), linear-gradient(var(--fg-transparent), var(--fg-transparent)), linear-gradient(var(--bg), var(--bg)); | ||||
|   --chat-bg: linear-gradient(var(--fg-transparent), var(--fg-transparent)), | ||||
|     linear-gradient(var(--bg), var(--bg)); | ||||
|   --autocomplete-bg: linear-gradient( | ||||
|       var(--fg-transparent), | ||||
|       var(--fg-transparent) | ||||
|     ), | ||||
|     linear-gradient(var(--fg-transparent), var(--fg-transparent)), | ||||
|     linear-gradient(var(--bg), var(--bg)); | ||||
| } | ||||
| 
 | ||||
| html { | ||||
|  | @ -158,13 +164,19 @@ button.small-button { | |||
|   overflow-wrap: break-word; | ||||
| } | ||||
| 
 | ||||
| .chat-message > strong, #viewer-list strong { | ||||
| .chat-message > strong, | ||||
| #viewer-list strong { | ||||
|   color: var(--user-color, var(--default-user-color)); | ||||
| } | ||||
| 
 | ||||
| @supports (-webkit-background-clip: text) { | ||||
|   .chat-message > strong, #viewer-list strong { | ||||
|     background: linear-gradient(var(--fg-transparent), var(--fg-transparent)), linear-gradient(var(--user-color, var(--default-user-color)), var(--user-color, var(--default-user-color))); | ||||
|   .chat-message > strong, | ||||
|   #viewer-list strong { | ||||
|     background: linear-gradient(var(--fg-transparent), var(--fg-transparent)), | ||||
|       linear-gradient( | ||||
|         var(--user-color, var(--default-user-color)), | ||||
|         var(--user-color, var(--default-user-color)) | ||||
|       ); | ||||
|     -webkit-background-clip: text; | ||||
|     color: transparent !important; | ||||
|   } | ||||
|  | @ -183,7 +195,7 @@ button.small-button { | |||
|   font-size: 0.85em; | ||||
| } | ||||
| 
 | ||||
| .chat-message.command-message{ | ||||
| .chat-message.command-message { | ||||
|   font-size: 0.85em; | ||||
| } | ||||
| 
 | ||||
|  | @ -240,9 +252,11 @@ button.small-button { | |||
|   position: absolute; | ||||
|   bottom: 3.25rem; | ||||
|   background-image: var(--autocomplete-bg); | ||||
|   padding: 0.25rem; | ||||
|   border-radius: 6px; | ||||
|   width: calc(100% - 4.5rem); | ||||
|   max-height: 8.5rem; | ||||
|   overflow-y: auto; | ||||
|   clip-path: inset(0 0 0 0 round 8px); | ||||
| } | ||||
| 
 | ||||
| #emoji-autocomplete:empty { | ||||
|  | @ -253,17 +267,29 @@ button.small-button { | |||
|   background: transparent; | ||||
|   font-size: 0.75rem; | ||||
|   text-align: left; | ||||
|   margin: 0 0 0.25rem; | ||||
|   margin: 0 0.25rem; | ||||
|   border-radius: 4px; | ||||
|   width: 100%; | ||||
|   width: calc(100% - 0.5rem); | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   padding: 0.25rem 0.5rem; | ||||
|   scroll-margin: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .emoji-option:hover, .emoji-option:focus { | ||||
|   background: var(--fg-transparent); | ||||
| .emoji-option:first-child { | ||||
|   margin-top: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .emoji-option:last-child { | ||||
|   margin: 0; | ||||
|   margin-bottom: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .emoji-option .emoji { | ||||
|   width: 1.25rem; | ||||
|   height: 1.25rem; | ||||
|   margin: 0 0.5rem 0 0; | ||||
| } | ||||
| 
 | ||||
| .emoji-option.selected { | ||||
|   background: var(--fg-transparent); | ||||
| } | ||||
| 
 | ||||
| #join-session-colour { | ||||
|  | @ -280,7 +306,10 @@ button.small-button { | |||
|   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. */ | ||||
| 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; | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue