import {ChannelTree} from "./ChannelTree"; import {ClientEntry, ClientEvents} from "./Client"; import * as log from "../log"; import {LogCategory, logInfo, LogType, logWarn} from "../log"; import {PermissionType} from "../permission/PermissionType"; import {settings, Settings} from "../settings"; import * as contextmenu from "../ui/elements/ContextMenu"; import {MenuEntryType} from "../ui/elements/ContextMenu"; import {Sound} from "../sound/Sounds"; import {createErrorModal, createInfoModal, createInputModal} from "../ui/elements/Modal"; import {CommandResult} from "../connection/ServerConnectionDeclaration"; import * as htmltags from "../ui/htmltags"; import {hashPassword} from "../utils/helpers"; import {openChannelInfo} from "../ui/modal/ModalChannelInfo"; import {formatMessage} from "../ui/frames/chat"; import {Registry} from "../events"; import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry"; import {spawnFileTransferModal} from "../ui/modal/transfer/ModalFileTransfer"; import {ErrorCode} from "../connection/ErrorCode"; import {ClientIcon} from "svg-sprites/client-icons"; import { tr } from "tc-shared/i18n/localize"; import {EventChannelData} from "tc-shared/connectionlog/Definitions"; import {spawnChannelEditNew} from "tc-shared/ui/modal/channel-edit/Controller"; import {spawnInviteGenerator} from "tc-shared/ui/modal/invite/Controller"; export enum ChannelType { PERMANENT, SEMI_PERMANENT, TEMPORARY } export namespace ChannelType { export function normalize(mode: ChannelType) { let value: string = ChannelType[mode]; value = value.toLowerCase(); return value[0].toUpperCase() + value.substr(1); } } export enum ChannelSubscribeMode { SUBSCRIBED, UNSUBSCRIBED, INHERITED } export enum ChannelConversationMode { Public = 0, Private = 1, None = 2, } export enum ChannelSidebarMode { Conversation = 0, Description = 1, FileTransfer = 2, /* Only used within client side */ Unknown = 0xFF } export class ChannelProperties { channel_order: number = 0; channel_name: string = ""; channel_name_phonetic: string = ""; channel_topic: string = ""; channel_password: string = ""; channel_codec: number = 4; channel_codec_quality: number = 6; channel_codec_is_unencrypted: boolean = false; channel_maxclients: number = -1; channel_maxfamilyclients: number = -1; channel_needed_talk_power: number = 0; channel_flag_permanent: boolean = false; channel_flag_semi_permanent: boolean = false; channel_flag_default: boolean = false; channel_flag_password: boolean = false; channel_flag_maxclients_unlimited: boolean = true; channel_flag_maxfamilyclients_inherited: boolean = false; channel_flag_maxfamilyclients_unlimited: boolean = true; channel_icon_id: number = 0; channel_delete_delay: number = 0; //Only after request channel_description: string = ""; channel_conversation_mode: ChannelConversationMode = ChannelConversationMode.Public; channel_conversation_history_length: number = -1; channel_sidebar_mode: ChannelSidebarMode = ChannelSidebarMode.Unknown; } export interface ChannelEvents extends ChannelTreeEntryEvents { notify_properties_updated: { updated_properties: {[Key in keyof ChannelProperties]: ChannelProperties[Key]}; channel_properties: ChannelProperties }, notify_cached_password_updated: { reason: "channel-password-changed" | "password-miss-match" | "password-entered"; new_hash?: string; }, notify_subscribe_state_changed: { channel_subscribed: boolean }, notify_collapsed_state_changed: { collapsed: boolean }, notify_description_changed: {} } export type ChannelNameAlignment = "center" | "right" | "left" | "normal" | "repetitive"; export class ChannelNameParser { readonly originalName: string; alignment: ChannelNameAlignment; text: string; /* does not contain any alignment codes */ uniqueId: string; constructor(name: string, hasParentChannel: boolean) { this.originalName = name; this.parse(hasParentChannel); } private parse(hasParentChannel: boolean) { this.alignment = "normal"; if(this.originalName.length < 3) { this.text = this.originalName; return; } parseType: if(!hasParentChannel && this.originalName.charAt(0) == '[') { let end = this.originalName.indexOf(']'); if(end === -1) { break parseType; } let options = this.originalName.substr(1, end - 1); const spacerIndex = options.indexOf("spacer"); if(spacerIndex === -1) break parseType; this.uniqueId = options.substring(spacerIndex + 6); options = options.substr(0, spacerIndex); if(options.length == 0) { options = "l"; } else if(options.length > 1) { options = options[0]; } switch (options) { case "r": this.alignment = "right"; break; case "l": this.alignment = "left"; break; case "c": this.alignment = "center"; break; case "*": this.alignment = "repetitive"; break; default: break parseType; } this.text = this.originalName.substr(end + 1); } if(!this.text && this.alignment === "normal") { this.text = this.originalName; } } } export class ChannelEntry extends ChannelTreeEntry { channelTree: ChannelTree; channelId: number; parent?: ChannelEntry; properties: ChannelProperties = new ChannelProperties(); channel_previous?: ChannelEntry; channel_next?: ChannelEntry; child_channel_head?: ChannelEntry; readonly events: Registry; parsed_channel_name: ChannelNameParser; private _family_index: number = 0; //HTML DOM elements private _destroyed = false; private cachedPasswordHash: string; private channelDescriptionCached: boolean; private channelDescriptionCallback: ((success: boolean) => void)[]; private channelDescriptionPromise: Promise; private collapsed: boolean; private subscribed: boolean; private subscriptionMode: ChannelSubscribeMode; private client_list: ClientEntry[] = []; /* this list is sorted correctly! */ private readonly clientPropertyChangedListener; constructor(channelTree: ChannelTree, channelId: number, channelName: string) { super(); this.channelTree = channelTree; this.events = new Registry(); this.subscribed = false; this.properties = new ChannelProperties(); this.channelId = channelId; this.properties.channel_name = channelName; this.parsed_channel_name = new ChannelNameParser(channelName, false); 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 => { this.channelTree?.events.fire("notify_channel_updated", { channel: this, channelProperties: event.channel_properties, updatedProperties: event.updated_properties }); }); this.collapsed = this.channelTree.client.settings.getValue(Settings.FN_SERVER_CHANNEL_COLLAPSED(this.channelId)); this.subscriptionMode = this.channelTree.client.settings.getValue(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this.channelId), ChannelSubscribeMode.INHERITED); this.channelDescriptionCached = false; this.channelDescriptionCallback = []; } destroy() { this._destroyed = true; this.channelDescriptionCallback.forEach(callback => callback(false)); this.channelDescriptionCallback = []; this.client_list.forEach(e => this.unregisterClient(e, true)); this.client_list = []; this.channel_previous = undefined; this.parent = undefined; this.channel_next = undefined; this.channelTree = undefined; } channelName(){ return this.properties.channel_name; } channelDepth() { let depth = 0; let parent = this.parent; while(parent) { depth++; parent = parent.parent; } return depth; } formattedChannelName() { return this.parsed_channel_name.text; } async getChannelDescription() : Promise { if(this.channelDescriptionPromise) { return this.channelDescriptionPromise; } const promise = this.doGetChannelDescription(); this.channelDescriptionPromise = promise; promise .then(() => this.channelDescriptionPromise = undefined) .catch(() => this.channelDescriptionPromise = undefined); return promise; } isDescriptionCached() { return this.channelDescriptionCached; } private async doGetChannelDescription() { if(!this.channelDescriptionCached) { await this.channelTree.client.serverConnection.send_command("channelgetdescription", { cid: this.channelId }); if(!this.channelDescriptionCached) { /* since the channel description is a low command it will not be processed in sync */ await new Promise((resolve, reject) => { this.channelDescriptionCallback.push(succeeded => { if(succeeded) { resolve(); } else { reject(tr("failed to receive description")); } }) }); } } return this.properties.channel_description; } registerClient(client: ClientEntry) { client.events.on("notify_properties_updated", this.clientPropertyChangedListener); this.client_list.push(client); this.reorderClientList(false); } unregisterClient(client: ClientEntry, noEvent?: boolean) { client.events.off("notify_properties_updated", this.clientPropertyChangedListener); if(!this.client_list.remove(client)) { logWarn(LogCategory.CHANNEL, tr("Unregistered unknown client from channel %s"), this.channelName()); } } private reorderClientList(fire_event: boolean) { const original_list = this.client_list.slice(0); this.client_list.sort((a, b) => { if(a.properties.client_talk_power < b.properties.client_talk_power) return 1; if(a.properties.client_talk_power > b.properties.client_talk_power) return -1; if(a.properties.client_nickname > b.properties.client_nickname) return 1; if(a.properties.client_nickname < b.properties.client_nickname) return -1; return 0; }); if(fire_event) { /* 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.channelTree.events.fire("notify_channel_client_order_changed", { channel: this }); break; } } } } parent_channel() { return this.parent; } hasParent(){ return this.parent != null; } getChannelId(){ return this.channelId; } children(deep = false) : ChannelEntry[] { const result: ChannelEntry[] = []; let head = this.child_channel_head; while(head) { result.push(head); head = head.channel_next; } if(deep) return result.map(e => e.children(true)).reduce((prv, now) => { prv.push(...now); return prv; }, []); return result; } clients(deep = false) : ClientEntry[] { const result: ClientEntry[] = this.client_list.slice(0); if(!deep) return result; return this.children(true).map(e => e.clients(false)).reduce((prev, cur) => { prev.push(...cur); return cur; }, result); } channelClientsOrdered() : ClientEntry[] { return this.client_list; } calculate_family_index(enforce_recalculate: boolean = false) : number { if(this._family_index !== undefined && !enforce_recalculate) return this._family_index; this._family_index = 0; let channel = this.parent_channel(); while(channel) { this._family_index++; channel = channel.parent_channel(); } return this._family_index; } showContextMenu(x: number, y: number, on_close: () => void = undefined) { let channelCreate = !![ PermissionType.B_CHANNEL_CREATE_TEMPORARY, PermissionType.B_CHANNEL_CREATE_SEMI_PERMANENT, PermissionType.B_CHANNEL_CREATE_PERMANENT ].find(e => this.channelTree.client.permissions.neededPermission(e).granted(1)); let channelModify = !![ PermissionType.B_CHANNEL_MODIFY_MAKE_DEFAULT, PermissionType.B_CHANNEL_MODIFY_MAKE_PERMANENT, PermissionType.B_CHANNEL_MODIFY_MAKE_SEMI_PERMANENT, PermissionType.B_CHANNEL_MODIFY_MAKE_TEMPORARY, PermissionType.B_CHANNEL_MODIFY_NAME, PermissionType.B_CHANNEL_MODIFY_TOPIC, PermissionType.B_CHANNEL_MODIFY_DESCRIPTION, PermissionType.B_CHANNEL_MODIFY_PASSWORD, PermissionType.B_CHANNEL_MODIFY_CODEC, PermissionType.B_CHANNEL_MODIFY_CODEC_QUALITY, PermissionType.B_CHANNEL_MODIFY_CODEC_LATENCY_FACTOR, PermissionType.B_CHANNEL_MODIFY_MAXCLIENTS, PermissionType.B_CHANNEL_MODIFY_MAXFAMILYCLIENTS, PermissionType.B_CHANNEL_MODIFY_SORTORDER, PermissionType.B_CHANNEL_MODIFY_NEEDED_TALK_POWER, PermissionType.B_CHANNEL_MODIFY_MAKE_CODEC_ENCRYPTED, PermissionType.B_CHANNEL_MODIFY_TEMP_DELETE_DELAY, PermissionType.B_ICON_MANAGE ].find(e => this.channelTree.client.permissions.neededPermission(e).granted(1)); let flagDelete = true; if(this.clients(true).length > 0) flagDelete = this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_DELETE_FLAG_FORCE).granted(1); if(flagDelete) { if (this.properties.channel_flag_permanent) flagDelete = this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_DELETE_PERMANENT).granted(1); else if (this.properties.channel_flag_semi_permanent) flagDelete = this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_DELETE_PERMANENT).granted(1); else flagDelete = this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_DELETE_TEMPORARY).granted(1); } let trigger_close = true; const collapse_expendable = !!this.child_channel_head || this.client_list.length > 0; const bold = text => contextmenu.get_provider().html_format_enabled() ? "" + text + "" : text; contextmenu.spawn_context_menu(x, y, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_switch", name: bold(tr("Switch to channel")), callback: () => this.joinChannel(), visible: this !== this.channelTree.client.getClient()?.currentChannel() }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-filetransfer", name: bold(tr("Open channel file browser")), callback: () => spawnFileTransferModal(this.getChannelId()), }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_switch", name: bold(tr("Join text channel")), callback: () => { const conversation = this.channelTree.client.getChannelConversations().findOrCreateConversation(this.getChannelId()); this.channelTree.client.getChannelConversations().setSelectedConversation(conversation); this.channelTree.client.getSideBar().showChannel(); }, visible: !settings.getValue(Settings.KEY_SWITCH_INSTANT_CHAT) }, { type: contextmenu.MenuEntryType.HR, name: '' }, { type: contextmenu.MenuEntryType.ENTRY, name: tr("Show channel info"), callback: () => { trigger_close = false; openChannelInfo(this); }, icon_class: "client-about" }, { type: contextmenu.MenuEntryType.ENTRY, name: tr("Invite People"), callback: () => spawnInviteGenerator(this), icon_class: ClientIcon.InviteBuddy }, ...(() => { const local_client = this.channelTree.client.getClient(); if (!local_client || local_client.currentChannel() !== this) return [ contextmenu.Entry.HR(), { type: contextmenu.MenuEntryType.ENTRY, icon: "client-subscribe_to_channel", name: bold(tr("Subscribe to channel")), callback: () => this.subscribe(), visible: !this.isSubscribed() }, { type: contextmenu.MenuEntryType.ENTRY, icon: "client-channel_unsubscribed", name: bold(tr("Unsubscribe from channel")), callback: () => this.unsubscribe(), visible: this.isSubscribed() }, { type: contextmenu.MenuEntryType.ENTRY, icon: "client-subscribe_mode", name: bold(tr("Use inherited subscribe mode")), callback: () => this.unsubscribe(true), visible: this.subscriptionMode != ChannelSubscribeMode.INHERITED } ]; return []; })(), contextmenu.Entry.HR(), { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_edit", name: tr("Edit channel"), invalidPermission: !channelModify, callback: () => { spawnChannelEditNew(this.channelTree.client, this, this.parent, (properties, permissions) => { const changedProperties = Object.keys(properties); if(changedProperties.length > 0) { properties["cid"] = this.channelId; this.channelTree.client.serverConnection.send_command("channeledit", properties).then(() => { this.channelTree.client.sound.play(Sound.CHANNEL_EDITED_SELF); }); logInfo(LogCategory.CHANNEL, tr("Changed channel properties of channel %s: %o"), this.channelName(), properties); } if(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"] = this.channelId; this.channelTree.client.serverConnection.send_command("channeladdperm", perms, { flagset: ["continueonerror"] }).then(() => { this.channelTree.client.sound.play(Sound.CHANNEL_EDITED_SELF); }); } }); } }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_delete", name: tr("Delete channel"), invalidPermission: !flagDelete, callback: () => { this.channelTree.client.serverConnection.send_command("channeldelete", {cid: this.channelId}); } }, contextmenu.Entry.HR(), { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-addon-collection", name: tr("Create music bot"), callback: () => { this.channelTree.client.serverConnection.send_command("musicbotcreate", {cid: this.channelId}).then(() => { createInfoModal(tr("Bot successfully created"), tr("Bot has been successfully created.")).open(); }).catch(error => { if(error instanceof CommandResult) { error = error.extra_message || error.message; } createErrorModal(tr("Failed to create bot"), formatMessage(tr("Failed to create the music bot:
{0}"), error)).open(); }); } }, { type: MenuEntryType.HR, name: "", visible: collapse_expendable }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_collapse_all", name: tr("Collapse sub channels"), visible: collapse_expendable, callback: () => this.channelTree.collapse_channels(this) }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_expand_all", name: tr("Expend sub channels"), visible: collapse_expendable, callback: () => this.channelTree.expand_channels(this) }, contextmenu.Entry.HR(), { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_create_sub", name: tr("Create sub channel"), invalidPermission: !(channelCreate && this.channelTree.client.permissions.neededPermission(PermissionType.B_CHANNEL_CREATE_CHILD).granted(1)), callback: () => this.channelTree.spawnCreateChannel(this) }, { type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-channel_create", name: tr("Create channel"), invalidPermission: !channelCreate, callback: () => this.channelTree.spawnCreateChannel() }, contextmenu.Entry.CLOSE(() => trigger_close && on_close ? on_close() : {}) ); } updateVariables(...variables: {key: string, value: string}[]) { /* devel-block(log-channel-property-updates) */ let group = log.group(log.LogType.DEBUG, LogCategory.CHANNEL_PROPERTIES, tr("Update properties (%i) of %s (%i)"), variables.length, this.channelName(), this.getChannelId()); { const entries = []; for(const variable of variables) entries.push({ key: variable.key, value: variable.value, type: typeof (this.properties[variable.key]) }); log.table(LogType.DEBUG, LogCategory.PERMISSIONS, "Clannel update properties", entries); } /* devel-block-end */ /* TODO: Validate values. Example: channel_conversation_mode */ for(const variable of variables) { let key = variable.key; let value = variable.value; const hasUpdate = JSON.map_field_to(this.properties, value, variable.key); if(key == "channel_description") { this.channelDescriptionCached = true; this.channelDescriptionCallback.forEach(callback => callback(true)); this.channelDescriptionCallback = []; } if(hasUpdate) { if(key == "channel_name") { this.parsed_channel_name = new ChannelNameParser(value, this.hasParent()); } else if(key == "channel_order") { let order = this.channelTree.findChannel(this.properties.channel_order); this.channelTree.moveChannel(this, order, this.parent, false); } else if(key === "channel_icon_id") { this.properties.channel_icon_id = variable.value as any >>> 0; /* unsigned 32 bit number! */ } else if(key === "channel_flag_conversation_private") { /* "fix" for older TeaSpeak server versions (pre. 1.4.22) */ this.properties.channel_conversation_mode = value === "1" ? 0 : 1; variables.push({ key: "channel_conversation_mode", value: this.properties.channel_conversation_mode + "" }); } } } /* devel-block(log-channel-property-updates) */ group.end(); /* devel-block-end */ { let properties = {}; for(const property of variables) properties[property.key] = this.properties[property.key]; this.events.fire("notify_properties_updated", { updated_properties: properties as any, channel_properties: this.properties }); } } generate_bbcode() { return "[url=channel://" + this.channelId + "/" + encodeURIComponent(this.properties.channel_name) + "]" + this.formattedChannelName() + "[/url]"; } generate_tag(braces: boolean = false) : JQuery { return $(htmltags.generate_channel({ channel_name: this.properties.channel_name, channel_id: this.channelId, add_braces: braces })); } channelType() : ChannelType { if(this.properties.channel_flag_permanent == true) return ChannelType.PERMANENT; if(this.properties.channel_flag_semi_permanent == true) return ChannelType.SEMI_PERMANENT; return ChannelType.TEMPORARY; } async joinChannel(ignorePasswordFlag?: boolean) : Promise { if(this.channelTree.client.getClient().currentChannel() === this) { return true; } if(this.properties.channel_flag_password === true && !this.cachedPasswordHash && !ignorePasswordFlag) { const password = await this.requestChannelPassword(PermissionType.B_CHANNEL_JOIN_IGNORE_PASSWORD); if(typeof password === "undefined") { /* aborted */ return; } } try { await this.channelTree.client.serverConnection.send_command("clientmove", { "clid": this.channelTree.client.getClientId(), "cid": this.getChannelId(), "cpw": this.cachedPasswordHash || "" }); this.channelTree.client.sound.play(Sound.CHANNEL_JOINED); return true; } catch (error) { if(error instanceof CommandResult) { if(error.id == ErrorCode.CHANNEL_INVALID_PASSWORD) { //Invalid password this.invalidateCachedPassword(); } } return false; } } async requestChannelPassword(ignorePermission: PermissionType) : Promise<{ hash: string } | undefined> { if(this.cachedPasswordHash) { return { hash: this.cachedPasswordHash }; } if(this.channelTree.client.permissions.neededPermission(ignorePermission).granted(1)) { return { hash: "having ignore permission" }; } const password = await new Promise(resolve => createInputModal(tr("Channel password"), tr("Channel password:"), () => true, resolve).open()) if(typeof(password) !== "string" || !password) { return; } const hash = await hashPassword(password); this.cachedPasswordHash = hash; this.events.fire("notify_cached_password_updated", { reason: "password-entered", new_hash: hash }); return { hash: this.cachedPasswordHash }; } invalidateCachedPassword() { this.cachedPasswordHash = undefined; this.events.fire("notify_cached_password_updated", { reason: "password-miss-match" }); } setCachedHashedPassword(passwordHash: string) { this.cachedPasswordHash = passwordHash; } getCachedPasswordHash() { return this.cachedPasswordHash; } async updateSubscribeMode() { let shouldBeSubscribed = false; switch (this.subscriptionMode) { case ChannelSubscribeMode.INHERITED: shouldBeSubscribed = this.channelTree.client.isSubscribeToAllChannels(); break; case ChannelSubscribeMode.SUBSCRIBED: shouldBeSubscribed = true; break; case ChannelSubscribeMode.UNSUBSCRIBED: shouldBeSubscribed = false; break; } if(this.subscribed === shouldBeSubscribed) { return; } const connection = this.channelTree.client.getServerConnection(); if(!connection.connected()) { this.setSubscribed(false); return; } if(shouldBeSubscribed) { await connection.send_command('channelsubscribe', { 'cid': this.getChannelId() }); } else { await connection.send_command('channelunsubscribe', { 'cid': this.getChannelId() }); } } async subscribe() : Promise { this.setSubscriptionMode(ChannelSubscribeMode.SUBSCRIBED); } async unsubscribe(inherited_subscription_mode?: boolean) : Promise { this.setSubscriptionMode(inherited_subscription_mode ? ChannelSubscribeMode.INHERITED : ChannelSubscribeMode.UNSUBSCRIBED); } isCollapsed() : boolean { return this.collapsed; } setCollapsed(flag: boolean) { if(this.collapsed === flag) { return; } this.collapsed = flag; this.events.fire("notify_collapsed_state_changed", { collapsed: flag }); this.channelTree.client.settings.setValue(Settings.FN_SERVER_CHANNEL_COLLAPSED(this.channelId), flag); } isSubscribed() : boolean { return this.subscribed; } /* Attention: This method is not to subscribe to a channel! It's used to update the current subscription state.*/ setSubscribed(flag: boolean) { if(this.subscribed === flag) { return; } this.subscribed = flag; this.events.fire("notify_subscribe_state_changed", { channel_subscribed: flag }); } getSubscriptionMode() : ChannelSubscribeMode { return this.subscriptionMode; } setSubscriptionMode(mode: ChannelSubscribeMode, dontSyncSubscribeMode?: boolean) { if(this.subscriptionMode === mode) { return; } this.subscriptionMode = mode; this.channelTree.client.settings.setValue(Settings.FN_SERVER_CHANNEL_SUBSCRIBE_MODE(this.channelId), mode); if(!dontSyncSubscribeMode) { this.updateSubscribeMode().then(undefined); } } log_data() : EventChannelData { return { channel_name: this.channelName(), channel_id: this.channelId } } getStatusIcon() : ClientIcon | undefined { if(this.parsed_channel_name.alignment !== "normal") { return undefined; } const subscribed = this.isSubscribed(); if (this.properties.channel_flag_password === true && !this.getCachedPasswordHash()) { 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; } } handleDescriptionChanged() { if(!this.channelDescriptionCached) { return; } this.channelDescriptionCached = false; this.properties.channel_description = undefined; this.events.fire("notify_description_changed"); } }