Compare commits
6 Commits
24f5560d8d
...
e9a1b762e7
Author | SHA1 | Date |
---|---|---|
easrng | e9a1b762e7 | |
easrng | e6699e05dd | |
easrng | 362c990d22 | |
easrng | 558617f644 | |
easrng | 1e73e0df72 | |
easrng | 6d57cbc4a1 |
|
@ -39,12 +39,12 @@
|
||||||
placeholder="English"
|
placeholder="English"
|
||||||
/>
|
/>
|
||||||
<button>Create</button>
|
<button>Create</button>
|
||||||
</form>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Already have a session?
|
Already have a session?
|
||||||
<a href="/">Join your session</a> instead.
|
<a href="/">Join your session</a> instead.
|
||||||
</p>
|
</p>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/create.mjs?v=9"></script>
|
<script type="module" src="/create.mjs?v=9"></script>
|
||||||
|
|
|
@ -41,11 +41,12 @@
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<button id="join-session-button">Join</button>
|
<button id="join-session-button">Join</button>
|
||||||
</form>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
No session to join? <a href="/create.html">Create a session</a> instead.
|
No session to join?
|
||||||
</p>
|
<a href="/create.html">Create a session</a> instead.
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="video-container"></div>
|
<div id="video-container"></div>
|
||||||
|
@ -53,7 +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)" />
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,110 @@
|
||||||
import { setDebounce, setVideoTime, setPlaying } from "./watch-session.mjs?v=9";
|
import { setDebounce, setVideoTime, setPlaying } from "./watch-session.mjs?v=9";
|
||||||
import { emojify } 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");
|
||||||
const chatForm = oldChatForm.cloneNode(true);
|
const chatForm = oldChatForm.cloneNode(true);
|
||||||
|
const messageInput = chatForm.querySelector("input");
|
||||||
|
const emojiAutocomplete = chatForm.querySelector("#emoji-autocomplete");
|
||||||
oldChatForm.replaceWith(chatForm);
|
oldChatForm.replaceWith(chatForm);
|
||||||
|
|
||||||
|
let autocompleting = false;
|
||||||
|
|
||||||
|
const replaceMessage = (message) => () => {
|
||||||
|
messageInput.value = message;
|
||||||
|
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);
|
||||||
|
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("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();
|
||||||
|
const content = messageInput.value;
|
||||||
const input = chatForm.querySelector("input");
|
|
||||||
const content = input.value;
|
|
||||||
if (content.trim().length) {
|
if (content.trim().length) {
|
||||||
input.value = "";
|
messageInput.value = "";
|
||||||
|
|
||||||
// handle commands
|
// handle commands
|
||||||
if (content.startsWith("/")) {
|
if (content.startsWith("/")) {
|
||||||
|
@ -59,7 +150,12 @@ 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";
|
" <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:
|
||||||
|
@ -82,39 +178,19 @@ const setupChatboxEvents = (socket) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const fixChatSize = () => {
|
|
||||||
const video = document.querySelector("video");
|
|
||||||
const chatbox = document.querySelector("#chatbox");
|
|
||||||
const chatboxContainer = document.querySelector("#chatbox-container");
|
|
||||||
|
|
||||||
if (video && chatbox && chatboxContainer) {
|
|
||||||
const delta = chatboxContainer.clientHeight - chatbox.clientHeight;
|
|
||||||
|
|
||||||
chatbox.style["height"] = `calc(${
|
|
||||||
window.innerHeight - video.clientHeight
|
|
||||||
}px - ${delta}px - 1em)`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {WebSocket} socket
|
* @param {WebSocket} socket
|
||||||
*/
|
*/
|
||||||
export const setupChat = async (socket) => {
|
export const setupChat = async (socket) => {
|
||||||
document.querySelector("#chatbox-container").style["display"] = "block";
|
document.querySelector("#chatbox-container").style["display"] = "flex";
|
||||||
setupChatboxEvents(socket);
|
setupChatboxEvents(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();
|
||||||
document.querySelector("#chatbox-send > input").focus();
|
|
||||||
} catch (_err) {}
|
} catch (_err) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
fixChatSize();
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
fixChatSize();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addToChat = (node) => {
|
const addToChat = (node) => {
|
||||||
|
@ -185,7 +261,7 @@ const printChatMessage = (eventType, user, colour, content) => {
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
const userName = document.createElement("strong");
|
const userName = document.createElement("strong");
|
||||||
userName.style = `color: #${colour}`;
|
userName.style = `--user-color: #${colour}`;
|
||||||
userName.textContent = user;
|
userName.textContent = user;
|
||||||
chatMessage.appendChild(userName);
|
chatMessage.appendChild(userName);
|
||||||
}
|
}
|
||||||
|
@ -210,7 +286,7 @@ const formatTime = (ms) => {
|
||||||
}:${seconds < 10 ? "0" + seconds : seconds}`;
|
}:${seconds < 10 ? "0" + seconds : seconds}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logEventToChat = (event) => {
|
export const logEventToChat = async (event) => {
|
||||||
if (checkDebounce(event)) {
|
if (checkDebounce(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -237,7 +313,7 @@ export const logEventToChat = (event) => {
|
||||||
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(...emojify(event.data));
|
messageContent.append(...(await emojify(event.data)));
|
||||||
printChatMessage(
|
printChatMessage(
|
||||||
"chat-message",
|
"chat-message",
|
||||||
event.user,
|
event.user,
|
||||||
|
@ -327,7 +403,7 @@ export const updateViewerList = (viewers) => {
|
||||||
const viewerElem = document.createElement("div");
|
const viewerElem = document.createElement("div");
|
||||||
const content = document.createElement("strong");
|
const content = document.createElement("strong");
|
||||||
content.textContent = viewer.nickname;
|
content.textContent = viewer.nickname;
|
||||||
content.style = `color: #${viewer.colour}`;
|
content.style = `--user-color: #${viewer.colour}`;
|
||||||
viewerElem.appendChild(content);
|
viewerElem.appendChild(content);
|
||||||
listContainer.appendChild(viewerElem);
|
listContainer.appendChild(viewerElem);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,26 @@
|
||||||
export function emojify(text) {
|
export async function emojify(text) {
|
||||||
let last = 0;
|
const emojiList = await emojis;
|
||||||
let nodes = [];
|
let last = 0;
|
||||||
text.replace(/:([^\s:]+):/g, (match, name, index) => {
|
let nodes = [];
|
||||||
if(last <= index) nodes.push(document.createTextNode(text.slice(last, index)))
|
text.replace(/:([^\s:]+):/g, (match, name, index) => {
|
||||||
nodes.push(Object.assign(new Image(), {src: `/emojis/${name}.png`, className: "emoji", alt: name}))
|
if (last <= index)
|
||||||
last = index + match.length
|
nodes.push(document.createTextNode(text.slice(last, index)));
|
||||||
})
|
if (!emojiList.includes(name)) {
|
||||||
if(last < text.length) nodes.push(document.createTextNode(text.slice(last)))
|
nodes.push(document.createTextNode(match));
|
||||||
return nodes
|
} else {
|
||||||
}
|
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)));
|
||||||
|
|
|
@ -72,13 +72,22 @@ export const setupJoinSessionForm = () => {
|
||||||
sessionId.value = window.location.hash.substring(1);
|
sessionId.value = window.location.hash.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
form.addEventListener("submit", (event) => {
|
form.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
|
|
||||||
saveNickname(nickname);
|
saveNickname(nickname);
|
||||||
saveColour(colour);
|
saveColour(colour);
|
||||||
joinSession(nickname.value, sessionId.value, colour.value.replace(/^#/, ""));
|
try {
|
||||||
|
await joinSession(
|
||||||
|
nickname.value,
|
||||||
|
sessionId.value,
|
||||||
|
colour.value.replace(/^#/, "")
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message);
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -140,7 +140,9 @@ const createVideoElement = (videoUrl, subtitles) => {
|
||||||
export const setupVideo = async (videoUrl, subtitles, currentTime, playing) => {
|
export const setupVideo = async (videoUrl, subtitles, currentTime, playing) => {
|
||||||
document.querySelector("#pre-join-controls").style["display"] = "none";
|
document.querySelector("#pre-join-controls").style["display"] = "none";
|
||||||
const video = createVideoElement(videoUrl, subtitles);
|
const video = createVideoElement(videoUrl, subtitles);
|
||||||
document.querySelector("#video-container").appendChild(video);
|
const videoContainer = document.querySelector("#video-container");
|
||||||
|
videoContainer.style.display = "block";
|
||||||
|
videoContainer.appendChild(video);
|
||||||
|
|
||||||
video.currentTime = currentTime / 1000.0;
|
video.currentTime = currentTime / 1000.0;
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} sessionId
|
* @param {string} sessionId
|
||||||
* @param {string} nickname
|
* @param {string} nickname
|
||||||
* @returns {WebSocket}
|
* @returns {ReconnectingWebSocket}
|
||||||
*/
|
*/
|
||||||
const createWebSocket = (sessionId, nickname, colour) => {
|
const createWebSocket = (sessionId, nickname, colour) => {
|
||||||
const wsUrl = new URL(
|
const wsUrl = new URL(
|
||||||
|
@ -13,8 +14,8 @@ const createWebSocket = (sessionId, nickname, colour) => {
|
||||||
`&colour=${encodeURIComponent(colour)}`,
|
`&colour=${encodeURIComponent(colour)}`,
|
||||||
window.location.href
|
window.location.href
|
||||||
);
|
);
|
||||||
wsUrl.protocol = { "http:": "ws:", "https:": "wss:" }[wsUrl.protocol];
|
wsUrl.protocol = "ws" + window.location.protocol.slice(4);
|
||||||
const socket = new WebSocket(wsUrl.toString());
|
const socket = new ReconnectingWebSocket(wsUrl);
|
||||||
|
|
||||||
return socket;
|
return socket;
|
||||||
};
|
};
|
||||||
|
@ -60,7 +61,7 @@ export const setPlaying = async (playing, video = null) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLVideoElement} video
|
* @param {HTMLVideoElement} video
|
||||||
* @param {WebSocket} socket
|
* @param {ReconnectingWebSocket} socket
|
||||||
*/
|
*/
|
||||||
const setupIncomingEvents = (video, socket) => {
|
const setupIncomingEvents = (video, socket) => {
|
||||||
socket.addEventListener("message", async (messageEvent) => {
|
socket.addEventListener("message", async (messageEvent) => {
|
||||||
|
@ -97,7 +98,7 @@ const setupIncomingEvents = (video, socket) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLVideoElement} video
|
* @param {HTMLVideoElement} video
|
||||||
* @param {WebSocket} socket
|
* @param {ReconnectingWebSocket} socket
|
||||||
*/
|
*/
|
||||||
const setupOutgoingEvents = (video, socket) => {
|
const setupOutgoingEvents = (video, socket) => {
|
||||||
const currentVideoTime = () => (video.currentTime * 1000) | 0;
|
const currentVideoTime = () => (video.currentTime * 1000) | 0;
|
||||||
|
@ -167,37 +168,66 @@ const setupOutgoingEvents = (video, socket) => {
|
||||||
* @param {string} sessionId
|
* @param {string} sessionId
|
||||||
*/
|
*/
|
||||||
export const joinSession = async (nickname, sessionId, colour) => {
|
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."
|
||||||
|
);
|
||||||
|
window.location.hash = sessionId;
|
||||||
|
let response, video_url, subtitle_tracks, current_time_ms, is_playing;
|
||||||
try {
|
try {
|
||||||
window.location.hash = sessionId;
|
response = await fetch(`/sess/${sessionId}`);
|
||||||
|
} catch (e) {
|
||||||
const { video_url, subtitle_tracks, current_time_ms, is_playing } =
|
console.error(e);
|
||||||
await fetch(`/sess/${sessionId}`).then((r) => r.json());
|
throw genericConnectionError;
|
||||||
|
|
||||||
const socket = createWebSocket(sessionId, nickname, colour);
|
|
||||||
socket.addEventListener("open", async () => {
|
|
||||||
const video = await setupVideo(
|
|
||||||
video_url,
|
|
||||||
subtitle_tracks,
|
|
||||||
current_time_ms,
|
|
||||||
is_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
|
|
||||||
// outgoing events.
|
|
||||||
if (current_time_ms != 0) {
|
|
||||||
video.controls = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupOutgoingEvents(video, socket);
|
|
||||||
setupIncomingEvents(video, socket);
|
|
||||||
setupChat(socket);
|
|
||||||
});
|
|
||||||
// TODO: Close listener ?
|
|
||||||
} catch (err) {
|
|
||||||
// TODO: Show an error on the screen
|
|
||||||
console.error(err);
|
|
||||||
}
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
({ error } = await response.json());
|
||||||
|
if (!error) throw new Error();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
throw genericConnectionError;
|
||||||
|
}
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
({ video_url, subtitle_tracks, current_time_ms, is_playing } =
|
||||||
|
await response.json());
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
throw genericConnectionError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const socket = createWebSocket(sessionId, nickname, colour);
|
||||||
|
socket.addEventListener("open", async () => {
|
||||||
|
const video = await setupVideo(
|
||||||
|
video_url,
|
||||||
|
subtitle_tracks,
|
||||||
|
current_time_ms,
|
||||||
|
is_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
|
||||||
|
// outgoing events.
|
||||||
|
if (current_time_ms != 0) {
|
||||||
|
video.controls = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupOutgoingEvents(video, socket);
|
||||||
|
setupIncomingEvents(video, socket);
|
||||||
|
setupChat(socket);
|
||||||
|
});
|
||||||
|
socket.addEventListener("reconnecting", (e) => {
|
||||||
|
console.log("Reconnecting...");
|
||||||
|
});
|
||||||
|
socket.addEventListener("reconnected", (e) => {
|
||||||
|
console.log("Reconnected.");
|
||||||
|
});
|
||||||
|
//} catch (e) {
|
||||||
|
// alert(e.message)
|
||||||
|
//}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,7 +1,25 @@
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: rgb(28, 23, 36);
|
--bg-rgb: 28, 23, 36;
|
||||||
--fg: rgb(234, 234, 248);
|
--fg-rgb: 234, 234, 248;
|
||||||
--accent: hsl(275, 57%, 68%);
|
--accent-rgb: 181, 127, 220;
|
||||||
|
--fg: rgb(var(--fg-rgb));
|
||||||
|
--bg: rgb(var(--bg-rgb));
|
||||||
|
--default-user-color: rgb(126, 208, 255);
|
||||||
|
--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));
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
@ -9,30 +27,34 @@ html {
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
|
||||||
overflow-y: scroll;
|
|
||||||
scrollbar-width: none;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
video {
|
video {
|
||||||
display: block;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
width: 100vw;
|
#video-container {
|
||||||
height: auto;
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
max-width: auto;
|
display: none;
|
||||||
max-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -45,8 +67,6 @@ label {
|
||||||
|
|
||||||
input[type="url"],
|
input[type="url"],
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
@ -60,8 +80,7 @@ input[type="text"] {
|
||||||
|
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
width: 500px;
|
width: 100%;
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
resize: none;
|
resize: none;
|
||||||
overflow-x: wrap;
|
overflow-x: wrap;
|
||||||
|
@ -82,12 +101,19 @@ button {
|
||||||
|
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
width: 500px;
|
width: 100%;
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
border: 1px solid rgba(0, 0, 0, 0);
|
border: 1px solid rgba(0, 0, 0, 0);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
filter: saturate(0.75);
|
||||||
|
opacity: 0.75;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.small-button {
|
button.small-button {
|
||||||
|
@ -108,20 +134,25 @@ button.small-button {
|
||||||
|
|
||||||
#pre-join-controls,
|
#pre-join-controls,
|
||||||
#create-controls {
|
#create-controls {
|
||||||
width: 60%;
|
margin: 0;
|
||||||
margin: 0 auto;
|
flex-grow: 1;
|
||||||
margin-top: 4em;
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#join-session-form,
|
#join-session-form,
|
||||||
#create-session-form {
|
#create-session-form {
|
||||||
margin-bottom: 4em;
|
width: 500px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#post-create-message {
|
#post-create-message {
|
||||||
display: none;
|
display: none;
|
||||||
width: 500px;
|
width: 100%;
|
||||||
max-width: 100%;
|
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,8 +164,22 @@ button.small-button {
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message > strong {
|
.chat-message > strong,
|
||||||
color: rgb(126, 208, 255);
|
#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))
|
||||||
|
);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
color: transparent !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message.user-join,
|
.chat-message.user-join,
|
||||||
|
@ -150,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,27 +213,34 @@ button.small-button {
|
||||||
|
|
||||||
#chatbox {
|
#chatbox {
|
||||||
padding: 0.5em 2em;
|
padding: 0.5em 2em;
|
||||||
min-height: 8em;
|
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#viewer-list {
|
#viewer-list {
|
||||||
padding: 0.5em 2em;
|
padding: 0.5em 2em;
|
||||||
/* TODO: turn this into max-height instead of fixed height without breaking the chatbox height */
|
/* TODO: turn this into max-height instead of fixed height without breaking the chatbox height */
|
||||||
height: 4em;
|
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
color: rgb(126, 208, 255);
|
|
||||||
border-bottom: var(--fg);
|
border-bottom: var(--fg);
|
||||||
border-bottom-style: solid;
|
border-bottom-style: solid;
|
||||||
|
max-height: 4rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chatbox-container {
|
#chatbox-container {
|
||||||
background-color: #222;
|
background-image: var(--chat-bg);
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-basis: 400px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chatbox-send {
|
#chatbox-send {
|
||||||
padding: 0 2em;
|
padding: 0 2em;
|
||||||
padding-bottom: 0.5em;
|
padding-bottom: 0.5em;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chatbox-send > input {
|
#chatbox-send > input {
|
||||||
|
@ -196,22 +248,79 @@ button.small-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-aspect-ratio: 4/3) {
|
#emoji-autocomplete {
|
||||||
#video-container video {
|
position: absolute;
|
||||||
width: calc(100vw - 400px);
|
bottom: 3.25rem;
|
||||||
position: absolute;
|
background-image: var(--autocomplete-bg);
|
||||||
height: 100vh;
|
border-radius: 6px;
|
||||||
background-color: black;
|
width: calc(100% - 4.5rem);
|
||||||
}
|
max-height: 8.5rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
clip-path: inset(0 0 0 0 round 8px);
|
||||||
|
}
|
||||||
|
|
||||||
#video-container {
|
#emoji-autocomplete:empty {
|
||||||
float: left;
|
display: none;
|
||||||
height: 100vh;
|
}
|
||||||
position: relative;
|
|
||||||
|
.emoji-option {
|
||||||
|
background: transparent;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: calc(100% - 0.5rem);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
-moz-appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
height: 2rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
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. */
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-aspect-ratio: 4/3) {
|
||||||
|
body {
|
||||||
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chatbox-container {
|
#chatbox-container {
|
||||||
float: right;
|
|
||||||
width: 400px;
|
width: 400px;
|
||||||
height: 100vh !important;
|
height: 100vh !important;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue