import { BatchUpdateAssignment, BatchUpdateType, ReactComponentBase } from "tc-shared/ui/react-elements/ReactComponentBase"; import * as React from "react"; import { ClientEntry as ClientEntryController, ClientEvents, ClientProperties, ClientType, LocalClientEntry, MusicClientEntry } from "../client"; import {EventHandler, ReactEventHandler} from "tc-shared/events"; import {Group, GroupEvents} from "tc-shared/permission/GroupManager"; import {Settings, settings} from "tc-shared/settings"; import {TreeEntry, UnreadMarker} from "tc-shared/ui/tree/TreeEntry"; import {LocalIconRenderer} from "tc-shared/ui/react-elements/Icon"; import * as DOMPurify from "dompurify"; const clientStyle = require("./Client.scss"); const viewStyle = require("./View.scss"); interface ClientIconProperties { client: ClientEntryController; } @ReactEventHandler(e => e.props.client.events) @BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE) class ClientSpeakIcon extends ReactComponentBase { private static readonly IconUpdateKeys: (keyof ClientProperties)[] = [ "client_away", "client_input_hardware", "client_output_hardware", "client_output_muted", "client_input_muted", "client_is_channel_commander", "client_talk_power" ]; render() { let icon: string = ""; let clicon: string = ""; const client = this.props.client; const properties = client.properties; if(properties.client_type_exact == ClientType.CLIENT_QUERY) { icon = "client-server_query"; } else { if (properties.client_away) { icon = "client-away"; } else if (!client.get_audio_handle() && !(this instanceof LocalClientEntry)) { icon = "client-input_muted_local"; } else if(!properties.client_output_hardware) { icon = "client-hardware_output_muted"; } else if(properties.client_output_muted) { icon = "client-output_muted"; } else if(!properties.client_input_hardware) { icon = "client-hardware_input_muted"; } else if(properties.client_input_muted) { icon = "client-input_muted"; } else { if(client.isSpeaking()) { if(properties.client_is_channel_commander) clicon = "client_cc_talk"; else clicon = "client_talk"; } else { if(properties.client_is_channel_commander) clicon = "client_cc_idle"; else clicon = "client_idle"; } } } if(clicon.length > 0) return
; else if(icon.length > 0) return
; else return null; } @EventHandler("notify_properties_updated") private handlePropertiesUpdated(event: ClientEvents["notify_properties_updated"]) { for(const key of ClientSpeakIcon.IconUpdateKeys) if(key in event.updated_properties) { this.forceUpdate(); return; } } @EventHandler("notify_mute_state_change") private handleMuteStateChange() { this.forceUpdate(); } @EventHandler("notify_speak_state_change") private handleSpeakStateChange() { this.forceUpdate(); } } interface ClientServerGroupIconsProperties { client: ClientEntryController; } @ReactEventHandler(e => e.props.client.events) @BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE) class ClientServerGroupIcons extends ReactComponentBase { private subscribed_groups: Group[] = []; private group_updated_callback; protected initialize() { this.group_updated_callback = (event: GroupEvents["notify_properties_updated"]) => { if(typeof event.updated_properties.iconid !== "undefined" || typeof event.updated_properties.sortid !== "undefined") this.forceUpdate(); }; } private unsubscribeGroupEvents() { this.subscribed_groups.forEach(e => e.events.off("notify_properties_updated", this.group_updated_callback)); this.subscribed_groups = []; } componentWillUnmount(): void { this.unsubscribeGroupEvents(); } render() { this.unsubscribeGroupEvents(); const groups = this.props.client.assignedServerGroupIds() .map(e => this.props.client.channelTree.client.groups.serverGroup(e)).filter(e => !!e); if(groups.length === 0) return null; groups.forEach(e => { e.events.on("notify_properties_updated", this.group_updated_callback); this.subscribed_groups.push(e); }); const group_icons = groups.filter(e => e?.properties.iconid) .sort((a, b) => a.properties.sortid - b.properties.sortid); if(group_icons.length === 0) return null; return [ group_icons.map(e => { return ; }) ]; } @EventHandler("notify_properties_updated") private handlePropertiesUpdated(event: ClientEvents["notify_properties_updated"]) { if(typeof event.updated_properties.client_servergroups) this.forceUpdate(); } } interface ClientChannelGroupIconProperties { client: ClientEntryController; } @ReactEventHandler(e => e.props.client.events) @BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE) class ClientChannelGroupIcon extends ReactComponentBase { private subscribed_group: Group | undefined; private group_updated_callback; protected initialize() { this.group_updated_callback = (event: GroupEvents["notify_properties_updated"]) => { if(typeof event.updated_properties.iconid !== "undefined" || typeof event.updated_properties.sortid !== "undefined") this.forceUpdate(); }; } private unsubscribeGroupEvent() { this.subscribed_group?.events.off("notify_properties_updated", this.group_updated_callback); } componentWillUnmount(): void { this.unsubscribeGroupEvent(); } render() { this.unsubscribeGroupEvent(); const cgid = this.props.client.assignedChannelGroup(); if(cgid === 0) return null; const channel_group = this.props.client.channelTree.client.groups.channelGroup(cgid); if(!channel_group) return null; channel_group.events.on("notify_properties_updated", this.group_updated_callback); this.subscribed_group = channel_group; if(channel_group.properties.iconid === 0) return null; return ; } @EventHandler("notify_properties_updated") private handlePropertiesUpdated(event: ClientEvents["notify_properties_updated"]) { if(typeof event.updated_properties.client_servergroups) this.forceUpdate(); } } interface ClientIconsProperties { client: ClientEntryController; } @ReactEventHandler(e => e.props.client.events) @BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE) class ClientIcons extends ReactComponentBase { render() { const icons = []; const talk_power = this.props.client.properties.client_talk_power; const needed_talk_power = this.props.client.currentChannel()?.properties.channel_needed_talk_power || 0; if(talk_power !== -1 && needed_talk_power !== 0 && needed_talk_power > talk_power) icons.push(
); icons.push(); icons.push(); if(this.props.client.properties.client_icon_id !== 0) icons.push(); return (
{icons}
) } @EventHandler("notify_properties_updated") private handlePropertiesUpdated(event: ClientEvents["notify_properties_updated"]) { if(typeof event.updated_properties.client_channel_group_id !== "undefined" || typeof event.updated_properties.client_talk_power !== "undefined" || typeof event.updated_properties.client_icon_id !== "undefined") this.forceUpdate(); } } interface ClientNameProperties { client: ClientEntryController; } interface ClientNameState { group_prefix: string; group_suffix: string; away_message: string; } /* group prefix & suffix, away message */ @BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE) class ClientName extends ReactComponentBase { protected defaultState(): ClientNameState { return { group_prefix: "", away_message: "", group_suffix: "" } } render() { return
{this.state.group_prefix + this.props.client.clientNickName() + this.state.group_suffix + this.state.away_message}
} @EventHandler("notify_properties_updated") private handlePropertiesChanged(event: ClientEvents["notify_properties_updated"]) { if(typeof event.updated_properties.client_away !== "undefined" || typeof event.updated_properties.client_away_message !== "undefined") { this.setState({ away_message: event.client_properties.client_away_message && " [" + event.client_properties.client_away_message + "]" }); } if(typeof event.updated_properties.client_servergroups !== "undefined" || typeof event.updated_properties.client_channel_group_id !== "undefined") { let prefix_groups: string[] = []; let suffix_groups: string[] = []; for(const group_id of this.props.client.assignedServerGroupIds()) { const group = this.props.client.channelTree.client.groups.serverGroup(group_id); if(!group) continue; if(group.properties.namemode == 1) prefix_groups.push(group.name); else if(group.properties.namemode == 2) suffix_groups.push(group.name); } const channel_group = this.props.client.channelTree.client.groups.channelGroup(this.props.client.assignedChannelGroup()); if(channel_group) { if(channel_group.properties.namemode == 1) prefix_groups.push(channel_group.name); else if(channel_group.properties.namemode == 2) suffix_groups.splice(0, 0, channel_group.name); } this.setState({ group_suffix: suffix_groups.map(e => "[" + e + "]").join(""), group_prefix: prefix_groups.map(e => "[" + e + "]").join("") }) } } } interface ClientNameEditProps { editFinished: (new_name?: string) => void; initialName: string; } class ClientNameEdit extends ReactComponentBase { private readonly ref_div: React.RefObject = React.createRef(); componentDidMount(): void { this.ref_div.current.focus(); } render() { return
this.onBlur()} onKeyPress={e => this.onKeyPress(e as any)} /> } private onBlur() { this.props.editFinished(this.ref_div.current.textContent); } private onKeyPress(event: KeyboardEvent) { if(event.key === "Enter") { event.preventDefault(); this.onBlur(); } } } export interface ClientEntryProperties { client: ClientEntryController; depth: number; offset: number; } export interface ClientEntryState { rename: boolean; renameInitialName?: string; } @ReactEventHandler(e => e.props.client.events) @BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE) export class ClientEntry extends TreeEntry { shouldComponentUpdate(nextProps: Readonly, nextState: Readonly, nextContext: any): boolean { return nextState.rename !== this.state.rename || nextProps.offset !== this.props.offset || nextProps.client !== this.props.client || nextProps.depth !== this.props.depth; } render() { return (
this.onDoubleClick()} onMouseUp={e => this.onMouseUp(e as any)} onContextMenu={e => this.onContextMenu(e as any)} > {this.state.rename ? this.onEditFinished(name)} initialName={this.state.renameInitialName || this.props.client.properties.client_nickname} /> : [, ] }
) } private onDoubleClick() { const client = this.props.client; if(client.channelTree.selection.is_multi_select()) return; if(this.props.client instanceof LocalClientEntry) { this.props.client.openRename(); } else if(this.props.client instanceof MusicClientEntry) { /* no action defined yet */ } else { this.props.client.open_text_chat(); } } private onEditFinished(new_name?: string) { if(!(this.props.client instanceof LocalClientEntry)) throw "Only local clients could be renamed"; if(new_name && new_name !== this.state.renameInitialName) { const client = this.props.client; client.renameSelf(new_name).then(result => { if(!result) this.setState({ rename: true, renameInitialName: new_name }); //TODO: Keep last name? }); } this.setState({ rename: false }); } private onMouseUp(event: MouseEvent) { if(event.button !== 0) return; /* only left mouse clicks */ const tree = this.props.client.channelTree; if(tree.isClientMoveActive()) return; tree.events.fire("action_select_entries", { entries: [this.props.client], mode: "auto" }); } private onContextMenu(event: MouseEvent) { if(settings.static(Settings.KEY_DISABLE_CONTEXT_MENU)) return; event.preventDefault(); const client = this.props.client; if(client.channelTree.selection.is_multi_select() && client.isSelected()) return; client.channelTree.events.fire("action_select_entries", { entries: [ client ], mode: "exclusive" }); client.showContextMenu(event.pageX, event.pageY); } @EventHandler("notify_select_state_change") private handleSelectChangeState() { this.forceUpdate(); } }