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', 'volumeMapMove']) /* * * 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 Shadow = { oncreate({dom, attrs}) { dom.listener = () => attrs.app.isOn = !attrs.app.isOn addEventListener(attrs.key, dom.listener) }, onremove({dom, attrs}) { removeEventListener(attrs.key, dom.listener) }, view({attrs}) { const style = { zIndex: 999, backgroundColor: 'rgba(0, 0, 0, 0.2)', visibility: attrs.app.isOn ? 'unset' : 'hidden', position: 'fixed', right: 0, top: 0, height: '100vh', width: '100vw', display: 'flex', alignItems: 'center', justifyContent: 'center', } const onclick = ({target: {classList}}) => { classList.contains('shadow') && signal({kind: attrs.key}) } return m('.shadow', {style, onclick}, m(attrs.app)) }, } 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 ? Headers.map(h => m(...h)) : [ m('form.login', {onsubmit: Base.sendLogin}, m('input', attrs), m('button', 're-join'), ), ], m('span.error', State.info), ), State.isConnected ? Apps.map(a => m(...a)) : null, ) }, } const Headers = [ ['button', {onclick: Base.sendLogout}, 'log-out'], // m('button', {onclick: VolumeMap.toggle}, 'volume'), // m('button', {onclick: ScreenShare.toggle}, 'screen'), ] const Apps = [ // m(Shadow, {key: 'map-shadow', app: VolumeMap}), // m(Shadow, {key: 'screen-shadow', app: ScreenShare}), ] addEventListener('load', () => m.mount(document.body, Base))