TeaWeb/shared/js/ui/modal/channel-edit/Renderer.tsx

1241 lines
No EOL
50 KiB
TypeScript

import * as React from "react";
import {useContext, useEffect, useRef, useState} from "react";
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
import {IpcRegistryDescription, Registry} from "tc-shared/events";
import {
ChannelEditablePermissions,
ChannelEditablePermissionValue,
ChannelEditableProperty,
ChannelEditEvents,
ChannelEditPermissionsState,
ChannelPropertyPermission
} from "tc-shared/ui/modal/channel-edit/Definitions";
import {BoxedInputField, Select} from "tc-shared/ui/react-elements/InputField";
import {Switch} from "tc-shared/ui/react-elements/Switch";
import {Button} from "tc-shared/ui/react-elements/Button";
import {Tab, TabEntry} from "tc-shared/ui/react-elements/Tab";
import {Settings, settings} from "tc-shared/settings";
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
import {IconTooltip} from "tc-shared/ui/react-elements/Tooltip";
import {RadioButton} from "tc-shared/ui/react-elements/RadioButton";
import {Slider} from "tc-shared/ui/react-elements/Slider";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
import {getIconManager} from "tc-shared/file/Icons";
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
import {ChannelNameAlignment, ChannelNameParser} from "tc-shared/tree/Channel";
const cssStyle = require("./Renderer.scss");
const ModalTypeContext = React.createContext<"channel-edit" | "channel-create">("channel-edit");
const EventContext = React.createContext<Registry<ChannelEditEvents>>(undefined);
type ChannelPropertyState<T extends keyof ChannelEditableProperty> = {
setPropertyValue: (value: ChannelEditableProperty[T], dontUpdateController?: boolean) => void
} & ({
propertyState: "loading",
propertyValue: undefined,
} | {
propertyState: "normal" | "applying",
propertyValue: ChannelEditableProperty[T],
})
const kPropertyLoading = "____loading_____";
function useProperty<T extends keyof ChannelEditableProperty>(property: T) : ChannelPropertyState<T> {
const events = useContext(EventContext);
const [ value, setValue ] = useState<ChannelEditableProperty[T] | typeof kPropertyLoading>(() => {
events.fire("query_property", { property: property });
return kPropertyLoading;
});
events.reactUse("notify_property", event => {
if(event.property !== property) {
return;
}
setValue(event.value as any);
}, undefined, []);
if(value === kPropertyLoading) {
return {
propertyState: "loading",
propertyValue: undefined,
setPropertyValue: _value => {}
};
} else {
return {
propertyState: "normal",
propertyValue: value,
setPropertyValue: (value, dontUpdateController?: boolean) => {
setValue(value);
if(!dontUpdateController) {
events.fire("action_change_property", { property: property, value: value });
}
}
};
}
}
function usePropertyPermission<T extends keyof ChannelPropertyPermission>(permission: T, defaultValue: ChannelPropertyPermission[T]) : ChannelPropertyPermission[T] {
const events = useContext(EventContext);
const [ value, setValue ] = useState<ChannelPropertyPermission[T]>(() => {
events.fire("query_property_permission", { permission: permission });
return defaultValue;
});
events.reactUse("notify_property_permission", event => event.permission === permission && setValue(event.value as any));
return value;
}
function usePermission(permission: ChannelEditablePermissions) : { permissionValue: ChannelEditablePermissionValue, setPermissionValue: (newValue: number, dontUpdateController?: boolean) => void } {
const events = useContext(EventContext);
const [ value, setValue ] = useState<ChannelEditablePermissionValue>(() => {
events.fire("query_permission", { permission: permission });
return { state: "loading" };
});
events.reactUse("notify_permission", event => event.permission === permission && setValue(event.value));
return {
permissionValue: value,
setPermissionValue: (newValue, dontUpdateController) => {
setValue({ value: newValue, state: "editable" });
if(!dontUpdateController) {
events.fire("action_change_permission", { permission: permission, value: newValue });
}
}
};
}
function useValidationState<T extends keyof ChannelEditableProperty>(property: T) : boolean {
const events = useContext(EventContext);
const [ valid, setValid ] = useState(() => {
events.fire("query_property_valid_state", { property: property });
return true;
});
events.reactUse("notify_property_validate_state", event => event.property === property && setValid(event.valid));
return valid;
}
const ChannelNameType = (props: { selected: ChannelNameAlignment }) => {
}
const ChannelName = React.memo(() => {
const modalType = useContext(ModalTypeContext);
const { propertyValue, propertyState, setPropertyValue } = useProperty("name");
const editable = usePropertyPermission("name", modalType === "channel-create");
const valid = useValidationState("name");
const refSelect = useRef<HTMLSelectElement>();
const setValue = (text: string | undefined, localOnly: boolean) => {
let rawName;
switch(propertyValue.hasParent ? "normal" : refSelect.current.value) {
case "center":
rawName = "[cspacer" + propertyValue.spacerUniqueId + "]" + text;
break;
case "left":
rawName = "[lspacer" + propertyValue.spacerUniqueId + "]" + text;
break;
case "right":
rawName = "[rspacer" + propertyValue.spacerUniqueId + "]" + text;
break;
case "repetitive":
rawName ="[*spacer" + propertyValue.spacerUniqueId + "]" + text;
break;
default:
case "normal":
rawName = text;
break;
}
setPropertyValue({
rawName,
parsedName: text,
hasParent: propertyValue.hasParent,
spacerUniqueId: propertyValue.spacerUniqueId,
maxNameLength: propertyValue.maxNameLength,
parsedAlignment: propertyValue.parsedAlignment
}, localOnly);
}
return (
<div className={joinClassList(cssStyle.channelName, propertyValue?.hasParent && cssStyle.hasParent)}>
<Select
value={propertyValue?.parsedAlignment || "loading"}
className={cssStyle.select}
onChange={() => setValue(propertyValue.parsedName, false)}
type={"boxed"}
title={useTr("Channel name mode")}
refSelect={refSelect}
disabled={!editable}
>
<option value={"loading"} style={{ display: "none" }}>{useTr("loading")}</option>
<option value={"normal"}>{useTr("Normal Name")}</option>
<option value={"center"}>{useTr("Centered")}</option>
<option value={"left"}>{useTr("Left Aligned")}</option>
<option value={"right"}>{useTr("Right Aligned")}</option>
<option value={"repetitive"}>{useTr("Repetitive")}</option>
</Select>
<BoxedInputField
className={cssStyle.input}
disabled={!editable || propertyState !== "normal"}
value={(propertyValue?.hasParent ? propertyValue?.rawName : propertyValue?.parsedName) || ""}
placeholder={propertyState === "normal" ? tr("Channel name") : tr("loading")}
onInput={value => setValue(value, true)}
onChange={value => setValue(value, false)}
isInvalid={!valid}
maxLength={propertyValue?.maxNameLength}
/>
</div>
);
});
const ChannelIcon = () => {
const events = useContext(EventContext);
const { propertyValue, propertyState, setPropertyValue } = useProperty("icon");
const permissionGranted = usePropertyPermission("icon", false);
const icon = getIconManager().resolveIcon(propertyValue ? propertyValue.iconId : 0, propertyValue?.remoteIcon.serverUniqueId, propertyValue?.remoteIcon.handlerId);
const enabled = permissionGranted && propertyState === "normal";
return (
<div className={cssStyle.iconContainer + " " + (enabled ? "" : cssStyle.disabled)}>
<div className={cssStyle.preview} onClick={() => enabled && events.fire("action_icon_select")}>
<RemoteIconRenderer icon={icon} key={"icon-" + icon.iconId} />
</div>
<div className={cssStyle.containerDropdown}>
<div className={cssStyle.button}>
<div className="arrow down" />
</div>
<div className={cssStyle.dropdown}>
<div className={cssStyle.entry} onClick={() => enabled && events.fire("action_icon_select")}><Translatable>Edit icon</Translatable></div>
<div className={cssStyle.entry} onClick={() => {
if(!enabled) {
return;
}
setPropertyValue({
iconId: 0,
remoteIcon: {
iconId: 0,
handlerId: propertyValue.remoteIcon.handlerId,
serverUniqueId: propertyValue.remoteIcon.serverUniqueId
}
});
}}><Translatable>Remove icon</Translatable></div>
</div>
</div>
</div>
)
}
const ChannelPassword = React.memo(() => {
const { propertyValue, propertyState, setPropertyValue } = useProperty("password");
const { editable } = usePropertyPermission("password", { enforced: false, editable: false });
const valid = useValidationState("password");
return (
<BoxedInputField
className={cssStyle.input}
disabled={!editable || propertyState !== "normal"}
value={propertyValue?.state === "set" ? propertyValue.password || "placeholder" : ""}
placeholder={propertyState === "normal" ? tr("Password") : tr("loading")}
onInput={value => {
if(value.length > 0) {
setPropertyValue({ state: "set", password: value }, true);
} else {
setPropertyValue({ state: "clear" }, true);
}
}}
onChange={value => {
if(value.length > 0) {
setPropertyValue({ state: "set", password: value });
} else {
setPropertyValue({ state: "clear" });
}
}}
isInvalid={!valid}
type={"password"}
/>
);
});
const ChannelTopic = React.memo(() => {
const { propertyValue, propertyState, setPropertyValue } = useProperty("topic");
const editable = usePropertyPermission("topic", false);
return (
<BoxedInputField
className={cssStyle.input}
disabled={!editable || propertyState !== "normal"}
value={propertyValue || ""}
placeholder={propertyState === "normal" ? tr("Topic") : tr("loading")}
onInput={value => setPropertyValue(value, true)}
onChange={value => setPropertyValue(value)}
/>
);
});
const BBCodeEditor = React.memo((props: { text: string, placeholder: string, disabled: boolean, callbackEdit: (value: string) => void }) => {
const refText = useRef<HTMLTextAreaElement>();
const insertTag = (open: string, close: string) => {
const element = refText.current;
if(!element) {
return;
}
if (element.selectionStart || element.selectionStart == 0) {
const startPos = element.selectionStart;
const endPos = element.selectionEnd;
element.value = element.value.substring(0, startPos) + open + element.value.substring(startPos, endPos) + close + element.value.substring(endPos);
element.selectionEnd = endPos + open.length;
element.selectionStart = element.selectionEnd;
} else {
element.value += open + close;
element.selectionEnd = element.value.length - close.length;
element.selectionStart = element.selectionEnd;
}
element.focus();
};
return (
<div className={cssStyle.bbcodeEditor}>
<div className={cssStyle.toolbar}>
<div className={cssStyle.button + " " + cssStyle.buttonBold} onClick={() => insertTag("[b]", "[/b]")}>B</div>
<div className={cssStyle.button + " " + cssStyle.buttonItalic} onClick={() => insertTag("[i]", "[/i]")}>I</div>
<div className={cssStyle.button + " " + cssStyle.buttonUnderline} onClick={() => insertTag("[u]", "[/u]")}>U</div>
<label className={cssStyle.button + " " + cssStyle.buttonColor}>
<input type="color" value="#FF0000" onInput={event => insertTag("[color=" + event.currentTarget.value + "]", "[/color]")} />
<a className={cssStyle.rainbowLetter}>C</a>
</label>
</div>
<textarea
ref={refText}
className={cssStyle.input + " input-boxed"}
placeholder={props.placeholder}
defaultValue={props.text}
disabled={props.disabled}
onChange={event => props.callbackEdit(event.target.value)}
/>
</div>
);
});
const ChannelDescription = React.memo(() => {
const { propertyValue, propertyState, setPropertyValue } = useProperty("description");
const editable = usePropertyPermission("description", false);
return (
<BBCodeEditor
text={propertyValue || ""}
disabled={!editable || propertyState !== "normal"}
placeholder={useTr("Description")}
callbackEdit={setPropertyValue}
/>
);
});
const BlockProperty = (props: { children: [React.ReactElement | string, React.ReactElement] }) => {
return (
<div className={cssStyle.blockProperty}>
<div className={cssStyle.title}>
{props.children[0]}
</div>
{props.children[1]}
</div>
)
};
const ChannelType = React.memo(() => {
const { propertyValue, propertyState, setPropertyValue } = useProperty("type");
const permissions = usePropertyPermission("channelType", { default: false, temporary: false, semiPermanent: false, permanent: false });
return (
<BlockProperty>
<Translatable>Channel Type</Translatable>
<Select
type={"boxed"}
value={propertyValue?.type || "loading"}
disabled={propertyState !== "normal" || (!permissions.temporary && !permissions.semiPermanent && !permissions.permanent) || propertyValue.originallyDefault}
onChange={event => setPropertyValue({ type: event.target.value as any })}
>
<option value={"temporary"} disabled={!permissions.temporary}>{useTr("Temporary")}</option>
<option value={"semi-permanent"} disabled={!permissions.semiPermanent}>{useTr("Semi-Permanent")}</option>
<option value={"permanent"} disabled={!permissions.permanent}>{useTr("Permanent")}</option>
<option value={"default"} disabled={!permissions.default}>{useTr("Default Channel")}</option>
<option value={"loading"} style={{ display: "none" }}>{useTr("loading")}</option>
</Select>
</BlockProperty>
);
});
const SidebarType = React.memo(() => {
const mode = useContext(ModalTypeContext);
let { propertyValue, propertyState, setPropertyValue } = useProperty("sideBar");
const permissionGranted = usePropertyPermission("sidebarMode", false);
if(propertyValue === "unknown" && mode === "channel-create") {
/* the default mode */
propertyValue = "conversation";
}
return (
<BlockProperty>
<Translatable>Sidebar Type</Translatable>
<Select
type={"boxed"}
value={propertyValue || "loading"}
disabled={propertyState !== "normal" || !permissionGranted || propertyValue === "not-supported"}
onChange={event => setPropertyValue(event.target.value as any)}
>
<option value={"unknown"} style={{ display: "none" }}>{useTr("Unknown mode")}</option>
<option value={"conversation"}>{useTr("Conversation")}</option>
<option value={"description"}>{useTr("Channel description")}</option>
<option value={"file-transfer"}>{useTr("File browser")}</option>
<option value={"not-supported"} style={{ display: "none" }}>{useTr("Option not supported")}</option>
<option value={"loading"} style={{ display: "none" }}>{useTr("loading")}</option>
</Select>
</BlockProperty>
);
});
type SimpleCodecQualityTemplate = { id: string, codec: number, quality: number, name: () => string };
const kCodecTemplates: SimpleCodecQualityTemplate[] = [
{
id: "mobile",
codec: 4,
quality: 4,
name: () => useTr("Mobile")
},
{
id: "voice",
codec: 4,
quality: 6,
name: () => useTr("Voice")
},
{
id: "music",
codec: 5,
quality: 6,
name: () => useTr("Music")
},
{
id: "loading",
codec: undefined,
quality: undefined,
name: () => useTr("loading")
}
];
const SimpleCodecQuality = React.memo(() => {
const { propertyValue, propertyState, setPropertyValue } = useProperty("codec");
const { opusMusic, opusVoice } = usePropertyPermission("codec", { opusVoice: false, opusMusic: false });
const value = kCodecTemplates.find(template => template.codec === propertyValue?.type && template.quality === propertyValue?.quality)?.id || "advanced";
const hasPermission = (codec: SimpleCodecQualityTemplate) => codec.codec === 4 ? opusMusic : codec.codec === 5 ? opusVoice : false;
return (
<BlockProperty>
<Translatable>Voice quality</Translatable>
<Select
type={"boxed"}
value={value}
disabled={propertyState !== "normal" || (!opusMusic || !opusVoice)}
onChange={event => {
const template = kCodecTemplates.find(template => template.id === event.target.value);
if(!template || !template.codec) {
return;
}
setPropertyValue({ type: template.codec, quality: template.quality });
}}
>
{
kCodecTemplates.map(template => (
<option style={{ display: template.id === "loading" ? "none" : undefined }} key={template.id} value={template.id} disabled={!hasPermission(template)}>{template.name()}</option>
))
}
<option style={{ display: "none" }} key={"advanced"} value={"advanced"}>{useTr("Custom (Advanced settings)")}</option>
</Select>
</BlockProperty>
);
});
const AdvancedCodecPresets = React.memo(() => {
const { propertyValue, propertyState, setPropertyValue } = useProperty("codec");
const { opusMusic, opusVoice } = usePropertyPermission("codec", { opusVoice: false, opusMusic: false });
const value = kCodecTemplates.find(template => template.codec === propertyValue?.type && template.quality === propertyValue?.quality)?.id || "advanced";
const hasPermission = (codec: SimpleCodecQualityTemplate) => codec.codec === 4 ? opusMusic : codec.codec === 5 ? opusVoice : false;
return (
<div className={cssStyle.radioSelect}>
{
kCodecTemplates.filter(template => !!template.codec).map(template => (
<RadioButton
key={template.id}
name={"advanced-codec"}
selected={template.id === value}
disabled={!hasPermission(template) || propertyState !== "normal"}
onChange={() => setPropertyValue({ quality: template.quality, type: template.codec})}
>
<div>{template.name()}</div>
</RadioButton>
))
}
<RadioButton key={"value"} name={"advanced-codec"} selected={value === "advanced"} onChange={() => {}} disabled={true}>
<div><Translatable>Custom</Translatable></div>
</RadioButton>
</div>
);
});
const CustomCodec = React.memo(() => {
const { propertyValue, propertyState, setPropertyValue } = useProperty("codec");
const { opusMusic, opusVoice } = usePropertyPermission("codec", { opusVoice: false, opusMusic: false });
return (
<BlockProperty>
<Translatable>Codec</Translatable>
<Select
type={"boxed"}
disabled={!opusVoice && !opusMusic || propertyState !== "normal"}
value={propertyValue?.type + "" || "loading"}
onChange={event => {
const value = parseInt(event.target.value);
if(!isNaN(value)) {
setPropertyValue({ type: value, quality: propertyValue.quality });
}
}}
>
<option value={"0"} style={{ display: "none" }}>{useTr("Unknown")}</option>
<option value={"4"}>{useTr("Opus Voice")}</option>
<option value={"5"}>{useTr("Opus Music")}</option>
<option value={"loading"} style={{ display: "none" }}>{useTr("loading")}</option>
</Select>
</BlockProperty>
);
});
const CustomQuality = React.memo(() => {
const { propertyValue, propertyState, setPropertyValue } = useProperty("codec");
const permissionGranted = usePropertyPermission("codecQuality", false);
const refQuality = useRef<HTMLDivElement>();
const refSlider = useRef<Slider>();
useEffect(() => {
if(propertyState === "normal") {
refSlider.current?.setState({ value: propertyValue.quality });
}
});
return (
<BlockProperty>
<div>
<Translatable>Quality:</Translatable>&nbsp;
<div style={{ display: "inline" }} ref={refQuality}>{propertyValue?.quality}</div>
</div>
<Slider
ref={refSlider}
minValue={1}
maxValue={10}
stepSize={1}
value={propertyValue?.quality || 1}
onInput={newValue => {
if(refQuality.current) {
refQuality.current.innerText = newValue + "";
}
}}
onChange={newValue => {
setPropertyValue({ quality: newValue, type: propertyValue.type });
}}
disabled={propertyState !== "normal" || !permissionGranted}
/>
</BlockProperty>
)
});
const kBandwidthInfo = [
/* SPEEX narrow */ [2.49, 2.69, 2.93, 3.17, 3.17, 3.56, 3.56, 4.05, 4.05, 4.44, 5.22],
/* SPEEX wide */ [2.69, 2.93, 3.17, 3.42, 3.76, 4.25, 4.74, 5.13, 5.62, 6.40, 7.37],
/* SPEEX ultra */ [2.73, 3.12, 3.37, 3.61, 4.00, 4.49, 4.93, 5.32, 5.81, 6.59, 7.57],
/* CELT */ [6.10, 6.10, 7.08, 7.08, 7.08, 8.06, 8.06, 8.06, 8.06, 10.01, 13.92],
/* Opus Voice */ [2.73, 3.22, 3.71, 4.20, 4.74, 5.22, 5.71, 6.20, 6.74, 7.23, 7.71],
/* Opus Music */ [3.08, 3.96, 4.83, 5.71, 6.59, 7.47, 8.35, 9.23, 10.11, 10.99, 11.87]
];
const CodecInfo = React.memo(() => {
const { propertyValue, propertyState } = useProperty("codec");
return (
<div className={cssStyle.codecInfo}>
<p>
Estimated needed bandwidth: <a>{propertyState !== "normal" ? tr("loading") : ((kBandwidthInfo[propertyValue.type] || [])[propertyValue.quality] || 0) + "KiB/s"}</a>
</p>
<p className={cssStyle.hint}>
For bad internet connection, lower settings are recommend to reduce bandwidth.
</p>
</div>
);
});
const SortOrder = React.memo(() => {
const mode = useContext(ModalTypeContext);
const { propertyValue, propertyState, setPropertyValue } = useProperty("sortingOrder");
const permissionGranted = usePropertyPermission("sortingOrder", false);
let value;
if(propertyState === "loading") {
value = "loading";
} else if(propertyValue.previousChannelId === 0) {
if(mode === "channel-create") {
/* We automatically use the last channel */
value = propertyValue.availableChannels?.last()?.channelId || "0";
} else {
value = "0";
}
} else {
value = propertyValue.availableChannels?.find(channel => channel.channelId === propertyValue.previousChannelId) ? propertyValue.previousChannelId.toString() : "unknown";
}
return (
<BlockProperty>
<Translatable>Sort this channel after</Translatable>
<Select
type={"boxed"}
value={value}
disabled={propertyState !== "normal" || !permissionGranted}
onChange={event => {
const channelId = parseInt(event.target.value);
if(isNaN(channelId)) { return; }
setPropertyValue({ availableChannels: propertyValue?.availableChannels, previousChannelId: channelId });
}}
>
<option style={{ display: "none" }} key={"0"} value={"0"} />
{
propertyValue?.availableChannels.map(channel => (
<option key={channel.channelId} value={channel.channelId}>{channel.channelName}</option>
))
}
<option style={{ display: "none" }} key={"loading"} value={"loading"}>{useTr("loading")}</option>
<option style={{ display: "none" }} key={"unknown"} value={"unknown"}>{useTr("Unknown channel id")}: {propertyValue?.previousChannelId}</option>
</Select>
</BlockProperty>
);
});
const TalkPower = React.memo(() => {
const { propertyValue, propertyState, setPropertyValue } = useProperty("talkPower");
const permissionGranted = usePropertyPermission("talkPower", false);
return (
<BlockProperty>
&nbsp;
<BoxedInputField
value={propertyValue === undefined ? "" : propertyValue + ""}
disabled={!permissionGranted || propertyState !== "normal"}
prefix={useTr("Talk power") + ":"}
rightIcon={() => (
<IconTooltip className={cssStyle.tooltip}>
<Translatable>Required power to talk in this channel</Translatable>
</IconTooltip>
)}
onInput={value => value.match(/^[0-9]{0,9}$/) && setPropertyValue(value ? parseInt(value) : 0, true)}
onChange={value => value.match(/^[0-9]{0,9}$/) && setPropertyValue(value ? parseInt(value) : 0)}
/>
</BlockProperty>
);
});
const MaxClients = React.memo(() => {
const { propertyValue, setPropertyValue } = useProperty("maxUsers");
const permissionGranted = usePropertyPermission("maxUsers", false);
const channelType = useProperty("type");
let disabled = !permissionGranted || channelType.propertyValue?.type === "temporary" || channelType.propertyValue?.type === "default";
useEffect(() => {
if(channelType.propertyValue?.type === "temporary" || channelType.propertyValue?.type === "default") {
setPropertyValue("unlimited");
disabled = true;
}
});
return (
<div className={cssStyle.radioSelect}>
<RadioButton
name={"max-users"}
selected={propertyValue === "unlimited"}
onChange={() => setPropertyValue("unlimited")}
disabled={disabled}
>
<div>
<Translatable>Unlimited</Translatable>
</div>
</RadioButton>
<RadioButton
name={"max-users"}
selected={typeof propertyValue === "number"}
onChange={() => setPropertyValue(0)}
disabled={disabled}
>
<div>
<Translatable>Limited</Translatable>
</div>
<BoxedInputField
value={typeof propertyValue === "number" ? propertyValue + "" : "0"}
disabled={typeof propertyValue !== "number"}
className={cssStyle.limit}
rightIcon={() => (
<IconTooltip className={cssStyle.tooltip}>
<Translatable>Max users which could join the channel</Translatable>
</IconTooltip>
)}
size={"small"}
onInput={value => value.match(/^[0-9]{0,9}$/) && setPropertyValue(value ? parseInt(value) : 0, true)}
onChange={value => value.match(/^[0-9]{0,9}$/) && setPropertyValue(value ? parseInt(value) : 0)}
/>
</RadioButton>
</div>
);
});
const MaxFamilyClients = React.memo(() => {
const { propertyValue, setPropertyValue } = useProperty("maxFamilyUsers");
const permissionGranted = usePropertyPermission("maxFamilyUsers", false);
const channelType = useProperty("type");
let disabled = !permissionGranted || channelType.propertyValue?.type === "temporary" || channelType.propertyValue?.type === "default";
useEffect(() => {
if(channelType.propertyValue?.type === "temporary" || channelType.propertyValue?.type === "default") {
setPropertyValue("unlimited");
disabled = true;
}
});
return (
<div className={cssStyle.radioSelect}>
<RadioButton
name={"max-family-users"}
selected={propertyValue === "unlimited"}
onChange={() => setPropertyValue("unlimited")}
disabled={disabled}
>
<div>
<Translatable>Unlimited</Translatable>
</div>
</RadioButton>
<RadioButton
name={"max-family-users"}
selected={propertyValue === "inherited"}
onChange={() => setPropertyValue("inherited")}
disabled={disabled}
>
<div>
<Translatable>Inherited</Translatable>
</div>
</RadioButton>
<RadioButton
name={"max-family-users"}
selected={typeof propertyValue === "number"}
onChange={() => setPropertyValue(0)}
disabled={disabled}
>
<div>
<Translatable>Limited</Translatable>
</div>
<BoxedInputField
value={typeof propertyValue === "number" ? propertyValue + "" : "0"}
disabled={typeof propertyValue !== "number"}
className={cssStyle.limit}
rightIcon={() => (
<IconTooltip className={cssStyle.tooltip}>
<Translatable>Max users which could join the channel family</Translatable>
</IconTooltip>
)}
size={"small"}
onInput={value => value.match(/^[0-9]{0,9}$/) && setPropertyValue(value ? parseInt(value) : 0, true)}
onChange={value => value.match(/^[0-9]{0,9}$/) && setPropertyValue(value ? parseInt(value) : 0)}
/>
</RadioButton>
</div>
);
});
const DeleteDelay = React.memo(() => {
const { propertyValue, setPropertyValue, propertyState } = useProperty("deleteDelay");
const permission = usePropertyPermission("deleteDelay", { maxDelay: -1, editable: false });
return (
<BlockProperty>
<Translatable>Delete delay:</Translatable>
<div className={cssStyle.deleteDelay}>
<BoxedInputField
className={cssStyle.input}
size={"small"}
value={propertyValue + "" || "0"}
onInput={value => value.match(/^[0-9]{0,9}$/) && setPropertyValue(value ? parseInt(value) : 0, true)}
onChange={value => value.match(/^[0-9]{0,9}$/) && setPropertyValue(value ? parseInt(value) : 0)}
rightIcon={() => (
<IconTooltip className={cssStyle.tooltip}>
<Translatable>Time in seconds before the channel gets deleted when its empty.</Translatable>
</IconTooltip>
)}
disabled={propertyState !== "normal" || !permission.editable}
/>
<Button
color={"blue"}
type={"small"}
className={cssStyle.button + " " + (permission.maxDelay === -1 ? cssStyle.hidden : "")}
onClick={() => setPropertyValue(permission.maxDelay)}
>
<Translatable>Max</Translatable>
</Button>
</div>
</BlockProperty>
);
});
const EncryptVoiceData = React.memo(() => {
const { propertyValue, setPropertyValue, propertyState } = useProperty("encryptedVoiceData");
const permissionGranted = usePropertyPermission("encryptVoiceData", false);
let serverHint;
switch (propertyValue?.serverSetting) {
case "encrypted":
serverHint = (
<React.Fragment key={"encrypted"}>
(<Translatable>Overridden by the server with encrypted</Translatable>)
</React.Fragment>
);
break;
case "unencrypted":
serverHint = (
<React.Fragment key={"unencrypted"}>
(<Translatable>Overridden by the server with unencrypted</Translatable>)
</React.Fragment>
);
break;
}
return (
<BlockProperty>
<Translatable>Encrypt voice data:</Translatable>
<div className={cssStyle.encryptVoiceData}>
<Switch
className={cssStyle.switch}
disabled={propertyState !== "normal" || !permissionGranted}
value={!!propertyValue?.encrypted}
onChange={newValue => setPropertyValue({ encrypted: newValue, serverSetting: propertyValue.serverSetting })}
/>
{serverHint}
</div>
</BlockProperty>
);
});
const GeneralContainer = () => {
return (
<div className={cssStyle.containerGeneral}>
<div className={cssStyle.row}>
<ChannelName />
<ChannelIcon />
</div>
<div className={cssStyle.row}>
<ChannelPassword />
</div>
{
/*
<div className={cssStyle.row}>
<ChannelTopic />
</div>
*/
}
<div className={cssStyle.row}>
<ChannelDescription />
</div>
</div>
);
};
const TabAdvancedSettingsStandard = React.memo(() => {
return (
<div className={cssStyle.containerTab}>
<div className={cssStyle.top}>
<div className={cssStyle.left}>
<div className={cssStyle.header}><Translatable>Channel and Sidebar Type</Translatable></div>
<div className={cssStyle.content}>
<ChannelType />
<SidebarType />
</div>
</div>
<div className={cssStyle.right}>
<div className={cssStyle.header}><Translatable>Sorting and Talk power</Translatable></div>
<div className={cssStyle.content}>
<SortOrder />
<TalkPower />
</div>
</div>
</div>
<div className={cssStyle.bottom}>
<div className={cssStyle.left}>
<div className={cssStyle.header}><Translatable>Max users</Translatable></div>
<div className={cssStyle.content}>
<MaxClients />
</div>
</div>
<div className={cssStyle.right}>
<div className={cssStyle.header}><Translatable>Max family users</Translatable></div>
<div className={cssStyle.content}>
<MaxFamilyClients />
</div>
</div>
</div>
</div>
);
});
const TabAdvancedSettingsAudio = () => {
return (
<div className={cssStyle.containerTab}>
<div className={cssStyle.top}>
<div className={cssStyle.left}>
<div className={cssStyle.header}>
<Translatable>Presets</Translatable>
</div>
<div className={cssStyle.content}>
<AdvancedCodecPresets />
</div>
</div>
<div className={cssStyle.right}>
<div className={cssStyle.header}>
<Translatable>Custom Settings</Translatable>
</div>
<div className={cssStyle.content}>
<CustomCodec />
<CustomQuality />
</div>
</div>
</div>
<div className={cssStyle.bottom}>
<div className={cssStyle.codecInfoContainer}>
<div className={cssStyle.header}>
<Translatable>Information</Translatable>
</div>
<div className={cssStyle.content}>
<CodecInfo />
</div>
</div>
</div>
</div>
);
}
const PermissionName = (props: { permission: ChannelEditablePermissions }) => {
switch (props.permission) {
case "join": return <Translatable>Join</Translatable>;
case "view": return <Translatable>View</Translatable>;
case "view-description": return <Translatable>View description</Translatable>;
case "subscribe": return <Translatable>Subscribe</Translatable>;
case "modify": return <Translatable>Modify</Translatable>;
case "delete": return <Translatable>Delete</Translatable>;
case "browse": return <Translatable>Browse</Translatable>;
case "upload": return <Translatable>Upload</Translatable>;
case "download": return <Translatable>Download</Translatable>;
case "rename": return <Translatable>Rename</Translatable>;
case "directory-create": return <Translatable>Create directory</Translatable>;
case "file-delete": return <Translatable>Delete file</Translatable>;
default: return props.permission;
}
}
const PermissionTooltip = (props: { permission: ChannelEditablePermissions }) => {
switch (props.permission) {
case "join": return <Translatable>Required power to join this channel</Translatable>;
case "view": return <Translatable>Required power to see this channel</Translatable>;
case "view-description": return <Translatable>Required power to see the channel description</Translatable>;
case "subscribe": return <Translatable>Required power to subscribe to this channel</Translatable>;
case "modify": return <Translatable>Required power to modify this channel permissions</Translatable>;
case "delete": return <Translatable>Required power to delete this channel</Translatable>;
case "browse": return <Translatable>Required power to browse all files and directories</Translatable>;
case "upload": return <Translatable>Required power to upload files</Translatable>;
case "download": return <Translatable>Required power to download files</Translatable>;
case "rename": return <Translatable>Required power to rename files within this channel</Translatable>;
case "directory-create": return <Translatable>Required power to create a directory</Translatable>;
case "file-delete": return <Translatable>Required power to delete a directory or file</Translatable>;
default: return <VariadicTranslatable text={"Missing description for {}"}>{props.permission}</VariadicTranslatable>;
}
}
const PermissionRenderer = (props: { permission: ChannelEditablePermissions }) => {
const { permissionValue, setPermissionValue } = usePermission(props.permission);
const placeholderLoading = useTr("loading");
const placeholderNotSupported = useTr("Not supported");
return (
<BlockProperty>
<PermissionName permission={props.permission} />
<BoxedInputField
type={"number"}
className={cssStyle.input}
size={"small"}
value={permissionValue.state === "editable" || permissionValue.state === "readonly" || permissionValue.state === "applying" ? permissionValue.value.toString() : ""}
placeholder={permissionValue.state === "unsupported" ? placeholderNotSupported : permissionValue.state === "loading" ? placeholderLoading : ""}
disabled={permissionValue.state !== "editable"}
onInput={value => value.match(/^[0-9]{0,9}$/) && setPermissionValue(value ? parseInt(value) : 0, true)}
onChange={value => value.match(/^[0-9]{0,9}$/) && setPermissionValue(value ? parseInt(value) : 0)}
rightIcon={() => <IconTooltip className={cssStyle.tooltip}><PermissionTooltip permission={props.permission} /></IconTooltip>}
/>
</BlockProperty>
);
}
const PermissionTable = () => (
<React.Fragment>
<div className={cssStyle.left}>
<div className={cssStyle.header}>
<Translatable>Regular needed powers:</Translatable>
</div>
<div className={cssStyle.content}>
<PermissionRenderer permission={"join"} />
<PermissionRenderer permission={"view"} />
<PermissionRenderer permission={"view-description"} />
<PermissionRenderer permission={"subscribe"} />
<PermissionRenderer permission={"modify"} />
<PermissionRenderer permission={"delete"} />
</div>
</div>
<div className={cssStyle.right}>
<div className={cssStyle.header}>
<Translatable>File transfer needed powers:</Translatable>
</div>
<div className={cssStyle.content}>
<PermissionRenderer permission={"browse"} />
<PermissionRenderer permission={"upload"} />
<PermissionRenderer permission={"download"} />
<PermissionRenderer permission={"rename"} />
<PermissionRenderer permission={"directory-create"} />
<PermissionRenderer permission={"file-delete"} />
</div>
</div>
</React.Fragment>
);
const TabAdvancedSettingsPermissions = React.memo(() => {
const events = useContext(EventContext);
const [ state, setState ] = useState<ChannelEditPermissionsState>(() => {
events.fire("query_permissions");
return { state: "loading" };
});
events.reactUse("notify_permissions", event => setState(event.state));
let content;
switch (state.state) {
case "loading":
content = (
<div className={cssStyle.overlay} key={"loading"}>
<div className={cssStyle.text}><Translatable>loading</Translatable> <LoadingDots /></div>
</div>
);
break;
case "no-permissions":
content = (
<div className={cssStyle.overlay} key={"no-permissions"}>
<div className={cssStyle.text}>
<Translatable>You don't have permissions to see the channel permissions.</Translatable><br/>
<Translatable>Failed on permission</Translatable>: <code>{state.failedPermission}</code>
</div>
</div>
);
break;
case "error":
content = (
<div className={cssStyle.overlay} key={"error"}>
<div className={cssStyle.text + " " + cssStyle.error}>
<Translatable>Failed to fetch channel permissions:</Translatable><br />
{state.reason}
</div>
</div>
);
break;
case "editable":
content = <PermissionTable key={"table"} />;
break;
}
return (
<div className={cssStyle.containerTab + " " + cssStyle.containerPermissions}>
{content}
</div>
);
});
const TabAdvancedSettingsMisc = () => {
return (
<div className={cssStyle.containerTab + " " + cssStyle.tabMisc}>
<div className={cssStyle.header}><Translatable>Other Settings</Translatable></div>
<DeleteDelay />
<EncryptVoiceData />
</div>
);
}
const ContainerAdvancedSettings = React.memo(() => {
return (
<Tab defaultTab={"standard"} permanentRender={true}>
<TabEntry id={"standard"}>
<Translatable>Standard</Translatable>
<TabAdvancedSettingsStandard />
</TabEntry>
<TabEntry id={"audio"}>
<Translatable>Audio</Translatable>
<TabAdvancedSettingsAudio />
</TabEntry>
<TabEntry id={"permissions"}>
<Translatable>Permissions</Translatable>
<TabAdvancedSettingsPermissions />
</TabEntry>
<TabEntry id={"misc"}>
<Translatable>Misc</Translatable>
<TabAdvancedSettingsMisc />
</TabEntry>
</Tab>
);
});
const ContainerSimpleSettings = React.memo(() => {
return (
<React.Fragment>
<div className={cssStyle.left}>
<div className={cssStyle.header}><Translatable>Channel Options</Translatable></div>
<div className={cssStyle.content}>
<ChannelType />
<SimpleCodecQuality />
</div>
</div>
<div className={cssStyle.right}>
<div className={cssStyle.header}><Translatable>Sorting and Talk power</Translatable></div>
<div className={cssStyle.content}>
<SortOrder />
<TalkPower />
</div>
</div>
</React.Fragment>
);
});
const SettingsContainer = React.memo(() => {
const [ advancedEnabled, setAdvancedEnabled ] = useState(settings.getValue(Settings.KEY_CHANNEL_EDIT_ADVANCED));
useEffect(() => settings.globalChangeListener(Settings.KEY_CHANNEL_EDIT_ADVANCED, newValue => setAdvancedEnabled(newValue)));
return (
<div className={cssStyle.containerSettings}>
<div className={cssStyle.settingsAdvanced + " " + (advancedEnabled ? "" : cssStyle.hidden)}>
<ContainerAdvancedSettings />
</div>
<div className={cssStyle.settingsSimple + " " + (!advancedEnabled ? "" : cssStyle.hidden)}>
<ContainerSimpleSettings />
</div>
</div>
)
});
const ButtonToggleAdvanced = React.memo(() => {
const [ advancedEnabled, setAdvancedEnabled ] = useState(settings.getValue(Settings.KEY_CHANNEL_EDIT_ADVANCED));
settings.globalChangeListener(Settings.KEY_CHANNEL_EDIT_ADVANCED, newValue => setAdvancedEnabled(newValue));
return (
<Switch
className={cssStyle.advancedSwitch}
value={advancedEnabled}
label={<Translatable>Advanced mode</Translatable>}
onChange={newState => settings.setValue(Settings.KEY_CHANNEL_EDIT_ADVANCED, newState)}
/>
)
});
const Buttons = React.memo(() => {
const type = useContext(ModalTypeContext);
const events = useContext(EventContext);
return (
<div className={cssStyle.buttons}>
<ButtonToggleAdvanced />
<div className={cssStyle.cancelCreate}>
<Button type={"small"} color={"red"} onClick={() => events.fire("action_cancel")}><Translatable>Cancel</Translatable></Button>
<Button type={"small"} color={"green"} onClick={() => events.fire("action_apply")}>
{type === "channel-create" ? <Translatable>Create</Translatable> : <Translatable>Apply</Translatable>}
</Button>
</div>
</div>
)
});
class ChannelEditModal extends AbstractModal {
private readonly events: Registry<ChannelEditEvents>;
private readonly isChannelCreate: boolean;
constructor(events: IpcRegistryDescription<ChannelEditEvents>, isChannelCreate: boolean) {
super();
this.events = Registry.fromIpcDescription(events);
this.isChannelCreate = isChannelCreate;
}
renderBody(): React.ReactElement {
return (
<EventContext.Provider value={this.events}>
<ModalTypeContext.Provider value={this.isChannelCreate ? "channel-create" : "channel-edit"}>
<div className={cssStyle.container}>
<GeneralContainer />
<SettingsContainer />
<Buttons />
</div>
</ModalTypeContext.Provider>
</EventContext.Provider>
);
}
renderTitle(): string | React.ReactElement {
if(this.isChannelCreate) {
return <Translatable key={"create"}>Create channel</Translatable>;
} else {
/* TODO: Channel name? */
return <Translatable key={"edit"}>Edit channel</Translatable>;
}
}
color(): "none" | "blue" {
return "blue";
}
}
export = ChannelEditModal;