TeaWeb/shared/js/ui/tree/Channel.tsx
2020-10-04 16:15:50 +02:00

377 lines
No EOL
15 KiB
TypeScript

import {
BatchUpdateAssignment,
BatchUpdateType,
ReactComponentBase
} from "tc-shared/ui/react-elements/ReactComponentBase";
import * as React from "react";
import {ChannelEntry as ChannelEntryController, ChannelEvents, ChannelProperties} from "../../tree/Channel";
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
import {EventHandler, ReactEventHandler} from "tc-shared/events";
import {Settings, settings} from "tc-shared/settings";
import {TreeEntry, UnreadMarker} from "tc-shared/ui/tree/TreeEntry";
import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
import {ClientIcon} from "svg-sprites/client-icons";
import {VoiceConnectionStatus} from "tc-shared/connection/VoiceConnection";
import {AbstractServerConnection} from "tc-shared/connection/ConnectionBase";
import {getIconManager} from "tc-shared/file/Icons";
const channelStyle = require("./Channel.scss");
const viewStyle = require("./View.scss");
interface ChannelEntryIconsProperties {
channel: ChannelEntryController;
}
interface ChannelEntryIconsState {
icons_shown: boolean;
is_default: boolean;
is_password_protected: boolean;
is_music_quality: boolean;
is_moderated: boolean;
is_codec_supported: boolean;
custom_icon_id: number;
}
@ReactEventHandler<ChannelEntryIcons>(e => e.props.channel.events)
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
class ChannelEntryIcons extends ReactComponentBase<ChannelEntryIconsProperties, ChannelEntryIconsState> {
private readonly listenerVoiceStatusChange;
private serverConnection: AbstractServerConnection;
constructor(props) {
super(props);
this.listenerVoiceStatusChange = () => {
let stateUpdate = {} as ChannelEntryIconsState;
this.updateVoiceStatus(stateUpdate, this.props.channel.properties.channel_codec);
this.setState(stateUpdate);
}
}
componentDidMount() {
const voiceConnection = this.serverConnection.getVoiceConnection();
voiceConnection.events.on("notify_connection_status_changed", this.listenerVoiceStatusChange);
}
componentWillUnmount() {
const voiceConnection = this.serverConnection.getVoiceConnection();
voiceConnection.events.off("notify_connection_status_changed", this.listenerVoiceStatusChange);
}
protected defaultState(): ChannelEntryIconsState {
this.serverConnection = this.props.channel.channelTree.client.serverConnection;
const properties = this.props.channel.properties;
const status = {
icons_shown: this.props.channel.parsed_channel_name.alignment === "normal",
custom_icon_id: properties.channel_icon_id,
is_music_quality: properties.channel_codec === 3 || properties.channel_codec === 5,
is_codec_supported: false,
is_default: properties.channel_flag_default,
is_password_protected: properties.channel_flag_password,
is_moderated: properties.channel_needed_talk_power !== 0
}
this.updateVoiceStatus(status, this.props.channel.properties.channel_codec);
return status;
}
render() {
let icons = [];
if (!this.state.icons_shown)
return null;
if (this.state.is_default) {
icons.push(<ClientIconRenderer key={"icon-default"} icon={ClientIcon.ChannelDefault}
title={tr("Default channel")}/>);
}
if (this.state.is_password_protected) {
icons.push(<ClientIconRenderer key={"icon-protected"} icon={ClientIcon.Register}
title={tr("The channel is password protected")}/>);
}
if (this.state.is_music_quality) {
icons.push(<ClientIconRenderer key={"icon-music"} icon={ClientIcon.Music} title={tr("Music quality")}/>);
}
if (this.state.is_moderated) {
icons.push(<ClientIconRenderer key={"icon-moderated"} icon={ClientIcon.Moderated}
title={tr("Channel is moderated")}/>);
}
if (this.state.custom_icon_id) {
const connection = this.props.channel.channelTree.client;
icons.push(<RemoteIconRenderer
key={"icon-custom"}
title={tr("Client icon")}
icon={getIconManager().resolveIcon(this.state.custom_icon_id, connection.getCurrentServerUniqueId(), connection.handlerId)} />);
}
if (!this.state.is_codec_supported) {
icons.push(<div key={"icon-unsupported"} className={channelStyle.icon_no_sound}>
<div className={"icon_entry icon client-conflict-icon"}
title={tr("You don't support the channel codec")}/>
<div className={channelStyle.background}/>
</div>);
}
return (
<span className={channelStyle.icons}>
{icons}
</span>
);
}
@EventHandler<ChannelEvents>("notify_properties_updated")
private handlePropertiesUpdate(event: ChannelEvents["notify_properties_updated"]) {
let updates = {} as ChannelEntryIconsState;
if (typeof event.updated_properties.channel_icon_id !== "undefined")
updates.custom_icon_id = event.updated_properties.channel_icon_id;
if (typeof event.updated_properties.channel_codec !== "undefined" || typeof event.updated_properties.channel_codec_quality !== "undefined") {
const codec = event.channel_properties.channel_codec;
updates.is_music_quality = codec === 3 || codec === 5;
}
if (typeof event.updated_properties.channel_codec !== "undefined") {
this.updateVoiceStatus(updates, event.channel_properties.channel_codec);
}
if (typeof event.updated_properties.channel_flag_default !== "undefined")
updates.is_default = event.updated_properties.channel_flag_default;
if (typeof event.updated_properties.channel_flag_password !== "undefined")
updates.is_password_protected = event.updated_properties.channel_flag_password;
if (typeof event.updated_properties.channel_needed_talk_power !== "undefined")
updates.is_moderated = event.updated_properties.channel_needed_talk_power !== 0;
if (typeof event.updated_properties.channel_name !== "undefined")
updates.icons_shown = this.props.channel.parsed_channel_name.alignment === "normal";
this.setState(updates);
}
private updateVoiceStatus(state: ChannelEntryIconsState, currentCodec: number) {
const voiceConnection = this.serverConnection.getVoiceConnection();
const voiceState = voiceConnection.getConnectionState();
switch (voiceState) {
case VoiceConnectionStatus.Connected:
state.is_codec_supported = voiceConnection.decodingSupported(currentCodec);
break;
default:
state.is_codec_supported = false;
}
}
}
interface ChannelEntryIconProperties {
channel: ChannelEntryController;
}
@ReactEventHandler<ChannelEntryIcon>(e => e.props.channel.events)
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
class ChannelEntryIcon extends ReactComponentBase<ChannelEntryIconProperties, {}> {
private static readonly IconUpdateKeys: (keyof ChannelProperties)[] = [
"channel_name",
"channel_flag_password",
"channel_maxclients",
"channel_flag_maxclients_unlimited",
"channel_maxfamilyclients",
"channel_flag_maxfamilyclients_inherited",
"channel_flag_maxfamilyclients_unlimited",
];
render() {
if (this.props.channel.formattedChannelName() !== this.props.channel.channelName())
return null;
const channel_properties = this.props.channel.properties;
const subscribed = this.props.channel.flag_subscribed;
let channelIcon: ClientIcon;
if (channel_properties.channel_flag_password === true && !this.props.channel.cached_password()) {
channelIcon = subscribed ? ClientIcon.ChannelYellowSubscribed : ClientIcon.ChannelYellow;
} else if (!channel_properties.channel_flag_maxclients_unlimited && this.props.channel.clients().length >= channel_properties.channel_maxclients) {
channelIcon = subscribed ? ClientIcon.ChannelRedSubscribed : ClientIcon.ChannelRed;
} else if (!channel_properties.channel_flag_maxfamilyclients_unlimited && channel_properties.channel_maxfamilyclients >= 0 && this.props.channel.clients(true).length >= channel_properties.channel_maxfamilyclients) {
channelIcon = subscribed ? ClientIcon.ChannelRedSubscribed : ClientIcon.ChannelRed;
} else {
channelIcon = subscribed ? ClientIcon.ChannelGreenSubscribed : ClientIcon.ChannelGreen;
}
return <ClientIconRenderer icon={channelIcon} className={channelStyle.channelType} />;
}
@EventHandler<ChannelEvents>("notify_properties_updated")
private handlePropertiesUpdate(event: ChannelEvents["notify_properties_updated"]) {
for (const key of ChannelEntryIcon.IconUpdateKeys) {
if (key in event.updated_properties) {
this.forceUpdate();
return;
}
}
}
/* A client change may cause the channel to show another flag */
@EventHandler<ChannelEvents>("notify_clients_changed")
private handleClientsUpdated() {
this.forceUpdate();
}
@EventHandler<ChannelEvents>("notify_cached_password_updated")
private handleCachedPasswordUpdate() {
this.forceUpdate();
}
@EventHandler<ChannelEvents>("notify_subscribe_state_changed")
private handleSubscribeModeChanges() {
this.forceUpdate();
}
}
interface ChannelEntryNameProperties {
channel: ChannelEntryController;
}
@ReactEventHandler<ChannelEntryName>(e => e.props.channel.events)
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
class ChannelEntryName extends ReactComponentBase<ChannelEntryNameProperties, {}> {
render() {
const name = this.props.channel.parsed_channel_name;
let class_name: string;
let text: string;
if (name.repetitive) {
class_name = "align-repetitive";
text = name.text;
if (text.length) {
while (text.length < 8000)
text += text;
}
} else {
text = name.text;
class_name = "align-" + name.alignment;
}
return <div className={this.classList(channelStyle.containerChannelName, channelStyle[class_name])}>
<a className={channelStyle.channelName}>{text}</a>
</div>;
}
@EventHandler<ChannelEvents>("notify_properties_updated")
private handlePropertiesUpdate(event: ChannelEvents["notify_properties_updated"]) {
if (typeof event.updated_properties.channel_name !== "undefined")
this.forceUpdate();
}
}
export interface ChannelEntryViewProperties {
channel: ChannelEntryController;
depth: number;
offset: number;
}
const ChannelCollapsedIndicator = (props: { collapsed: boolean, onToggle: () => void }) => {
return <div className={channelStyle.containerArrow + (!props.collapsed ? " " + channelStyle.down : "")}>
<div className={"arrow " + (props.collapsed ? "right" : "down")} onClick={event => {
event.preventDefault();
props.onToggle();
}}/>
</div>
};
@ReactEventHandler<ChannelEntryView>(e => e.props.channel.events)
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
export class ChannelEntryView extends TreeEntry<ChannelEntryViewProperties, {}> {
shouldComponentUpdate(nextProps: Readonly<ChannelEntryViewProperties>, nextState: Readonly<{}>, nextContext: any): boolean {
if (nextProps.offset !== this.props.offset)
return true;
if (nextProps.depth !== this.props.depth)
return true;
return nextProps.channel !== this.props.channel;
}
render() {
const collapsed_indicator = this.props.channel.child_channel_head || this.props.channel.clients(false).length > 0;
return <div
className={this.classList(viewStyle.treeEntry, channelStyle.channelEntry, this.props.channel.isSelected() && viewStyle.selected)}
style={{paddingLeft: this.props.depth * 16 + 2, top: this.props.offset}}
onMouseUp={e => this.onMouseUp(e)}
onDoubleClick={() => this.onDoubleClick()}
onContextMenu={e => this.onContextMenu(e)}
onMouseDown={e => this.onMouseDown(e)}
>
<UnreadMarker entry={this.props.channel}/>
{collapsed_indicator &&
<ChannelCollapsedIndicator key={"collapsed-indicator"} onToggle={() => this.onCollapsedToggle()}
collapsed={this.props.channel.collapsed}/>}
<ChannelEntryIcon channel={this.props.channel}/>
<ChannelEntryName channel={this.props.channel}/>
<ChannelEntryIcons channel={this.props.channel}/>
</div>;
}
private onCollapsedToggle() {
this.props.channel.collapsed = !this.props.channel.collapsed;
}
private onMouseUp(event: React.MouseEvent) {
if (event.button !== 0) return; /* only left mouse clicks */
const channel = this.props.channel;
if (channel.channelTree.isClientMoveActive()) return;
channel.channelTree.events.fire("action_select_entries", {
entries: [channel],
mode: "auto"
});
}
private onDoubleClick() {
const channel = this.props.channel;
if (channel.channelTree.selection.is_multi_select()) return;
channel.joinChannel();
}
private onMouseDown(event: React.MouseEvent) {
if (event.buttons !== 4)
return;
spawnFileTransferModal(this.props.channel.getChannelId());
}
private onContextMenu(event: React.MouseEvent) {
if (settings.static(Settings.KEY_DISABLE_CONTEXT_MENU))
return;
event.preventDefault();
const channel = this.props.channel;
if (channel.channelTree.selection.is_multi_select() && channel.isSelected())
return;
channel.channelTree.events.fire("action_select_entries", {
entries: [channel],
mode: "exclusive"
});
channel.showContextMenu(event.pageX, event.pageY);
}
@EventHandler<ChannelEvents>("notify_select_state_change")
private handleSelectStateChange() {
this.forceUpdate();
}
}