import range from 'lodash/range' import ReconnectingWebSocket from 'reconnecting-websocket' import { h, Fragment, render } from 'preact' import { useEffect, useState, useCallback, useRef } from 'preact/hooks' import { State } from 'xstate' import styled, { css } from 'styled-components' import '../index.css' import { GRID_COUNT } from '../constants' import SoundIcon from '../static/volume-up-solid.svg' import ReloadIcon from '../static/redo-alt-solid.svg' import LifeRingIcon from '../static/life-ring-regular.svg' function emptyStateIdxMap() { return new Map( range(GRID_COUNT * GRID_COUNT).map((idx) => [ idx, { streamId: null, url: null, state: State.from({}), isListening: false, }, ]), ) } function App({ wsEndpoint }) { const wsRef = useRef() const [isConnected, setIsConnected] = useState(false) const [streams, setStreams] = useState([]) const [customStreams, setCustomStreams] = useState([]) const [stateIdxMap, setStateIdxMap] = useState(emptyStateIdxMap()) useEffect(() => { const ws = new ReconnectingWebSocket(wsEndpoint, [], { maxReconnectionDelay: 5000, minReconnectionDelay: 1000 + Math.random() * 500, reconnectionDelayGrowFactor: 1.1, }) ws.addEventListener('open', () => setIsConnected(true)) ws.addEventListener('close', () => setIsConnected(false)) ws.addEventListener('message', (ev) => { const msg = JSON.parse(ev.data) if (msg.type === 'state') { const { streams: newStreams, views, customStreams: newCustomStreams, } = msg.state const newStateIdxMap = emptyStateIdxMap() const allStreams = [...newStreams, ...newCustomStreams] for (const viewState of views) { const { pos, url } = viewState.context if (!url) { continue } const streamId = allStreams.find((d) => d.Link === url)?._id const state = State.from(viewState.state) const isListening = state.matches('displaying.running.listening') for (const space of pos.spaces) { Object.assign(newStateIdxMap.get(space), { streamId, url, state, isListening, }) } } setStateIdxMap(newStateIdxMap) setStreams(newStreams) setCustomStreams(newCustomStreams) } else { console.warn('unexpected ws message', msg) } }) wsRef.current = ws }, []) const handleSetView = useCallback( (idx, streamId) => { const newSpaceIdxMap = new Map(stateIdxMap) const url = [...streams, ...customStreams].find((d) => d._id === streamId) ?.Link if (url) { newSpaceIdxMap.set(idx, { ...newSpaceIdxMap.get(idx), streamId, url, }) } else { newSpaceIdxMap.set(idx, { ...newSpaceIdxMap.get(idx), streamId: null, url: null, }) } const views = Array.from(newSpaceIdxMap, ([space, { url }]) => [ space, url, ]) wsRef.current.send(JSON.stringify({ type: 'set-views', views })) }, [streams, customStreams, stateIdxMap], ) const handleSetListening = useCallback((idx, listening) => { wsRef.current.send( JSON.stringify({ type: 'set-listening-view', viewIdx: listening ? idx : null, }), ) }, []) const handleReloadView = useCallback((idx) => { wsRef.current.send( JSON.stringify({ type: 'reload-view', viewIdx: idx, }), ) }, []) const handleBrowse = useCallback((url) => { wsRef.current.send( JSON.stringify({ type: 'browse', url, }), ) }, []) const handleChangeCustomStream = useCallback((idx, customStream) => { let newCustomStreams = [...customStreams] newCustomStreams[idx] = customStream newCustomStreams = newCustomStreams.filter((s) => s.Link) wsRef.current.send( JSON.stringify({ type: 'set-custom-streams', streams: newCustomStreams, }), ) }) return (