import {Registry} from "tc-shared/events"; import { AwayState, Bookmark, ConnectionState, ControlBarEvents, ControlBarMode, HostButtonInfo, MicrophoneState, VideoDeviceInfo, VideoState } from "tc-shared/ui/frames/control-bar/Definitions"; import * as React from "react"; import {useContext, useRef, useState} from "react"; import {DropdownEntry} from "tc-shared/ui/frames/control-bar/DropDown"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {Button} from "tc-shared/ui/frames/control-bar/Button"; import {spawnContextMenu} from "tc-shared/ui/ContextMenu"; import {ClientIcon} from "svg-sprites/client-icons"; import {createErrorModal} from "tc-shared/ui/elements/Modal"; import {VideoBroadcastType} from "tc-shared/connection/VideoConnection"; import {Settings, settings} from "tc-shared/settings"; const cssStyle = require("./Renderer.scss"); const cssButtonStyle = require("./Button.scss"); const Events = React.createContext<Registry<ControlBarEvents>>(undefined); const ModeContext = React.createContext<ControlBarMode>(undefined); const ConnectButton = () => { const events = useContext(Events); const [ state, setState ] = useState<ConnectionState>(() => { events.fire("query_connection_state"); return undefined; }); events.reactUse("notify_connection_state", event => setState(event.state)); let subentries = []; if(state?.multisession) { if(!state.currentlyConnected) { subentries.push( <DropdownEntry key={"connect-server"} icon={"client-connect"} text={<Translatable>Connect to a server</Translatable>} onClick={() => events.fire("action_connection_connect", { newTab: false })} /> ); } else { subentries.push( <DropdownEntry key={"disconnect-current-a"} icon={"client-disconnect"} text={<Translatable>Disconnect from current server</Translatable>} onClick={() => events.fire("action_connection_disconnect", { generally: false })} /> ); } if(state.generallyConnected) { subentries.push( <DropdownEntry key={"disconnect-current-b"} icon={"client-disconnect"} text={<Translatable>Disconnect from all servers</Translatable>} onClick={() => events.fire("action_connection_disconnect", { generally: true })}/> ); } subentries.push( <DropdownEntry key={"connect-new-tab"} icon={"client-connect"} text={<Translatable>Connect to a server in another tab</Translatable>} onClick={() => events.fire("action_connection_connect", { newTab: true })} /> ); } if(state?.currentlyConnected) { return ( <Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-disconnect"} tooltip={tr("Disconnect from server")} onToggle={() => events.fire("action_connection_disconnect", { generally: false })} key={"connected"}> {subentries} </Button> ); } else { return ( <Button colorTheme={"default"} autoSwitch={false} iconNormal={"client-connect"} tooltip={tr("Connect to a server")} onToggle={() => events.fire("action_connection_connect", { newTab: false })} key={"disconnected"}> {subentries} </Button> ); } }; const BookmarkRenderer = (props: { bookmark: Bookmark, refButton: React.RefObject<Button> }) => { const events = useContext(Events); if(typeof props.bookmark.children !== "undefined") { return ( <DropdownEntry key={props.bookmark.uniqueId} text={props.bookmark.label} > {props.bookmark.children.map(entry => <BookmarkRenderer bookmark={entry} key={entry.uniqueId} refButton={props.refButton} />)} </DropdownEntry> ); } else { return ( <DropdownEntry key={props.bookmark.uniqueId} icon={props.bookmark.icon} text={props.bookmark.label} onClick={() => events.fire("action_bookmark_connect", { bookmarkUniqueId: props.bookmark.uniqueId, newTab: false })} onAuxClick={event => event.button === 1 && events.fire("action_bookmark_connect", { bookmarkUniqueId: props.bookmark.uniqueId, newTab: true })} onContextMenu={event => { event.preventDefault(); props.refButton.current?.setState({ dropdownForceShow: true }); spawnContextMenu({ pageY: event.pageY, pageX: event.pageX }, [ { type: "normal", icon: ClientIcon.Connect, label: tr("Connect"), click: () => events.fire("action_bookmark_connect", { bookmarkUniqueId: props.bookmark.uniqueId, newTab: false }) }, { type: "normal", icon: ClientIcon.Connect, label: tr("Connect in a new tab"), click: () => events.fire("action_bookmark_connect", { bookmarkUniqueId: props.bookmark.uniqueId, newTab: true }) } ], () => props.refButton.current?.setState({ dropdownForceShow: false })); }} /> ); } }; const BookmarkButton = () => { const events = useContext(Events); const mode = useContext(ModeContext); const refButton = useRef<Button>(); const [ bookmarks, setBookmarks ] = useState<Bookmark[]>(() => { events.fire("query_bookmarks"); return []; }); events.reactUse("notify_bookmarks", event => setBookmarks(event.marks.slice())); let entries = []; if(mode === "main") { entries.push( <DropdownEntry icon={"client-bookmark_manager"} text={<Translatable>Manage bookmarks</Translatable>} onClick={() => events.fire("action_bookmark_manage")} key={"manage"} /> ); entries.push( <DropdownEntry icon={"client-bookmark_add"} text={<Translatable>Add current server to bookmarks</Translatable>} onClick={() => events.fire("action_bookmark_add_current_server")} key={"add"} /> ); } if(bookmarks.length > 0) { if(entries.length > 0) { entries.push(<hr key={"hr"} />); } entries.push(...bookmarks.map(mark => <BookmarkRenderer key={mark.uniqueId} bookmark={mark} refButton={refButton} />)); } return ( <Button ref={refButton} className={cssButtonStyle.buttonBookmarks + " " + cssStyle.hideSmallPopout} autoSwitch={false} iconNormal={"client-bookmark_manager"}> {entries} </Button> ) }; const AwayButton = () => { const events = useContext(Events); const [ state, setState ] = useState<AwayState>(() => { events.fire("query_away_state"); return undefined; }); events.on("notify_away_state", event => setState(event.state)); let dropdowns = []; if(state?.locallyAway) { dropdowns.push(<DropdownEntry key={"cgo"} icon={ClientIcon.Present} text={<Translatable>Go online</Translatable>} onClick={() => events.fire("action_toggle_away", { away: false, globally: false })} />); } else { dropdowns.push(<DropdownEntry key={"sas"} icon={ClientIcon.Away} text={<Translatable>Set away on this server</Translatable>} onClick={() => events.fire("action_toggle_away", { away: true, globally: false })} />); } dropdowns.push(<DropdownEntry key={"sam"} icon={ClientIcon.Away} text={<Translatable>Set away message on this server</Translatable>} onClick={() => events.fire("action_toggle_away", { away: true, globally: false, promptMessage: true })} />); dropdowns.push(<hr key={"-hr"} />); if(state?.globallyAway !== "none") { dropdowns.push(<DropdownEntry key={"goa"} icon={ClientIcon.Present} text={<Translatable>Go online for all servers</Translatable>} onClick={() => events.fire("action_toggle_away", { away: false, globally: true })} />); } if(state?.globallyAway !== "full") { dropdowns.push(<DropdownEntry key={"saa"} icon={ClientIcon.Away} text={<Translatable>Set away on all servers</Translatable>} onClick={() => events.fire("action_toggle_away", { away: true, globally: true })} />); } dropdowns.push(<DropdownEntry key={"sama"} icon={ClientIcon.Away} text={<Translatable>Set away message for all servers</Translatable>} onClick={() => events.fire("action_toggle_away", { away: true, globally: true, promptMessage: true })} />); return ( <Button autoSwitch={false} switched={!!state?.locallyAway} iconNormal={ClientIcon.Away} iconSwitched={ClientIcon.Present} onToggle={target => events.fire("action_toggle_away", { away: target, globally: false })} > {dropdowns} </Button> ); }; const VideoDeviceList = React.memo(() => { const events = useContext(Events); const [ devices, setDevices ] = useState<VideoDeviceInfo[]>(() => { events.fire("query_camera_list"); return []; }); events.reactUse("notify_camera_list", event => setDevices(event.devices)); if(devices.length === 0) { return null; } return ( <> <hr key={"hr"} /> {devices.map(device => ( <DropdownEntry text={device.name || tr("Unknown device name")} key={device.id} onClick={() => events.fire("action_toggle_video", {enable: true, broadcastType: "camera", deviceId: device.id, quickStart: true})} /> ))} </> ); }); const VideoButton = (props: { type: VideoBroadcastType }) => { const events = useContext(Events); const [ state, setState ] = useState<VideoState>(() => { events.fire("query_video_state", { broadcastType: props.type }); return "unsupported"; }); events.on("notify_video_state", event => event.broadcastType === props.type && setState(event.state)); let icon: ClientIcon = props.type === "camera" ? ClientIcon.VideoMuted : ClientIcon.ShareScreen; switch (state) { case "unsupported": { let tooltip = props.type === "camera" ? tr("Video broadcasting not supported") : tr("Screen sharing not supported"); let modalTitle = props.type === "camera" ? tr("Video broadcasting unsupported") : tr("Screen sharing unsupported"); let modalBody = props.type === "camera" ? tr("Video broadcasting isn't supported by the target server.") : tr("Screen sharing isn't supported by the target server."); let dropdownText = props.type === "camera" ? tr("Start video broadcasting") : tr("Start screen sharing"); return ( <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={icon} tooltip={tooltip} key={"unsupported"} onToggle={() => createErrorModal(modalTitle, modalBody).open()} > <DropdownEntry text={dropdownText} onClick={() => createErrorModal(modalTitle, modalBody).open()} /> </Button> ); } case "unavailable": { let tooltip = props.type === "camera" ? tr("Video broadcasting not available") : tr("Screen sharing not available"); let modalTitle = props.type === "camera" ? tr("Video broadcasting unavailable") : tr("Screen sharing unavailable"); let modalBody = props.type === "camera" ? tr("Video broadcasting isn't available right now.") : tr("Screen sharing isn't available right now."); let dropdownText = props.type === "camera" ? tr("Start video broadcasting") : tr("Start screen sharing"); return ( <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={icon} tooltip={tooltip} key={"unavailable"} onToggle={() => createErrorModal(modalTitle, modalBody).open()} > <DropdownEntry text={dropdownText} onClick={() => createErrorModal(modalTitle, modalBody).open()} /> </Button> ); } case "disconnected": case "disabled": { let tooltip = props.type === "camera" ? tr("Start video broadcasting") : tr("Start screen sharing"); let dropdownText = props.type === "camera" ? tr("Start video broadcasting") : tr("Start screen sharing"); return ( <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={icon} onToggle={() => events.fire("action_toggle_video", {enable: true, broadcastType: props.type, quickStart: settings.getValue(Settings.KEY_VIDEO_QUICK_SETUP)})} tooltip={tooltip} key={"enable"}> <DropdownEntry icon={icon} text={dropdownText} onClick={() => events.fire("action_toggle_video", {enable: true, broadcastType: props.type})} /> {props.type === "camera" ? <VideoDeviceList key={"list"} /> : null} </Button> ); } case "enabled": { let tooltip = props.type === "camera" ? tr("Stop video broadcasting") : tr("Stop screen sharing"); let dropdownTextManage = props.type === "camera" ? tr("Configure/change video broadcasting") : tr("Configure/change screen sharing"); let dropdownTextStop = props.type === "camera" ? tr("Stop video broadcasting") : tr("Stop screen sharing"); return ( <Button switched={false} colorTheme={"red"} autoSwitch={false} iconNormal={icon} onToggle={() => events.fire("action_toggle_video", {enable: false, broadcastType: props.type})} tooltip={tooltip} key={"disable"}> <DropdownEntry icon={icon} text={dropdownTextManage} onClick={() => events.fire("action_manage_video", { broadcastType: props.type })} /> <DropdownEntry icon={icon} text={dropdownTextStop} onClick={() => events.fire("action_toggle_video", {enable: false, broadcastType: props.type})} /> {props.type === "camera" ? <VideoDeviceList key={"list"} /> : null} </Button> ); } } } const MicrophoneButton = () => { const events = useContext(Events); const [ state, setState ] = useState<MicrophoneState>(() => { events.fire("query_microphone_state"); return undefined; }); events.on("notify_microphone_state", event => setState(event.state)); if(state === "muted") { return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={ClientIcon.InputMuted} tooltip={tr("Unmute microphone")} onToggle={() => events.fire("action_toggle_microphone", { enabled: true })} key={"muted"} />; } else if(state === "enabled") { return <Button colorTheme={"red"} autoSwitch={false} iconNormal={ClientIcon.InputMuted} tooltip={tr("Mute microphone")} onToggle={() => events.fire("action_toggle_microphone", { enabled: false })} key={"enabled"} />; } else { return <Button autoSwitch={false} iconNormal={ClientIcon.ActivateMicrophone} tooltip={tr("Enable your microphone on this server")} onToggle={() => events.fire("action_toggle_microphone", { enabled: true })} key={"disabled"} />; } } const SpeakerButton = () => { const events = useContext(Events); const [ enabled, setEnabled ] = useState<boolean>(() => { events.fire("query_speaker_state"); return true; }); events.on("notify_speaker_state", event => setEnabled(event.enabled)); if(enabled) { return <Button colorTheme={"red"} autoSwitch={false} iconNormal={ClientIcon.OutputMuted} tooltip={tr("Mute headphones")} onToggle={() => events.fire("action_toggle_speaker", { enabled: false })} key={"enabled"} />; } else { return <Button switched={true} colorTheme={"red"} autoSwitch={false} iconNormal={ClientIcon.OutputMuted} tooltip={tr("Unmute headphones")} onToggle={() => events.fire("action_toggle_speaker", { enabled: true })} key={"disabled"} />; } } const SubscribeButton = () => { const events = useContext(Events); const [ subscribe, setSubscribe ] = useState<boolean>(() => { events.fire("query_subscribe_state"); return true; }); events.on("notify_subscribe_state", event => setSubscribe(event.subscribe)); return <Button switched={subscribe} autoSwitch={false} iconNormal={ClientIcon.UnsubscribeFromAllChannels} iconSwitched={ClientIcon.SubscribeToAllChannels} className={cssStyle.hideSmallPopout} onToggle={flag => events.fire("action_toggle_subscribe", { subscribe: flag })} />; } const QueryButton = () => { const events = useContext(Events); const mode = useContext(ModeContext); const [ shown, setShown ] = useState<boolean>(() => { events.fire("query_query_state"); return true; }); events.on("notify_query_state", event => setShown(event.shown)); if(mode === "channel-popout") { return ( <Button switched={shown} autoSwitch={false} iconNormal={ClientIcon.ServerQuery} className={cssStyle.hideSmallPopout} onToggle={flag => events.fire("action_toggle_query", { show: flag })} key={"mode-channel-popout"} /> ); } else { let toggle; if(shown) { toggle = <DropdownEntry key={"query-show"} icon={ClientIcon.ToggleServerQueryClients} text={<Translatable>Hide server queries</Translatable>} onClick={() => events.fire("action_toggle_query", { show: false })} />; } else { toggle = <DropdownEntry key={"query-hide"} icon={ClientIcon.ToggleServerQueryClients} text={<Translatable>Show server queries</Translatable>} onClick={() => events.fire("action_toggle_query", { show: true })}/>; } return ( <Button switched={shown} autoSwitch={false} iconNormal={ClientIcon.ServerQuery} className={cssStyle.hideSmallPopout} onToggle={flag => events.fire("action_toggle_query", { show: flag })} key={"mode-full"} > {toggle} <DropdownEntry icon={ClientIcon.ServerQuery} text={<Translatable>Manage server queries</Translatable>} onClick={() => events.fire("action_query_manage")} key={"manage-entries"} /> </Button> ); } }; const HostButton = () => { const events = useContext(Events); const [ hostButton, setHostButton ] = useState<HostButtonInfo>(() => { events.fire("query_host_button"); return undefined; }); events.reactUse("notify_host_button", event => setHostButton(event.button)); if(!hostButton) { return null; } else { return ( <a className={cssButtonStyle.button + " " + cssButtonStyle.buttonHostbutton + " " + cssStyle.hideSmallPopout} title={hostButton.title || tr("Hostbutton")} onClick={event => { window.open(hostButton.target || hostButton.url, '_blank'); event.preventDefault(); }} > <img alt={tr("Hostbutton")} src={hostButton.url} /> </a> ); } }; export const ControlBar2 = (props: { events: Registry<ControlBarEvents>, className?: string }) => { const [ mode, setMode ] = useState<ControlBarMode>(() => { props.events.fire("query_mode"); return undefined; }); props.events.reactUse("notify_mode", event => setMode(event.mode)); const items = []; if(mode !== "channel-popout") { items.push(<ConnectButton key={"connect"} />); items.push(<BookmarkButton key={"bookmarks"} />); items.push(<div className={cssStyle.divider + " " + cssStyle.hideSmallPopout} key={"divider-1"} />); } items.push(<VideoButton key={"video"} type={"camera"} />); items.push(<VideoButton key={"screen"} type={"screen"} />); items.push(<MicrophoneButton key={"microphone"} />); items.push(<SpeakerButton key={"speaker"} />); items.push(<div className={cssStyle.divider + " " + cssStyle.hideSmallPopout} key={"divider-2"} />); items.push(<AwayButton key={"away"} />); items.push(<SubscribeButton key={"subscribe"} />); items.push(<QueryButton key={"query"} />); items.push(<div className={cssStyle.spacer} key={"spacer"} />); items.push(<HostButton key={"hostbutton"} />); return ( <Events.Provider value={props.events}> <ModeContext.Provider value={mode}> <div className={cssStyle.controlBar + " " + cssStyle["mode-" + mode] + " " + props.className}> {items} </div> </ModeContext.Provider> </Events.Provider> ) };