TeaWeb/shared/js/ui/frames/control-bar/Renderer.tsx

569 lines
25 KiB
TypeScript

import {Registry} from "tc-shared/events";
import {
AwayState,
Bookmark,
ConnectionState,
ControlBarEvents,
ControlBarMode,
HostButtonInfo, MicrophoneDeviceInfo,
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"}>
<DropdownEntry
icon={ClientIcon.InputMuted}
text={<Translatable>Unmute microphone</Translatable>}
onClick={() => events.fire("action_toggle_microphone", { enabled: true })}
/>
<DropdownEntry
icon={ClientIcon.Settings}
text={<Translatable>Open microphone settings</Translatable>}
onClick={() => events.fire("action_open_microphone_settings", {})}
/>
<MicrophoneDeviceList />
</Button>
);
} 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"}>
<DropdownEntry
icon={ClientIcon.InputMuted}
text={<Translatable>Mute microphone</Translatable>}
onClick={() => events.fire("action_toggle_microphone", { enabled: false })}
/>
<DropdownEntry
icon={ClientIcon.Settings}
text={<Translatable>Open microphone settings</Translatable>}
onClick={() => events.fire("action_open_microphone_settings", {})}
/>
<MicrophoneDeviceList />
</Button>
);
} 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"}>
<DropdownEntry
icon={ClientIcon.ActivateMicrophone}
text={<Translatable>Enable your microphone</Translatable>}
onClick={() => events.fire("action_toggle_microphone", { enabled: true })}
/>
<DropdownEntry
icon={ClientIcon.Settings}
text={<Translatable>Open microphone settings</Translatable>}
onClick={() => events.fire("action_open_microphone_settings", {})}
/>
<MicrophoneDeviceList />
</Button>
);
}
}
/* This should be above all driver weights */
const kDriverWeightSelected = 1000;
const kDriverWeights = {
"MME": 100,
"Windows DirectSound": 80,
"Windows WASAPI": 50
};
const MicrophoneDeviceList = React.memo(() => {
const events = useContext(Events);
const [ deviceList, setDeviceList ] = useState<MicrophoneDeviceInfo[]>(() => {
events.fire("query_microphone_list");
return [];
});
events.reactUse("notify_microphone_list", event => setDeviceList(event.devices));
if(deviceList.length <= 1) {
/* we don't need a select here */
return null;
}
const devices: {[key: string]: { weight: number, device: MicrophoneDeviceInfo }} = {};
for(const entry of deviceList) {
const weight = entry.selected ? kDriverWeightSelected : (kDriverWeights[entry.driver] | 0);
if(typeof devices[entry.name] !== "undefined" && devices[entry.name].weight >= weight) {
continue;
}
devices[entry.name] = {
weight,
device: entry
}
}
return (
<>
<hr key={"hr"} />
{Object.values(devices).map(({ device }) => (
<DropdownEntry
text={device.name || tr("Unknown device name")}
key={"m-" + device.id}
icon={device.selected ? ClientIcon.Apply : undefined}
onClick={() => events.fire("action_toggle_microphone", { enabled: true, targetDeviceId: device.id })}
/>
))}
</>
);
});
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>
)
};