aboutsummaryrefslogtreecommitdiff
path: root/client-web/source
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-01-27 19:59:39 +0100
committermetamuffin <metamuffin@disroot.org>2024-01-27 19:59:39 +0100
commit91259369b2b87eb647e9743c874d7e58894149c1 (patch)
treec717d3c4fb3322bb559a50eab1a2331f9c351a70 /client-web/source
parent0d8a3082fe32e9dd89deea9f051f6e53df591646 (diff)
downloadkeks-meet-91259369b2b87eb647e9743c874d7e58894149c1.tar
keks-meet-91259369b2b87eb647e9743c874d7e58894149c1.tar.bz2
keks-meet-91259369b2b87eb647e9743c874d7e58894149c1.tar.zst
handle room joins without page reload.
Diffstat (limited to 'client-web/source')
-rw-r--r--client-web/source/chat.ts14
-rw-r--r--client-web/source/index.ts68
-rw-r--r--client-web/source/keybinds.ts15
-rw-r--r--client-web/source/menu.ts16
-rw-r--r--client-web/source/protocol/mod.ts10
-rw-r--r--client-web/source/room.ts23
-rw-r--r--client-web/source/room_watches.ts9
-rw-r--r--client-web/source/user/remote.ts5
8 files changed, 101 insertions, 59 deletions
diff --git a/client-web/source/chat.ts b/client-web/source/chat.ts
index 6025878..5d911c6 100644
--- a/client-web/source/chat.ts
+++ b/client-web/source/chat.ts
@@ -14,11 +14,10 @@ import { Room } from "./room.ts";
import { LocalUser } from "./user/local.ts";
import { User } from "./user/mod.ts";
-export let GLOBAL_CHAT: Chat;
-
interface ControlMessage {
join?: User,
leave?: User,
+ change_room?: string,
}
export class Chat {
@@ -27,8 +26,9 @@ export class Chat {
send_el: HTMLInputElement
element: HTMLElement
- constructor(public room: Room) {
- GLOBAL_CHAT = this;
+ public room?: Room
+
+ constructor() {
const send = document.createElement("input")
send.ariaLabel = "send message"
send.type = "text"
@@ -74,8 +74,10 @@ export class Chat {
focus() { this.send_el.focus() }
send(msg: ChatMessage) {
- this.room.local_user.chat(msg)
- this.add_message(this.room.local_user, msg)
+ if (this.room) {
+ this.room.local_user.chat(msg)
+ this.add_message(this.room.local_user, msg)
+ }
}
remove_oldest_message() {
diff --git a/client-web/source/index.ts b/client-web/source/index.ts
index 796937a..8bc7edc 100644
--- a/client-web/source/index.ts
+++ b/client-web/source/index.ts
@@ -13,8 +13,10 @@ import { load_params, PREFS } from "./preferences/mod.ts";
import { SignalingConnection } from "./protocol/mod.ts";
import { Room } from "./room.ts"
import { control_bar, info_br } from "./menu.ts";
+import { Chat } from "./chat.ts"
export const VERSION = "1.0.0"
+window.onload = () => main()
export interface ClientConfig {
appearance?: {
@@ -37,19 +39,29 @@ export interface User {
stream: MediaStream,
}
-window.onload = () => main()
-window.onhashchange = () => {
- // TODO might just destroy room object and create a new one, but cleanup probably wont work. lets reload instead
- window.location.reload()
+
+export interface AppState {
+ conn: SignalingConnection,
+ room?: Room
+ chat: Chat,
+ center: HTMLElement
}
-window.onbeforeunload = ev => {
- if (r.local_user.resources.size != 0 && PREFS.enable_onbeforeunload) {
- ev.preventDefault()
- return ev.returnValue = "You have local resources shared. Really quit?"
+
+function set_room(state: AppState, secret: string, rtc_config: RTCConfiguration) {
+ if (state.room) {
+ state.center.removeChild(state.room.element)
+ state.room.destroy()
+ }
+ if (secret.length < 8) log({ scope: "crypto", warn: true }, "Room name is very short. E2EE is insecure!")
+ if (secret.split("#").length > 1) document.title = `${secret.split("#")[0]} | keks-meet`
+ state.room = new Room(state.conn, state.chat, rtc_config)
+ state.chat.room = state.room
+ state.conn.join(secret)
+ state.room.on_ready = () => {
+ state.center.prepend(state.room!.element)
}
}
-let r: Room;
export async function main() {
document.body.append(LOGGER_CONTAINER)
log("*", "loading client config")
@@ -59,19 +71,22 @@ export async function main() {
log("*", "config loaded. starting")
document.body.querySelectorAll(".loading").forEach(e => e.remove())
- const room_secret = load_params().rsecret
- if (!globalThis.isSecureContext) log({ scope: "*", warn: true }, "This page is not in a 'Secure Context'")
+ if (!globalThis.isSecureContext) log({ scope: "*", warn: true }, "This page is not a 'Secure Context'")
if (!globalThis.RTCPeerConnection) return log({ scope: "webrtc", error: true }, "WebRTC not supported.")
if (!globalThis.crypto.subtle) return log({ scope: "crypto", error: true }, "SubtleCrypto not availible")
if (!globalThis.navigator.serviceWorker) log({ scope: "*", warn: true }, "Your browser does not support the Service Worker API, forced automatic updates are unavoidable.")
- if (room_secret.length < 8) log({ scope: "crypto", warn: true }, "Room name is very short. e2ee is insecure!")
- if (room_secret.length == 0) return window.location.href = "/" // send them back to the start page
- if (PREFS.warn_redirect) log({ scope: "crypto", warn: true }, "You were redirected from the old URL format. The server knows the room secret now - e2ee is insecure!")
+ if (PREFS.warn_redirect) log({ scope: "crypto", warn: true }, "You were redirected from the old URL format. The server knows the room secret now - E2EE is insecure!")
+
- if (room_secret.split("#").length > 1) document.title = `${room_secret.split("#")[0]} | keks-meet`
+ const sud = e("div", { class: "side-ui" })
+ const state: AppState = {
+ chat: new Chat(),
+ conn: await (new SignalingConnection().connect()),
+ center: e("div", { class: "main" }, info_br(), sud)
+ };
+ document.body.append(state.center, control_bar(state, sud))
- const conn = await (new SignalingConnection().connect())
const rtc_config: RTCConfiguration = {
iceCandidatePoolSize: 10,
iceServers: [{
@@ -81,15 +96,22 @@ export async function main() {
}]
}
- r = new Room(conn, rtc_config)
+ setup_keybinds(state)
- setup_keybinds(r)
- r.on_ready = () => {
- const sud = e("div", { class: "side-ui" })
- const center = e("div", { class: "main" }, r.element, info_br(), sud)
- document.body.append(center, control_bar(r, sud))
+ const room_secret = load_params().rsecret
+ if (room_secret.length != 0) {
+ set_room(state, room_secret, rtc_config)
+ }
+ window.onhashchange = () => {
+ const new_secret = load_params().rsecret;
+ set_room(state, new_secret, rtc_config)
+ }
+ window.onbeforeunload = ev => {
+ if (state.room && state.room.local_user.resources.size != 0 && PREFS.enable_onbeforeunload) {
+ ev.preventDefault()
+ return ev.returnValue = "You have local resources shared. Really quit?"
+ }
}
if (globalThis.navigator.serviceWorker) init_serviceworker()
- await conn.join(room_secret)
}
diff --git a/client-web/source/keybinds.ts b/client-web/source/keybinds.ts
index dfc04b4..09fbb34 100644
--- a/client-web/source/keybinds.ts
+++ b/client-web/source/keybinds.ts
@@ -5,13 +5,12 @@
*/
/// <reference lib="dom" />
-import { GLOBAL_CHAT } from "./chat.ts";
+import { AppState } from "./index.ts";
import { chat_control } from "./menu.ts";
import { create_camera_res, create_mic_res, create_screencast_res } from "./resource/track.ts";
-import { Room } from "./room.ts"
import { update_serviceworker } from "./sw/client.ts";
-export function setup_keybinds(room: Room) {
+export function setup_keybinds(state: AppState) {
document.body.addEventListener("keydown", ev => {
// TODO is there a proper solution?
if (ev.target instanceof HTMLInputElement && !(ev.target.type == "button")) return
@@ -21,12 +20,12 @@ export function setup_keybinds(room: Room) {
ev.preventDefault() // so focused buttons dont trigger
}
if (ev.shiftKey) {
- if (ev.code == "KeyM" || ev.code == "KeyR") room.local_user.await_add_resource(create_mic_res())
- if (ev.code == "KeyS") room.local_user.await_add_resource(create_screencast_res())
- if (ev.code == "KeyC" && !ev.ctrlKey) room.local_user.await_add_resource(create_camera_res())
- if (ev.code == "KeyC" && ev.ctrlKey) room.local_user.resources.forEach(t => t.destroy())
+ if (ev.code == "KeyM" || ev.code == "KeyR") state.room?.local_user.await_add_resource(create_mic_res())
+ if (ev.code == "KeyS") state.room?.local_user.await_add_resource(create_screencast_res())
+ if (ev.code == "KeyC" && !ev.ctrlKey) state.room?.local_user.await_add_resource(create_camera_res())
+ if (ev.code == "KeyC" && ev.ctrlKey) state.room?.local_user.resources.forEach(t => t.destroy())
if (ev.code == "KeyU") if (window.confirm("really update?")) update_serviceworker()
- if (ev.code == "KeyV") GLOBAL_CHAT.remove_oldest_message()
+ if (ev.code == "KeyV") state.chat?.remove_oldest_message()
}
})
}
diff --git a/client-web/source/menu.ts b/client-web/source/menu.ts
index ddc49bc..f253bc3 100644
--- a/client-web/source/menu.ts
+++ b/client-web/source/menu.ts
@@ -6,11 +6,11 @@
/// <reference lib="dom" />
import { e, sleep } from "./helper.ts"
+import { AppState } from "./index.ts";
import { VERSION } from "./index.ts"
import { ui_preferences } from "./preferences/ui.ts"
import { create_file_res } from "./resource/file.ts";
import { create_camera_res, create_mic_res, create_screencast_res } from "./resource/track.ts";
-import { Room } from "./room.ts"
import { ui_room_watches } from "./room_watches.ts";
export function info_br() {
@@ -36,16 +36,16 @@ export function info_br() {
export let chat_control: (s?: boolean) => void;
-export function control_bar(room: Room, side_ui_container: HTMLElement): HTMLElement {
+export function control_bar(state: AppState, side_ui_container: HTMLElement): HTMLElement {
const leave = e("button", { icon: "leave", class: "abort", onclick() { window.location.href = "/" } }, "Leave")
- const chat = side_ui(side_ui_container, room.chat.element, "chat", "Chat", room.chat)
+ const chat = side_ui(side_ui_container, state.chat.element, "chat", "Chat", state.chat)
const prefs = side_ui(side_ui_container, ui_preferences(), "settings", "Settings")
- const rwatches = side_ui(side_ui_container, ui_room_watches(room.signaling), "room", "Known Rooms")
+ const rwatches = side_ui(side_ui_container, ui_room_watches(state.conn), "room", "Known Rooms")
const local_controls = [ //ediv({ class: "local-controls", aria_label: "local resources" },
- e("button", { icon: "microphone", onclick: () => room.local_user.await_add_resource(create_mic_res()) }, "Microphone"),
- e("button", { icon: "camera", onclick: () => room.local_user.await_add_resource(create_camera_res()) }, "Camera"),
- e("button", { icon: "screen", onclick: () => room.local_user.await_add_resource(create_screencast_res()) }, "Screen"),
- e("button", { icon: "file", onclick: () => room.local_user.await_add_resource(create_file_res()) }, "File"),
+ e("button", { icon: "microphone", onclick: () => state.room?.local_user.await_add_resource(create_mic_res()) }, "Microphone"),
+ e("button", { icon: "camera", onclick: () => state.room?.local_user.await_add_resource(create_camera_res()) }, "Camera"),
+ e("button", { icon: "screen", onclick: () => state.room?.local_user.await_add_resource(create_screencast_res()) }, "Screen"),
+ e("button", { icon: "file", onclick: () => state.room?.local_user.await_add_resource(create_file_res()) }, "File"),
]
chat_control = chat.set_state;
return e("nav", { class: "control-bar" }, leave, "|", chat.el, prefs.el, rwatches.el, "|", ...local_controls)
diff --git a/client-web/source/protocol/mod.ts b/client-web/source/protocol/mod.ts
index 690805c..e82cf94 100644
--- a/client-web/source/protocol/mod.ts
+++ b/client-web/source/protocol/mod.ts
@@ -43,7 +43,7 @@ export class SignalingConnection {
}
on_open() {
log("ws", "websocket opened");
- setInterval(() => this.send_control({ ping: null }), 30000) // stupid workaround for nginx disconnecting inactive connections
+ setInterval(() => this.send_control({ ping: null }), 30000) // stupid workaround for reverse proxies disconnecting inactive connections
}
async join(room: string) {
@@ -57,7 +57,13 @@ export class SignalingConnection {
log({ scope: "ws", error: true }, "websocket error occurred!")
}
async on_message(data: string) {
- const packet: ClientboundPacket = JSON.parse(data) // TODO dont crash if invalid
+ let packet: ClientboundPacket
+ try {
+ packet = JSON.parse(data)
+ } catch (_e) {
+ log({ scope: "ws", warn: true }, "server sent invalid json")
+ return
+ }
this.control_handler.dispatch(packet)
if (packet.init) this.my_id = packet.init.your_id;
if (packet.message) {
diff --git a/client-web/source/room.ts b/client-web/source/room.ts
index a38071c..53b8740 100644
--- a/client-web/source/room.ts
+++ b/client-web/source/room.ts
@@ -10,36 +10,43 @@ import { RemoteUser } from "./user/remote.ts";
import { LocalUser } from "./user/local.ts";
import { ClientboundPacket, RelayMessage } from "../../common/packets.d.ts";
import { SignalingConnection } from "./protocol/mod.ts";
-import { Chat } from "./chat.ts";
import { e } from "./helper.ts";
+import { Chat } from "./chat.ts";
export class Room {
public remote_users: Map<number, RemoteUser> = new Map()
public local_user!: LocalUser
- public my_id!: number
- public chat: Chat = new Chat(this)
public element: HTMLElement
public on_ready = () => { };
+ public destroy: () => void
- constructor(public signaling: SignalingConnection, public rtc_config: RTCConfiguration) {
+ constructor(public signaling: SignalingConnection, public chat: Chat, public rtc_config: RTCConfiguration) {
this.element = e("div", { class: "room", aria_label: "user list" })
- signaling.control_handler.add_listener(p => this.control_handler(p))
- signaling.relay_handler.add_listener(([a, b]) => this.relay_handler(a, b))
+ const h1 = ([a, b]: [number, RelayMessage]) => this.relay_handler(a, b);
+ const h2 = (p: ClientboundPacket) => this.control_handler(p)
+ signaling.relay_handler.add_listener(h1)
+ signaling.control_handler.add_listener(h2)
+ this.destroy = () => {
+ signaling.relay_handler.remove_listener(h1)
+ signaling.control_handler.remove_listener(h2)
+ this.remote_users.forEach(v => v.leave())
+ this.local_user.resources.forEach(r => r.destroy())
+ this.remote_users = new Map()
+ }
}
control_handler(packet: ClientboundPacket) {
if (packet.message) return // let the relay handler do that
if (packet.init) {
log("ws", `<- [init packet]: `, packet);
- this.my_id = packet.init.your_id
// no need to check compat for now because this is hosted in the same place
log("*", `server: ${packet.init.version}`)
} else if (packet.client_join) {
log("ws", `<- [client join]: `, packet);
const p = packet.client_join
log("*", `${p.id} joined`);
- if (p.id == this.my_id) {
+ if (p.id == this.signaling.my_id) {
this.local_user = new LocalUser(this, p.id);
this.on_ready()
} else {
diff --git a/client-web/source/room_watches.ts b/client-web/source/room_watches.ts
index 3e41507..2e8d2f9 100644
--- a/client-web/source/room_watches.ts
+++ b/client-web/source/room_watches.ts
@@ -70,7 +70,14 @@ export function ui_room_watches(conn: SignalingConnection): HTMLElement {
update_watches()
input.value = ""
}
- }, "Add")
+ }, "Add"),
+ e("button", {
+ async onclick() {
+ if (!conn.room) return
+ await add_watch(conn.room)
+ update_watches()
+ }
+ }, "Add current room")
))
}
}
diff --git a/client-web/source/user/remote.ts b/client-web/source/user/remote.ts
index 9964937..262efcd 100644
--- a/client-web/source/user/remote.ts
+++ b/client-web/source/user/remote.ts
@@ -6,7 +6,6 @@
/// <reference lib="dom" />
import { RelayMessage } from "../../../common/packets.d.ts";
-import { GLOBAL_CHAT } from "../chat.ts";
import { notify } from "../helper.ts";
import { log } from "../logger.ts"
import { PREFS } from "../preferences/mod.ts";
@@ -81,7 +80,7 @@ export class RemoteUser extends User {
this.room.remote_users.delete(this.id)
this.room.element.removeChild(this.el)
if (PREFS.notify_leave) notify(`${this.display_name} left`)
- GLOBAL_CHAT.add_control_message({ leave: this })
+ this.room.chat.add_control_message({ leave: this })
}
on_relay(message: RelayMessage) {
if (message.chat) this.room.chat.add_message(this, message.chat)
@@ -91,7 +90,7 @@ export class RemoteUser extends User {
if (message.identify) {
this.name = message.identify.username
if (PREFS.notify_join) notify(`${this.display_name} joined`)
- GLOBAL_CHAT.add_control_message({ join: this })
+ this.room.chat.add_control_message({ join: this })
}
if (message.provide) {
const d = new_remote_resource(this, message.provide)