const State = Object.seal({ username: null, websocket: null, online: [], get isConnected() { return State.websocket && State.websocket.readyState === 1 }, }) const params = (new URL(document.location)).searchParams /* * * SIGNALING * */ addEventListener('resize', () => m.redraw()) const wire = (message) => State.websocket.send(JSON.stringify(message)) const signal = (message) => dispatchEvent(new CustomEvent(message.kind, {detail: message})) const listen = (kind, handler) => { addEventListener(kind, handler) } listen('login', ({detail}) => { State.username = detail.value }) listen('logout', ({detail}) => { State.online = [] }) listen('state', ({detail}) => { delete detail.ts delete detail.kind Object.assign(State, detail) }) const doNotLog = new Set(['login', 'state', 'post', 'peerInfo']) /* * * UTILS * */ const autoFocus = (vnode) => { vnode.dom.focus() } /* * * WEBSOCKET * */ const connect = (username) => { const wsUrl = location.href.replace('http', 'ws') State.websocket = new WebSocket(wsUrl) State.websocket.onopen = (e) => { wire({kind: 'login', value: username}) } State.websocket.onmessage = (e) => { const message = JSON.parse(e.data) if(message.online) { const difference = (l1, l2) => l1.filter(u => !l2.includes(u)) difference(message.online, State.online).forEach(username => { if(username === State.username) return signal({kind: 'post', ts: message.ts, value: `${username} joined`}) }) difference(State.online, message.online).forEach(username => { if(username === State.username) return signal({kind: 'post', ts: message.ts, value: `${username} left`}) }) } if(!doNotLog.has(message.kind)) { console.log(message) } signal(message) m.redraw() } State.websocket.onclose = (e) => { State.online.forEach(signalPeerStop) if(!e.wasClean) { setTimeout(connect, 1000, username) } m.redraw() } } /* * * BASE * */ const Settings = { get(key) { try { return JSON.parse(localStorage.getItem(key)) } catch(error) { return null } }, set(key, value) { localStorage.setItem(key, JSON.stringify(value)) }, multiField: ([key, options]) => { let current = Settings.get(key) if(!options.includes(current)) { Settings.set(key, options[0]) } return m('.field', m('label', key), options.map(value => { const style = { fontWeight: Settings.get(key) == value ? 'bold' : 'unset' } const onclick = () => Settings.set(key, value) return m('button', {style, onclick}, `${value}`) }) ) }, view() { return m('.settings', Object.entries({ blackBars: [true, false], }).map(Settings.multiField) ) }, } const Base = { oncreate: () => { const randomName = ('' + Math.random()).substring(2) connect(localStorage.username || randomName) }, sendLogin: (e) => { e.preventDefault() const username = e.target.username.value localStorage.username = username connect(username) }, sendLogout: (e) => { e.preventDefault() wire({kind: 'logout'}) signal({kind: 'logout'}) }, view() { const attrs = { oncreate: autoFocus, name: 'username', autocomplete: 'off', value: localStorage.username, } const mainStyle = { position: 'fixed', width: '100%', display: 'grid', gridTemplateRows: 'auto 1fr', height: window.innerHeight + 'px', overflow: 'hidden', } const headerStyle = { display: 'grid', gridAutoFlow: 'column', justifyItems: 'start', marginRight: 'auto', } return m('main', {style: mainStyle}, m('header', {style: headerStyle}, State.isConnected ? [ m('button', {onclick: Base.sendLogout}, 'settings'), m(VideoConfig), m(ChatConfig), ] : [ m('form.login', {onsubmit: Base.sendLogin}, m('input', attrs), m('button', 're-join'), ), ], m('span.error', State.info), ), State.isConnected ? [ m(StreamContainer), m(Chat), ] : m(Settings), ) }, } m.mount(document.body, Base)