ui and emoji changes

easrng 2022-02-15 19:30:22 -05:00
parent 362c990d22
commit e6699e05dd
7 changed files with 249 additions and 119 deletions

View File

@ -43,7 +43,8 @@
<button id="join-session-button">Join</button> <button id="join-session-button">Join</button>
<p> <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> </p>
</form> </form>
</div> </div>
@ -53,8 +54,13 @@
<div id="viewer-list"></div> <div id="viewer-list"></div>
<div id="chatbox"></div> <div id="chatbox"></div>
<form id="chatbox-send"> <form id="chatbox-send">
<input type="text" placeholder="Message... (/help for commands)" list="emoji-autocomplete" /> <input
<div id="emoji-autocomplete"></div> <!-- DO NOT ADD SPACING INSIDE THE TAG IT WILL BREAK THE CSS kthxbye --> 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> </form>
</div> </div>

View File

@ -1,6 +1,23 @@
import { setDebounce, setVideoTime, setPlaying } from "./watch-session.mjs?v=9"; import { setDebounce, setVideoTime, setPlaying } from "./watch-session.mjs?v=9";
import { emojify, emojis } from "./emojis.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) => { const setupChatboxEvents = (socket) => {
// clear events by just reconstructing the form // clear events by just reconstructing the form
const oldChatForm = document.querySelector("#chatbox-send"); const oldChatForm = document.querySelector("#chatbox-send");
@ -11,25 +28,77 @@ const setupChatboxEvents = (socket) => {
let autocompleting = false; let autocompleting = false;
const replaceMessage = message => () => { const replaceMessage = (message) => () => {
messageInput.value = message; messageInput.value = message;
autocomplete(); autocomplete();
} };
async function autocomplete(){ async function autocomplete() {
if(autocompleting) return; if (autocompleting) return;
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:]*$/); 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);
const suffix = messageInput.value.slice(messageInput.selectionStart); 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)}))) 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; autocompleting = false;
} }
messageInput.addEventListener("input", autocomplete) messageInput.addEventListener("input", autocomplete);
messageInput.addEventListener("selectionchange", 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) => { chatForm.addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
@ -81,7 +150,12 @@ const setupChatboxEvents = (socket) => {
"&emsp;<code>/ping [message]</code> - ping all viewers<br>" + "&emsp;<code>/ping [message]</code> - ping all viewers<br>" +
"&emsp;<code>/sync</code> - resyncs you with other viewers"; "&emsp;<code>/sync</code> - resyncs you with other viewers";
printChatMessage("command-message", "/help", "b57fdc", helpMessageContent); printChatMessage(
"command-message",
"/help",
"b57fdc",
helpMessageContent
);
handled = true; handled = true;
break; break;
default: default:
@ -114,8 +188,7 @@ export const setupChat = async (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) messageInput.focus();
messageInput.focus();
} catch (_err) {} } catch (_err) {}
}); });
}; };

View File

@ -2,11 +2,20 @@ export function emojify(text) {
let last = 0; let last = 0;
let nodes = []; let nodes = [];
text.replace(/:([^\s:]+):/g, (match, name, index) => { text.replace(/:([^\s:]+):/g, (match, name, index) => {
if(last <= index) nodes.push(document.createTextNode(text.slice(last, index))) if (last <= index)
nodes.push(Object.assign(new Image(), {src: `/emojis/${name}.png`, className: "emoji", alt: name})) nodes.push(document.createTextNode(text.slice(last, index)));
last = index + match.length nodes.push(
Object.assign(new Image(), {
src: `/emojis/${name}.png`,
className: "emoji",
alt: name,
}) })
if(last < text.length) nodes.push(document.createTextNode(text.slice(last))) );
return nodes 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)));

View File

@ -80,9 +80,13 @@ export const setupJoinSessionForm = () => {
saveNickname(nickname); saveNickname(nickname);
saveColour(colour); saveColour(colour);
try { try {
await joinSession(nickname.value, sessionId.value, colour.value.replace(/^#/, "")); await joinSession(
nickname.value,
sessionId.value,
colour.value.replace(/^#/, "")
);
} catch (e) { } catch (e) {
alert(e.message) alert(e.message);
button.disabled = false; button.disabled = false;
} }
}); });

View File

@ -1,9 +1,9 @@
export default class ReconnectingWebSocket { export default class ReconnectingWebSocket {
constructor(url){ constructor(url) {
if(url instanceof URL) { if (url instanceof URL) {
this.url = url; this.url = url;
} else { } else {
this.url = new URL(url) this.url = new URL(url);
} }
this.connected = false; this.connected = false;
this._eventTarget = new EventTarget(); this._eventTarget = new EventTarget();
@ -14,46 +14,52 @@ export default class ReconnectingWebSocket {
this._connect(true); this._connect(true);
} }
_connect(first) { _connect(first) {
if(this._socket) try { this._socket.close() } catch (e) {}; if (this._socket)
try {
this._socket.close();
} catch (e) {}
try { try {
this._socket = new WebSocket(this.url.href); this._socket = new WebSocket(this.url.href);
} catch (e) { } catch (e) {
this._reconnecting = false; this._reconnecting = false;
return this._reconnect() return this._reconnect();
} }
this._socket.addEventListener("close", () => this._reconnect()) this._socket.addEventListener("close", () => this._reconnect());
this._socket.addEventListener("error", () => this._reconnect()) this._socket.addEventListener("error", () => this._reconnect());
this._socket.addEventListener("message", ({data}) => this._eventTarget.dispatchEvent(new MessageEvent("message", {data}))) this._socket.addEventListener("message", ({ data }) =>
this._socket.addEventListener("open", e => { this._eventTarget.dispatchEvent(new MessageEvent("message", { data }))
if(first) this._eventTarget.dispatchEvent(new Event("open")); );
if(this._reconnecting) this._eventTarget.dispatchEvent(new Event("reconnected")); 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._reconnecting = false;
this._backoff = 250; this._backoff = 250;
this.connected = true; 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(){ _reconnect() {
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;
this.connected = false; this.connected = false;
this._backoff *= 2; // exponential backoff this._backoff *= 2; // exponential backoff
setTimeout(() => { setTimeout(() => {
this._connect(); 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) { send(message) {
if(this.connected) { if (this.connected) {
this._socket.send(message); this._socket.send(message);
} else { } else {
this._unsent.push(message); this._unsent.push(message);
} }
} }
addEventListener(...a) { addEventListener(...a) {
return this._eventTarget.addEventListener(...a) return this._eventTarget.addEventListener(...a);
} }
removeEventListener(...a) { removeEventListener(...a) {
return this._eventTarget.removeEventListener(...a) return this._eventTarget.removeEventListener(...a);
} }
} }

View File

@ -1,6 +1,6 @@
import { setupVideo } from "./video.mjs?v=9"; import { setupVideo } from "./video.mjs?v=9";
import { setupChat, logEventToChat, updateViewerList } from "./chat.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 * @param {string} sessionId
@ -169,7 +169,9 @@ const setupOutgoingEvents = (video, socket) => {
*/ */
export const joinSession = async (nickname, sessionId, colour) => { export const joinSession = async (nickname, sessionId, colour) => {
// try { // we are handling errors in the join form. // 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; 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 {
@ -178,19 +180,20 @@ export const joinSession = async (nickname, sessionId, colour) => {
console.error(e); console.error(e);
throw genericConnectionError; throw genericConnectionError;
} }
if(!response.ok) { if (!response.ok) {
let error; let error;
try { try {
({ error } = await response.json()); ({ error } = await response.json());
if(!error) throw new Error(); if (!error) throw new Error();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
throw genericConnectionError; throw genericConnectionError;
} }
throw new Error(error) throw new Error(error);
} }
try { 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) { } catch (e) {
console.error(e); console.error(e);
throw genericConnectionError; throw genericConnectionError;
@ -216,10 +219,10 @@ export const joinSession = async (nickname, sessionId, colour) => {
setupIncomingEvents(video, socket); setupIncomingEvents(video, socket);
setupChat(socket); setupChat(socket);
}); });
socket.addEventListener("reconnecting", e => { socket.addEventListener("reconnecting", (e) => {
console.log("Reconnecting..."); console.log("Reconnecting...");
}); });
socket.addEventListener("reconnected", e => { socket.addEventListener("reconnected", (e) => {
console.log("Reconnected."); console.log("Reconnected.");
}); });
//} catch (e) { //} catch (e) {

View File

@ -12,8 +12,14 @@
--accent: rgb(var(--accent-rgb)); --accent: rgb(var(--accent-rgb));
--fg-transparent: rgba(var(--fg-rgb), 0.125); --fg-transparent: rgba(var(--fg-rgb), 0.125);
--bg-transparent: rgba(var(--bg-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)); --chat-bg: linear-gradient(var(--fg-transparent), var(--fg-transparent)),
--autocomplete-bg: linear-gradient(var(--fg-transparent), var(--fg-transparent)), linear-gradient(var(--fg-transparent), var(--fg-transparent)), linear-gradient(var(--bg), var(--bg)); 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 { html {
@ -158,13 +164,19 @@ button.small-button {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.chat-message > strong, #viewer-list strong { .chat-message > strong,
#viewer-list strong {
color: var(--user-color, var(--default-user-color)); color: var(--user-color, var(--default-user-color));
} }
@supports (-webkit-background-clip: text) { @supports (-webkit-background-clip: text) {
.chat-message > strong, #viewer-list strong { .chat-message > 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))); #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; -webkit-background-clip: text;
color: transparent !important; color: transparent !important;
} }
@ -183,7 +195,7 @@ button.small-button {
font-size: 0.85em; font-size: 0.85em;
} }
.chat-message.command-message{ .chat-message.command-message {
font-size: 0.85em; font-size: 0.85em;
} }
@ -240,9 +252,11 @@ button.small-button {
position: absolute; position: absolute;
bottom: 3.25rem; bottom: 3.25rem;
background-image: var(--autocomplete-bg); background-image: var(--autocomplete-bg);
padding: 0.25rem;
border-radius: 6px; border-radius: 6px;
width: calc(100% - 4.5rem); width: calc(100% - 4.5rem);
max-height: 8.5rem;
overflow-y: auto;
clip-path: inset(0 0 0 0 round 8px);
} }
#emoji-autocomplete:empty { #emoji-autocomplete:empty {
@ -253,17 +267,29 @@ button.small-button {
background: transparent; background: transparent;
font-size: 0.75rem; font-size: 0.75rem;
text-align: left; text-align: left;
margin: 0 0 0.25rem; margin: 0 0.25rem;
border-radius: 4px; 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:first-child {
.emoji-option:hover, .emoji-option:focus { margin-top: 0.25rem;
background: var(--fg-transparent);
} }
.emoji-option:last-child { .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 { #join-session-colour {
@ -280,7 +306,10 @@ button.small-button {
cursor: pointer; 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; border: none;
margin: 0; margin: 0;
padding: 0; padding: 0;