Reworked the channel tree events

canary
WolverinDEV 2020-09-26 21:34:46 +02:00
parent 3fb5ccf8cf
commit 3c70722bfd
14 changed files with 266 additions and 178 deletions

View File

@ -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

View File

@ -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);

View File

@ -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) {

View File

@ -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<Events, T = keyof Events> {
readonly type: T;
@ -39,6 +40,9 @@ export class Registry<Events extends { [key: string]: any } = { [key: string]: a
private debugPrefix = undefined;
private warnUnhandledEvents = false;
private pendingCallbacks: { type: any, data: any }[] = [];
private pendingCallbacksTimeout: number = 0;
constructor() {
this.registryUuid = "evreg_data_" + guid();
}
@ -200,11 +204,23 @@ export class Registry<Events extends { [key: string]: any } = { [key: string]: a
}
fire_async<T extends keyof Events>(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);
}
});
}

View File

@ -33,6 +33,8 @@ export class GroupPermissionRequest {
promise: LaterPromise<PermissionValue[]>;
}
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<PermissionValue[]> {

View File

@ -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<ChannelEvents> {
child_channel_head?: ChannelEntry;
readonly events: Registry<ChannelEvents>;
readonly view: React.RefObject<ChannelEntryView>;
parsed_channel_name: ParsedChannelName;
@ -184,13 +177,12 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
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<ChannelEvents>();
this.view = React.createRef<ChannelEntryView>();
this.properties = new ChannelProperties();
this.channelId = channelId;
@ -199,9 +191,10 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
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<ChannelEvents> {
}
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<ChannelEvents> {
/* 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<ChannelEvents> {
}, result);
}
clients_ordered() : ClientEntry[] {
channelClientsOrdered() : ClientEntry[] {
return this.client_list;
}
@ -595,7 +584,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
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<ChannelEvents> {
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<ChannelEvents> {
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;
}
}
}

View File

@ -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<ChannelTreeView>;
readonly view_move: React.RefObject<TreeEntryMove>;
//readonly view: React.RefObject<ChannelTreeView>;
//readonly view_move: React.RefObject<TreeEntryMove>;
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([
<ChannelTreeView key={"tree"} onMoveStart={(a,b) => this.onChannelEntryMove(a, b)} tree={this} ref={this.view} />,
<TreeEntryMove key={"move"} onMoveEnd={(point) => 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;
}
}

View File

@ -5,12 +5,18 @@ export interface ChannelTreeEntryEvents {
notify_unread_state_change: { unread: boolean }
}
export class ChannelTreeEntry<Events extends ChannelTreeEntryEvents> {
let treeEntryIdCounter = 0;
export abstract class ChannelTreeEntry<Events extends ChannelTreeEntryEvents> {
readonly events: Registry<Events>;
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<Events extends ChannelTreeEntryEvents> {
this.events.fire("notify_unread_state_change", { unread: flag });
}
isUnread() { return this.unread_; }
abstract showContextMenu(pageX: number, pageY: number, on_close?);
}

View File

@ -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<ClientEvents> {
readonly events: Registry<ClientEvents>;
readonly view: React.RefObject<ClientEntryView> = React.createRef<ClientEntryView>();
channelTree: ChannelTree;
protected _clientId: number;
@ -997,7 +991,7 @@ export class LocalClientEntry extends ClientEntry {
tr("Change name") +
(contextmenu.get_provider().html_format_enabled() ? "</b>" : ""),
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<ChannelTreeUIEvents>) : void {
events.fire("notify_client_name_edit", { initialValue: this.clientNickName(), treeEntryId: this.uniqueEntryId });
}
}

View File

@ -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<ServerEvents> {
properties: ServerProperties;
readonly events: Registry<ServerEvents>;
readonly view: React.Ref<ServerEntryView>;
private info_request_promise: Promise<void> = undefined;
private info_request_promise_resolve: any = undefined;
@ -157,7 +154,6 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
super();
this.events = new Registry<ServerEvents>();
this.view = React.createRef();
this.properties = new ServerProperties();
this.channelTree = tree;
@ -266,7 +262,7 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
];
}
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)
);

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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 <T>(events: Registry<T>, userData: any) => AbstractModal;

View File

@ -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")
});