lotsa frontend changes
parent
1e73e0df72
commit
558617f644
|
@ -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,11 @@
|
||||||
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? <a href="/create.html">Create a session</a> instead.
|
||||||
</p>
|
</p>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="video-container"></div>
|
<div id="video-container"></div>
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
<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 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 -->
|
<div id="emoji-autocomplete"></div> <!-- DO NOT ADD SPACING INSIDE THE TAG IT WILL BREAK THE CSS kthxbye -->
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ const setupChatboxEvents = (socket) => {
|
||||||
autocompleting = false;
|
autocompleting = false;
|
||||||
}
|
}
|
||||||
messageInput.addEventListener("input", autocomplete)
|
messageInput.addEventListener("input", autocomplete)
|
||||||
|
messageInput.addEventListener("selectionchange", autocomplete);
|
||||||
|
|
||||||
chatForm.addEventListener("submit", async (e) => {
|
chatForm.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -72,13 +72,18 @@ 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,59 @@
|
||||||
|
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,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,63 @@ 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,3 +1,7 @@
|
||||||
|
* {
|
||||||
|
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;
|
||||||
|
@ -57,8 +61,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);
|
||||||
|
@ -72,8 +74,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;
|
||||||
|
@ -84,7 +85,7 @@ button {
|
||||||
background-color: var(--accent);
|
background-color: var(--accent);
|
||||||
border: var(--accent);
|
border: var(--accent);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: var(--fg);
|
color: #fff;
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 1em;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
@ -94,13 +95,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;
|
cursor: pointer;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
filter: saturate(0.75);
|
||||||
|
opacity: 0.75;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.small-button {
|
button.small-button {
|
||||||
|
@ -121,20 +128,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,13 +257,35 @@ button.small-button {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-option:hover, .emoji-option:focus {
|
.emoji-option:hover, .emoji-option:focus {
|
||||||
background: var(--fg-transparent);
|
background: var(--fg-transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-option:last-child {
|
.emoji-option:last-child {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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) {
|
@media (min-aspect-ratio: 4/3) {
|
||||||
body {
|
body {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
Loading…
Reference in New Issue