import {EventHandler, Registry} from "tc-shared/events"; import { ChannelEntryInfo, ChannelIcons, ChannelTreeUIEvents, ClientIcons, ClientNameInfo, ClientTalkIconState, ServerState } from "tc-shared/ui/tree/Definitions"; import {ChannelTreeView, PopoutButton} from "tc-shared/ui/tree/RendererView"; import * as React from "react"; import {ChannelIconClass, ChannelIconsRenderer, RendererChannel} from "tc-shared/ui/tree/RendererChannel"; import {ClientIcon} from "svg-sprites/client-icons"; import {UnreadMarkerRenderer} from "tc-shared/ui/tree/RendererTreeEntry"; import {LogCategory, logError} from "tc-shared/log"; import { ClientIconsRenderer, ClientName, ClientStatus, ClientTalkStatusIcon, RendererClient } from "tc-shared/ui/tree/RendererClient"; import {ServerRenderer} from "tc-shared/ui/tree/RendererServer"; import {RendererMove} from "./RendererMove"; function isEquivalent(a, b) { const typeA = typeof a; const typeB = typeof b; if(typeA !== typeB) { return false; } if(typeA === "function") { throw "cant compare function"; } else if(typeA === "object") { if(Array.isArray(a)) { if(!Array.isArray(b) || b.length !== a.length) { return false; } for(let index = 0; index < a.length; index++) { if(!isEquivalent(a[index], b[index])) { return false; } } return true; } else { const keys = Object.keys(a); for(const key of keys) { if(!(key in b)) { return false; } if(!isEquivalent(a[key], b[key])) { return false; } } return true; } } else { return a === b; } } export class RDPChannelTree { readonly events: Registry; readonly handlerId: string; private registeredEventHandlers = []; readonly refMove = React.createRef(); readonly refTree = React.createRef(); readonly refPopoutButton = React.createRef(); popoutShown: boolean = false; popoutButtonShown: boolean = false; private treeRevision: number = 0; private orderedTree: RDPEntry[] = []; private treeEntries: {[key: number]: RDPEntry} = {}; constructor(events: Registry, handlerId: string) { this.events = events; this.handlerId = handlerId; } initialize() { this.events.register_handler(this); const events = this.registeredEventHandlers; events.push(this.events.on("notify_unread_state", event => { const entry = this.treeEntries[event.treeEntryId]; if(!entry) { logError(LogCategory.CHANNEL, tr("Received unread notify for invalid tree entry %o."), event.treeEntryId); return; } entry.handleUnreadUpdate(event.unread); })); events.push(this.events.on("notify_select_state", event => { const entry = this.treeEntries[event.treeEntryId]; if(!entry) { logError(LogCategory.CHANNEL, tr("Received select notify for invalid tree entry %o."), event.treeEntryId); return; } entry.handleSelectUpdate(event.selected); })); events.push(this.events.on("notify_channel_info", event => { const entry = this.treeEntries[event.treeEntryId]; if(!entry || !(entry instanceof RDPChannel)) { logError(LogCategory.CHANNEL, tr("Received channel info notify for invalid tree entry %o."), event.treeEntryId); return; } entry.handleInfoUpdate(event.info); })); events.push(this.events.on("notify_channel_icon", event => { const entry = this.treeEntries[event.treeEntryId]; if(!entry || !(entry instanceof RDPChannel)) { logError(LogCategory.CHANNEL, tr("Received channel icon notify for invalid tree entry %o."), event.treeEntryId); return; } entry.handleIconUpdate(event.icon); })); events.push(this.events.on("notify_channel_icons", event => { const entry = this.treeEntries[event.treeEntryId]; if(!entry || !(entry instanceof RDPChannel)) { logError(LogCategory.CHANNEL, tr("Received channel icons notify for invalid tree entry %o."), event.treeEntryId); return; } entry.handleIconsUpdate(event.icons); })); events.push(this.events.on("notify_client_status", event => { const entry = this.treeEntries[event.treeEntryId]; if(!entry || !(entry instanceof RDPClient)) { logError(LogCategory.CHANNEL, tr("Received client status notify for invalid tree entry %o."), event.treeEntryId); return; } entry.handleStatusUpdate(event.status); })); events.push(this.events.on("notify_client_name", event => { const entry = this.treeEntries[event.treeEntryId]; if(!entry || !(entry instanceof RDPClient)) { logError(LogCategory.CHANNEL, tr("Received client name notify for invalid tree entry %o."), event.treeEntryId); return; } entry.handleNameUpdate(event.info); })); events.push(this.events.on("notify_client_icons", event => { const entry = this.treeEntries[event.treeEntryId]; if(!entry || !(entry instanceof RDPClient)) { logError(LogCategory.CHANNEL, tr("Received client icons notify for invalid tree entry %o."), event.treeEntryId); return; } entry.handleIconsUpdate(event.icons); })); events.push(this.events.on("notify_client_talk_status", event => { const entry = this.treeEntries[event.treeEntryId]; if(!entry || !(entry instanceof RDPClient)) { logError(LogCategory.CHANNEL, tr("Received client talk notify for invalid tree entry %o."), event.treeEntryId); return; } entry.handleTalkStatusUpdate(event.status, event.requestMessage); })); events.push(this.events.on("notify_client_name_edit", event => { const entry = this.treeEntries[event.treeEntryId]; if(!entry || !(entry instanceof RDPClient)) { logError(LogCategory.CHANNEL, tr("Received client name edit notify for invalid tree entry %o."), event.treeEntryId); return; } entry.handleOpenRename(event.initialValue); })); events.push(this.events.on("notify_server_state", event => { const entry = this.treeEntries[event.treeEntryId]; if(!entry || !(entry instanceof RDPServer)) { logError(LogCategory.CHANNEL, tr("Received server state notify for invalid tree entry %o."), event.treeEntryId); return; } entry.handleStateUpdate(event.state); })); this.events.fire("query_tree_entries"); this.events.fire("query_popout_state"); } destroy() { this.events.unregister_handler(this); this.registeredEventHandlers.forEach(callback => callback()); this.registeredEventHandlers = []; } getTreeEntries() { return this.orderedTree; } @EventHandler("notify_tree_entries") private handleNotifyTreeEntries(event: ChannelTreeUIEvents["notify_tree_entries"]) { const oldEntryInstances = this.treeEntries; this.treeEntries = {}; this.orderedTree = event.entries.map((entry, index) => { let result: RDPEntry; if(oldEntryInstances[entry.entryId]) { result = oldEntryInstances[entry.entryId]; delete oldEntryInstances[entry.entryId]; } else { switch (entry.type) { case "channel": result = new RDPChannel(this, entry.entryId); break; case "client": case "client-local": result = new RDPClient(this, entry.entryId, entry.type === "client-local"); break; case "server": result = new RDPServer(this, entry.entryId); break; default: throw "invalid channel entry type " + entry.type; } result.queryState(); } this.treeEntries[entry.entryId] = result; result.handlePositionUpdate(index * ChannelTreeView.EntryHeight, entry.depth); return result; }).filter(e => !!e); Object.keys(oldEntryInstances).map(key => oldEntryInstances[key]).forEach(entry => { entry.destroy(); }); this.refTree.current?.setState({ tree: this.orderedTree.slice(), treeRevision: ++this.treeRevision }); } @EventHandler("notify_entry_move") private handleNotifyEntryMove(event: ChannelTreeUIEvents["notify_entry_move"]) { if(!this.refMove.current) { this.events.fire_async("action_move_entries", { treeEntryId: 0 }); return; } this.refMove.current.enableEntryMove(event.entries, event.begin, event.current); } @EventHandler("notify_popout_state") private handleNotifyPopoutState(event: ChannelTreeUIEvents["notify_popout_state"]) { this.popoutShown = event.shown; this.popoutButtonShown = event.showButton; this.refPopoutButton.current?.forceUpdate(); } } export abstract class RDPEntry { readonly handle: RDPChannelTree; readonly entryId: number; readonly refUnread = React.createRef(); offsetTop: number; offsetLeft: number; selected: boolean = false; unread: boolean = false; private renderedInstance: React.ReactElement; private destroyed = false; protected constructor(handle: RDPChannelTree, entryId: number) { this.handle = handle; this.entryId = entryId; } destroy() { if(this.destroyed) { throw "can not destry an entry twice"; } this.renderedInstance = undefined; this.destroyed = true; } /* returns true if this element does not longer exists, but it's still rendered */ isDestroyed() { return this.destroyed; } getEvents() : Registry { return this.handle.events; } getHandlerId() : string { return this.handle.handlerId; } /* do the initial state query */ queryState() { const events = this.getEvents(); events.fire("query_unread_state", { treeEntryId: this.entryId }); events.fire("query_select_state", { treeEntryId: this.entryId }); } handleUnreadUpdate(value: boolean) { if(this.unread === value) { return; } this.unread = value; this.refUnread.current?.forceUpdate(); } handleSelectUpdate(value: boolean) { if(this.selected === value) { return; } this.selected = value; this.renderSelectStateUpdate(); } handlePositionUpdate(offsetTop: number, offsetLeft: number) { if(this.offsetLeft === offsetLeft && this.offsetTop === offsetTop) { return; } this.offsetTop = offsetTop; this.offsetLeft = offsetLeft; this.renderPositionUpdate(); } render() : React.ReactElement { if(this.renderedInstance) { return this.renderedInstance; } return this.renderedInstance = this.doRender(); } protected abstract doRender() : React.ReactElement; protected abstract renderSelectStateUpdate(); protected abstract renderPositionUpdate(); } export class RDPChannel extends RDPEntry { readonly refIcon = React.createRef(); readonly refIcons = React.createRef(); readonly refChannel = React.createRef(); /* if uninitialized, undefined */ info: ChannelEntryInfo; /* if uninitialized, undefined */ icon: ClientIcon; /* if uninitialized, undefined */ icons: ChannelIcons; constructor(handle: RDPChannelTree, entryId: number) { super(handle, entryId); } doRender(): React.ReactElement { return ; } queryState() { super.queryState(); const events = this.getEvents(); events.fire("query_channel_info", { treeEntryId: this.entryId }); events.fire("query_channel_icons", { treeEntryId: this.entryId }); events.fire("query_channel_icon", { treeEntryId: this.entryId }); } renderSelectStateUpdate() { this.refChannel.current?.forceUpdate(); } protected renderPositionUpdate() { this.refChannel.current?.forceUpdate(); } handleIconUpdate(newIcon: ClientIcon) { if(newIcon === this.icon) { return; } this.icon = newIcon; this.refIcon.current?.forceUpdate(); } handleIconsUpdate(newIcons: ChannelIcons) { if(isEquivalent(newIcons, this.icons)) { return; } this.icons = newIcons; this.refIcons.current?.forceUpdate(); } handleInfoUpdate(newInfo: ChannelEntryInfo) { if(isEquivalent(newInfo, this.info)) { return; } this.info = newInfo; this.refChannel.current?.forceUpdate(); } } export class RDPClient extends RDPEntry { readonly refClient = React.createRef(); readonly refStatus = React.createRef(); readonly refName = React.createRef(); readonly refTalkStatus = React.createRef(); readonly refIcons = React.createRef(); readonly localClient: boolean; name: ClientNameInfo; status: ClientIcon; info: ClientNameInfo; icons: ClientIcons; rename: boolean = false; renameDefault: string; talkStatus: ClientTalkIconState; talkRequestMessage: string; constructor(handle: RDPChannelTree, entryId: number, localClient: boolean) { super(handle, entryId); this.localClient = localClient; } doRender(): React.ReactElement { return ; } queryState() { super.queryState(); const events = this.getEvents(); events.fire("query_client_name", { treeEntryId: this.entryId }); events.fire("query_client_status", { treeEntryId: this.entryId }); events.fire("query_client_talk_status", { treeEntryId: this.entryId }); events.fire("query_client_icons", { treeEntryId: this.entryId }); } protected renderPositionUpdate() { this.refClient.current?.forceUpdate(); } protected renderSelectStateUpdate() { this.refClient.current?.forceUpdate(); } handleStatusUpdate(newStatus: ClientIcon) { if(newStatus === this.status) { return; } this.status = newStatus; this.refStatus.current?.forceUpdate(); } handleNameUpdate(newName: ClientNameInfo) { if(isEquivalent(newName, this.name)) { return; } this.name = newName; this.refName.current?.forceUpdate(); } handleTalkStatusUpdate(newStatus: ClientTalkIconState, requestMessage: string) { if(this.talkStatus === newStatus && this.talkRequestMessage === requestMessage) { return; } this.talkStatus = newStatus; this.talkRequestMessage = requestMessage; this.refTalkStatus.current?.forceUpdate(); } handleIconsUpdate(newIcons: ClientIcons) { if(isEquivalent(newIcons, this.icons)) { return; } this.icons = newIcons; this.refIcons.current?.forceUpdate(); } handleOpenRename(initialValue: string) { if(!initialValue) { this.rename = false; this.renameDefault = undefined; this.refClient.current?.forceUpdate(); return; } if(!this.handle.refTree.current || !this.refClient.current) { /* TODO: Send error */ return; } this.handle.refTree.current.scrollEntryInView(this.entryId, () => { this.rename = true; this.renameDefault = initialValue; this.refClient.current?.forceUpdate(); }); } } export class RDPServer extends RDPEntry { readonly refServer = React.createRef(); state: ServerState; constructor(handle: RDPChannelTree, entryId: number) { super(handle, entryId); } queryState() { super.queryState(); const events = this.getEvents(); events.fire("query_server_state", { treeEntryId: this.entryId }); } protected doRender(): React.ReactElement { return ; } protected renderPositionUpdate() { this.refServer.current?.forceUpdate(); } protected renderSelectStateUpdate() { this.refServer.current?.forceUpdate(); } handleStateUpdate(newState: ServerState) { if(isEquivalent(newState, this.state)) { return; } this.state = newState; this.refServer.current?.forceUpdate(); } }