forked from lavender/watch-party
		
	Compare commits
	
		
			7 commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fbedc0ba23 | |||
| 437004fb9b | |||
| fb136a1899 | |||
| 1bf13d9776 | |||
| a514241bee | |||
| cdec8b72a9 | |||
| 92860f1ae6 | 
					 17 changed files with 553 additions and 810 deletions
				
			
		|  | @ -3,7 +3,7 @@ | ||||||
|   <head> |   <head> | ||||||
|     <meta charset="utf-8" /> |     <meta charset="utf-8" /> | ||||||
|     <title>watch party :D</title> |     <title>watch party :D</title> | ||||||
|     <link rel="stylesheet" href="/styles.css?v=bfdcf2" /> |     <link rel="stylesheet" href="/styles.css?v=048af96" /> | ||||||
|   </head> |   </head> | ||||||
| 
 | 
 | ||||||
|   <body> |   <body> | ||||||
|  | @ -47,6 +47,6 @@ | ||||||
|       </form> |       </form> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <script type="module" src="/create.mjs?v=bfdcf2"></script> |     <script type="module" src="/create.mjs?v=048af96"></script> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { setupCreateSessionForm } from "./lib/create-session.mjs?v=bfdcf2"; | import { setupCreateSessionForm } from "./lib/create-session.mjs?v=048af96"; | ||||||
| 
 | 
 | ||||||
| const main = () => { | const main = () => { | ||||||
|   setupCreateSessionForm(); |   setupCreateSessionForm(); | ||||||
|  |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								frontend/emojis/blobcat.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/emojis/blobcat.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.7 KiB | 
|  | @ -3,7 +3,7 @@ | ||||||
|   <head> |   <head> | ||||||
|     <meta charset="utf-8" /> |     <meta charset="utf-8" /> | ||||||
|     <title>watch party :D</title> |     <title>watch party :D</title> | ||||||
|     <link rel="stylesheet" href="/styles.css?v=bfdcf2" /> |     <link rel="stylesheet" href="/styles.css?v=048af96" /> | ||||||
|   </head> |   </head> | ||||||
| 
 | 
 | ||||||
|   <body> |   <body> | ||||||
|  | @ -30,10 +30,8 @@ | ||||||
|           required |           required | ||||||
|         /> |         /> | ||||||
| 
 | 
 | ||||||
|         <label id="join-session-colour-label" for="join-session-colour"> |         <label for="join-session-colour">Colour:</label> | ||||||
|           Personal Colour: |         <input type="color" id="join-session-colour" value="#7ed0ff" required /> | ||||||
|         </label> |  | ||||||
|         <input type="color" id="join-session-colour" value="#ffffff" required /> |  | ||||||
| 
 | 
 | ||||||
|         <label for="join-session-id">Session ID:</label> |         <label for="join-session-id">Session ID:</label> | ||||||
|         <input |         <input | ||||||
|  | @ -66,19 +64,6 @@ | ||||||
|       </form> |       </form> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <script type="module" src="/main.mjs?v=bfdcf2"></script> |     <script type="module" src="/main.mjs?v=048af96"></script> | ||||||
|     <script> |  | ||||||
|       const updateColourLabel = () => { |  | ||||||
|         const colour = document.querySelector("#join-session-colour").value; |  | ||||||
|         document.querySelector( |  | ||||||
|           "#join-session-colour-label" |  | ||||||
|         ).textContent = `Personal Colour: ${colour}`; |  | ||||||
|       }; |  | ||||||
| 
 |  | ||||||
|       document |  | ||||||
|         .querySelector("#join-session-colour") |  | ||||||
|         .addEventListener("input", updateColourLabel); |  | ||||||
|       updateColourLabel(); |  | ||||||
|     </script> |  | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
|  | @ -2,12 +2,11 @@ import { | ||||||
|   setDebounce, |   setDebounce, | ||||||
|   setVideoTime, |   setVideoTime, | ||||||
|   setPlaying, |   setPlaying, | ||||||
| } from "./watch-session.mjs?v=bfdcf2"; | } from "./watch-session.mjs?v=048af96"; | ||||||
| import { emojify, findEmojis } from "./emojis.mjs?v=bfdcf2"; | import { emojify, findEmojis } from "./emojis.mjs?v=048af96"; | ||||||
| import { linkify } from "./links.mjs?v=bfdcf2"; | 
 | ||||||
| import { joinSession } from "./watch-session.mjs?v=bfdcf2"; | let nickname = ""; | ||||||
| import { pling } from "./pling.mjs?v=bfdcf2"; | let kisses = {}; | ||||||
| import { state } from "./state.mjs"; |  | ||||||
| 
 | 
 | ||||||
| function setCaretPosition(elem, caretPos) { | function setCaretPosition(elem, caretPos) { | ||||||
|   if (elem.createTextRange) { |   if (elem.createTextRange) { | ||||||
|  | @ -39,12 +38,11 @@ const setupChatboxEvents = (socket) => { | ||||||
|   }; |   }; | ||||||
|   async function autocomplete(fromListTimeout) { |   async function autocomplete(fromListTimeout) { | ||||||
|     if (autocompleting) return; |     if (autocompleting) return; | ||||||
|     try { |  | ||||||
|     clearInterval(showListTimer); |     clearInterval(showListTimer); | ||||||
|     emojiAutocomplete.textContent = ""; |     emojiAutocomplete.textContent = ""; | ||||||
|     autocompleting = true; |     autocompleting = true; | ||||||
|     let text = messageInput.value.slice(0, messageInput.selectionStart); |     let text = messageInput.value.slice(0, messageInput.selectionStart); | ||||||
|       const match = text.match(/(:[^\s:]+)?:([^\s:]{2,})$/); |     const match = text.match(/(:[^\s:]+)?:([^\s:]*)$/); | ||||||
|     if (!match || match[1]) return (autocompleting = false); // We don't need to autocomplete.
 |     if (!match || match[1]) return (autocompleting = false); // We don't need to autocomplete.
 | ||||||
|     const prefix = text.slice(0, match.index); |     const prefix = text.slice(0, match.index); | ||||||
|     const search = text.slice(match.index + 1); |     const search = text.slice(match.index + 1); | ||||||
|  | @ -111,9 +109,6 @@ const setupChatboxEvents = (socket) => { | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     autocompleting = false; |     autocompleting = false; | ||||||
|     } catch (e) { |  | ||||||
|       autocompleting = false; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|   messageInput.addEventListener("input", () => autocomplete()); |   messageInput.addEventListener("input", () => autocomplete()); | ||||||
|   messageInput.addEventListener("selectionchange", () => autocomplete()); |   messageInput.addEventListener("selectionchange", () => autocomplete()); | ||||||
|  | @ -133,7 +128,7 @@ const setupChatboxEvents = (socket) => { | ||||||
|       selected.classList.add("selected"); |       selected.classList.add("selected"); | ||||||
|       selected.scrollIntoView({ scrollMode: "if-needed", block: "nearest" }); |       selected.scrollIntoView({ scrollMode: "if-needed", block: "nearest" }); | ||||||
|     } |     } | ||||||
|     if (event.key == "Tab" || event.key == "Enter") { |     if (event.key == "Tab") { | ||||||
|       let selected = document.querySelector(".emoji-option.selected"); |       let selected = document.querySelector(".emoji-option.selected"); | ||||||
|       if (!selected) return; |       if (!selected) return; | ||||||
|       event.preventDefault(); |       event.preventDefault(); | ||||||
|  | @ -192,10 +187,23 @@ const setupChatboxEvents = (socket) => { | ||||||
|             ); |             ); | ||||||
|             handled = true; |             handled = true; | ||||||
|             break; |             break; | ||||||
|           case "/join": |           case "/votekiss": | ||||||
|             state().sessionId = args; | 		    if(kisses[args]&&kisses[args][nickname]) | ||||||
|             joinSession(); |               printChatMessage( | ||||||
|             handled = true; |                 "vote-kiss", | ||||||
|  |                 "/votekiss", | ||||||
|  |                 "b57fdc", | ||||||
|  |                 document.createTextNode("you already voted to kiss " + args) | ||||||
|  |               ); | ||||||
|  | 			else | ||||||
|  |               printChatMessage( | ||||||
|  |                 "vote-kiss", | ||||||
|  |                 "/votekiss", | ||||||
|  |                 "b57fdc", | ||||||
|  |                 document.createTextNode("you voted to kiss " + args) | ||||||
|  |               ); | ||||||
|  |             handled = false; | ||||||
|  |             // we also handle this on receive
 | ||||||
|             break; |             break; | ||||||
|           case "/help": |           case "/help": | ||||||
|             const helpMessageContent = document.createElement("span"); |             const helpMessageContent = document.createElement("span"); | ||||||
|  | @ -205,7 +213,7 @@ const setupChatboxEvents = (socket) => { | ||||||
|               " <code>/ping [message]</code> - ping all viewers<br>" + |               " <code>/ping [message]</code> - ping all viewers<br>" + | ||||||
|               " <code>/sync</code> - resyncs you with other viewers<br>" + |               " <code>/sync</code> - resyncs you with other viewers<br>" + | ||||||
|               " <code>/shrug</code> - appends ¯\\_(ツ)_/¯ to your message<br>" + |               " <code>/shrug</code> - appends ¯\\_(ツ)_/¯ to your message<br>" + | ||||||
|               " <code>/join [session id]</code> - joins another session"; |               " <code>/votekiss</code> - like votekick but gay"; | ||||||
| 
 | 
 | ||||||
|             printChatMessage( |             printChatMessage( | ||||||
|               "command-message", |               "command-message", | ||||||
|  | @ -238,9 +246,17 @@ const setupChatboxEvents = (socket) => { | ||||||
| /** | /** | ||||||
|  * @param {WebSocket} socket |  * @param {WebSocket} socket | ||||||
|  */ |  */ | ||||||
| export const setupChat = async (socket) => { | export const setupChat = async (socket, _nickname) => { | ||||||
|  |   nickname = _nickname; // We need this for commands
 | ||||||
|   document.querySelector("#chatbox-container").style["display"] = "flex"; |   document.querySelector("#chatbox-container").style["display"] = "flex"; | ||||||
|   setupChatboxEvents(socket); |   setupChatboxEvents(socket); | ||||||
|  | 
 | ||||||
|  |   window.addEventListener("keydown", (event) => { | ||||||
|  |     try { | ||||||
|  |       const isSelectionEmpty = window.getSelection().toString().length === 0; | ||||||
|  |       if (event.code.match(/Key\w/) && isSelectionEmpty) messageInput.focus(); | ||||||
|  |     } catch (_err) {} | ||||||
|  |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const addToChat = (node) => { | const addToChat = (node) => { | ||||||
|  | @ -303,7 +319,7 @@ const matpad = (n) => { | ||||||
|  * @param {string?} user |  * @param {string?} user | ||||||
|  * @param {Node?} content |  * @param {Node?} content | ||||||
|  */ |  */ | ||||||
| export const printChatMessage = (eventType, user, colour, 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); | ||||||
|  | @ -334,6 +350,31 @@ const formatTime = (ms) => { | ||||||
|   }:${seconds < 10 ? "0" + seconds : seconds}`;
 |   }:${seconds < 10 ? "0" + seconds : seconds}`;
 | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | function handleClientCommand(content, user) { | ||||||
|  |   let handled = false; | ||||||
|  |   if (content.startsWith("/")) { | ||||||
|  |     const command = content.toLowerCase().match(/^\/\S+/)[0]; | ||||||
|  |     const args = content.slice(command.length).trim(); | ||||||
|  |     switch (command) { | ||||||
|  |       case "/votekiss": | ||||||
|  |         kisses[args] = kisses[args] || {}; | ||||||
|  |         kisses[args][user] = true; | ||||||
|  |         if (Object.keys(kisses[args]).length >= 3) { | ||||||
|  |           printChatMessage( | ||||||
|  |             "user-kissed", | ||||||
|  |             args, | ||||||
|  |             "ff6094", | ||||||
|  |             document.createTextNode("was kissed 💋") | ||||||
|  |           ); | ||||||
|  |           kisses[args] = {}; | ||||||
|  |         } | ||||||
|  |         handled = true; | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return handled; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export const logEventToChat = async (event) => { | export const logEventToChat = async (event) => { | ||||||
|   if (checkDebounce(event)) { |   if (checkDebounce(event)) { | ||||||
|     return; |     return; | ||||||
|  | @ -356,12 +397,15 @@ export const logEventToChat = async (event) => { | ||||||
|         event.colour, |         event.colour, | ||||||
|         document.createTextNode("left") |         document.createTextNode("left") | ||||||
|       ); |       ); | ||||||
|  |       for (let kissed in kisses) delete kisses[kissed][event.user]; | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case "ChatMessage": { |     case "ChatMessage": { | ||||||
|       const messageContent = document.createElement("span"); |       const messageContent = document.createElement("span"); | ||||||
|       messageContent.classList.add("message-content"); |       messageContent.classList.add("message-content"); | ||||||
|       messageContent.append(...(await linkify(event.data, emojify))); |       if (handleClientCommand(event.data, event.user)) break; | ||||||
|  |       messageContent.append(...(await emojify(event.data))); | ||||||
|  | 
 | ||||||
|       printChatMessage( |       printChatMessage( | ||||||
|         "chat-message", |         "chat-message", | ||||||
|         event.user, |         event.user, | ||||||
|  | @ -418,30 +462,32 @@ export const logEventToChat = async (event) => { | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       printChatMessage("ping", event.user, event.colour, messageContent); |       printChatMessage("ping", event.user, event.colour, messageContent); | ||||||
|       pling(); |       beep(); | ||||||
|       if ("Notification" in window) { |  | ||||||
|         const title = "watch party :)"; |  | ||||||
|         const options = { |  | ||||||
|           body: event.data |  | ||||||
|             ? `${event.user} pinged saying: ${event.data}` |  | ||||||
|             : `${event.user} pinged`, |  | ||||||
|         }; |  | ||||||
|         if (Notification.permission === "granted") { |  | ||||||
|           new Notification(title, options); |  | ||||||
|         } else if (Notification.permission !== "denied") { |  | ||||||
|           Notification.requestPermission().then(function (permission) { |  | ||||||
|             if (permission === "granted") { |  | ||||||
|               new Notification(title, options); |  | ||||||
|             } |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const updateViewerList = (viewers) => { | const beep = () => { | ||||||
|  |   const context = new AudioContext(); | ||||||
|  | 
 | ||||||
|  |   const gain = context.createGain(); | ||||||
|  |   gain.connect(context.destination); | ||||||
|  |   gain.gain.value = 0.1; | ||||||
|  | 
 | ||||||
|  |   const oscillator = context.createOscillator(); | ||||||
|  |   oscillator.connect(gain); | ||||||
|  |   oscillator.frequency.value = 520; | ||||||
|  |   oscillator.type = "square"; | ||||||
|  | 
 | ||||||
|  |   oscillator.start(context.currentTime); | ||||||
|  |   oscillator.stop(context.currentTime + 0.22); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | let viewers = []; | ||||||
|  | 
 | ||||||
|  | export const updateViewerList = (_viewers) => { | ||||||
|  |   viewers = _viewers; | ||||||
|   const listContainer = document.querySelector("#viewer-list"); |   const listContainer = document.querySelector("#viewer-list"); | ||||||
| 
 | 
 | ||||||
|   // empty out the current list
 |   // empty out the current list
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { createSession } from "./watch-session.mjs?v=bfdcf2"; | import { createSession } from "./watch-session.mjs?v=048af96"; | ||||||
| 
 | 
 | ||||||
| export const setupCreateSessionForm = () => { | export const setupCreateSessionForm = () => { | ||||||
|   const form = document.querySelector("#create-session-form"); |   const form = document.querySelector("#create-session-form"); | ||||||
|  |  | ||||||
|  | @ -5,10 +5,7 @@ export async function emojify(text) { | ||||||
|   text.replace(/:([^\s:]+):/g, (match, name, index) => { |   text.replace(/:([^\s:]+):/g, (match, name, index) => { | ||||||
|     if (last <= index) |     if (last <= index) | ||||||
|       nodes.push(document.createTextNode(text.slice(last, index))); |       nodes.push(document.createTextNode(text.slice(last, index))); | ||||||
|     let emoji; |     let emoji = emojis[name.toLowerCase()[0]].find((e) => e[0] == name); | ||||||
|     try { |  | ||||||
|       emoji = emojis[name.toLowerCase()[0]].find((e) => e[0] == name); |  | ||||||
|     } catch (e) {} |  | ||||||
|     if (!emoji) { |     if (!emoji) { | ||||||
|       nodes.push(document.createTextNode(match)); |       nodes.push(document.createTextNode(match)); | ||||||
|     } else { |     } else { | ||||||
|  | @ -32,15 +29,7 @@ export async function emojify(text) { | ||||||
| const emojis = {}; | const emojis = {}; | ||||||
| 
 | 
 | ||||||
| export const emojisLoaded = Promise.all([ | export const emojisLoaded = Promise.all([ | ||||||
|   fetch("/emojis/unicode.json") |   fetch("/emojis") | ||||||
|     .then((e) => e.json()) |  | ||||||
|     .then((a) => { |  | ||||||
|       for (let e of a) { |  | ||||||
|         emojis[e[0][0]] = emojis[e[0][0]] || []; |  | ||||||
|         emojis[e[0][0]].push([e[0], e[1], null, e[0]]); |  | ||||||
|       } |  | ||||||
|     }), |  | ||||||
|   fetch("/emojos") |  | ||||||
|     .then((e) => e.json()) |     .then((e) => e.json()) | ||||||
|     .then((a) => { |     .then((a) => { | ||||||
|       for (let e of a) { |       for (let e of a) { | ||||||
|  | @ -50,6 +39,14 @@ export const emojisLoaded = Promise.all([ | ||||||
|         emojis[lower[0]].push([name, ":" + name + ":", e.slice(-4), lower]); |         emojis[lower[0]].push([name, ":" + name + ":", e.slice(-4), lower]); | ||||||
|       } |       } | ||||||
|     }), |     }), | ||||||
|  |   fetch("/emojis/unicode.json") | ||||||
|  |     .then((e) => e.json()) | ||||||
|  |     .then((a) => { | ||||||
|  |       for (let e of a) { | ||||||
|  |         emojis[e[0][0]] = emojis[e[0][0]] || []; | ||||||
|  |         emojis[e[0][0]].push([e[0], e[1], null, e[0]]); | ||||||
|  |       } | ||||||
|  |     }), | ||||||
| ]); | ]); | ||||||
| 
 | 
 | ||||||
| export async function findEmojis(search) { | export async function findEmojis(search) { | ||||||
|  | @ -68,5 +65,5 @@ export async function findEmojis(search) { | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   return [...groups[1], ...groups[0]]; |   return [...groups[0], ...groups[1]]; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| import { joinSession } from "./watch-session.mjs?v=bfdcf2"; | import { joinSession } from "./watch-session.mjs?v=048af96"; | ||||||
| import { state } from "./state.mjs"; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @param {HTMLInputElement} field |  * @param {HTMLInputElement} field | ||||||
|  | @ -81,10 +80,11 @@ export const setupJoinSessionForm = () => { | ||||||
|     saveNickname(nickname); |     saveNickname(nickname); | ||||||
|     saveColour(colour); |     saveColour(colour); | ||||||
|     try { |     try { | ||||||
|       state().nickname = nickname.value; |       await joinSession( | ||||||
|       state().sessionId = sessionId.value; |         nickname.value, | ||||||
|       state().colour = colour.value.replace(/^#/, ""); |         sessionId.value, | ||||||
|       await joinSession(); |         colour.value.replace(/^#/, "") | ||||||
|  |       ); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       alert(e.message); |       alert(e.message); | ||||||
|       button.disabled = false; |       button.disabled = false; | ||||||
|  |  | ||||||
|  | @ -1,121 +0,0 @@ | ||||||
| import { joinSession } from "./watch-session.mjs?v=bfdcf2"; |  | ||||||
| import { state } from "./state.mjs"; |  | ||||||
| 
 |  | ||||||
| export async function linkify( |  | ||||||
|   text, |  | ||||||
|   next = async (t) => [document.createTextNode(t)] |  | ||||||
| ) { |  | ||||||
|   let last = 0; |  | ||||||
|   let nodes = []; |  | ||||||
|   let promise = Promise.resolve(); |  | ||||||
|   // matching non-urls isn't a problem, we use the browser's url parser to filter them out
 |  | ||||||
|   text.replace( |  | ||||||
|     /[^:/?#\s]+:\/\/\S+/g, |  | ||||||
|     (match, index) => |  | ||||||
|       (promise = promise.then(async () => { |  | ||||||
|         if (last <= index) nodes.push(...(await next(text.slice(last, index)))); |  | ||||||
|         let url; |  | ||||||
|         try { |  | ||||||
|           url = new URL(match); |  | ||||||
|           if (url.protocol === "javascript:") throw new Error(); |  | ||||||
|         } catch (e) { |  | ||||||
|           url = null; |  | ||||||
|         } |  | ||||||
|         if (!url) { |  | ||||||
|           nodes.push(...(await next(match))); |  | ||||||
|         } else { |  | ||||||
|           let s; |  | ||||||
|           if ( |  | ||||||
|             url.origin == location.origin && |  | ||||||
|             url.pathname == "/" && |  | ||||||
|             url.hash.length > 1 |  | ||||||
|           ) { |  | ||||||
|             nodes.push( |  | ||||||
|               Object.assign(document.createElement("a"), { |  | ||||||
|                 textContent: "Join Session", |  | ||||||
|                 className: "chip join-chip", |  | ||||||
|                 onclick: () => { |  | ||||||
|                   state().sessionId = url.hash.substring(1); |  | ||||||
|                   joinSession(); |  | ||||||
|                 }, |  | ||||||
|               }) |  | ||||||
|             ); |  | ||||||
|           } else if ( |  | ||||||
|             url.hostname == "xiv.st" && |  | ||||||
|             (s = url.pathname.match(/(\d?\d).?(\d\d)/)) |  | ||||||
|           ) { |  | ||||||
|             if (s) { |  | ||||||
|               const date = new Date(); |  | ||||||
|               date.setUTCSeconds(0); |  | ||||||
|               date.setUTCMilliseconds(0); |  | ||||||
|               date.setUTCHours(s[1]), date.setUTCMinutes(s[2]); |  | ||||||
|               nodes.push( |  | ||||||
|                 Object.assign(document.createElement("a"), { |  | ||||||
|                   href: url.href, |  | ||||||
|                   textContent: date.toLocaleString([], { |  | ||||||
|                     hour: "2-digit", |  | ||||||
|                     minute: "2-digit", |  | ||||||
|                   }), |  | ||||||
|                   className: "chip time-chip", |  | ||||||
|                   target: "_blank", |  | ||||||
|                 }) |  | ||||||
|               ); |  | ||||||
|             } |  | ||||||
|           } else { |  | ||||||
|             nodes.push( |  | ||||||
|               Object.assign(document.createElement("a"), { |  | ||||||
|                 href: url.href, |  | ||||||
|                 textContent: url.href, |  | ||||||
|                 target: "_blank", |  | ||||||
|               }) |  | ||||||
|             ); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         last = index + match.length; |  | ||||||
|       })) |  | ||||||
|   ); |  | ||||||
|   await promise; |  | ||||||
|   if (last < text.length) nodes.push(...(await next(text.slice(last)))); |  | ||||||
|   return nodes; |  | ||||||
| } |  | ||||||
| const emojis = {}; |  | ||||||
| 
 |  | ||||||
| export const emojisLoaded = Promise.all([ |  | ||||||
|   fetch("/emojis") |  | ||||||
|     .then((e) => e.json()) |  | ||||||
|     .then((a) => { |  | ||||||
|       for (let e of a) { |  | ||||||
|         const name = e.slice(0, -4), |  | ||||||
|           lower = name.toLowerCase(); |  | ||||||
|         emojis[lower[0]] = emojis[lower[0]] || []; |  | ||||||
|         emojis[lower[0]].push([name, ":" + name + ":", e.slice(-4), lower]); |  | ||||||
|       } |  | ||||||
|     }), |  | ||||||
|   fetch("/emojis/unicode.json") |  | ||||||
|     .then((e) => e.json()) |  | ||||||
|     .then((a) => { |  | ||||||
|       for (let e of a) { |  | ||||||
|         emojis[e[0][0]] = emojis[e[0][0]] || []; |  | ||||||
|         emojis[e[0][0]].push([e[0], e[1], null, e[0]]); |  | ||||||
|       } |  | ||||||
|     }), |  | ||||||
| ]); |  | ||||||
| 
 |  | ||||||
| export async function findEmojis(search) { |  | ||||||
|   await emojisLoaded; |  | ||||||
|   let groups = [[], []]; |  | ||||||
|   if (search.length < 1) { |  | ||||||
|     for (let letter of Object.keys(emojis).sort()) |  | ||||||
|       for (let emoji of emojis[letter]) { |  | ||||||
|         (emoji[1][0] === ":" ? groups[0] : groups[1]).push(emoji); |  | ||||||
|       } |  | ||||||
|   } else { |  | ||||||
|     search = search.toLowerCase(); |  | ||||||
|     for (let emoji of emojis[search[0]]) { |  | ||||||
|       if (search.length == 1 || emoji[3].startsWith(search)) { |  | ||||||
|         (emoji[1][0] === ":" ? groups[0] : groups[1]).push(emoji); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return [...groups[0], ...groups[1]]; |  | ||||||
| } |  | ||||||
|  | @ -1,80 +0,0 @@ | ||||||
| export const pling = () => { |  | ||||||
|   const maxGain = 0.3; |  | ||||||
|   const duration = 0.22; |  | ||||||
|   const fadeDuration = 0.1; |  | ||||||
|   const secondBeepOffset = 0.05; |  | ||||||
|   const thirdBeepOffset = 2 * secondBeepOffset; |  | ||||||
| 
 |  | ||||||
|   const ctx = new AudioContext(); |  | ||||||
| 
 |  | ||||||
|   const firstBeepGain = ctx.createGain(); |  | ||||||
|   firstBeepGain.connect(ctx.destination); |  | ||||||
|   firstBeepGain.gain.setValueAtTime(0.01, ctx.currentTime); |  | ||||||
|   firstBeepGain.gain.exponentialRampToValueAtTime( |  | ||||||
|     maxGain, |  | ||||||
|     ctx.currentTime + fadeDuration |  | ||||||
|   ); |  | ||||||
|   firstBeepGain.gain.setValueAtTime( |  | ||||||
|     maxGain, |  | ||||||
|     ctx.currentTime + (duration - fadeDuration) |  | ||||||
|   ); |  | ||||||
|   firstBeepGain.gain.exponentialRampToValueAtTime( |  | ||||||
|     0.01, |  | ||||||
|     ctx.currentTime + duration |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   const firstBeep = ctx.createOscillator(); |  | ||||||
|   firstBeep.connect(firstBeepGain); |  | ||||||
|   firstBeep.frequency.value = 400; |  | ||||||
|   firstBeep.type = "sine"; |  | ||||||
| 
 |  | ||||||
|   const secondBeepGain = ctx.createGain(); |  | ||||||
|   secondBeepGain.connect(ctx.destination); |  | ||||||
|   secondBeepGain.gain.setValueAtTime(0.01, ctx.currentTime + secondBeepOffset); |  | ||||||
|   secondBeepGain.gain.exponentialRampToValueAtTime( |  | ||||||
|     maxGain, |  | ||||||
|     ctx.currentTime + secondBeepOffset + fadeDuration |  | ||||||
|   ); |  | ||||||
|   secondBeepGain.gain.setValueAtTime( |  | ||||||
|     maxGain, |  | ||||||
|     ctx.currentTime + secondBeepOffset + (duration - fadeDuration) |  | ||||||
|   ); |  | ||||||
|   secondBeepGain.gain.exponentialRampToValueAtTime( |  | ||||||
|     0.01, |  | ||||||
|     ctx.currentTime + secondBeepOffset + duration |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   const secondBeep = ctx.createOscillator(); |  | ||||||
|   secondBeep.connect(secondBeepGain); |  | ||||||
|   secondBeep.frequency.value = 600; |  | ||||||
|   secondBeep.type = "sine"; |  | ||||||
| 
 |  | ||||||
|   const thirdBeepGain = ctx.createGain(); |  | ||||||
|   thirdBeepGain.connect(ctx.destination); |  | ||||||
|   thirdBeepGain.gain.setValueAtTime(0.01, ctx.currentTime + thirdBeepOffset); |  | ||||||
|   thirdBeepGain.gain.exponentialRampToValueAtTime( |  | ||||||
|     maxGain, |  | ||||||
|     ctx.currentTime + thirdBeepOffset + fadeDuration |  | ||||||
|   ); |  | ||||||
|   thirdBeepGain.gain.setValueAtTime( |  | ||||||
|     maxGain, |  | ||||||
|     ctx.currentTime + thirdBeepOffset + (duration - fadeDuration) |  | ||||||
|   ); |  | ||||||
|   thirdBeepGain.gain.exponentialRampToValueAtTime( |  | ||||||
|     0.01, |  | ||||||
|     ctx.currentTime + thirdBeepOffset + duration |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   const thirdBeep = ctx.createOscillator(); |  | ||||||
|   thirdBeep.connect(thirdBeepGain); |  | ||||||
|   thirdBeep.frequency.value = 900; |  | ||||||
|   thirdBeep.type = "sine"; |  | ||||||
| 
 |  | ||||||
|   firstBeep.start(ctx.currentTime); |  | ||||||
|   firstBeep.stop(ctx.currentTime + duration); |  | ||||||
|   secondBeep.start(ctx.currentTime + secondBeepOffset); |  | ||||||
|   secondBeep.stop(ctx.currentTime + (secondBeepOffset + duration)); |  | ||||||
|   thirdBeep.start(ctx.currentTime + thirdBeepOffset); |  | ||||||
|   thirdBeep.stop(ctx.currentTime + (thirdBeepOffset + duration)); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
|  | @ -11,7 +11,6 @@ export default class ReconnectingWebSocket { | ||||||
|     this._lastConnect = 0; |     this._lastConnect = 0; | ||||||
|     this._socket = null; |     this._socket = null; | ||||||
|     this._unsent = []; |     this._unsent = []; | ||||||
|     this._closing = false; |  | ||||||
|     this._connect(true); |     this._connect(true); | ||||||
|   } |   } | ||||||
|   _connect(first) { |   _connect(first) { | ||||||
|  | @ -41,7 +40,6 @@ export default class ReconnectingWebSocket { | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|   _reconnect() { |   _reconnect() { | ||||||
|     if (this._closing) return; |  | ||||||
|     if (this._reconnecting) return; |     if (this._reconnecting) return; | ||||||
|     this._eventTarget.dispatchEvent(new Event("reconnecting")); |     this._eventTarget.dispatchEvent(new Event("reconnecting")); | ||||||
|     this._reconnecting = true; |     this._reconnecting = true; | ||||||
|  | @ -58,10 +56,6 @@ export default class ReconnectingWebSocket { | ||||||
|       this._unsent.push(message); |       this._unsent.push(message); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   close() { |  | ||||||
|     this._closing = true; |  | ||||||
|     this._socket.close(); |  | ||||||
|   } |  | ||||||
|   addEventListener(...a) { |   addEventListener(...a) { | ||||||
|     return this._eventTarget.addEventListener(...a); |     return this._eventTarget.addEventListener(...a); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -1,7 +0,0 @@ | ||||||
| let instance = null; |  | ||||||
| export const state = () => { |  | ||||||
|   if (!instance) { |  | ||||||
|     instance = {}; |  | ||||||
|   } |  | ||||||
|   return instance; |  | ||||||
| }; |  | ||||||
|  | @ -51,12 +51,7 @@ const saveCaptionsTrack = (track) => { | ||||||
|  * @param {{name: string, url: string}[]} subtitles |  * @param {{name: string, url: string}[]} subtitles | ||||||
|  */ |  */ | ||||||
| const createVideoElement = (videoUrl, subtitles) => { | const createVideoElement = (videoUrl, subtitles) => { | ||||||
|   const oldVideo = document.getElementById("video"); |  | ||||||
|   if (oldVideo) { |  | ||||||
|     oldVideo.remove(); |  | ||||||
|   } |  | ||||||
|   const video = document.createElement("video"); |   const video = document.createElement("video"); | ||||||
|   video.id = "video"; |  | ||||||
|   video.controls = true; |   video.controls = true; | ||||||
|   video.autoplay = false; |   video.autoplay = false; | ||||||
|   video.volume = loadVolume(); |   video.volume = loadVolume(); | ||||||
|  | @ -79,7 +74,7 @@ const createVideoElement = (videoUrl, subtitles) => { | ||||||
|     track.src = url; |     track.src = url; | ||||||
|     track.kind = "captions"; |     track.kind = "captions"; | ||||||
| 
 | 
 | ||||||
|     if (id == storedTrack || storedTrack == -1) { |     if (id == storedTrack) { | ||||||
|       track.default = true; |       track.default = true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,23 +1,21 @@ | ||||||
| import { setupVideo } from "./video.mjs?v=bfdcf2"; | import { setupVideo } from "./video.mjs?v=048af96"; | ||||||
| import { | import { | ||||||
|   setupChat, |   setupChat, | ||||||
|   logEventToChat, |   logEventToChat, | ||||||
|   updateViewerList, |   updateViewerList, | ||||||
|   printChatMessage, | } from "./chat.mjs?v=048af96"; | ||||||
| } from "./chat.mjs?v=bfdcf2"; |  | ||||||
| import ReconnectingWebSocket from "./reconnecting-web-socket.mjs"; | import ReconnectingWebSocket from "./reconnecting-web-socket.mjs"; | ||||||
| import { state } from "./state.mjs"; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * @param {string} sessionId |  * @param {string} sessionId | ||||||
|  * @param {string} nickname |  * @param {string} nickname | ||||||
|  * @returns {ReconnectingWebSocket} |  * @returns {ReconnectingWebSocket} | ||||||
|  */ |  */ | ||||||
| const createWebSocket = () => { | const createWebSocket = (sessionId, nickname, colour) => { | ||||||
|   const wsUrl = new URL( |   const wsUrl = new URL( | ||||||
|     `/sess/${state().sessionId}/subscribe` + |     `/sess/${sessionId}/subscribe` + | ||||||
|       `?nickname=${encodeURIComponent(state().nickname)}` + |       `?nickname=${encodeURIComponent(nickname)}` + | ||||||
|       `&colour=${encodeURIComponent(state().colour)}`, |       `&colour=${encodeURIComponent(colour)}`, | ||||||
|     window.location.href |     window.location.href | ||||||
|   ); |   ); | ||||||
|   wsUrl.protocol = "ws" + window.location.protocol.slice(4); |   wsUrl.protocol = "ws" + window.location.protocol.slice(4); | ||||||
|  | @ -168,29 +166,19 @@ const setupOutgoingEvents = (video, socket) => { | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const joinSession = async () => { | /** | ||||||
|   if (state().activeSession) { |  * @param {string} nickname | ||||||
|     if (state().activeSession === state().sessionId) { |  * @param {string} sessionId | ||||||
|       // we are already in this session, dont rejoin
 |  */ | ||||||
|       return; | export const joinSession = async (nickname, sessionId, colour) => { | ||||||
|     } |  | ||||||
|     // we are joining a new session from an existing session
 |  | ||||||
|     const messageContent = document.createElement("span"); |  | ||||||
|     messageContent.appendChild(document.createTextNode("joining new session ")); |  | ||||||
|     messageContent.appendChild(document.createTextNode(state().sessionId)); |  | ||||||
| 
 |  | ||||||
|     printChatMessage("join-session", "watch-party", "#fffff", messageContent); |  | ||||||
|   } |  | ||||||
|   state().activeSession = state().sessionId; |  | ||||||
| 
 |  | ||||||
|   // try { // we are handling errors in the join form.
 |   // try { // we are handling errors in the join form.
 | ||||||
|   const genericConnectionError = new Error( |   const genericConnectionError = new Error( | ||||||
|     "There was an issue getting the session information." |     "There was an issue getting the session information." | ||||||
|   ); |   ); | ||||||
|   window.location.hash = state().sessionId; |   window.location.hash = sessionId; | ||||||
|   let response, video_url, subtitle_tracks, current_time_ms, is_playing; |   let response, video_url, subtitle_tracks, current_time_ms, is_playing; | ||||||
|   try { |   try { | ||||||
|     response = await fetch(`/sess/${state().sessionId}`); |     response = await fetch(`/sess/${sessionId}`); | ||||||
|   } catch (e) { |   } catch (e) { | ||||||
|     console.error(e); |     console.error(e); | ||||||
|     throw genericConnectionError; |     throw genericConnectionError; | ||||||
|  | @ -214,12 +202,7 @@ export const joinSession = async () => { | ||||||
|     throw genericConnectionError; |     throw genericConnectionError; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (state().socket) { |   const socket = createWebSocket(sessionId, nickname, colour); | ||||||
|     state().socket.close(); |  | ||||||
|     state().socket = null; |  | ||||||
|   } |  | ||||||
|   const socket = createWebSocket(); |  | ||||||
|   state().socket = socket; |  | ||||||
|   socket.addEventListener("open", async () => { |   socket.addEventListener("open", async () => { | ||||||
|     const video = await setupVideo( |     const video = await setupVideo( | ||||||
|       video_url, |       video_url, | ||||||
|  | @ -228,24 +211,16 @@ export const joinSession = async () => { | ||||||
|       is_playing |       is_playing | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     // TODO: Allow the user to set this somewhere
 |  | ||||||
|     let defaultAllowControls = false; |  | ||||||
|     try { |  | ||||||
|       defaultAllowControls = localStorage.getItem( |  | ||||||
|         "watch-party-default-allow-controls" |  | ||||||
|       ); |  | ||||||
|     } catch (_err) {} |  | ||||||
| 
 |  | ||||||
|     // By default, we should disable video controls if the video is already 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
 |     // This solves an issue where Safari users join and seek to 00:00:00 because of
 | ||||||
|     // outgoing events.
 |     // outgoing events.
 | ||||||
|     if (current_time_ms != 0 || !defaultAllowControls) { |     if (current_time_ms != 0) { | ||||||
|       video.controls = false; |       video.controls = false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     setupOutgoingEvents(video, socket); |     setupOutgoingEvents(video, socket); | ||||||
|     setupIncomingEvents(video, socket); |     setupIncomingEvents(video, socket); | ||||||
|     setupChat(socket); |     setupChat(socket, nickname); | ||||||
|   }); |   }); | ||||||
|   socket.addEventListener("reconnecting", (e) => { |   socket.addEventListener("reconnecting", (e) => { | ||||||
|     console.log("Reconnecting..."); |     console.log("Reconnecting..."); | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { setupJoinSessionForm } from "./lib/join-session.mjs?v=bfdcf2"; | import { setupJoinSessionForm } from "./lib/join-session.mjs?v=048af96"; | ||||||
| 
 | 
 | ||||||
| const main = () => { | const main = () => { | ||||||
|   setupJoinSessionForm(); |   setupJoinSessionForm(); | ||||||
|  |  | ||||||
|  | @ -1,6 +1,4 @@ | ||||||
| *, | * { | ||||||
| *:before, |  | ||||||
| *:after { |  | ||||||
|   box-sizing: border-box; |   box-sizing: border-box; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -19,12 +17,6 @@ | ||||||
|       var(--fg-transparent) |       var(--fg-transparent) | ||||||
|     ), |     ), | ||||||
|     linear-gradient(var(--bg), var(--bg)); |     linear-gradient(var(--bg), var(--bg)); | ||||||
|   --chip-bg: linear-gradient( |  | ||||||
|       var(--accent-transparent), |  | ||||||
|       var(--accent-transparent) |  | ||||||
|     ), |  | ||||||
|     linear-gradient(var(--bg), var(--bg)); |  | ||||||
|   --accent-transparent: rgba(var(--accent-rgb), 0.25); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| html { | html { | ||||||
|  | @ -66,45 +58,6 @@ a { | ||||||
|   color: var(--accent); |   color: var(--accent); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .chip { |  | ||||||
|   color: var(--fg); |  | ||||||
|   background: var(--chip-bg); |  | ||||||
|   text-decoration: none; |  | ||||||
|   padding: 0 0.5rem 0 1.45rem; |  | ||||||
|   display: inline-flex; |  | ||||||
|   position: relative; |  | ||||||
|   font-size: 0.9rem; |  | ||||||
|   height: 1.125rem; |  | ||||||
|   align-items: center; |  | ||||||
|   border-radius: 2rem; |  | ||||||
|   overflow: hidden; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .chip::before { |  | ||||||
|   content: ""; |  | ||||||
|   position: absolute; |  | ||||||
|   left: 0; |  | ||||||
|   top: 0; |  | ||||||
|   width: 1.125rem; |  | ||||||
|   height: 100%; |  | ||||||
|   display: flex; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: center; |  | ||||||
|   text-align: center; |  | ||||||
|   background: var(--accent-transparent); |  | ||||||
|   background-repeat: no-repeat; |  | ||||||
|   background-size: 18px; |  | ||||||
|   background-position: center; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .join-chip::before { |  | ||||||
|   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTggNXYxNGwxMS03eiIvPjwvc3ZnPg=="); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .time-chip::before { |  | ||||||
|   background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2ZmZiI+PHBhdGggZD0iTTExLjk5IDJDNi40NyAyIDIgNi40OCAyIDEyczQuNDcgMTAgOS45OSAxMEMxNy41MiAyMiAyMiAxNy41MiAyMiAxMlMxNy41MiAyIDExLjk5IDJ6TTEyIDIwYy00LjQyIDAtOC0zLjU4LTgtOHMzLjU4LTggOC04IDggMy41OCA4IDgtMy41OCA4LTggOHoiLz48cGF0aCBkPSJNMTIuNSA3SDExdjZsNS4yNSAzLjE1Ljc1LTEuMjMtNC41LTIuNjd6Ii8+PC9zdmc+"); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| label { | label { | ||||||
|   display: block; |   display: block; | ||||||
| } | } | ||||||
|  | @ -211,7 +164,6 @@ button.small-button { | ||||||
| 
 | 
 | ||||||
| .chat-message { | .chat-message { | ||||||
|   overflow-wrap: break-word; |   overflow-wrap: break-word; | ||||||
|   margin-bottom: 0.125rem; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .chat-message > strong, | .chat-message > strong, | ||||||
|  | @ -219,15 +171,34 @@ button.small-button { | ||||||
|   color: var(--user-color, var(--default-user-color)); |   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)) | ||||||
|  |       ); | ||||||
|  |     -webkit-background-clip: text; | ||||||
|  |     color: transparent !important; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | */ | ||||||
|  | 
 | ||||||
| .chat-message.user-join, | .chat-message.user-join, | ||||||
| .chat-message.user-leave, | .chat-message.user-leave, | ||||||
| .chat-message.ping { | .chat-message.ping, | ||||||
|  | .chat-message.user-kissed { | ||||||
|   font-style: italic; |   font-style: italic; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .chat-message.user-kissed { | ||||||
|  |   color: #ff6094; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .chat-message.set-time, | .chat-message.set-time, | ||||||
| .chat-message.set-playing, | .chat-message.set-playing { | ||||||
| .chat-message.join-session { |  | ||||||
|   font-style: italic; |   font-style: italic; | ||||||
|   text-align: right; |   text-align: right; | ||||||
|   font-size: 0.85em; |   font-size: 0.85em; | ||||||
|  | @ -238,8 +209,7 @@ button.small-button { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .chat-message.set-time > strong, | .chat-message.set-time > strong, | ||||||
| .chat-message.set-playing > strong, | .chat-message.set-playing > strong { | ||||||
| .chat-message.join-session > strong { |  | ||||||
|   color: unset !important; |   color: unset !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -315,11 +285,9 @@ button.small-button { | ||||||
|   padding: 0.25rem 0.5rem; |   padding: 0.25rem 0.5rem; | ||||||
|   scroll-margin: 0.25rem; |   scroll-margin: 0.25rem; | ||||||
| } | } | ||||||
| 
 |  | ||||||
| .emoji-option:first-child { | .emoji-option:first-child { | ||||||
|   margin-top: 0.25rem; |   margin-top: 0.25rem; | ||||||
| } | } | ||||||
| 
 |  | ||||||
| .emoji-option:last-child { | .emoji-option:last-child { | ||||||
|   margin-bottom: 0.25rem; |   margin-bottom: 0.25rem; | ||||||
| } | } | ||||||
|  | @ -359,19 +327,10 @@ button.small-button { | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| input[type="color"]::-moz-color-swatch { | input[type="color"]::-moz-color-swatch, | ||||||
|   border: none; | input[type="color"]::-webkit-color-swatch, | ||||||
|   margin: 0; |  | ||||||
|   padding: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| input[type="color"]::-webkit-color-swatch { |  | ||||||
|   border: none; |  | ||||||
|   margin: 0; |  | ||||||
|   padding: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| input[type="color"]::-webkit-color-swatch-wrapper { | 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; |   border: none; | ||||||
|   margin: 0; |   margin: 0; | ||||||
|   padding: 0; |   padding: 0; | ||||||
|  |  | ||||||
|  | @ -62,7 +62,7 @@ async fn main() { | ||||||
|             warb::reply::json(&json!({ "id": session_uuid.to_string(), "session": session_view })) |             warb::reply::json(&json!({ "id": session_uuid.to_string(), "session": session_view })) | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|     let get_emoji_route = warb::path!("emojos").and_then(get_emoji_list); |     let get_emoji_route = warb::path!("emojis").and_then(get_emoji_list); | ||||||
| 
 | 
 | ||||||
|     enum RequestedSession { |     enum RequestedSession { | ||||||
|         Session(Uuid, WatchSession), |         Session(Uuid, WatchSession), | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue