TeaWeb/shared/js/tree/ChannelTree.tsx

1014 lines
41 KiB
TypeScript

import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {MenuEntryType} from "tc-shared/ui/elements/ContextMenu";
import {LogCategory, logDebug, logError, logWarn} from "tc-shared/log";
import {PermissionType} from "tc-shared/permission/PermissionType";
import {Sound} from "tc-shared/sound/Sounds";
import {Group} from "tc-shared/permission/GroupManager";
import {ServerAddress, ServerEntry} from "./Server";
import {ChannelEntry, ChannelProperties, ChannelSubscribeMode} from "./Channel";
import {ClientEntry, LocalClientEntry, MusicClientEntry} from "./Client";
import {ChannelTreeEntry} from "./ChannelTreeEntry";
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
import {Registry} from "tc-shared/events";
import * as React from "react";
import {batch_updates, BatchUpdateType, flush_batched_updates} from "tc-shared/ui/react-elements/ReactComponentBase";
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 {tr, tra} from "tc-shared/i18n/localize";
import {initializeChannelTreeUiEvents} from "tc-shared/ui/tree/Controller";
import {ChannelTreePopoutController} from "tc-shared/ui/tree/popout/Controller";
import {Settings, settings} from "tc-shared/settings";
import {ClientIcon} from "svg-sprites/client-icons";
import "./EntryTagsHandler";
import {spawnChannelEditNew} from "tc-shared/ui/modal/channel-edit/Controller";
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
export interface ChannelTreeEvents {
/* general tree notified */
notify_tree_reset: {},
notify_query_view_state_changed: { queries_shown: boolean },
notify_popout_state_changed: { popoutShown: boolean },
notify_entry_move_begin: {},
notify_entry_move_end: {},
/* channel tree events */
notify_channel_created: { channel: ChannelEntry },
notify_channel_moved: {
channel: ChannelEntry,
previousParent: ChannelEntry | undefined,
previousOrder: ChannelEntry | undefined,
},
notify_channel_deleted: { channel: ChannelEntry },
notify_channel_client_order_changed: { channel: ChannelEntry },
notify_channel_updated: {
channel: ChannelEntry,
channelProperties: ChannelProperties,
updatedProperties: ChannelProperties
},
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 | undefined,
newChannel: ChannelEntry
}
notify_client_leave_view: {
client: ClientEntry,
reason: ViewReasonId,
message?: string,
isServerLeave: boolean,
sourceChannel: ChannelEntry
},
notify_selected_entry_changed: {
oldEntry: ChannelTreeEntry<any> | undefined,
newEntry: ChannelTreeEntry<any> | undefined
}
}
export class ChannelTree {
readonly events: Registry<ChannelTreeEvents>;
client: ConnectionHandler;
server: ServerEntry;
channels: ChannelEntry[] = [];
clients: ClientEntry[] = [];
/* whatever all channels have been initialized */
channelsInitialized: boolean = false;
readonly popoutController: ChannelTreePopoutController;
/*
* We're constantly keeping the UI controller used (IDK yet how fast event attachment/detachment is)
* The main background is to speed up server tab switching.
*/
mainTreeUiEvents: Registry<ChannelTreeUIEvents>;
private selectedEntry: ChannelTreeEntry<any> | undefined;
private showQueries: boolean;
private channelLast?: ChannelEntry;
private channelFirst?: ChannelEntry;
constructor(client: ConnectionHandler) {
this.events = new Registry<ChannelTreeEvents>();
this.events.enableDebug("channel-tree");
this.client = client;
this.server = new ServerEntry(this, "undefined", undefined);
this.popoutController = new ChannelTreePopoutController(this);
this.mainTreeUiEvents = initializeChannelTreeUiEvents(this, { popoutButton: true });
this.events.on("notify_channel_list_received", () => {
if(!this.selectedEntry) {
this.setSelectedEntry(this.client.getClient().currentChannel());
}
});
this.reset();
}
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.channelFirst && visit(this.channelFirst);
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;
}
getSelectedEntry() : ChannelTreeEntry<any> | undefined {
return this.selectedEntry;
}
setSelectedEntry(entry: ChannelTreeEntry<any> | undefined) {
if(this.selectedEntry === entry) { return; }
const oldEntry = this.selectedEntry;
this.selectedEntry = entry;
this.events.fire("notify_selected_entry_changed", { newEntry: entry, oldEntry: oldEntry });
if(this.selectedEntry instanceof ClientEntry) {
if(settings.getValue(Settings.KEY_SWITCH_INSTANT_CLIENT)) {
if(this.selectedEntry instanceof MusicClientEntry) {
this.client.getSideBar().showMusicPlayer(this.selectedEntry);
} else {
this.client.getSideBar().showClientInfo(this.selectedEntry);
}
}
} else if(this.selectedEntry instanceof ChannelEntry) {
if(settings.getValue(Settings.KEY_SWITCH_INSTANT_CHAT)) {
const conversation = this.client.getChannelConversations().findOrCreateConversation(this.selectedEntry.channelId);
this.client.getChannelConversations().setSelectedConversation(conversation);
this.client.getSideBar().showChannel();
}
} else if(this.selectedEntry instanceof ServerEntry) {
if(settings.getValue(Settings.KEY_SWITCH_INSTANT_CHAT)) {
const conversation = this.client.getChannelConversations().findOrCreateConversation(0);
this.client.getChannelConversations().setSelectedConversation(conversation);
this.client.getSideBar().showServer();
}
}
}
destroy() {
this.mainTreeUiEvents?.fire("notify_destroy");
this.mainTreeUiEvents?.destroy();
this.mainTreeUiEvents = undefined;
if(this.server) {
this.server.destroy();
this.server = undefined;
}
this.reset(); /* cleanup channel and clients */
this.channelFirst = undefined;
this.channelLast = undefined;
this.popoutController.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;
}
rootChannel() : ChannelEntry[] {
const result = [];
let first = this.channelFirst;
while(first) {
result.push(first);
first = first.channel_next;
}
return result;
}
deleteChannel(channel: ChannelEntry) {
if(this.selectedEntry === channel) {
this.setSelectedEntry(undefined);
}
channel.channelTree = null;
batch_updates(BatchUpdateType.CHANNEL_TREE);
try {
if(!this.channels.remove(channel)) {
logWarn(LogCategory.CHANNEL, tr("Deleting an unknown channel!"));
}
channel.children(false).forEach(e => this.deleteChannel(e));
if(channel.clients(false).length !== 0) {
logWarn(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 });
}
}
this.unregisterChannelFromTree(channel);
this.events.fire("notify_channel_deleted", { channel: channel });
} finally {
flush_batched_updates(BatchUpdateType.CHANNEL_TREE);
}
}
handleChannelCreated(previous: ChannelEntry, parent: ChannelEntry, channelId: number, channelName: string) : ChannelEntry {
const channel = new ChannelEntry(this, channelId, channelName);
this.channels.push(channel);
this.moveChannel(channel, previous, parent, true);
this.events.fire("notify_channel_created", { channel: channel });
return channel;
}
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;
}
/**
* Resolve a channel by its path
*/
resolveChannelPath(target: string) : ChannelEntry | undefined {
if(target.match(/^\/[0-9]+$/)) {
const channelId = parseInt(target.substring(1));
return this.findChannel(channelId);
} else {
/* TODO: Resolve the whole channel path */
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) {
if(channel.parent) {
if(channel.parent.child_channel_head === channel) {
channel.parent.child_channel_head = channel.channel_next;
}
}
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.channelLast) {
this.channelLast = channel.channel_previous;
}
if(channel === this.channelFirst) {
this.channelFirst = channel.channel_next;
}
channel.channel_next = undefined;
channel.channel_previous = undefined;
channel.parent = undefined;
}
moveChannel(channel: ChannelEntry, channelPrevious: ChannelEntry, parent: ChannelEntry, isInsertMove: boolean) {
if(channelPrevious != null && channelPrevious.parent != parent) {
logError(LogCategory.CHANNEL, tr("Invalid channel move (different parents! (%o|%o)"), channelPrevious.parent, parent);
return;
}
if(!isInsertMove && channel.channel_previous === channelPrevious && channel.parent === parent) {
return;
}
const previousParent = channel.parent_channel();
const previousOrder = channel.channel_previous;
this.unregisterChannelFromTree(channel);
channel.channel_previous = channelPrevious;
channel.channel_next = undefined;
channel.parent = parent;
if(channelPrevious) {
if(channelPrevious == this.channelLast) {
this.channelLast = channel;
}
channel.channel_next = channelPrevious.channel_next;
channelPrevious.channel_next = channel;
if(channel.channel_next) {
channel.channel_next.channel_previous = channel;
}
} 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;
}
} else {
channel.channel_next = this.channelFirst;
if(this.channelFirst) {
this.channelFirst.channel_previous = channel;
}
this.channelFirst = channel;
this.channelLast = this.channelLast || channel;
}
}
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(!isInsertMove) {
this.events.fire("notify_channel_moved", {
channel: channel,
previousOrder: previousOrder,
previousParent: previousParent
});
}
channel.properties.channel_order = previousOrder ? previousOrder.channelId : 0;
}
deleteClient(client: ClientEntry, reason: { reason: ViewReasonId, message?: string, serverLeave: boolean }) {
if(this.selectedEntry === client) {
this.setSelectedEntry(undefined);
}
const oldChannel = client.currentChannel();
oldChannel?.unregisterClient(client);
this.unregisterClient(client);
if(oldChannel) {
this.events.fire("notify_client_leave_view", { client: client, message: reason.message, reason: reason.reason, isServerLeave: reason.serverLeave, sourceChannel: oldChannel });
} else {
logWarn(LogCategory.CHANNEL, tr("Deleting client %s from channel tree which hasn't a channel."), client.clientId());
}
client.destroy();
}
registerClient(client: ClientEntry) {
this.clients.push(client);
const isLocalClient = client instanceof LocalClientEntry;
if(isLocalClient) {
if(client.channelTree !== this) {
throw tr("client channel tree missmatch");
}
} else {
client.channelTree = this;
}
if(!isLocalClient) {
const voiceConnection = this.client.serverConnection.getVoiceConnection();
try {
client.setVoiceClient(voiceConnection.registerVoiceClient(client.clientId()));
} catch (error) {
logError(LogCategory.AUDIO, tr("Failed to register a voice client for %d: %o"), client.clientId(), error);
}
}
const videoConnection = this.client.serverConnection.getVideoConnection();
try {
client.setVideoClient(videoConnection.registerVideoClient(client.clientId()));
} catch (error) {
logError(LogCategory.VIDEO, tr("Failed to register a video client for %d: %o"), client.clientId(), error);
}
}
unregisterClient(client: ClientEntry) {
if(!this.clients.remove(client)) {
return;
}
const voiceConnection = this.client.serverConnection.getVoiceConnection();
if(client.getVoiceClient()) {
voiceConnection.unregisterVoiceClient(client.getVoiceClient());
client.setVoiceClient(undefined);
}
const videoConnection = this.client.serverConnection.getVideoConnection();
if(client.getVideoClient()) {
videoConnection.unregisterVideoClient(client.getVideoClient());
client.setVideoClient(undefined);
}
}
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, targetChannel: channel });
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);
this.events.fire("notify_client_moved", { oldChannel: oldChannel, newChannel: targetChannel, client: client });
} 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)
);
}
public showMultiSelectContextMenu(entries: ChannelTreeEntry<any>[], 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 channelMenu: 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:<br>"), text => true, result => {
if (typeof(result) === "string") {
for (const client of clients) {
this.client.serverConnection.send_command("clientpoke", {
clid: client.clientId(),
msg: result
});
}
}
}, {width: 400, maxLength: 512}).open();
}
});
client_menu.push({
type: contextmenu.MenuEntryType.ENTRY,
icon_class: ClientIcon.ChangeNickname,
name: tr("Send private message"),
callback: () => {
createInputModal(tr("Send private message"), tr("Message:<br>"), text => !!text, result => {
if (typeof(result) === "string") {
for (const client of clients) {
this.client.serverConnection.send_command("sendtextmessage", {
target: client.clientId(),
msg: result,
targetmode: 1
});
}
}
}, {width: 400, maxLength: 1024 * 8}).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
});
}
}
});
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:<br>"), 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();
}
});
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: () => {
createInputModal(tr("Poke clients"), tr("Poke message:<br>"), 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: () => {
createInputModal(tr("Kick clients from server"), tr("Kick reason:<br>"), 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: () => {
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
});
}
}
});
},
type: contextmenu.MenuEntryType.ENTRY
});
}
}
}
if(channels.length > 0) {
channelMenu = [];
channelMenu.push({
type: MenuEntryType.ENTRY,
name: tr("Subscribe to channels"),
icon_class: ClientIcon.SubscribeToAllChannels,
callback: () => {
const bulks = channels.filter(channel => {
channel.setSubscriptionMode(ChannelSubscribeMode.SUBSCRIBED, false);
return !channel.isSubscribed();
}).map(channel => {
return {
cid: channel.channelId
};
});
if(bulks.length === 0) {
/* shall not happen */
return;
}
this.client.serverConnection.send_command("channelsubscribe", bulks);
},
visible: channels.findIndex(channel => channel.getSubscriptionMode() !== ChannelSubscribeMode.SUBSCRIBED) !== -1
});
channelMenu.push({
type: MenuEntryType.ENTRY,
name: tr("Unsubscribe from channels"),
icon_class: ClientIcon.UnsubscribeFromAllChannels,
callback: () => {
const bulks = channels.filter(channel => {
channel.setSubscriptionMode(ChannelSubscribeMode.UNSUBSCRIBED, false);
return channel.isSubscribed();
}).map(channel => {
return {
cid: channel.channelId
};
});
if(bulks.length === 0) {
/* shall not happen */
return;
}
this.client.serverConnection.send_command("channelunsubscribe", bulks);
},
visible: channels.findIndex(channel => channel.getSubscriptionMode() !== ChannelSubscribeMode.UNSUBSCRIBED) !== -1
});
channelMenu.push({
type: MenuEntryType.ENTRY,
name: tr("Use inherited subscribe mode"),
icon_class: ClientIcon.SubscribeToAllChannels,
callback: () => {
const inheritedSubscribe = this.client.isSubscribeToAllChannels();
const bulks = channels.filter(channel => {
channel.setSubscriptionMode(ChannelSubscribeMode.INHERITED, false);
return channel.isSubscribed() != inheritedSubscribe;
}).map(channel => {
return {
cid: channel.channelId
};
});
if(bulks.length === 0) {
/* might happen */
return;
}
this.client.serverConnection.send_command(inheritedSubscribe ? "channelsubscribe" : "channelunsubscribe", bulks);
},
visible: channels.findIndex(channel => channel.getSubscriptionMode() !== ChannelSubscribeMode.INHERITED) !== -1
});
channelMenu.push(contextmenu.Entry.HR());
//TODO: Subscribe mode settings
channelMenu.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 });
}
}
});
}
});
}
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: channelMenu,
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() {
this.channelsInitialized = false;
batch_updates(BatchUpdateType.CHANNEL_TREE);
try {
this.setSelectedEntry(undefined);
const voiceConnection = this.client.serverConnection ? this.client.serverConnection.getVoiceConnection() : undefined;
const videoConnection = this.client.serverConnection ? this.client.serverConnection.getVideoConnection() : undefined;
for(const client of this.clients) {
if(client.getVoiceClient() && videoConnection) {
voiceConnection.unregisterVoiceClient(client.getVoiceClient());
client.setVoiceClient(undefined);
}
if(client.getVideoClient()) {
videoConnection.unregisterVideoClient(client.getVideoClient());
client.setVideoClient(undefined);
}
client.destroy();
}
this.clients = [];
for(const channel of this.channels)
channel.destroy();
this.channels = [];
this.channelLast = undefined;
this.channelFirst = undefined;
this.events.fire("notify_tree_reset");
} finally {
flush_batched_updates(BatchUpdateType.CHANNEL_TREE);
}
}
spawnCreateChannel(parent?: ChannelEntry) {
spawnChannelEditNew(this.client, undefined, parent, (properties, permissions) => {
properties["cpid"] = parent ? parent.channelId : 0;
logDebug(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) {
logError(LogCategory.CHANNEL, tr("Failed to resolve channel after creation. Could not apply permissions!"));
return;
}
channel.setCachedHashedPassword(properties.channel_password);
if(permissions && permissions.length > 0) {
let perms = [];
for(let perm of permissions) {
perms.push({
permvalue: perm.value,
permnegated: false,
permskip: false,
permsid: perm.permission
});
}
perms[0]["cid"] = channel.channelId;
return this.client.serverConnection.send_command("channeladdperm", perms, {
flagset: ["continueonerror"]
}).then(() => new Promise<ChannelEntry>(resolve => { resolve(channel); }));
}
return new Promise<ChannelEntry>(resolve => { resolve(channel); })
});
});
}
toggle_server_queries(flag: boolean) {
if(this.showQueries == flag) return;
this.showQueries = flag;
this.events.fire("notify_query_view_state_changed", { queries_shown: flag });
}
areServerQueriesShown() { return this.showQueries; }
get_first_channel?() : ChannelEntry {
return this.channelFirst;
}
unsubscribe_all_channels() {
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.getSubscriptionMode() == ChannelSubscribeMode.SUBSCRIBED) {
channels.push(channel.getChannelId());
}
}
if(channels.length > 0) {
this.client.serverConnection.send_command('channelsubscribe', channels.map(e => { return {cid: e}; })).catch(error => {
logWarn(LogCategory.NETWORKING, tr("Failed to subscribe to specific channels (%o): %o"), channels, error);
});
}
}).catch(error => {
logWarn(LogCategory.NETWORKING, 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.getSubscriptionMode() == ChannelSubscribeMode.UNSUBSCRIBED) {
channels.push(channel.getChannelId());
}
}
if(channels.length > 0) {
this.client.serverConnection.send_command('channelunsubscribe', channels.map(e => { return {cid: e}; })).catch(error => {
logWarn(LogCategory.CHANNEL, tr("Failed to unsubscribe to specific channels (%o): %o"), channels, error);
});
}
}).catch(error => {
logWarn(LogCategory.CHANNEL, 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.setCollapsed(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.setCollapsed(true);
for(const child of root.children(false))
this.collapse_channels(child);
}
}
}