TeaWeb/shared/js/ui/modal/connect/Renderer.tsx
2021-03-23 13:42:32 +01:00

420 lines
No EOL
17 KiB
TypeScript

import {
ConnectHistoryEntry,
ConnectUiEvents, ConnectUiVariables, kUnknownHistoryServerUniqueId,
} from "tc-shared/ui/modal/connect/Definitions";
import * as React from "react";
import {useContext} from "react";
import {IpcRegistryDescription, Registry} from "tc-shared/events";
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {Button} from "tc-shared/ui/react-elements/Button";
import {ControlledFlatInputField, ControlledSelect, FlatInputField} from "tc-shared/ui/react-elements/InputField";
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
import {ClientIcon} from "svg-sprites/client-icons";
import {getIconManager} from "tc-shared/file/Icons";
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
import {CountryCode} from "tc-shared/ui/react-elements/CountryCode";
const EventContext = React.createContext<Registry<ConnectUiEvents>>(undefined);
const VariablesContext = React.createContext<UiVariableConsumer<ConnectUiVariables>>(undefined);
const ConnectDefaultNewTabContext = React.createContext<boolean>(false);
const cssStyle = require("./Renderer.scss");
const InputServerAddress = React.memo(() => {
const events = useContext(EventContext);
const newTab = useContext(ConnectDefaultNewTabContext);
const variables = useContext(VariablesContext);
const address = variables.useVariable("server_address");
const addressValid = variables.useReadOnly("server_address_valid", undefined, true) || address.localValue !== address.remoteValue;
return (
<ControlledFlatInputField
value={address.localValue?.currentAddress || ""}
placeholder={address.remoteValue?.defaultAddress || tr("Please enter a address")}
className={cssStyle.inputAddress}
label={<Translatable>Server address</Translatable>}
labelType={"static"}
invalid={addressValid ? undefined : <Translatable>Please enter a valid server address</Translatable>}
editable={address.status === "loaded"}
onInput={value => address.setValue({ currentAddress: value }, true)}
onBlur={() => address.setValue({ currentAddress: address.localValue?.currentAddress })}
onEnter={() => {
/* Setting the address just to ensure */
address.setValue({ currentAddress: address.localValue?.currentAddress });
events.fire("action_connect", { newTab });
}}
/>
)
});
const InputServerPassword = () => {
const variables = useContext(VariablesContext);
const password = variables.useVariable("password");
return (
<FlatInputField
className={cssStyle.inputPassword}
value={!password.localValue?.hashed ? password.localValue?.password || "" : ""}
placeholder={password.localValue?.hashed ? tr("Password Hidden") : null}
type={"password"}
label={<Translatable>Server password</Translatable>}
labelType={password.localValue?.hashed ? "static" : "floating"}
onInput={value => password.setValue({ password: value, hashed: false }, true)}
onBlur={() => password.setValue(password.localValue)}
/>
)
}
const InputNickname = () => {
const variables = useContext(VariablesContext);
const nickname = variables.useVariable("nickname");
const validState = variables.useReadOnly("nickname_valid", undefined, true) || nickname.localValue !== nickname.remoteValue;
return (
<ControlledFlatInputField
className={cssStyle.inputNickname}
value={nickname.localValue?.currentNickname || ""}
placeholder={nickname.remoteValue ? nickname.remoteValue.defaultNickname ? nickname.remoteValue.defaultNickname : tr("Please enter a nickname") : tr("loading...")}
label={<Translatable>Nickname</Translatable>}
labelType={"static"}
invalid={validState ? undefined : <Translatable>Nickname too short or too long</Translatable>}
onInput={value => nickname.setValue({ currentNickname: value }, true)}
onBlur={() => nickname.setValue({ currentNickname: nickname.localValue?.currentNickname })}
/>
);
}
const InputProfile = () => {
const events = useContext(EventContext);
const variables = useContext(VariablesContext);
const profiles = variables.useVariable("profiles");
const selectedProfile = profiles.remoteValue?.profiles.find(profile => profile.id === profiles.remoteValue?.selected);
let invalidMarker;
if(profiles) {
if(!profiles.remoteValue?.selected) {
/* We have to select a profile. */
/* TODO: Only show if we've tried to press connect */
//invalidMarker = <Translatable key={"no-profile"}>Please select a profile</Translatable>;
} else if(!selectedProfile) {
invalidMarker = <Translatable key={"no-profile"}>Unknown select profile</Translatable>;
} else if(!selectedProfile.valid) {
invalidMarker = <Translatable key={"invalid"}>Selected profile has an invalid config</Translatable>
}
}
return (
<div className={cssStyle.inputProfile}>
<ControlledSelect
className={cssStyle.input}
value={selectedProfile ? selectedProfile.id : profiles.remoteValue?.selected ? "invalid" : profiles ? "no-selected" : "loading"}
type={"flat"}
label={<Translatable>Connect profile</Translatable>}
invalid={invalidMarker}
invalidClassName={cssStyle.invalidFeedback}
onChange={event => profiles.setValue({ selected: event.target.value })}
>
<option value={"no-selected"} style={{ display: "none" }}>{useTr("please select")}</option>
<option value={"invalid"} style={{ display: "none" }}>{useTr("unknown profile")}</option>
<option value={"loading"} style={{ display: "none" }}>{useTr("loading") + "..."}</option>
<React.Fragment>
{
profiles.remoteValue?.profiles.map(profile => (
<option key={"profile-" + profile.id} value={profile.id}>{profile.name}</option>
))
}
</React.Fragment>
</ControlledSelect>
<Button className={cssStyle.button} type={"small"} color={"none"} onClick={() => events.fire("action_manage_profiles")}>
<Translatable>Profiles</Translatable>
</Button>
</div>
);
}
const ConnectContainer = () => (
<div className={cssStyle.connectContainer}>
<div className={cssStyle.row}>
{/* <InputServerAddress /> */}
<InputServerAddress />
<InputServerPassword />
</div>
<div className={cssStyle.row + " " + cssStyle.smallColumn}>
<InputNickname />
<InputProfile />
</div>
</div>
);
const ButtonToggleHistory = () => {
const variables = useContext(VariablesContext);
const historyShown = variables.useVariable("historyShown");
let body;
if(historyShown.localValue) {
body = (
<React.Fragment key={"hide"}>
<div className={cssStyle.containerText}><Translatable>Hide connect history</Translatable></div>
<div className={cssStyle.containerArrow}><div className={"arrow down"} /></div>
</React.Fragment>
);
} else {
body = (
<React.Fragment key={"show"}>
<div className={cssStyle.containerText}><Translatable>Show connect history</Translatable></div>
<div className={cssStyle.containerArrow}><div className={"arrow up"} /></div>
</React.Fragment>
);
}
return (
<Button
className={cssStyle.buttonShowHistory + " " + cssStyle.button}
type={"small"}
color={"none"}
onClick={() => historyShown.setValue(!historyShown.localValue)}
>
{body}
</Button>
);
}
const ButtonsConnect = () => {
const connectNewTab = useContext(ConnectDefaultNewTabContext);
const events = useContext(EventContext);
let left;
if(connectNewTab) {
left = (
<Button
color={"green"}
type={"small"}
key={"same-tab"}
onClick={() => events.fire("action_connect", { newTab: false })}
className={cssStyle.button}
>
<Translatable>Connect in the same tab</Translatable>
</Button>
);
} else {
left = (
<Button
color={"green"}
type={"small"}
key={"new-tab"}
onClick={() => events.fire("action_connect", { newTab: true })}
className={cssStyle.button}
>
<Translatable>Connect in a new tab</Translatable>
</Button>
);
}
return (
<div className={cssStyle.buttonsConnect}>
{left}
<Button
color={"green"}
type={"small"}
onClick={() => events.fire("action_connect", { newTab: connectNewTab })}
className={cssStyle.button}
>
<Translatable>Connect</Translatable>
</Button>
</div>
);
};
const ButtonContainer = () => (
<div className={cssStyle.buttonContainer}>
<ButtonToggleHistory />
<ButtonsConnect />
</div>
);
const HistoryTableEntryConnectCount = React.memo((props: { entry: ConnectHistoryEntry }) => {
const targetType = props.entry.uniqueServerId === kUnknownHistoryServerUniqueId ? "address" : "server-unique-id";
const target = targetType === "address" ? props.entry.targetAddress : props.entry.uniqueServerId;
const value = useContext(VariablesContext).useReadOnly("history_connections", {
target,
targetType
}, -1);
if(value >= 0) {
return <React.Fragment key={"set"}>{value}</React.Fragment>;
} else {
return null;
}
});
const HistoryTableEntry = React.memo((props: { entry: ConnectHistoryEntry, selected: boolean }) => {
const events = useContext(EventContext);
const connectNewTab = useContext(ConnectDefaultNewTabContext);
const variables = useContext(VariablesContext);
const info = variables.useReadOnly("history_entry", { serverUniqueId: props.entry.uniqueServerId }, undefined);
const icon = getIconManager().resolveIcon(info ? info.icon.iconId : 0, info?.icon.serverUniqueId, info?.icon.handlerId);
return (
<div className={cssStyle.row + " " + (props.selected ? cssStyle.selected : "")}
onClick={event => {
if(event.isDefaultPrevented()) {
return;
}
events.fire("action_select_history", { id: props.entry.id });
}}
onDoubleClick={() => events.fire("action_connect", { newTab: connectNewTab })}
>
<div className={cssStyle.column + " " + cssStyle.delete} onClick={event => {
event.preventDefault();
if(props.entry.uniqueServerId === kUnknownHistoryServerUniqueId) {
events.fire("action_delete_history", {
targetType: "address",
target: props.entry.targetAddress
});
} else {
events.fire("action_delete_history", {
targetType: "server-unique-id",
target: props.entry.uniqueServerId
});
}
}}>
<ClientIconRenderer icon={ClientIcon.Delete} />
</div>
<div className={cssStyle.column + " " + cssStyle.name}>
<RemoteIconRenderer icon={icon} className={cssStyle.iconContainer} />
{info?.name}
</div>
<div className={cssStyle.column + " " + cssStyle.address}>
{props.entry.targetAddress}
</div>
<div className={cssStyle.column + " " + cssStyle.password}>
{info ? info.password ? tr("Yes") : tr("No") : ""}
</div>
<div className={cssStyle.column + " " + cssStyle.country}>
{info ? <CountryCode alphaCode={info.country || "xx"} key={"country"} /> : null}
</div>
<div className={cssStyle.column + " " + cssStyle.clients}>
{info && info.maxClients !== -1 ? `${info.clients}/${info.maxClients}` : ""}
</div>
<div className={cssStyle.column + " " + cssStyle.connections}>
<HistoryTableEntryConnectCount entry={props.entry} />
</div>
</div>
);
});
const HistoryTable = () => {
const history = useContext(VariablesContext).useReadOnly("history", undefined, undefined);
let body;
if(history) {
if(history.history.length > 0) {
body = history.history.map(entry => <HistoryTableEntry entry={entry} key={"entry-" + entry.id} selected={entry.id === history.selected} />);
} else {
body = (
<div className={cssStyle.bodyEmpty} key={"no-history"}>
<a><Translatable>No connections yet made</Translatable></a>
</div>
);
}
} else {
return null;
}
return (
<div className={cssStyle.historyTable}>
<div className={cssStyle.head}>
<div className={cssStyle.column + " " + cssStyle.delete} />
<div className={cssStyle.column + " " + cssStyle.name}>
<a title={useTr("Name")}><Translatable>Name</Translatable></a>
</div>
<div className={cssStyle.column + " " + cssStyle.address}>
<a title={useTr("Address")}><Translatable>Address</Translatable></a>
</div>
<div className={cssStyle.column + " " + cssStyle.password}>
<a title={useTr("Password")}><Translatable>Password</Translatable></a>
</div>
<div className={cssStyle.column + " " + cssStyle.country}>
<a title={useTr("Country")}><Translatable>Country</Translatable></a>
</div>
<div className={cssStyle.column + " " + cssStyle.clients}>
<a title={useTr("Clients")}><Translatable>Clients</Translatable></a>
</div>
<div className={cssStyle.column + " " + cssStyle.connections}>
<a title={useTr("Connections")}><Translatable>Connections</Translatable></a>
</div>
</div>
<div className={cssStyle.body}>
{body}
</div>
</div>
)
}
const HistoryContainer = () => {
const variables = useContext(VariablesContext);
const historyShown = variables.useReadOnly("historyShown", undefined, false);
return (
<div className={joinClassList(cssStyle.historyContainer, historyShown && cssStyle.shown)}>
<HistoryTable />
</div>
)
}
class ConnectModal extends AbstractModal {
private readonly events: Registry<ConnectUiEvents>;
private readonly variables: UiVariableConsumer<ConnectUiVariables>;
private readonly connectNewTabByDefault: boolean;
constructor(events: IpcRegistryDescription<ConnectUiEvents>, variables: IpcVariableDescriptor<ConnectUiVariables>, connectNewTabByDefault: boolean) {
super();
this.variables = createIpcUiVariableConsumer(variables);
this.events = Registry.fromIpcDescription(events);
this.connectNewTabByDefault = connectNewTabByDefault;
}
protected onDestroy() {
super.onDestroy();
this.variables.destroy();
}
renderBody(): React.ReactElement {
return (
<EventContext.Provider value={this.events}>
<VariablesContext.Provider value={this.variables}>
<ConnectDefaultNewTabContext.Provider value={this.connectNewTabByDefault}>
<div className={cssStyle.container}>
<ConnectContainer />
<ButtonContainer />
<HistoryContainer />
</div>
</ConnectDefaultNewTabContext.Provider>
</VariablesContext.Provider>
</EventContext.Provider>
);
}
renderTitle(): string | React.ReactElement {
return <Translatable>Connect to a server</Translatable>;
}
verticalAlignment(): "top" | "center" | "bottom" {
return "top";
}
}
export = ConnectModal;