forked from lavender/watch-party
		
	add linkification and time and join chips
This commit is contained in:
		
							parent
							
								
									60672a04ef
								
							
						
					
					
						commit
						2d544620ed
					
				
					 4 changed files with 245 additions and 94 deletions
				
			
		|  | @ -4,6 +4,7 @@ import { | |||
|   setPlaying, | ||||
| } from "./watch-session.mjs?v=048af96"; | ||||
| import { emojify, findEmojis } from "./emojis.mjs?v=048af96"; | ||||
| import { linkify } from "./links.mjs"; | ||||
| 
 | ||||
| function setCaretPosition(elem, caretPos) { | ||||
|   if (elem.createTextRange) { | ||||
|  | @ -35,6 +36,7 @@ const setupChatboxEvents = (socket) => { | |||
|   }; | ||||
|   async function autocomplete(fromListTimeout) { | ||||
|     if (autocompleting) return; | ||||
|     try { | ||||
|       clearInterval(showListTimer); | ||||
|       emojiAutocomplete.textContent = ""; | ||||
|       autocompleting = true; | ||||
|  | @ -106,6 +108,9 @@ const setupChatboxEvents = (socket) => { | |||
|         } | ||||
|       } | ||||
|       autocompleting = false; | ||||
|     } catch (e) { | ||||
|       autocompleting = false; | ||||
|     } | ||||
|   } | ||||
|   messageInput.addEventListener("input", () => autocomplete()); | ||||
|   messageInput.addEventListener("selectionchange", () => autocomplete()); | ||||
|  | @ -227,13 +232,6 @@ const setupChatboxEvents = (socket) => { | |||
| export const setupChat = async (socket) => { | ||||
|   document.querySelector("#chatbox-container").style["display"] = "flex"; | ||||
|   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) => { | ||||
|  | @ -354,7 +352,7 @@ export const logEventToChat = async (event) => { | |||
|     case "ChatMessage": { | ||||
|       const messageContent = document.createElement("span"); | ||||
|       messageContent.classList.add("message-content"); | ||||
|       messageContent.append(...(await emojify(event.data))); | ||||
|       messageContent.append(...(await linkify(event.data, emojify))); | ||||
|       printChatMessage( | ||||
|         "chat-message", | ||||
|         event.user, | ||||
|  |  | |||
|  | @ -5,7 +5,10 @@ export async function emojify(text) { | |||
|   text.replace(/:([^\s:]+):/g, (match, name, index) => { | ||||
|     if (last <= index) | ||||
|       nodes.push(document.createTextNode(text.slice(last, index))); | ||||
|     let emoji = emojis[name.toLowerCase()[0]].find((e) => e[0] == name); | ||||
|     let emoji; | ||||
|     try { | ||||
|       emoji = emojis[name.toLowerCase()[0]].find((e) => e[0] == name); | ||||
|     } catch (e) {} | ||||
|     if (!emoji) { | ||||
|       nodes.push(document.createTextNode(match)); | ||||
|     } else { | ||||
|  |  | |||
							
								
								
									
										115
									
								
								frontend/lib/links.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								frontend/lib/links.mjs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,115 @@ | |||
| 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"), { | ||||
|                 href: url.href, | ||||
|                 textContent: "Join Session", | ||||
|                 className: "chip join-chip", | ||||
|               }) | ||||
|             ); | ||||
|           } 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,4 +1,6 @@ | |||
| * { | ||||
| *, | ||||
| *:before, | ||||
| *:after { | ||||
|   box-sizing: border-box; | ||||
| } | ||||
| 
 | ||||
|  | @ -17,6 +19,12 @@ | |||
|       var(--fg-transparent) | ||||
|     ), | ||||
|     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 { | ||||
|  | @ -58,6 +66,45 @@ a { | |||
|   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 { | ||||
|   display: block; | ||||
| } | ||||
|  | @ -164,6 +211,7 @@ button.small-button { | |||
| 
 | ||||
| .chat-message { | ||||
|   overflow-wrap: break-word; | ||||
|   margin-bottom: 0.125rem; | ||||
| } | ||||
| 
 | ||||
| .chat-message > strong, | ||||
|  | @ -171,21 +219,6 @@ button.small-button { | |||
|   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-leave, | ||||
| .chat-message.ping { | ||||
|  | @ -280,9 +313,11 @@ button.small-button { | |||
|   padding: 0.25rem 0.5rem; | ||||
|   scroll-margin: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .emoji-option:first-child { | ||||
|   margin-top: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .emoji-option:last-child { | ||||
|   margin-bottom: 0.25rem; | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue