import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; import {MenuEntryType} from "tc-shared/ui/elements/ContextMenu"; import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; import {Settings, settings} from "tc-shared/settings"; import {PermissionType} from "tc-shared/permission/PermissionType"; import {KeyCode, SpecialKey} from "tc-shared/PPTListener"; import {Sound} from "tc-shared/sound/Sounds"; import {Group} from "tc-shared/permission/GroupManager"; import {ServerAddress, ServerEntry} from "tc-shared/ui/server"; import {ChannelEntry, ChannelProperties, ChannelSubscribeMode} from "tc-shared/ui/channel"; import {ClientEntry, LocalClientEntry, MusicClientEntry} from "tc-shared/ui/client"; import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler"; import {createChannelModal} from "tc-shared/ui/modal/ModalCreateChannel"; import {Registry} from "tc-shared/events"; import {ChannelTreeView} from "tc-shared/ui/tree/View"; import * as ReactDOM from "react-dom"; import * as React from "react"; import * as ppt from "tc-backend/ppt"; import {batch_updates, BatchUpdateType, flush_batched_updates} from "tc-shared/ui/react-elements/ReactComponentBase"; import {ChannelTreeEntry} from "tc-shared/ui/TreeEntry"; import {createInputModal} from "tc-shared/ui/elements/Modal"; import {spawnBanClient} from "tc-shared/ui/modal/ModalBanClient"; import {formatMessage} from "tc-shared/ui/frames/chat"; import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo"; import {tra} from "tc-shared/i18n/localize"; import {TreeEntryMove} from "tc-shared/ui/tree/TreeEntryMove"; import {EventType} from "tc-shared/ui/frames/log/Definitions"; export interface ChannelTreeEvents { action_select_entries: { entries: ChannelTreeEntry[], /** * auto := Select/unselect/add/remove depending on the selected state & shift key state * exclusive := Only selected these entries * append := Append these entries to the current selection * remove := Remove these entries from the current selection */ mode: "auto" | "exclusive" | "append" | "remove"; }, notify_selection_changed: {}, notify_root_channel_changed: {}, notify_tree_reset: {}, notify_query_view_state_changed: { queries_shown: boolean }, notify_entry_move_begin: {}, notify_entry_move_end: {}, notify_client_enter_view: { client: ClientEntry, reason: ViewReasonId, isServerJoin: boolean }, notify_client_leave_view: { client: ClientEntry, reason: ViewReasonId, message?: string, isServerLeave: boolean }, notify_channel_updated: { channel: ChannelEntry, channelProperties: ChannelProperties, updatedProperties: ChannelProperties }, notify_channel_list_received: {} } export class ChannelTreeEntrySelect { readonly handle: ChannelTree; selected_entries: ChannelTreeEntry[] = []; private readonly handler_select_entries; constructor(handle: ChannelTree) { this.handle = handle; this.handler_select_entries = e => { batch_updates(BatchUpdateType.CHANNEL_TREE); try { this.handleSelectEntries(e) } finally { flush_batched_updates(BatchUpdateType.CHANNEL_TREE); } }; this.handle.events.on("action_select_entries", this.handler_select_entries); } reset() { this.selected_entries.splice(0, this.selected_entries.length); } destroy() { this.handle.events.off("action_select_entries", this.handler_select_entries); this.selected_entries.splice(0, this.selected_entries.length); } is_multi_select() { return this.selected_entries.length > 1; } is_anything_selected() { return this.selected_entries.length > 0; } clear_selection() { this.handleSelectEntries({ entries: [], mode: "exclusive" }); } private handleSelectEntries(event: ChannelTreeEvents["action_select_entries"]) { if(event.mode === "exclusive") { let deleted_entries = this.selected_entries; let new_entries = []; this.selected_entries = []; for(const new_entry of event.entries) { if(!deleted_entries.remove(new_entry)) new_entries.push(new_entry); this.selected_entries.push(new_entry); } for(const deleted of deleted_entries) deleted["onUnselect"](); for(const new_entry of new_entries) new_entry["onSelect"](!this.is_multi_select()); if(deleted_entries.length !== 0 || new_entries.length !== 0) this.handle.events.fire("notify_selection_changed"); } else if(event.mode === "append") { let new_entries = []; for(const entry of event.entries) { if(this.selected_entries.findIndex(e => e === entry) !== -1) continue; this.selected_entries.push(entry); new_entries.push(entry); } for(const new_entry of new_entries) new_entry["onSelect"](!this.is_multi_select()); if(new_entries.length !== 0) this.handle.events.fire("notify_selection_changed"); } else if(event.mode === "remove") { let deleted_entries = []; for(const entry of event.entries) { if(this.selected_entries.remove(entry)) deleted_entries.push(entry); } for(const deleted of deleted_entries) deleted["onUnselect"](); if(deleted_entries.length !== 0) this.handle.events.fire("notify_selection_changed"); } else if(event.mode === "auto") { let deleted_entries = []; let new_entries = []; if(ppt.key_pressed(SpecialKey.SHIFT)) { for(const entry of event.entries) { const index = this.selected_entries.findIndex(e => e === entry); if(index === -1) { this.selected_entries.push(entry); new_entries.push(entry); } else { this.selected_entries.splice(index, 1); deleted_entries.push(entry); } } } else { deleted_entries = this.selected_entries.splice(0, this.selected_entries.length); if(event.entries.length !== 0) { const entry = event.entries[event.entries.length - 1]; this.selected_entries.push(entry); if(!deleted_entries.remove(entry)) new_entries.push(entry); /* entry wans't selected yet */ } } for(const deleted of deleted_entries) deleted["onUnselect"](); for(const new_entry of new_entries) new_entry["onSelect"](!this.is_multi_select()); if(deleted_entries.length !== 0 || new_entries.length !== 0) this.handle.events.fire("notify_selection_changed"); } else { console.warn("Received entry select event with unknown mode: %s", event.mode); } if(this.selected_entries.length === 1) this.handle.view.current?.scrollEntryInView(this.selected_entries[0] as any); } } export class ChannelTree { readonly events: Registry; client: ConnectionHandler; server: ServerEntry; channels: ChannelEntry[] = []; clients: ClientEntry[] = []; readonly view: React.RefObject; readonly view_move: React.RefObject; readonly selection: ChannelTreeEntrySelect; private readonly _tag_container: JQuery; private _show_queries: boolean; private channel_last?: ChannelEntry; private channel_first?: ChannelEntry; private _tag_container_focused = false; private _listener_document_click; private _listener_document_key; constructor(client) { this.events = new Registry(); this.events.enableDebug("channel-tree"); this.client = client; this.view = React.createRef(); this.view_move = React.createRef(); this.server = new ServerEntry(this, "undefined", undefined); this.selection = new ChannelTreeEntrySelect(this); this._tag_container = $.spawn("div").addClass("channel-tree-container"); ReactDOM.render([ this.onChannelEntryMove(a, b)} tree={this} ref={this.view} />, this.onMoveEnd(point.x, point.y)} ref={this.view_move} /> ], this._tag_container[0]); this.reset(); if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) { this._tag_container.on("contextmenu", (event) => { event.preventDefault(); const entry = this.view.current?.getEntryFromPoint(event.pageX, event.pageY); if(entry) { if(this.selection.is_multi_select()) this.open_multiselect_context_menu(this.selection.selected_entries, event.pageX, event.pageY); } else { this.selection.clear_selection(); this.showContextMenu(event.pageX, event.pageY); } }); } this._listener_document_key = event => this.handle_key_press(event); this._listener_document_click = event => { this._tag_container_focused = false; let element = event.target as HTMLElement; while(element) { if(element === this._tag_container[0]) { this._tag_container_focused = true; break; } element = element.parentNode as HTMLElement; } }; document.addEventListener('click', this._listener_document_click); document.addEventListener('keydown', this._listener_document_key); } tag_tree() : JQuery { return this._tag_container; } channelsOrdered() : ChannelEntry[] { const result = []; const visit = (channel: ChannelEntry) => { result.push(channel); channel.child_channel_head && visit(channel.child_channel_head); channel.channel_next && visit(channel.channel_next); }; this.channel_first && visit(this.channel_first); return result; } destroy() { ReactDOM.unmountComponentAtNode(this._tag_container[0]); this._listener_document_click && document.removeEventListener('click', this._listener_document_click); this._listener_document_click = undefined; this._listener_document_key && document.removeEventListener('keydown', this._listener_document_key); this._listener_document_key = undefined; if(this.server) { this.server.destroy(); this.server = undefined; } this.reset(); /* cleanup channel and clients */ this.channel_first = undefined; this.channel_last = undefined; this._tag_container.remove(); this.selection.destroy(); this.events.destroy(); } initialiseHead(serverName: string, address: ServerAddress) { this.server.reset(); this.server.remote_address = Object.assign({}, address); this.server.properties.virtualserver_name = serverName; this.events.fire("notify_root_channel_changed"); } rootChannel() : ChannelEntry[] { const result = []; let first = this.channel_first; while(first) { result.push(first); first = first.channel_next; } return result; } deleteChannel(channel: ChannelEntry) { channel.channelTree = null; batch_updates(BatchUpdateType.CHANNEL_TREE); try { if(!this.channels.remove(channel)) log.warn(LogCategory.CHANNEL, tr("Deleting an unknown channel!")); channel.children(false).forEach(e => this.deleteChannel(e)); if(channel.clients(false).length !== 0) { log.warn(LogCategory.CHANNEL, tr("Deleting a non empty channel! This could cause some errors.")); for(const client of channel.clients(false)) this.deleteClient(client, { reason: ViewReasonId.VREASON_SYSTEM, serverLeave: false }); } const is_root_tree = !channel.parent; this.unregisterChannelFromTree(channel); if(is_root_tree) this.events.fire("notify_root_channel_changed"); } finally { flush_batched_updates(BatchUpdateType.CHANNEL_TREE); } } insertChannel(channel: ChannelEntry, previous: ChannelEntry, parent: ChannelEntry) { channel.channelTree = this; this.channels.push(channel); this.moveChannel(channel, previous, parent); } findChannel(channelId: number) : ChannelEntry | undefined { if(typeof channelId === "string") /* legacy fix */ channelId = parseInt(channelId); for(let index = 0; index < this.channels.length; index++) if(this.channels[index].channelId === channelId) return this.channels[index]; return undefined; } find_channel_by_name(name: string, parent?: ChannelEntry, force_parent: boolean = true) : ChannelEntry | undefined { for(let index = 0; index < this.channels.length; index++) if(this.channels[index].channelName() == name && (!force_parent || parent == this.channels[index].parent)) return this.channels[index]; return undefined; } private unregisterChannelFromTree(channel: ChannelEntry, new_parent?: ChannelEntry) { let oldChannelParent; if(channel.parent) { if(channel.parent.child_channel_head === channel) channel.parent.child_channel_head = channel.channel_next; /* We need only trigger this once. If the new parent is equal to the old one with applying the "new" parent this event will get triggered */ oldChannelParent = channel.parent; } if(channel.channel_previous) channel.channel_previous.channel_next = channel.channel_next; if(channel.channel_next) channel.channel_next.channel_previous = channel.channel_previous; if(channel === this.channel_last) this.channel_last = channel.channel_previous; if(channel === this.channel_first) this.channel_first = channel.channel_next; channel.channel_next = undefined; channel.channel_previous = undefined; channel.parent = undefined; if(oldChannelParent && oldChannelParent !== new_parent) oldChannelParent.events.fire("notify_children_changed"); } moveChannel(channel: ChannelEntry, channel_previous: ChannelEntry, parent: ChannelEntry) { if(channel_previous != null && channel_previous.parent != parent) { console.error(tr("Invalid channel move (different parents! (%o|%o)"), channel_previous.parent, parent); return; } let root_tree_updated = !channel.parent; this.unregisterChannelFromTree(channel, parent); channel.channel_previous = channel_previous; channel.channel_next = undefined; channel.parent = parent; if(channel_previous) { if(channel_previous == this.channel_last) this.channel_last = channel; channel.channel_next = channel_previous.channel_next; channel_previous.channel_next = channel; if(channel.channel_next) channel.channel_next.channel_previous = channel; if(!channel.parent_channel()) root_tree_updated = true; else channel.parent.events.fire("notify_children_changed"); } else { if(parent) { let children = parent.children(); parent.child_channel_head = channel; if(children.length === 0) { //Self should be already in there channel.channel_next = undefined; } else { channel.channel_next = children[0]; channel.channel_next.channel_previous = channel; } parent.events.fire("notify_children_changed"); } else { channel.channel_next = this.channel_first; if(this.channel_first) this.channel_first.channel_previous = channel; this.channel_first = channel; this.channel_last = this.channel_last || channel; root_tree_updated = true; } } //channel.update_family_index(); //channel.children(true).forEach(e => e.update_family_index()); //channel.clients(true).forEach(e => e.update_family_index()); if(channel.channel_previous == channel) { /* shall never happen */ channel.channel_previous = undefined; debugger; } if(channel.channel_next == channel) { /* shall never happen */ channel.channel_next = undefined; debugger; } if(root_tree_updated) this.events.fire("notify_root_channel_changed"); } deleteClient(client: ClientEntry, reason: { reason: ViewReasonId, message?: string, serverLeave: boolean }) { const old_channel = client.currentChannel(); old_channel?.unregisterClient(client); this.clients.remove(client); client.events.fire("notify_left_view", reason); if(old_channel) { this.events.fire("notify_client_leave_view", { client: client, message: reason.message, reason: reason.reason, isServerLeave: reason.serverLeave }); this.client.side_bar.info_frame().update_channel_client_count(old_channel); } //FIXME: Trigger the notify_clients_changed event! const voice_connection = this.client.serverConnection.getVoiceConnection(); if(client.get_audio_handle()) { if(!voice_connection) { log.warn(LogCategory.VOICE, tr("Deleting client with a voice handle, but we haven't a voice connection!")); } else { voice_connection.unregister_client(client.get_audio_handle()); } } client.set_audio_handle(undefined); client.destroy(); } registerClient(client: ClientEntry) { this.clients.push(client); client.channelTree = this; const voice_connection = this.client.serverConnection.getVoiceConnection(); if(voice_connection) client.set_audio_handle(voice_connection.register_client(client.clientId())); } unregisterClient(client: ClientEntry) { if(!this.clients.remove(client)) return; } insertClient(client: ClientEntry, channel: ChannelEntry, reason: { reason: ViewReasonId, isServerJoin: boolean }) : ClientEntry { batch_updates(BatchUpdateType.CHANNEL_TREE); try { let newClient = this.findClient(client.clientId()); if(newClient) client = newClient; //Got new client :) else { this.registerClient(client); } client.currentChannel()?.unregisterClient(client); client["_channel"] = channel; channel.registerClient(client); this.events.fire("notify_client_enter_view", { client: client, reason: reason.reason, isServerJoin: reason.isServerJoin }); return client; } finally { flush_batched_updates(BatchUpdateType.CHANNEL_TREE); } } moveClient(client: ClientEntry, targetChannel: ChannelEntry) { batch_updates(BatchUpdateType.CHANNEL_TREE); try { let oldChannel = client.currentChannel(); oldChannel?.unregisterClient(client); client["_channel"] = targetChannel; targetChannel?.registerClient(client); if(oldChannel) { this.client.side_bar.info_frame().update_channel_client_count(oldChannel); } if(targetChannel) { this.client.side_bar.info_frame().update_channel_client_count(targetChannel); } if(oldChannel && targetChannel) { client.events.fire("notify_client_moved", { oldChannel: oldChannel, newChannel: targetChannel }); } } finally { flush_batched_updates(BatchUpdateType.CHANNEL_TREE); } } findClient?(clientId: number) : ClientEntry { for(let index = 0; index < this.clients.length; index++) { if(this.clients[index].clientId() == clientId) return this.clients[index]; } return undefined; } find_client_by_dbid?(client_dbid: number) : ClientEntry { for(let index = 0; index < this.clients.length; index++) { if(this.clients[index].properties.client_database_id == client_dbid) return this.clients[index]; } return undefined; } find_client_by_unique_id?(unique_id: string) : ClientEntry { for(let index = 0; index < this.clients.length; index++) { if(this.clients[index].properties.client_unique_identifier == unique_id) return this.clients[index]; } return undefined; } showContextMenu(x: number, y: number, on_close: () => void = undefined) { let channelCreate = this.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_TEMPORARY).granted(1) || this.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT).granted(1) || this.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_PERMANENT).granted(1); contextmenu.spawn_context_menu(x, y, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_create", name: tr("Create channel"), invalidPermission: !channelCreate, callback: () => this.spawnCreateChannel() }, contextmenu.Entry.HR(), { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_collapse_all", name: tr("Collapse all channels"), callback: () => this.collapse_channels() }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_expand_all", name: tr("Expend all channels"), callback: () => this.expand_channels() }, contextmenu.Entry.CLOSE(on_close) ); } private open_multiselect_context_menu(entries: ChannelTreeEntry[], x: number, y: number) { const clients = entries.filter(e => e instanceof ClientEntry) as ClientEntry[]; const channels = entries.filter(e => e instanceof ChannelEntry) as ChannelEntry[]; const server = entries.find(e => e instanceof ServerEntry) as ServerEntry; let client_menu: contextmenu.MenuEntry[]; let channel_menu: contextmenu.MenuEntry[]; let server_menu: contextmenu.MenuEntry[]; if(clients.length > 0) { client_menu = []; const music_only = clients.map(e => e instanceof MusicClientEntry ? 0 : 1).reduce((a, b) => a + b, 0) == 0; const music_entry = clients.map(e => e instanceof MusicClientEntry ? 1 : 0).reduce((a, b) => a + b, 0) > 0; const local_client = clients.map(e => e instanceof LocalClientEntry ? 1 : 0).reduce((a, b) => a + b, 0) > 0; if (!music_entry && !local_client) { //Music bots or local client cant be poked client_menu.push({ type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-poke", name: tr("Poke clients"), callback: () => { createInputModal(tr("Poke clients"), tr("Poke message:
"), text => true, result => { if (typeof(result) === "string") { for (const client of clients) this.client.serverConnection.send_command("clientpoke", { clid: client.clientId(), msg: result }); this.selection.clear_selection(); } }, {width: 400, maxLength: 512}).open(); } }); } client_menu.push({ type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-move_client_to_own_channel", name: tr("Move clients to your channel"), callback: () => { const target = this.client.getClient().currentChannel().getChannelId(); for(const client of clients) this.client.serverConnection.send_command("clientmove", { clid: client.clientId(), cid: target }); this.selection.clear_selection(); } }); if (!local_client) {//local client cant be kicked and/or banned or kicked client_menu.push(contextmenu.Entry.HR()); client_menu.push({ type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-kick_channel", name: tr("Kick clients from channel"), callback: () => { createInputModal(tr("Kick clients from channel"), tr("Kick reason:
"), text => true, result => { if (result) { for (const client of clients) this.client.serverConnection.send_command("clientkick", { clid: client.clientId(), reasonid: ViewReasonId.VREASON_CHANNEL_KICK, reasonmsg: result }); } }, {width: 400, maxLength: 255}).open(); this.selection.clear_selection(); } }); if (!music_entry) { //Music bots cant be poked, banned or kicked client_menu.push({ type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-poke", name: tr("Poke clients"), callback: () => { this.selection.clear_selection(); createInputModal(tr("Poke clients"), tr("Poke message:
"), text => true, result => { if (result) { const elements = clients.map(e => { return { clid: e.clientId() } as any }); elements[0].msg = result; this.client.serverConnection.send_command("clientpoke", elements); } }, {width: 400, maxLength: 255}).open(); } }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-kick_server", name: tr("Kick clients fom server"), callback: () => { this.selection.clear_selection(); createInputModal(tr("Kick clients from server"), tr("Kick reason:
"), text => true, result => { if (result) { for (const client of clients) this.client.serverConnection.send_command("clientkick", { clid: client.clientId(), reasonid: ViewReasonId.VREASON_SERVER_KICK, reasonmsg: result }); } }, {width: 400, maxLength: 255}).open(); } }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-ban_client", name: tr("Ban clients"), invalidPermission: !this.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1), callback: () => { this.selection.clear_selection(); spawnBanClient(this.client, (clients).map(entry => { return { name: entry.clientNickName(), unique_id: entry.properties.client_unique_identifier } }), (data) => { for (const client of clients) this.client.serverConnection.send_command("banclient", { uid: client.properties.client_unique_identifier, banreason: data.reason, time: data.length }, { flagset: [data.no_ip ? "no-ip" : "", data.no_hwid ? "no-hardware-id" : "", data.no_name ? "no-nickname" : ""] }).then(() => { this.client.sound.play(Sound.USER_BANNED); }); }); } }); } if(music_only) { client_menu.push(contextmenu.Entry.HR()); client_menu.push({ name: tr("Delete bots"), icon_class: "client-delete", disabled: false, callback: () => { const param_string = clients.map((_, index) => "{" + index + "}").join(', '); const param_values = clients.map(client => client.createChatTag(true)); const tag = $.spawn("div").append(...formatMessage(tr("Do you really want to delete ") + param_string, ...param_values)); const tag_container = $.spawn("div").append(tag); spawnYesNo(tr("Are you sure?"), tag_container, result => { if(result) { for(const client of clients) this.client.serverConnection.send_command("musicbotdelete", { botid: client.properties.client_database_id }); this.selection.clear_selection(); } }); }, type: contextmenu.MenuEntryType.ENTRY }); } } } if(channels.length > 0) { channel_menu = []; //TODO: Subscribe mode settings channel_menu.push({ type: MenuEntryType.ENTRY, name: tr("Delete all channels"), icon_class: "client-delete", callback: () => { spawnYesNo(tr("Are you sure?"), tra("Do you really want to delete {0} channels?", channels.length), result => { if(typeof result === "boolean" && result) { for(const channel of channels) this.client.serverConnection.send_command("channeldelete", { cid: channel.channelId }); this.selection.clear_selection(); } }); } }); } if(server) server_menu = server.contextMenuItems(); const menus = [ { text: tr("Apply to all clients"), menu: client_menu, icon: "client-user-account" }, { text: tr("Apply to all channels"), menu: channel_menu, icon: "client-channel_green" }, { text: tr("Server actions"), menu: server_menu, icon: "client-server_green" } ].filter(e => !!e.menu); if(menus.length === 1) { contextmenu.spawn_context_menu(x, y, ...menus[0].menu); } else { contextmenu.spawn_context_menu(x, y, ...menus.map(e => { return { icon_class: e.icon, name: e.text, type: MenuEntryType.SUB_MENU, sub_menu: e.menu } as contextmenu.MenuEntry })); } } clientsByGroup(group: Group) : ClientEntry[] { let result = []; for(let client of this.clients) { if(client.groupAssigned(group)) result.push(client); } return result; } clientsByChannel(channel: ChannelEntry) : ClientEntry[] { let result = []; for(let client of this.clients) { if(client.currentChannel() == channel) result.push(client); } return result; } reset() { batch_updates(BatchUpdateType.CHANNEL_TREE); this.selection.clear_selection(); try { this.selection.reset(); const voice_connection = this.client.serverConnection ? this.client.serverConnection.getVoiceConnection() : undefined; for(const client of this.clients) { if(client.get_audio_handle() && voice_connection) { voice_connection.unregister_client(client.get_audio_handle()); client.set_audio_handle(undefined); } client.destroy(); } this.clients = []; for(const channel of this.channels) channel.destroy(); this.channels = []; this.channel_last = undefined; this.channel_first = undefined; this.events.fire("notify_tree_reset"); } finally { flush_batched_updates(BatchUpdateType.CHANNEL_TREE); } } spawnCreateChannel(parent?: ChannelEntry) { createChannelModal(this.client, undefined, parent, this.client.permissions, (properties?, permissions?) => { if(!properties) return; properties["cpid"] = parent ? parent.channelId : 0; log.debug(LogCategory.CHANNEL, tr("Creating a new channel.\nProperties: %o\nPermissions: %o"), properties); this.client.serverConnection.send_command("channelcreate", properties).then(() => { let channel = this.find_channel_by_name(properties.channel_name, parent, true); if(!channel) { log.error(LogCategory.CHANNEL, tr("Failed to resolve channel after creation. Could not apply permissions!")); return; } if(permissions && permissions.length > 0) { let perms = []; for(let perm of permissions) { perms.push({ permvalue: perm.value, permnegated: false, permskip: false, permid: perm.type.id }); } perms[0]["cid"] = channel.channelId; return this.client.serverConnection.send_command("channeladdperm", perms, { flagset: ["continueonerror"] }).then(() => new Promise(resolve => { resolve(channel); })); } return new Promise(resolve => { resolve(channel); }) }).then(channel => { this.client.log.log(EventType.CHANNEL_CREATE_OWN, { channel: channel.log_data(), creator: this.client.getClient().log_data(), }); this.client.sound.play(Sound.CHANNEL_CREATED); }); }); } private select_next_channel(channel: ChannelEntry, select_client: boolean) { if(select_client) { const clients = channel.clients_ordered(); if(clients.length > 0) { this.events.fire("action_select_entries", { mode: "exclusive", entries: [ clients[0] ] }); return; } } const children = channel.children(); if(children.length > 0) { this.events.fire("action_select_entries", { mode: "exclusive", entries: [ children[0] ] }); return; } const next = channel.channel_next; if(next) { this.events.fire("action_select_entries", { mode: "exclusive", entries: [ next ] }); return; } let parent = channel.parent_channel(); while(parent) { const p_next = parent.channel_next; if(p_next) { this.events.fire("action_select_entries", { mode: "exclusive", entries: [ p_next ] }); return; } parent = parent.parent_channel(); } } handle_key_press(event: KeyboardEvent) { if(!this._tag_container_focused || !this.selection.is_anything_selected() || this.selection.is_multi_select()) return; const selected = this.selection.selected_entries[0]; if(event.keyCode == KeyCode.KEY_UP) { event.preventDefault(); if(selected instanceof ChannelEntry) { let previous = selected.channel_previous; if(previous) { while(true) { const siblings = previous.children(); if(siblings.length == 0) break; previous = siblings.last(); } const clients = previous.clients_ordered(); if(clients.length > 0) { this.events.fire("action_select_entries", { mode: "exclusive", entries: [ clients.last() ] }); return; } else { this.events.fire("action_select_entries", { mode: "exclusive", entries: [ previous ] }); return; } } else if(selected.hasParent()) { const channel = selected.parent_channel(); const clients = channel.clients_ordered(); if(clients.length > 0) { this.events.fire("action_select_entries", { mode: "exclusive", entries: [ clients.last() ] }); return; } else { this.events.fire("action_select_entries", { mode: "exclusive", entries: [ channel ] }); return; } } else { this.events.fire("action_select_entries", { mode: "exclusive", entries: [ this.server ] }); } } else if(selected instanceof ClientEntry) { const channel = selected.currentChannel(); const clients = channel.clients_ordered(); const index = clients.indexOf(selected); if(index > 0) { this.events.fire("action_select_entries", { mode: "exclusive", entries: [ clients[index - 1] ] }); return; } this.events.fire("action_select_entries", { mode: "exclusive", entries: [ channel ] }); return; } } else if(event.keyCode == KeyCode.KEY_DOWN) { event.preventDefault(); if(selected instanceof ChannelEntry) { this.select_next_channel(selected, true); } else if(selected instanceof ClientEntry){ const channel = selected.currentChannel(); const clients = channel.clients_ordered(); const index = clients.indexOf(selected); if(index + 1 < clients.length) { this.events.fire("action_select_entries", { mode: "exclusive", entries: [ clients[index + 1] ] }); return; } this.select_next_channel(channel, false); } else if(selected instanceof ServerEntry) this.events.fire("action_select_entries", { mode: "exclusive", entries: [ this.channel_first ] }); } else if(event.keyCode == KeyCode.KEY_RETURN) { if(selected instanceof ChannelEntry) { selected.joinChannel(); } } } toggle_server_queries(flag: boolean) { if(this._show_queries == flag) return; this._show_queries = flag; this.events.fire("notify_query_view_state_changed", { queries_shown: flag }); } areServerQueriesShown() { return this._show_queries; } get_first_channel?() : ChannelEntry { return this.channel_first; } unsubscribe_all_channels(subscribe_specified?: boolean) { if(!this.client.serverConnection || !this.client.serverConnection.connected()) return; this.client.serverConnection.send_command('channelunsubscribeall').then(() => { const channels: number[] = []; for(const channel of this.channels) { if(channel.subscribe_mode == ChannelSubscribeMode.SUBSCRIBED) channels.push(channel.getChannelId()); } if(channels.length > 0) { this.client.serverConnection.send_command('channelsubscribe', channels.map(e => { return {cid: e}; })).catch(error => { console.warn(tr("Failed to subscribe to specific channels (%o)"), channels); }); } }).catch(error => { console.warn(tr("Failed to unsubscribe to all channels! (%o)"), error); }); } subscribe_all_channels() { if(!this.client.serverConnection || !this.client.serverConnection.connected()) return; this.client.serverConnection.send_command('channelsubscribeall').then(() => { const channels: number[] = []; for(const channel of this.channels) { if(channel.subscribe_mode == ChannelSubscribeMode.UNSUBSCRIBED) channels.push(channel.getChannelId()); } if(channels.length > 0) { this.client.serverConnection.send_command('channelunsubscribe', channels.map(e => { return {cid: e}; })).catch(error => { console.warn(tr("Failed to unsubscribe to specific channels (%o)"), channels); }); } }).catch(error => { console.warn(tr("Failed to subscribe to all channels! (%o)"), error); }); } expand_channels(root?: ChannelEntry) { if(typeof root === "undefined") this.rootChannel().forEach(e => this.expand_channels(e)); else { root.collapsed = false; for(const child of root.children(false)) this.expand_channels(child); } } collapse_channels(root?: ChannelEntry) { if(typeof root === "undefined") this.rootChannel().forEach(e => this.collapse_channels(e)); else { root.collapsed = true; for(const child of root.children(false)) this.collapse_channels(child); } } private onChannelEntryMove(start, current) { const move = this.view_move.current; if(!move) return; const target = this.view.current.getEntryFromPoint(start.x, start.y); if(target && this.selection.selected_entries.findIndex(e => e === target) === -1) this.events.fire("action_select_entries", { mode: "auto", entries: [ target ]}); const selection = this.selection.selected_entries; if(selection.length === 0 || selection.filter(e => !(e instanceof ClientEntry)).length > 0) return; move.enableEntryMove(this.view.current, selection.map(e => e as ClientEntry).map(e => e.clientNickName()).join(","), start, current, () => { this.events.fire("notify_entry_move_begin"); }); } private onMoveEnd(x: number, y: number) { batch_updates(BatchUpdateType.CHANNEL_TREE); try { this.events.fire("notify_entry_move_end"); const selection = this.selection.selected_entries.filter(e => e instanceof ClientEntry) as ClientEntry[]; if(selection.length === 0) return; this.selection.clear_selection(); const target = this.view.current.getEntryFromPoint(x, y); let target_channel: ChannelEntry; if(target instanceof ClientEntry) target_channel = target.currentChannel(); else if(target instanceof ChannelEntry) target_channel = target; if(!target_channel) return; selection.filter(e => e.currentChannel() !== target_channel).forEach(e => { this.client.serverConnection.send_command("clientmove", { clid: e.clientId(), cid: target_channel.channelId }); }); } finally { flush_batched_updates(BatchUpdateType.CHANNEL_TREE); } } isClientMoveActive() { return !!this.view_move.current?.isActive(); } }