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");
|
||||
|
@ -8,28 +25,80 @@ const setupChatboxEvents = (socket) => {
|
|||
const messageInput = chatForm.querySelector("input");
|
||||
const emojiAutocomplete = chatForm.querySelector("#emoji-autocomplete");
|
||||
oldChatForm.replaceWith(chatForm);
|
||||
|
||||
|
||||
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) {}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
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
|
||||
}
|
||||
export const emojis = Promise.resolve(["blobcat", "blobhaj"])
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
this.url = url;
|
||||
} else {
|
||||
this.url = new URL(url)
|
||||
}
|
||||
this.connected = false;
|
||||
this._eventTarget = new EventTarget();
|
||||
this._backoff = 250; // milliseconds, doubled before use
|
||||
this._lastConnect = 0;
|
||||
this._socket = null;
|
||||
this._unsent = [];
|
||||
this._connect(true);
|
||||
}
|
||||
_connect(first) {
|
||||
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()
|
||||
}
|
||||
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())
|
||||
})
|
||||
}
|
||||
_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)))
|
||||
}
|
||||
send(message) {
|
||||
if(this.connected) {
|
||||
this._socket.send(message);
|
||||
} else {
|
||||
this._unsent.push(message);
|
||||
}
|
||||
}
|
||||
addEventListener(...a) {
|
||||
return this._eventTarget.addEventListener(...a)
|
||||
}
|
||||
removeEventListener(...a) {
|
||||
return this._eventTarget.removeEventListener(...a)
|
||||
}
|
||||
}
|
||||
export default class ReconnectingWebSocket {
|
||||
constructor(url) {
|
||||
if (url instanceof URL) {
|
||||
this.url = url;
|
||||
} else {
|
||||
this.url = new URL(url);
|
||||
}
|
||||
this.connected = false;
|
||||
this._eventTarget = new EventTarget();
|
||||
this._backoff = 250; // milliseconds, doubled before use
|
||||
this._lastConnect = 0;
|
||||
this._socket = null;
|
||||
this._unsent = [];
|
||||
this._connect(true);
|
||||
}
|
||||
_connect(first) {
|
||||
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();
|
||||
}
|
||||
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());
|
||||
});
|
||||
}
|
||||
_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));
|
||||
}
|
||||
send(message) {
|
||||
if (this.connected) {
|
||||
this._socket.send(message);
|
||||
} else {
|
||||
this._unsent.push(message);
|
||||
}
|
||||
}
|
||||
addEventListener(...a) {
|
||||
return this._eventTarget.addEventListener(...a);
|
||||
}
|
||||
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