diff options
Diffstat (limited to 'client-web')
-rw-r--r-- | client-web/source/chat.ts | 14 | ||||
-rw-r--r-- | client-web/source/index.ts | 68 | ||||
-rw-r--r-- | client-web/source/keybinds.ts | 15 | ||||
-rw-r--r-- | client-web/source/menu.ts | 16 | ||||
-rw-r--r-- | client-web/source/protocol/mod.ts | 10 | ||||
-rw-r--r-- | client-web/source/room.ts | 23 | ||||
-rw-r--r-- | client-web/source/room_watches.ts | 9 | ||||
-rw-r--r-- | client-web/source/user/remote.ts | 5 |
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) |