From 5bdc99acb0cccc47ccfcda5e75bc44f9eaecbe1a Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Sat, 26 Sep 2020 21:34:46 +0200 Subject: [PATCH] Reworked the channel tree events --- ChangeLog.md | 4 + shared/js/ConnectionHandler.ts | 2 +- shared/js/connection/CommandHandler.ts | 6 +- shared/js/events.ts | 26 ++- shared/js/permission/GroupManager.ts | 56 +++-- shared/js/tree/Channel.ts | 81 ++++---- shared/js/tree/ChannelTree.tsx | 194 ++++++++++-------- shared/js/tree/ChannelTreeEntry.ts | 10 +- shared/js/tree/Client.ts | 42 ++-- shared/js/tree/Server.ts | 6 +- shared/js/ui/frames/chat_frame.ts | 7 +- shared/js/ui/htmltags.ts | 2 +- .../external-modal/PopoutEntrypoint.ts | 3 + .../external-modal/PopoutRegistry.ts | 5 + 14 files changed, 266 insertions(+), 178 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 3d058bac..fd45c5ed 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,8 @@ # Changelog: +* **16.09.20** + - Updating group prefix/suffixes when the group naming mode changes + - Added an client talk power indicator + * **25.09.20** - Update the translation files - Made the server tabs moveable diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 5916e32f..48570a7d 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -197,6 +197,7 @@ export class ConnectionHandler { this.serverConnection.getVoiceConnection().setWhisperSessionInitializer(this.initializeWhisperSession.bind(this)); this.serverFeatures = new ServerFeatures(this); + this.groups = new GroupManager(this); this.channelTree = new ChannelTree(this); this.fileManager = new FileManager(this); @@ -209,7 +210,6 @@ export class ConnectionHandler { this.sound = new SoundManager(this); this.hostbanner = new Hostbanner(this); - this.groups = new GroupManager(this); this._local_client = new LocalClientEntry(this); this.event_registry.register_handler(this); diff --git a/shared/js/connection/CommandHandler.ts b/shared/js/connection/CommandHandler.ts index d775aac6..021ca48e 100644 --- a/shared/js/connection/CommandHandler.ts +++ b/shared/js/connection/CommandHandler.ts @@ -315,7 +315,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { if(ignoreOrder) { for(let ch of tree.channels) { if(ch.properties.channel_order == channel.channelId) { - tree.moveChannel(ch, channel, channel.parent); //Corrent the order :) + tree.moveChannel(ch, channel, channel.parent, true); //Corrent the order :) } } } @@ -324,7 +324,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { key: string, value: string }[] = []; - for(let key in json) { + for(let key of Object.keys(json)) { if(key === "cid") continue; if(key === "cpid") continue; if(key === "invokerid") continue; @@ -692,7 +692,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { return 0; } - tree.moveChannel(channel, prev, parent); + tree.moveChannel(channel, prev, parent, true); } handleNotifyChannelEdited(json) { diff --git a/shared/js/events.ts b/shared/js/events.ts index 189b01ed..a0dbccbc 100644 --- a/shared/js/events.ts +++ b/shared/js/events.ts @@ -3,6 +3,7 @@ import {LogCategory} from "./log"; import {guid} from "./crypto/uid"; import * as React from "react"; import {useEffect} from "react"; +import {unstable_batchedUpdates} from "react-dom"; export interface Event { readonly type: T; @@ -39,6 +40,9 @@ export class Registry(event_type: T, data?: Events[T], callback?: () => void) { - /* TODO: Optimize, bundle them */ - setTimeout(() => { - this.fire(event_type, data); - if(typeof callback === "function") - callback(); + if(!this.pendingCallbacksTimeout) { + this.pendingCallbacksTimeout = setTimeout(() => this.invokeAsyncCallbacks()); + this.pendingCallbacks = []; + } + this.pendingCallbacks.push({ type: event_type, data: data }); + } + + private invokeAsyncCallbacks() { + const callbacks = this.pendingCallbacks; + this.pendingCallbacksTimeout = 0; + this.pendingCallbacks = undefined; + + unstable_batchedUpdates(() => { + let index = callbacks.length; + while(index--) { + this.fire(callbacks[index].type, callbacks[index].data); + } }); } diff --git a/shared/js/permission/GroupManager.ts b/shared/js/permission/GroupManager.ts index 002262b0..0f18858d 100644 --- a/shared/js/permission/GroupManager.ts +++ b/shared/js/permission/GroupManager.ts @@ -33,6 +33,8 @@ export class GroupPermissionRequest { promise: LaterPromise; } +export type GroupUpdate = { key: GroupProperty, group: Group, oldValue: any, newValue: any }; + export interface GroupManagerEvents { notify_reset: {}, notify_groups_created: { @@ -42,7 +44,11 @@ export interface GroupManagerEvents { notify_groups_deleted: { groups: Group[], cause: "list-update" | "reset" | "user-action" - } + }, + notify_groups_updated: { updates: GroupUpdate[] }, + + /* will be fired when all server and channel groups have been received */ + notify_groups_received: {} } export type GroupProperty = "name" | "icon" | "sort-id" | "save-db" | "name-mode"; @@ -82,41 +88,43 @@ export class Group { this.name = name; } - updatePropertiesFromGroupList(data: any) { - const updates: GroupProperty[] = []; + updatePropertiesFromGroupList(data: any) : GroupUpdate[] { + const updates = [] as GroupUpdate[]; if(this.name !== data["name"]) { + updates.push({ key: "name", group: this, oldValue: this.name, newValue: data["name"] }); this.name = data["name"]; - updates.push("name"); } /* icon */ let value = parseInt(data["iconid"]) >>> 0; if(value !== this.properties.iconid) { + updates.push({ key: "icon", group: this, oldValue: this.properties.iconid, newValue: value }); this.properties.iconid = value; - updates.push("icon"); } value = parseInt(data["sortid"]); if(value !== this.properties.sortid) { + updates.push({ key: "sort-id", group: this, oldValue: this.properties.sortid, newValue: value }); this.properties.sortid = value; - updates.push("sort-id"); } let flag = parseInt(data["savedb"]) >= 1; if(flag !== this.properties.savedb) { + updates.push({ key: "save-db", group: this, oldValue: this.properties.savedb, newValue: flag }); this.properties.savedb = flag; - updates.push("save-db"); } value = parseInt(data["namemode"]); if(value !== this.properties.namemode) { + updates.push({ key: "name-mode", group: this, oldValue: this.properties.namemode, newValue: flag }); this.properties.namemode = value; - updates.push("name-mode"); } - if(updates.length > 0) - this.events.fire("notify_properties_updated", { updated_properties: updates }); + if(updates.length > 0) { + this.events.fire("notify_properties_updated", { updated_properties: updates.map(e => e.key) }); + } + return updates; } } @@ -147,6 +155,7 @@ export class GroupManager extends AbstractCommandHandler { serverGroups: Group[] = []; channelGroups: Group[] = []; + private allGroupsReceived = false; private readonly connectionStateListener; private groupPermissionRequests: GroupPermissionRequest[] = []; @@ -155,8 +164,9 @@ export class GroupManager extends AbstractCommandHandler { this.connectionHandler = client; this.connectionStateListener = (event: ConnectionEvents["notify_connection_state_changed"]) => { - if(event.new_state === ConnectionState.DISCONNECTING || event.new_state === ConnectionState.UNCONNECTED || event.new_state === ConnectionState.CONNECTING) + if(event.new_state === ConnectionState.DISCONNECTING || event.new_state === ConnectionState.UNCONNECTED || event.new_state === ConnectionState.CONNECTING) { this.reset(); + } }; client.serverConnection.command_handler_boss().register_handler(this); @@ -174,15 +184,18 @@ export class GroupManager extends AbstractCommandHandler { } reset() { - if(this.serverGroups.length === 0 && this.channelGroups.length === 0) + if(this.serverGroups.length === 0 && this.channelGroups.length === 0) { return; + } log.debug(LogCategory.PERMISSIONS, tr("Resetting server/channel groups")); this.serverGroups = []; this.channelGroups = []; + this.allGroupsReceived = false; - for(const permission of this.groupPermissionRequests) + for(const permission of this.groupPermissionRequests) { permission.promise.rejected(tr("Group manager reset")); + } this.groupPermissionRequests = []; this.events.fire("notify_reset"); } @@ -215,9 +228,11 @@ export class GroupManager extends AbstractCommandHandler { } findServerGroup(id: number) : Group | undefined { - for(let group of this.serverGroups) - if(group.id === id) + for(let group of this.serverGroups) { + if(group.id === id) { return group; + } + } return undefined; } @@ -232,6 +247,7 @@ export class GroupManager extends AbstractCommandHandler { let groupList = target == GroupTarget.SERVER ? this.serverGroups : this.channelGroups; const deleteGroups = groupList.slice(0); const newGroups: Group[] = []; + const groupUpdates: GroupUpdate[] = []; const isInitialList = groupList.length === 0; for(const groupData of json) { @@ -253,14 +269,15 @@ export class GroupManager extends AbstractCommandHandler { group = new Group(this, groupId, target, type, groupData["name"]); groupList.push(group); newGroups.push(group); + group.updatePropertiesFromGroupList(groupData); } else { group = deleteGroups.splice(groupIndex, 1)[0]; + groupUpdates.push(...group.updatePropertiesFromGroupList(groupData)); } group.requiredMemberRemovePower = parseInt(groupData["n_member_removep"]); group.requiredMemberAddPower = parseInt(groupData["n_member_addp"]); group.requiredModifyPower = parseInt(groupData["n_modifyp"]); - group.updatePropertiesFromGroupList(groupData); group.events.fire("notify_needed_powers_updated"); } @@ -276,6 +293,13 @@ export class GroupManager extends AbstractCommandHandler { if(deleteGroups.length !== 0) { this.events.fire("notify_groups_deleted", { groups: deleteGroups, cause: "list-update" }); } + + this.events.fire("notify_groups_updated", { updates: groupUpdates }); + + if(!this.allGroupsReceived && this.serverGroups.length > 0 && this.channelGroups.length > 0) { + this.allGroupsReceived = true; + this.events.fire("notify_groups_received"); + } } request_permissions(group: Group) : Promise { diff --git a/shared/js/tree/Channel.ts b/shared/js/tree/Channel.ts index 436484c1..c32c1d45 100644 --- a/shared/js/tree/Channel.ts +++ b/shared/js/tree/Channel.ts @@ -15,14 +15,13 @@ import {openChannelInfo} from "../ui/modal/ModalChannelInfo"; import {createChannelModal} from "../ui/modal/ModalCreateChannel"; import {formatMessage} from "../ui/frames/chat"; -import * as React from "react"; import {Registry} from "../events"; import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry"; -import {ChannelEntryView as ChannelEntryView} from "../ui/tree/Channel"; import {spawnFileTransferModal} from "../ui/modal/transfer/ModalFileTransfer"; import {ViewReasonId} from "../ConnectionHandler"; import {EventChannelData} from "../ui/frames/log/Definitions"; import {ErrorCode} from "../connection/ErrorCode"; +import {ClientIcon} from "svg-sprites/client-icons"; export enum ChannelType { PERMANENT, @@ -94,32 +93,28 @@ export interface ChannelEvents extends ChannelTreeEntryEvents { }, notify_collapsed_state_changed: { collapsed: boolean - }, - - notify_children_changed: {}, - notify_clients_changed: {}, /* will also be fired when clients haven been reordered */ + } } export class ParsedChannelName { - readonly original_name: string; - alignment: "center" | "right" | "left" | "normal"; - repetitive: boolean; + readonly originalName: string; + alignment: "center" | "right" | "left" | "normal" | "repetitive"; text: string; /* does not contain any alignment codes */ - constructor(name: string, has_parent_channel: boolean) { - this.original_name = name; - this.parse(has_parent_channel); + constructor(name: string, hasParentChannel: boolean) { + this.originalName = name; + this.parse(hasParentChannel); } private parse(has_parent_channel: boolean) { this.alignment = "normal"; parse_type: - if(!has_parent_channel && this.original_name.charAt(0) == '[') { - let end = this.original_name.indexOf(']'); + if(!has_parent_channel && this.originalName.charAt(0) == '[') { + let end = this.originalName.indexOf(']'); if(end === -1) break parse_type; - let options = this.original_name.substr(1, end - 1); + let options = this.originalName.substr(1, end - 1); if(options.indexOf("spacer") === -1) break parse_type; options = options.substr(0, options.indexOf("spacer")); @@ -139,17 +134,16 @@ export class ParsedChannelName { this.alignment = "center"; break; case "*": - this.alignment = "center"; - this.repetitive = true; + this.alignment = "repetitive"; break; default: break parse_type; } - this.text = this.original_name.substr(end + 1); + this.text = this.originalName.substr(end + 1); } if(!this.text && this.alignment === "normal") - this.text = this.original_name; + this.text = this.originalName; } } @@ -164,7 +158,6 @@ export class ChannelEntry extends ChannelTreeEntry { child_channel_head?: ChannelEntry; readonly events: Registry; - readonly view: React.RefObject; parsed_channel_name: ParsedChannelName; @@ -184,14 +177,13 @@ export class ChannelEntry extends ChannelTreeEntry { private _subscribe_mode: ChannelSubscribeMode; private client_list: ClientEntry[] = []; /* this list is sorted correctly! */ - private readonly client_property_listener; + private readonly clientPropertyChangedListener; constructor(channelId, channelName) { super(); this.events = new Registry(); - this.view = React.createRef(); - + this.properties = new ChannelProperties(); this.channelId = channelId; this.properties.channel_name = channelName; @@ -199,9 +191,10 @@ export class ChannelEntry extends ChannelTreeEntry { this.parsed_channel_name = new ParsedChannelName("undefined", false); - this.client_property_listener = (event: ClientEvents["notify_properties_updated"]) => { - if(typeof event.updated_properties.client_nickname !== "undefined" || typeof event.updated_properties.client_talk_power !== "undefined") + this.clientPropertyChangedListener = (event: ClientEvents["notify_properties_updated"]) => { + if("client_nickname" in event.updated_properties || "client_talk_power" in event.updated_properties) { this.reorderClientList(true); + } }; this.events.on("notify_properties_updated", event => { @@ -262,20 +255,16 @@ export class ChannelEntry extends ChannelTreeEntry { } registerClient(client: ClientEntry) { - client.events.on("notify_properties_updated", this.client_property_listener); + client.events.on("notify_properties_updated", this.clientPropertyChangedListener); this.client_list.push(client); this.reorderClientList(false); - - this.events.fire("notify_clients_changed"); } - unregisterClient(client: ClientEntry, no_event?: boolean) { - client.events.off("notify_properties_updated", this.client_property_listener); - if(!this.client_list.remove(client)) + unregisterClient(client: ClientEntry, noEvent?: boolean) { + client.events.off("notify_properties_updated", this.clientPropertyChangedListener); + if(!this.client_list.remove(client)) { log.warn(LogCategory.CHANNEL, tr("Unregistered unknown client from channel %s"), this.channelName()); - - if(!no_event) - this.events.fire("notify_clients_changed"); + } } private reorderClientList(fire_event: boolean) { @@ -299,7 +288,7 @@ export class ChannelEntry extends ChannelTreeEntry { /* only fire if really something has changed ;) */ for(let index = 0; index < this.client_list.length; index++) { if(this.client_list[index] !== original_list[index]) { - this.events.fire("notify_clients_changed"); + this.channelTree.events.fire("notify_channel_client_order_changed", { channel: this }); break; } } @@ -333,7 +322,7 @@ export class ChannelEntry extends ChannelTreeEntry { }, result); } - clients_ordered() : ClientEntry[] { + channelClientsOrdered() : ClientEntry[] { return this.client_list; } @@ -595,7 +584,7 @@ export class ChannelEntry extends ChannelTreeEntry { info_update = true; } else if(key == "channel_order") { let order = this.channelTree.findChannel(this.properties.channel_order); - this.channelTree.moveChannel(this, order, this.parent); + this.channelTree.moveChannel(this, order, this.parent, true); } else if(key === "channel_icon_id") { this.properties.channel_icon_id = variable.value as any >>> 0; /* unsigned 32 bit number! */ } else if(key == "channel_description") { @@ -746,7 +735,6 @@ export class ChannelEntry extends ChannelTreeEntry { this._flag_collapsed = flag; this.events.fire("notify_collapsed_state_changed", { collapsed: flag }); - this.view.current?.forceUpdate(); this.channelTree.client.settings.changeServer(Settings.FN_SERVER_CHANNEL_COLLAPSED(this.channelId), flag); } @@ -780,4 +768,21 @@ export class ChannelEntry extends ChannelTreeEntry { channel_id: this.channelId } } + + getStatusIcon() : ClientIcon | undefined { + if(this.parsed_channel_name.alignment !== "normal") { + return undefined; + } + + const subscribed = this.flag_subscribed; + if (this.properties.channel_flag_password === true && !this.cached_password()) { + return subscribed ? ClientIcon.ChannelYellowSubscribed : ClientIcon.ChannelYellow; + } else if (!this.properties.channel_flag_maxclients_unlimited && this.clients().length >= this.properties.channel_maxclients) { + return subscribed ? ClientIcon.ChannelRedSubscribed : ClientIcon.ChannelRed; + } else if (!this.properties.channel_flag_maxfamilyclients_unlimited && this.properties.channel_maxfamilyclients >= 0 && this.clients(true).length >= this.properties.channel_maxfamilyclients) { + return subscribed ? ClientIcon.ChannelRedSubscribed : ClientIcon.ChannelRed; + } else { + return subscribed ? ClientIcon.ChannelGreenSubscribed : ClientIcon.ChannelGreen; + } + } } \ No newline at end of file diff --git a/shared/js/tree/ChannelTree.tsx b/shared/js/tree/ChannelTree.tsx index d6218c1c..2386e853 100644 --- a/shared/js/tree/ChannelTree.tsx +++ b/shared/js/tree/ChannelTree.tsx @@ -1,7 +1,7 @@ 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 {LogCategory, logWarn} 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"; @@ -14,7 +14,6 @@ import {ChannelTreeEntry} from "./ChannelTreeEntry"; 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"; @@ -25,8 +24,8 @@ 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"; +import {renderChannelTree} from "tc-shared/ui/tree/Controller"; export interface ChannelTreeEvents { action_select_entries: { @@ -40,25 +39,19 @@ export interface ChannelTreeEvents { mode: "auto" | "exclusive" | "append" | "remove"; }, - notify_selection_changed: {}, - notify_root_channel_changed: {}, + /* general tree notified */ notify_tree_reset: {}, + notify_selection_changed: {}, 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 - }, + /* channel tree events */ + notify_channel_created: { channel: ChannelEntry }, + notify_channel_moved: { channel: ChannelEntry }, + notify_channel_deleted: { channel: ChannelEntry }, + notify_channel_client_order_changed: { channel: ChannelEntry }, notify_channel_updated: { channel: ChannelEntry, @@ -67,6 +60,26 @@ export interface ChannelTreeEvents { }, notify_channel_list_received: {} + + /* client events */ + notify_client_enter_view: { + client: ClientEntry, + reason: ViewReasonId, + isServerJoin: boolean, + targetChannel: ChannelEntry + }, + notify_client_moved: { + client: ClientEntry, + oldChannel: ChannelEntry, + newChannel: ChannelEntry + } + notify_client_leave_view: { + client: ClientEntry, + reason: ViewReasonId, + message?: string, + isServerLeave: boolean, + sourceChannel: ChannelEntry + } } export class ChannelTreeEntrySelect { @@ -198,8 +211,11 @@ export class ChannelTreeEntrySelect { console.warn("Received entry select event with unknown mode: %s", event.mode); } + /* + TODO! if(this.selected_entries.length === 1) this.handle.view.current?.scrollEntryInView(this.selected_entries[0] as any); + */ } } @@ -212,8 +228,8 @@ export class ChannelTree { channels: ChannelEntry[] = []; clients: ClientEntry[] = []; - readonly view: React.RefObject; - readonly view_move: React.RefObject; + //readonly view: React.RefObject; + //readonly view_move: React.RefObject; readonly selection: ChannelTreeEntrySelect; private readonly _tag_container: JQuery; @@ -231,33 +247,38 @@ export class ChannelTree { 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"); + renderChannelTree(this, this._tag_container[0]); + /* 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)) { + /* + TODO: Move this into the channel tree renderer 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()) + 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); @@ -293,6 +314,25 @@ export class ChannelTree { return result; } + findEntryId(entryId: number) : ServerEntry | ChannelEntry | ClientEntry { + /* TODO: Build a cache and don't iterate over everything */ + if(this.server.uniqueEntryId === entryId) { + return this.server; + } + + const channelIndex = this.channels.findIndex(channel => channel.uniqueEntryId === entryId); + if(channelIndex !== -1) { + return this.channels[channelIndex]; + } + + const clientIndex = this.clients.findIndex(client => client.uniqueEntryId === entryId); + if(clientIndex !== -1) { + return this.clients[clientIndex]; + } + + return undefined; + } + destroy() { ReactDOM.unmountComponentAtNode(this._tag_container[0]); @@ -320,7 +360,6 @@ export class ChannelTree { 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[] { @@ -338,19 +377,20 @@ export class ChannelTree { batch_updates(BatchUpdateType.CHANNEL_TREE); try { - if(!this.channels.remove(channel)) + 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)) + 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"); + this.events.fire("notify_channel_deleted", { channel: channel }); } finally { flush_batched_updates(BatchUpdateType.CHANNEL_TREE); } @@ -360,7 +400,8 @@ export class ChannelTree { channel.channelTree = this; this.channels.push(channel); - this.moveChannel(channel, previous, parent); + this.moveChannel(channel, previous, parent, false); + this.events.fire("notify_channel_created", { channel: channel }); } findChannel(channelId: number) : ChannelEntry | undefined { @@ -379,63 +420,56 @@ export class ChannelTree { return undefined; } - private unregisterChannelFromTree(channel: ChannelEntry, new_parent?: ChannelEntry) { - let oldChannelParent; + private unregisterChannelFromTree(channel: ChannelEntry) { if(channel.parent) { - if(channel.parent.child_channel_head === channel) + 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) + if(channel.channel_previous) { channel.channel_previous.channel_next = channel.channel_next; + } - if(channel.channel_next) + if(channel.channel_next) { channel.channel_next.channel_previous = channel.channel_previous; + } - if(channel === this.channel_last) + if(channel === this.channel_last) { this.channel_last = channel.channel_previous; + } - if(channel === this.channel_first) + 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); + moveChannel(channel: ChannelEntry, channelPrevious: ChannelEntry, parent: ChannelEntry, triggerMoveEvent: boolean) { + if(channelPrevious != null && channelPrevious.parent != parent) { + console.error(tr("Invalid channel move (different parents! (%o|%o)"), channelPrevious.parent, parent); return; } - let root_tree_updated = !channel.parent; - this.unregisterChannelFromTree(channel, parent); - channel.channel_previous = channel_previous; + this.unregisterChannelFromTree(channel); + channel.channel_previous = channelPrevious; channel.channel_next = undefined; channel.parent = parent; - if(channel_previous) { - if(channel_previous == this.channel_last) + if(channelPrevious) { + if(channelPrevious == this.channel_last) { this.channel_last = channel; + } - channel.channel_next = channel_previous.channel_next; - channel_previous.channel_next = channel; + channel.channel_next = channelPrevious.channel_next; + channelPrevious.channel_next = channel; - if(channel.channel_next) + 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(); @@ -446,7 +480,6 @@ export class ChannelTree { 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) @@ -454,14 +487,9 @@ export class ChannelTree { 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; @@ -471,22 +499,23 @@ export class ChannelTree { debugger; } - if(root_tree_updated) - this.events.fire("notify_root_channel_changed"); + if(triggerMoveEvent) { + this.events.fire("notify_channel_moved", { channel: channel }); + } } deleteClient(client: ClientEntry, reason: { reason: ViewReasonId, message?: string, serverLeave: boolean }) { - const old_channel = client.currentChannel(); - old_channel?.unregisterClient(client); + const oldChannel = client.currentChannel(); + oldChannel?.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); + if(oldChannel) { + this.events.fire("notify_client_leave_view", { client: client, message: reason.message, reason: reason.reason, isServerLeave: reason.serverLeave, sourceChannel: oldChannel }); + this.client.side_bar.info_frame().update_channel_client_count(oldChannel); + } else { + logWarn(LogCategory.CHANNEL, tr("Deleting client %s from channel tree which hasn't a channel."), client.clientId()); } - //FIXME: Trigger the notify_clients_changed event! const voice_connection = this.client.serverConnection.getVoiceConnection(); if(client.getVoiceClient()) { const voiceClient = client.getVoiceClient(); @@ -530,7 +559,7 @@ export class ChannelTree { client["_channel"] = channel; channel.registerClient(client); - this.events.fire("notify_client_enter_view", { client: client, reason: reason.reason, isServerJoin: reason.isServerJoin }); + this.events.fire("notify_client_enter_view", { client: client, reason: reason.reason, isServerJoin: reason.isServerJoin, targetChannel: channel }); return client; } finally { flush_batched_updates(BatchUpdateType.CHANNEL_TREE); @@ -553,9 +582,7 @@ export class ChannelTree { this.client.side_bar.info_frame().update_channel_client_count(targetChannel); } - if(oldChannel && targetChannel) { - client.events.fire("notify_client_moved", { oldChannel: oldChannel, newChannel: targetChannel }); - } + this.events.fire("notify_client_moved", { oldChannel: oldChannel, newChannel: targetChannel, client: client }); } finally { flush_batched_updates(BatchUpdateType.CHANNEL_TREE); } @@ -916,7 +943,7 @@ export class ChannelTree { private select_next_channel(channel: ChannelEntry, select_client: boolean) { if(select_client) { - const clients = channel.clients_ordered(); + const clients = channel.channelClientsOrdered(); if(clients.length > 0) { this.events.fire("action_select_entries", { mode: "exclusive", @@ -974,7 +1001,7 @@ export class ChannelTree { if(siblings.length == 0) break; previous = siblings.last(); } - const clients = previous.clients_ordered(); + const clients = previous.channelClientsOrdered(); if(clients.length > 0) { this.events.fire("action_select_entries", { mode: "exclusive", @@ -990,7 +1017,7 @@ export class ChannelTree { } } else if(selected.hasParent()) { const channel = selected.parent_channel(); - const clients = channel.clients_ordered(); + const clients = channel.channelClientsOrdered(); if(clients.length > 0) { this.events.fire("action_select_entries", { mode: "exclusive", @@ -1012,7 +1039,7 @@ export class ChannelTree { } } else if(selected instanceof ClientEntry) { const channel = selected.currentChannel(); - const clients = channel.clients_ordered(); + const clients = channel.channelClientsOrdered(); const index = clients.indexOf(selected); if(index > 0) { this.events.fire("action_select_entries", { @@ -1034,7 +1061,7 @@ export class ChannelTree { this.select_next_channel(selected, true); } else if(selected instanceof ClientEntry){ const channel = selected.currentChannel(); - const clients = channel.clients_ordered(); + const clients = channel.channelClientsOrdered(); const index = clients.indexOf(selected); if(index + 1 < clients.length) { this.events.fire("action_select_entries", { @@ -1131,6 +1158,7 @@ export class ChannelTree { } } + /* private onChannelEntryMove(start, current) { const move = this.view_move.current; if(!move) return; @@ -1175,8 +1203,10 @@ export class ChannelTree { flush_batched_updates(BatchUpdateType.CHANNEL_TREE); } } + */ isClientMoveActive() { - return !!this.view_move.current?.isActive(); + //return !!this.view_move.current?.isActive(); + return false; } } \ No newline at end of file diff --git a/shared/js/tree/ChannelTreeEntry.ts b/shared/js/tree/ChannelTreeEntry.ts index cb7bb1a8..4796a9ab 100644 --- a/shared/js/tree/ChannelTreeEntry.ts +++ b/shared/js/tree/ChannelTreeEntry.ts @@ -5,12 +5,18 @@ export interface ChannelTreeEntryEvents { notify_unread_state_change: { unread: boolean } } -export class ChannelTreeEntry { +let treeEntryIdCounter = 0; +export abstract class ChannelTreeEntry { readonly events: Registry; + readonly uniqueEntryId: number; protected selected_: boolean = false; protected unread_: boolean = false; + protected constructor() { + this.uniqueEntryId = treeEntryIdCounter++; + } + /* called from the channel tree */ protected onSelect(singleSelect: boolean) { if(this.selected_ === true) return; @@ -36,4 +42,6 @@ export class ChannelTreeEntry { this.events.fire("notify_unread_state_change", { unread: flag }); } isUnread() { return this.unread_; } + + abstract showContextMenu(pageX: number, pageY: number, on_close?); } \ No newline at end of file diff --git a/shared/js/tree/Client.ts b/shared/js/tree/Client.ts index 8f235e5a..e618a6d5 100644 --- a/shared/js/tree/Client.ts +++ b/shared/js/tree/Client.ts @@ -19,8 +19,6 @@ import {spawnChangeLatency} from "../ui/modal/ModalChangeLatency"; import {formatMessage} from "../ui/frames/chat"; import {spawnYesNo} from "../ui/modal/ModalYesNo"; import * as hex from "../crypto/hex"; -import {ClientEntry as ClientEntryView} from "../ui/tree/Client"; -import * as React from "react"; import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry"; import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "../ui/modal/ModalChangeVolumeNew"; import {spawnPermissionEditorModal} from "../ui/modal/permission/ModalPermissionEditor"; @@ -30,6 +28,7 @@ import {global_client_actions} from "../events/GlobalEvents"; import {ClientIcon} from "svg-sprites/client-icons"; import {VoiceClient} from "../voice/VoiceClient"; import {VoicePlayerEvents, VoicePlayerState} from "../voice/VoicePlayer"; +import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions"; export enum ClientType { CLIENT_VOICE, @@ -83,6 +82,10 @@ export class ClientProperties { client_total_bytes_downloaded: number = 0; client_talk_power: number = 0; + client_talk_request: number = 0; + client_talk_request_msg: string = ""; + client_is_talker: boolean = false; + client_is_priority_speaker: boolean = false; } @@ -139,14 +142,6 @@ export class ClientConnectionInfo { } export interface ClientEvents extends ChannelTreeEntryEvents { - notify_enter_view: {}, - notify_client_moved: { oldChannel: ChannelEntry, newChannel: ChannelEntry } - notify_left_view: { - reason: ViewReasonId; - message?: string; - serverLeave: boolean; - }, - notify_properties_updated: { updated_properties: {[Key in keyof ClientProperties]: ClientProperties[Key]}; client_properties: ClientProperties @@ -183,7 +178,6 @@ const StatusIconUpdateKeys: (keyof ClientProperties)[] = [ export class ClientEntry extends ChannelTreeEntry { readonly events: Registry; - readonly view: React.RefObject = React.createRef(); channelTree: ChannelTree; protected _clientId: number; @@ -997,7 +991,7 @@ export class LocalClientEntry extends ClientEntry { tr("Change name") + (contextmenu.get_provider().html_format_enabled() ? "" : ""), icon_class: "client-change_nickname", - callback: () => this.openRename(), + callback: () => this.openRenameModal(), /* FIXME: Pass the UI event registry */ type: contextmenu.MenuEntryType.ENTRY }, { name: tr("Change description"), @@ -1046,20 +1040,20 @@ export class LocalClientEntry extends ClientEntry { }); } - openRename() : void { - const view = this.channelTree.view.current; - if(!view) return; //TODO: Fallback input modal - view.scrollEntryInView(this, () => { - const own_view = this.view.current; - if(!own_view) { - return; //TODO: Fallback input modal + openRenameModal() { + createInputModal(tr("Enter your new name"), tr("Enter your new client name"), text => text.length >= 3 && text.length <= 30, value => { + if(value) { + this.renameSelf(value as string).then(result => { + if(!result) { + createErrorModal(tr("Failed change nickname"), tr("Failed to change your client nickname")).open(); + } + }); } + }).open(); + } - own_view.setState({ - rename: true, - renameInitialName: this.properties.client_nickname - }); - }); + openRename(events: Registry) : void { + events.fire("notify_client_name_edit", { initialValue: this.clientNickName(), treeEntryId: this.uniqueEntryId }); } } diff --git a/shared/js/tree/Server.ts b/shared/js/tree/Server.ts index a25932d9..747be4db 100644 --- a/shared/js/tree/Server.ts +++ b/shared/js/tree/Server.ts @@ -13,8 +13,6 @@ import {spawnAvatarList} from "../ui/modal/ModalAvatarList"; import {connection_log} from "../ui/modal/ModalConnect"; import * as top_menu from "../ui/frames/MenuBar"; import {control_bar_instance} from "../ui/frames/control-bar"; -import { ServerEntry as ServerEntryView } from "../ui/tree/Server"; -import * as React from "react"; import {Registry} from "../events"; import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry"; @@ -138,7 +136,6 @@ export class ServerEntry extends ChannelTreeEntry { properties: ServerProperties; readonly events: Registry; - readonly view: React.Ref; private info_request_promise: Promise = undefined; private info_request_promise_resolve: any = undefined; @@ -157,7 +154,6 @@ export class ServerEntry extends ChannelTreeEntry { super(); this.events = new Registry(); - this.view = React.createRef(); this.properties = new ServerProperties(); this.channelTree = tree; @@ -266,7 +262,7 @@ export class ServerEntry extends ChannelTreeEntry { ]; } - spawnContextMenu(x: number, y: number, on_close: () => void = () => {}) { + showContextMenu(x: number, y: number, on_close: () => void = () => {}) { contextmenu.spawn_context_menu(x, y, ...this.contextMenuItems(), contextmenu.Entry.CLOSE(on_close) ); diff --git a/shared/js/ui/frames/chat_frame.ts b/shared/js/ui/frames/chat_frame.ts index d324f6dd..c6d7e629 100644 --- a/shared/js/ui/frames/chat_frame.ts +++ b/shared/js/ui/frames/chat_frame.ts @@ -184,10 +184,13 @@ export class InfoFrame { } update_channel_client_count(channel: ChannelEntry) { - if(channel === this._channel_text) + if(channel === this._channel_text) { this.update_channel_limit(channel, this._html_tag.find(".value-text-limit")); - if(channel === this._channel_voice) + } + + if(channel === this._channel_voice) { this.update_channel_limit(channel, this._html_tag.find(".value-voice-limit")); + } } private update_channel_limit(channel: ChannelEntry, tag: JQuery) { diff --git a/shared/js/ui/htmltags.ts b/shared/js/ui/htmltags.ts index 48a47a3a..fb5cd88f 100644 --- a/shared/js/ui/htmltags.ts +++ b/shared/js/ui/htmltags.ts @@ -152,7 +152,7 @@ export namespace callbacks { if(!client) { if(current_connection.channelTree.server.properties.virtualserver_unique_identifier === client_unique_id) { - current_connection.channelTree.server.spawnContextMenu(mouse_coordinates.x, mouse_coordinates.y); + current_connection.channelTree.server.showContextMenu(mouse_coordinates.x, mouse_coordinates.y); return; } } diff --git a/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts b/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts index 66886917..ceece180 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts +++ b/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts @@ -12,6 +12,9 @@ import {WebModalRenderer} from "../../../ui/react-elements/external-modal/Popout import {ClientModalRenderer} from "../../../ui/react-elements/external-modal/PopoutRendererClient"; import {setupJSRender} from "../../../ui/jsrender"; +import "../../../file/RemoteAvatars"; +import "../../../file/RemoteIcons"; + let modalRenderer: ModalRenderer; let modalInstance: AbstractModal; let modalClass: new (events: Registry, userData: any) => AbstractModal; diff --git a/shared/js/ui/react-elements/external-modal/PopoutRegistry.ts b/shared/js/ui/react-elements/external-modal/PopoutRegistry.ts index 87cc5c1d..19cf3f54 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutRegistry.ts +++ b/shared/js/ui/react-elements/external-modal/PopoutRegistry.ts @@ -31,3 +31,8 @@ registerHandler({ name: "css-editor", loadClass: async () => await import("tc-shared/ui/modal/css-editor/Renderer") }); + +registerHandler({ + name: "channel-tree", + loadClass: async () => await import("tc-shared/ui/tree/RendererModal") +});