diff --git a/ChangeLog.md b/ChangeLog.md index 3d46bcbf..19acf68e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -7,6 +7,7 @@ - Enabled context menus for all clickable client tags - Allowing to drag client tags - Fixed the context menu within popout windows for the web client + - Reworked the whole sidebar (Hightly decreased memory footprint) * **08.12.20** - Fixed the permission editor not resolving unique ids diff --git a/shared/js/ConnectionHandler.ts b/shared/js/ConnectionHandler.ts index 7f646b00..6f0612d9 100644 --- a/shared/js/ConnectionHandler.ts +++ b/shared/js/ConnectionHandler.ts @@ -13,7 +13,6 @@ import * as htmltags from "./ui/htmltags"; import {FilterMode, InputState, MediaStreamRequestResult} from "./voice/RecorderBase"; import {CommandResult} from "./connection/ServerConnectionDeclaration"; import {defaultRecorder, RecorderProfile} from "./voice/RecorderProfile"; -import {Frame} from "./ui/frames/chat_frame"; import {Hostbanner} from "./ui/frames/hostbanner"; import {connection_log, Regex} from "./ui/modal/ModalConnect"; import {formatMessage} from "./ui/frames/chat"; @@ -40,7 +39,8 @@ import {ChannelVideoFrame} from "tc-shared/ui/frames/video/Controller"; import {global_client_actions} from "tc-shared/events/GlobalEvents"; import {ChannelConversationManager} from "./conversations/ChannelConversationManager"; import {PrivateConversationManager} from "tc-shared/conversations/PrivateConversationManager"; -import {ChannelConversationController} from "./ui/frames/side/ChannelConversationController"; +import {SelectedClientInfo} from "./SelectedClientInfo"; +import {SideBarManager} from "tc-shared/SideBarManager"; export enum InputHardwareState { MISSING, @@ -145,7 +145,6 @@ export class ConnectionHandler { permissions: PermissionManager; groups: GroupManager; - side_bar: Frame; video_frame: ChannelVideoFrame; settings: ServerSettings; @@ -157,9 +156,13 @@ export class ConnectionHandler { serverFeatures: ServerFeatures; + private sideBar: SideBarManager; + private channelConversations: ChannelConversationManager; private privateConversations: PrivateConversationManager; + private clientInfoManager: SelectedClientInfo; + private _clientId: number = 0; private localClient: LocalClientEntry; @@ -211,14 +214,15 @@ export class ConnectionHandler { this.fileManager = new FileManager(this); this.permissions = new PermissionManager(this); + this.sideBar = new SideBarManager(this); this.privateConversations = new PrivateConversationManager(this); this.channelConversations = new ChannelConversationManager(this); + this.clientInfoManager = new SelectedClientInfo(this); this.pluginCmdRegistry = new PluginCmdRegistry(this); this.video_frame = new ChannelVideoFrame(this); this.log = new ServerEventLog(this); - this.side_bar = new Frame(this); this.sound = new SoundManager(this); this.hostbanner = new Hostbanner(this); @@ -358,6 +362,14 @@ export class ConnectionHandler { return this.channelConversations; } + getSelectedClientInfo() : SelectedClientInfo { + return this.clientInfoManager; + } + + getSideBar() : SideBarManager { + return this.sideBar; + } + initializeLocalClient(clientId: number, acceptedName: string) { this._clientId = clientId; this.localClient["_clientId"] = clientId; @@ -1042,8 +1054,11 @@ export class ConnectionHandler { this.channelTree?.destroy(); this.channelTree = undefined; - this.side_bar?.destroy(); - this.side_bar = undefined; + this.sideBar?.destroy(); + this.sideBar = undefined; + + this.clientInfoManager?.destroy(); + this.clientInfoManager = undefined; this.log?.destroy(); this.log = undefined; diff --git a/shared/js/ConnectionManager.ts b/shared/js/ConnectionManager.ts index 9a41ebb9..71df28cf 100644 --- a/shared/js/ConnectionManager.ts +++ b/shared/js/ConnectionManager.ts @@ -5,6 +5,7 @@ import {Stage} from "tc-loader"; import {FooterRenderer} from "tc-shared/ui/frames/footer/Renderer"; import * as React from "react"; import * as ReactDOM from "react-dom"; +import {SideBarController} from "tc-shared/ui/frames/SideBarController"; export let server_connections: ConnectionManager; @@ -36,17 +37,21 @@ export class ConnectionManager { private containerSideBar: HTMLDivElement; private containerFooter: HTMLDivElement; + private sideBarController: SideBarController; + constructor() { this.event_registry = new Registry(); this.event_registry.enableDebug("connection-manager"); + this.sideBarController = new SideBarController(); + this.containerChannelVideo = new ReplaceableContainer(document.getElementById("channel-video") as HTMLDivElement); - this.containerSideBar = document.getElementById("chat") as HTMLDivElement; this._container_log_server = $("#server-log"); this._container_channel_tree = $("#channelTree"); this._container_hostbanner = $("#hostbanner"); this.containerFooter = document.getElementById("container-footer") as HTMLDivElement; + this.sideBarController.renderInto(document.getElementById("chat") as HTMLDivElement); this.set_active_connection(undefined); } @@ -111,6 +116,8 @@ export class ConnectionManager { } private set_active_connection_(handler: ConnectionHandler) { + this.sideBarController.setConnection(handler); + this._container_channel_tree.children().detach(); this._container_log_server.children().detach(); this._container_hostbanner.children().detach(); @@ -120,8 +127,8 @@ export class ConnectionManager { this._container_hostbanner.append(handler.hostbanner.html_tag); this._container_channel_tree.append(handler.channelTree.tag_tree()); this._container_log_server.append(handler.log.getHTMLTag()); - handler.side_bar.renderInto(this.containerSideBar); } + const old_handler = this.active_handler; this.active_handler = handler; this.event_registry.fire("notify_active_handler_changed", { diff --git a/shared/js/SelectedClientInfo.ts b/shared/js/SelectedClientInfo.ts new file mode 100644 index 00000000..b0462286 --- /dev/null +++ b/shared/js/SelectedClientInfo.ts @@ -0,0 +1,248 @@ +import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler"; +import { + ClientForumInfo, + ClientInfoType, + ClientStatusInfo, + ClientVersionInfo +} from "tc-shared/ui/frames/side/ClientInfoDefinitions"; +import {ClientEntry, ClientType, LocalClientEntry} from "tc-shared/tree/Client"; +import {Registry} from "tc-shared/events"; +import * as i18nc from "tc-shared/i18n/country"; + +export type CachedClientInfoCategory = "name" | "description" | "online-state" | "country" | "volume" | "status" | "forum-account" | "group-channel" | "groups-server" | "version"; + +export type CachedClientInfo = { + type: ClientInfoType; + name: string, + uniqueId: string, + databaseId: number, + clientId: number, + + description: string, + joinTimestamp: number, + leaveTimestamp: number, + country: { name: string, flag: string }, + volume: { volume: number, muted: boolean }, + status: ClientStatusInfo, + forumAccount: ClientForumInfo | undefined, + channelGroup: number, + serverGroups: number[], + version: ClientVersionInfo +} + +export interface ClientInfoManagerEvents { + notify_client_changed: { newClient: ClientEntry | undefined }, + notify_cache_changed: { category: CachedClientInfoCategory }, +} + +export class SelectedClientInfo { + readonly events: Registry; + + private readonly connection: ConnectionHandler; + private readonly listenerConnection: (() => void)[]; + + private listenerClient: (() => void)[]; + private currentClient: ClientEntry | undefined; + private currentClientStatus: CachedClientInfo | undefined; + + constructor(connection: ConnectionHandler) { + this.connection = connection; + this.events = new Registry(); + + this.listenerClient = []; + this.listenerConnection = []; + this.listenerConnection.push(connection.channelTree.events.on("notify_client_leave_view", event => { + if(event.client !== this.currentClient) { + return; + } + + this.currentClientStatus.leaveTimestamp = Date.now() / 1000; + this.currentClientStatus.clientId = 0; + this.currentClient = undefined; + this.unregisterClientEvents(); + this.events.fire("notify_cache_changed", { category: "online-state" }); + })); + + this.listenerConnection.push(connection.events().on("notify_connection_state_changed", event => { + if(event.newState !== ConnectionState.CONNECTED && this.currentClientStatus) { + this.currentClient = undefined; + this.currentClientStatus.leaveTimestamp = Date.now() / 1000; + this.events.fire("notify_cache_changed", { category: "online-state" }); + } + })); + } + + destroy() { + this.listenerConnection.forEach(callback => callback()); + this.listenerConnection.splice(0, this.listenerConnection.length); + + this.unregisterClientEvents(); + } + + getInfo() : CachedClientInfo { + return this.currentClientStatus; + } + + setClient(client: ClientEntry | undefined) { + if(this.currentClient === client) { + return; + } + + if(client.channelTree.client !== this.connection) { + throw tr("client does not belong to current connection handler"); + } + + this.unregisterClientEvents(); + this.currentClient = client; + this.currentClientStatus = undefined; + if(this.currentClient) { + this.currentClient.updateClientVariables().then(undefined); + this.registerClientEvents(this.currentClient); + this.initializeClientInfo(this.currentClient); + } + + this.events.fire("notify_client_changed", { newClient: client }); + } + + getClient() : ClientEntry | undefined { + return this.currentClient; + } + + private unregisterClientEvents() { + this.listenerClient.forEach(callback => callback()); + this.listenerClient = []; + } + + private registerClientEvents(client: ClientEntry) { + const events = this.listenerClient; + + events.push(client.events.on("notify_properties_updated", event => { + if('client_nickname' in event.updated_properties) { + this.currentClientStatus.name = event.client_properties.client_nickname; + this.events.fire("notify_cache_changed", { category: "name" }); + } + + if('client_description' in event.updated_properties) { + this.currentClientStatus.description = event.client_properties.client_description; + this.events.fire("notify_cache_changed", { category: "description" }); + } + + if('client_channel_group_id' in event.updated_properties) { + this.currentClientStatus.channelGroup = event.client_properties.client_channel_group_id; + this.events.fire("notify_cache_changed", { category: "group-channel" }); + } + + if('client_servergroups' in event.updated_properties) { + this.currentClientStatus.serverGroups = client.assignedServerGroupIds(); + this.events.fire("notify_cache_changed", { category: "groups-server" }); + } + + /* Can happen since that variable isn't in view on client appearance */ + if('client_lastconnected' in event.updated_properties) { + this.currentClientStatus.joinTimestamp = event.client_properties.client_lastconnected; + this.events.fire("notify_cache_changed", { category: "online-state" }); + } + + if('client_country' in event.updated_properties) { + this.updateCachedCountry(client); + this.events.fire("notify_cache_changed", { category: "country" }); + } + + for(const key of ["client_away", "client_away_message", "client_input_muted", "client_input_hardware", "client_output_muted", "client_output_hardware"]) { + if(key in event.updated_properties) { + this.updateCachedClientStatus(client); + this.events.fire("notify_cache_changed", { category: "status" }); + break; + } + } + + if('client_platform' in event.updated_properties || 'client_version' in event.updated_properties) { + this.currentClientStatus.version = { + platform: client.properties.client_platform, + version: client.properties.client_version + }; + this.events.fire("notify_cache_changed", { category: "version" }); + } + + if('client_teaforo_flags' in event.updated_properties || 'client_teaforo_name' in event.updated_properties || 'client_teaforo_id' in event.updated_properties) { + this.updateForumAccount(client); + this.events.fire("notify_cache_changed", { category: "forum-account" }); + } + })); + + events.push(client.events.on("notify_audio_level_changed", () => { + this.updateCachedVolume(client); + this.events.fire("notify_cache_changed", { category: "volume" }); + })); + + events.push(client.events.on("notify_mute_state_change", () => { + this.updateCachedVolume(client); + this.events.fire("notify_cache_changed", { category: "volume" }); + })); + } + + + private updateCachedClientStatus(client: ClientEntry) { + this.currentClientStatus.status = { + away: client.properties.client_away ? client.properties.client_away_message ? client.properties.client_away_message : true : false, + microphoneMuted: client.properties.client_input_muted, + microphoneDisabled: !client.properties.client_input_hardware, + speakerMuted: client.properties.client_output_muted, + speakerDisabled: !client.properties.client_output_hardware + }; + } + + private updateCachedCountry(client: ClientEntry) { + this.currentClientStatus.country = { + flag: client.properties.client_country, + name: i18nc.country_name(client.properties.client_country.toUpperCase()), + }; + } + + private updateCachedVolume(client: ClientEntry) { + this.currentClientStatus.volume = { + volume: client.getAudioVolume(), + muted: client.isMuted() + } + } + + private updateForumAccount(client: ClientEntry) { + if(client.properties.client_teaforo_id) { + this.currentClientStatus.forumAccount = { + flags: client.properties.client_teaforo_flags, + nickname: client.properties.client_teaforo_name, + userId: client.properties.client_teaforo_id + }; + } else { + this.currentClientStatus.forumAccount = undefined; + } + } + + private initializeClientInfo(client: ClientEntry) { + this.currentClientStatus = { + type: client instanceof LocalClientEntry ? "self" : client.properties.client_type === ClientType.CLIENT_QUERY ? "query" : "voice", + name: client.properties.client_nickname, + databaseId: client.properties.client_database_id, + uniqueId: client.properties.client_unique_identifier, + clientId: client.clientId(), + + description: client.properties.client_description, + channelGroup: client.properties.client_channel_group_id, + serverGroups: client.assignedServerGroupIds(), + country: undefined, + forumAccount: undefined, + joinTimestamp: client.properties.client_lastconnected, + leaveTimestamp: 0, + status: undefined, + volume: undefined, + version: { + platform: client.properties.client_platform, + version: client.properties.client_version + } + }; + this.updateCachedClientStatus(client); + this.updateCachedCountry(client); + this.updateCachedVolume(client); + this.updateForumAccount(client); + } +} \ No newline at end of file diff --git a/shared/js/SideBarManager.ts b/shared/js/SideBarManager.ts new file mode 100644 index 00000000..d79073d2 --- /dev/null +++ b/shared/js/SideBarManager.ts @@ -0,0 +1,58 @@ +import {SideBarType} from "tc-shared/ui/frames/SideBarDefinitions"; +import {Registry} from "tc-shared/events"; +import {ClientEntry, MusicClientEntry} from "tc-shared/tree/Client"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; + +export interface SideBarManagerEvents { + notify_content_type_changed: { newContent: SideBarType } +} + +export class SideBarManager { + readonly events: Registry; + private readonly connection: ConnectionHandler; + private currentType: SideBarType; + + + constructor(connection: ConnectionHandler) { + this.events = new Registry(); + this.connection = connection; + this.currentType = "channel-chat"; + } + + destroy() {} + + getSideBarContent() : SideBarType { + return this.currentType; + } + + setSideBarContent(content: SideBarType) { + if(this.currentType === content) { + return; + } + + this.currentType = content; + this.events.fire("notify_content_type_changed", { newContent: content }); + } + + showPrivateConversations() { + this.setSideBarContent("private-chat"); + } + + showChannelConversations() { + this.setSideBarContent("channel-chat"); + } + + showClientInfo(client: ClientEntry) { + this.connection.getSelectedClientInfo().setClient(client); + this.setSideBarContent("client-info"); + } + + showMusicPlayer(_client: MusicClientEntry) { + /* FIXME: TODO! */ + this.setSideBarContent("music-manage"); + } + + clearSideBar() { + this.setSideBarContent("none"); + } +} \ No newline at end of file diff --git a/shared/js/conversations/AbstractConversion.ts b/shared/js/conversations/AbstractConversion.ts index 9960f4b5..7a5a2f2e 100644 --- a/shared/js/conversations/AbstractConversion.ts +++ b/shared/js/conversations/AbstractConversion.ts @@ -4,7 +4,7 @@ import { ChatMessage, ChatState, ConversationHistoryResponse -} from "tc-shared/ui/frames/side/ConversationDefinitions"; +} from "../ui/frames/side/AbstractConversationDefinitions"; import {Registry} from "tc-shared/events"; import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {preprocessChatMessageForSend} from "tc-shared/text/chat"; @@ -16,7 +16,7 @@ import {guid} from "tc-shared/crypto/uid"; export const kMaxChatFrameMessageSize = 50; /* max 100 messages, since the server does not support more than 100 messages queried at once */ -export interface AbstractChatEvents { +export interface AbstractConversationEvents { notify_chat_event: { triggerUnread: boolean, event: ChatEvent @@ -45,7 +45,7 @@ export interface AbstractChatEvents { } } -export abstract class AbstractChat { +export abstract class AbstractChat { readonly events: Registry; protected readonly connection: ConnectionHandler; @@ -341,7 +341,7 @@ export interface AbstractChatManagerEvents { } } -export abstract class AbstractChatManager, ConversationType extends AbstractChat, ConversationEvents extends AbstractChatEvents> { +export abstract class AbstractChatManager, ConversationType extends AbstractChat, ConversationEvents extends AbstractConversationEvents> { readonly events: Registry; readonly connection: ConnectionHandler; protected readonly listenerConnection: (() => void)[]; @@ -351,6 +351,12 @@ export abstract class AbstractChatManager(); @@ -418,6 +424,8 @@ export abstract class AbstractChatManager "conversation-" + uniqueId; diff --git a/shared/js/conversations/PrivateConversationManager.ts b/shared/js/conversations/PrivateConversationManager.ts index 7dce6e37..059cbff0 100644 --- a/shared/js/conversations/PrivateConversationManager.ts +++ b/shared/js/conversations/PrivateConversationManager.ts @@ -1,11 +1,11 @@ import { AbstractChat, - AbstractChatEvents, + AbstractConversationEvents, AbstractChatManager, AbstractChatManagerEvents } from "tc-shared/conversations/AbstractConversion"; import {ClientEntry} from "tc-shared/tree/Client"; -import {ChatEvent, ChatMessage, ConversationHistoryResponse} from "tc-shared/ui/frames/side/ConversationDefinitions"; +import {ChatEvent, ChatMessage, ConversationHistoryResponse} from "../ui/frames/side/AbstractConversationDefinitions"; import {ChannelTreeEvents} from "tc-shared/tree/ChannelTree"; import {queryConversationEvents, registerConversationEvent} from "tc-shared/conversations/PrivateConversationHistory"; import {LogCategory, logWarn} from "tc-shared/log"; @@ -19,7 +19,7 @@ export type OutOfViewClient = { let receivingEventUniqueIdIndex = 0; -export interface PrivateConversationEvents extends AbstractChatEvents { +export interface PrivateConversationEvents extends AbstractConversationEvents { notify_partner_typing: {}, notify_partner_changed: { chatId: string, diff --git a/shared/js/tree/Channel.ts b/shared/js/tree/Channel.ts index 26ec80c4..19371a97 100644 --- a/shared/js/tree/Channel.ts +++ b/shared/js/tree/Channel.ts @@ -412,7 +412,7 @@ export class ChannelEntry extends ChannelTreeEntry { callback: () => { const conversation = this.channelTree.client.getChannelConversations().findOrCreateConversation(this.getChannelId()); this.channelTree.client.getChannelConversations().setSelectedConversation(conversation); - this.channelTree.client.side_bar.showChannelConversations(); + this.channelTree.client.getSideBar().showChannelConversations(); }, visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT) }, { diff --git a/shared/js/tree/ChannelTree.tsx b/shared/js/tree/ChannelTree.tsx index 67e8dd97..a60f3c17 100644 --- a/shared/js/tree/ChannelTree.tsx +++ b/shared/js/tree/ChannelTree.tsx @@ -168,23 +168,22 @@ export class ChannelTree { if(this.selectedEntry instanceof ClientEntry) { if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)) { if(this.selectedEntry instanceof MusicClientEntry) { - this.client.side_bar.showMusicPlayer(this.selectedEntry); + this.client.getSideBar().showMusicPlayer(this.selectedEntry); } else { - this.client.side_bar.showClientInfo(this.selectedEntry); + this.client.getSideBar().showClientInfo(this.selectedEntry); } } } else if(this.selectedEntry instanceof ChannelEntry) { if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) { const conversation = this.client.getChannelConversations().findOrCreateConversation(this.selectedEntry.channelId); this.client.getChannelConversations().setSelectedConversation(conversation); - this.client.side_bar.showChannelConversations(); + this.client.getSideBar().showChannelConversations(); } } else if(this.selectedEntry instanceof ServerEntry) { if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) { - const sidebar = this.client.side_bar; const conversation = this.client.getChannelConversations().findOrCreateConversation(0); this.client.getChannelConversations().setSelectedConversation(conversation); - sidebar.showChannelConversations(); + this.client.getSideBar().showChannelConversations() } } } diff --git a/shared/js/tree/Client.ts b/shared/js/tree/Client.ts index 9e2b6693..28f84c51 100644 --- a/shared/js/tree/Client.ts +++ b/shared/js/tree/Client.ts @@ -382,7 +382,7 @@ export class ClientEntry extends ChannelTreeEntry { type: contextmenu.MenuEntryType.ENTRY, name: this.properties.client_type_exact === ClientType.CLIENT_MUSIC ? tr("Show bot info") : tr("Show client info"), callback: () => { - this.channelTree.client.side_bar.showClientInfo(this); + this.channelTree.client.getSideBar().showClientInfo(this); }, icon_class: "client-about", visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT) @@ -519,12 +519,13 @@ export class ClientEntry extends ChannelTreeEntry { open_text_chat() { const privateConversations = this.channelTree.client.getPrivateConversations(); - const sideBar = this.channelTree.client.side_bar; const conversation = privateConversations.findOrCreateConversation(this); conversation.setActiveClientEntry(this); privateConversations.setSelectedConversation(conversation); - sideBar.showPrivateConversations(); - sideBar.privateConversationsController().focusInput(); + + this.channelTree.client.getSideBar().showPrivateConversations(); + /* FIXME: Draw focus to the input box! */ + //sideBar.privateConversationsController().focusInput(); } showContextMenu(x: number, y: number, on_close: () => void = undefined) { diff --git a/shared/js/tree/Server.ts b/shared/js/tree/Server.ts index 4154e891..a0db1dea 100644 --- a/shared/js/tree/Server.ts +++ b/shared/js/tree/Server.ts @@ -193,7 +193,7 @@ export class ServerEntry extends ChannelTreeEntry { name: tr("Join server text channel"), callback: () => { this.channelTree.client.getChannelConversations().setSelectedConversation(this.channelTree.client.getChannelConversations().findOrCreateConversation(0)); - this.channelTree.client.side_bar.showChannelConversations(); + this.channelTree.client.getSideBar().showChannelConversations(); }, visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT) }, { diff --git a/shared/js/ui/frames/chat_frame.ts b/shared/js/ui/frames/SideBarController.ts similarity index 51% rename from shared/js/ui/frames/chat_frame.ts rename to shared/js/ui/frames/SideBarController.ts index ebbadd24..5b1b9072 100644 --- a/shared/js/ui/frames/chat_frame.ts +++ b/shared/js/ui/frames/SideBarController.ts @@ -1,61 +1,64 @@ -import {ClientEntry, MusicClientEntry} from "../../tree/Client"; import {ConnectionHandler} from "../../ConnectionHandler"; -import {MusicInfo} from "../../ui/frames/side/music_info"; import {ChannelConversationController} from "./side/ChannelConversationController"; import {PrivateConversationController} from "./side/PrivateConversationController"; import {ClientInfoController} from "tc-shared/ui/frames/side/ClientInfoController"; -import {SideHeader} from "tc-shared/ui/frames/side/HeaderController"; +import {SideHeaderController} from "tc-shared/ui/frames/side/HeaderController"; import * as ReactDOM from "react-dom"; import {SideBarRenderer} from "tc-shared/ui/frames/SideBarRenderer"; import * as React from "react"; import {SideBarEvents, SideBarType} from "tc-shared/ui/frames/SideBarDefinitions"; import {Registry} from "tc-shared/events"; +import {LogCategory, logWarn} from "tc-shared/log"; -const cssStyle = require("./SideBar.scss"); +export class SideBarController { + private readonly uiEvents: Registry; -export class Frame { - readonly handle: ConnectionHandler; - private htmlTag: HTMLDivElement; + private currentConnection: ConnectionHandler; + private listenerConnection: (() => void)[]; - private currentType: SideBarType; - - private uiEvents: Registry; - private header: SideHeader; - - private musicInfo: MusicInfo; + private header: SideHeaderController; private clientInfo: ClientInfoController; private channelConversations: ChannelConversationController; private privateConversations: PrivateConversationController; - constructor(handle: ConnectionHandler) { - this.handle = handle; + constructor() { + this.listenerConnection = []; - this.currentType = "none"; this.uiEvents = new Registry(); - this.uiEvents.on("query_content", () => this.uiEvents.fire_react("notify_content", { content: this.currentType })); + this.uiEvents.on("query_content", () => this.sendContent()); this.uiEvents.on("query_content_data", event => this.sendContentData(event.content)); - this.privateConversations = new PrivateConversationController(handle); - this.channelConversations = new ChannelConversationController(handle); - this.clientInfo = new ClientInfoController(handle); - this.musicInfo = new MusicInfo(this); - this.header = new SideHeader(); - - this.handle.events().one("notify_handler_initialized", () => this.header.setConnectionHandler(handle)); - - this.createHtmlTag(); - this.showChannelConversations(); + this.privateConversations = new PrivateConversationController(); + this.channelConversations = new ChannelConversationController(); + this.clientInfo = new ClientInfoController(); + this.header = new SideHeaderController(); } - html_tag() : HTMLDivElement { return this.htmlTag; } + setConnection(connection: ConnectionHandler) { + if(this.currentConnection === connection) { + return; + } + + this.listenerConnection.forEach(callback => callback()); + this.listenerConnection = []; + + this.currentConnection = connection; + this.header.setConnectionHandler(connection); + this.clientInfo.setConnectionHandler(connection); + this.channelConversations.setConnectionHandler(connection); + this.privateConversations.setConnectionHandler(connection); + + if(connection) { + this.listenerConnection.push(connection.getSideBar().events.on("notify_content_type_changed", () => this.sendContent())); + } + + this.sendContent(); + } destroy() { this.header?.destroy(); this.header = undefined; - this.htmlTag && this.htmlTag.remove(); - this.htmlTag = undefined; - this.clientInfo?.destroy(); this.clientInfo = undefined; @@ -64,51 +67,21 @@ export class Frame { this.channelConversations?.destroy(); this.channelConversations = undefined; - - this.musicInfo && this.musicInfo.destroy(); - this.musicInfo = undefined; - - this.privateConversations && this.privateConversations.destroy(); - this.privateConversations = undefined; - - this.channelConversations && this.channelConversations.destroy(); - this.channelConversations = undefined; } renderInto(container: HTMLDivElement) { ReactDOM.render(React.createElement(SideBarRenderer, { - key: this.handle.handlerId, - handlerId: this.handle.handlerId, events: this.uiEvents, eventsHeader: this.header["uiEvents"], }), container); } - private createHtmlTag() { - this.htmlTag = document.createElement("div"); - this.htmlTag.classList.add(cssStyle.container); - } - - - privateConversationsController() : PrivateConversationController { - return this.privateConversations; - } - - getClientInfo() : ClientInfoController { - return this.clientInfo; - } - - music_info() : MusicInfo { - return this.musicInfo; - } - - private setCurrentContent(type: SideBarType) { - if(this.currentType === type) { - return; + private sendContent() { + if(this.currentConnection) { + this.uiEvents.fire("notify_content", { content: this.currentConnection.getSideBar().getSideBarContent() }); + } else { + this.uiEvents.fire("notify_content", { content: "none" }); } - - this.currentType = type; - this.uiEvents.fire_react("notify_content", { content: this.currentType }); } private sendContentData(content: SideBarType) { @@ -121,26 +94,41 @@ export class Frame { break; case "channel-chat": + if(!this.currentConnection) { + logWarn(LogCategory.GENERAL, tr("Received channel chat content data request without an active connection.")); + return; + } + this.uiEvents.fire_react("notify_content_data", { content: "channel-chat", data: { events: this.channelConversations["uiEvents"], - handlerId: this.handle.handlerId + handlerId: this.currentConnection.handlerId } }); break; case "private-chat": + if(!this.currentConnection) { + logWarn(LogCategory.GENERAL, tr("Received private chat content data request without an active connection.")); + return; + } + this.uiEvents.fire_react("notify_content_data", { content: "private-chat", data: { events: this.privateConversations["uiEvents"], - handlerId: this.handle.handlerId + handlerId: this.currentConnection.handlerId } }); break; case "client-info": + if(!this.currentConnection) { + logWarn(LogCategory.GENERAL, tr("Received client info content data request without an active connection.")); + return; + } + this.uiEvents.fire_react("notify_content_data", { content: "client-info", data: { @@ -150,6 +138,11 @@ export class Frame { break; case "music-manage": + if(!this.currentConnection) { + logWarn(LogCategory.GENERAL, tr("Received music bot content data request without an active connection.")); + return; + } + this.uiEvents.fire_react("notify_content_data", { content: "music-manage", data: { } @@ -157,26 +150,4 @@ export class Frame { break; } } - - showPrivateConversations() { - this.setCurrentContent("private-chat"); - } - - showChannelConversations() { - this.setCurrentContent("channel-chat"); - } - - showClientInfo(client: ClientEntry) { - this.clientInfo.setClient(client); - this.setCurrentContent("client-info"); - } - - showMusicPlayer(client: MusicClientEntry) { - this.musicInfo.set_current_bot(client); - this.setCurrentContent("music-manage"); - } - - clearSideBar() { - this.setCurrentContent("none"); - } } \ No newline at end of file diff --git a/shared/js/ui/frames/SideBarDefinitions.ts b/shared/js/ui/frames/SideBarDefinitions.ts index 737c42ec..5fc58888 100644 --- a/shared/js/ui/frames/SideBarDefinitions.ts +++ b/shared/js/ui/frames/SideBarDefinitions.ts @@ -1,7 +1,8 @@ import {Registry} from "tc-shared/events"; import {PrivateConversationUIEvents} from "tc-shared/ui/frames/side/PrivateConversationDefinitions"; -import {ConversationUIEvents} from "tc-shared/ui/frames/side/ConversationDefinitions"; +import {AbstractConversationUiEvents} from "./side/AbstractConversationDefinitions"; import {ClientInfoEvents} from "tc-shared/ui/frames/side/ClientInfoDefinitions"; +import {SideHeaderEvents} from "tc-shared/ui/frames/side/HeaderDefinitions"; /* TODO: Somehow outsource the event registries to IPC? */ @@ -9,7 +10,7 @@ export type SideBarType = "none" | "channel-chat" | "private-chat" | "client-inf export interface SideBarTypeData { "none": {}, "channel-chat": { - events: Registry, + events: Registry, handlerId: string }, "private-chat": { @@ -32,7 +33,9 @@ export type SideBarNotifyContentData = { export interface SideBarEvents { query_content: {}, query_content_data: { content: SideBarType }, + query_header_data: {}, notify_content: { content: SideBarType }, - notify_content_data: SideBarNotifyContentData + notify_content_data: SideBarNotifyContentData, + notify_header_data: { events: Registry } } \ No newline at end of file diff --git a/shared/js/ui/frames/SideBar.scss b/shared/js/ui/frames/SideBarRenderer.scss similarity index 100% rename from shared/js/ui/frames/SideBar.scss rename to shared/js/ui/frames/SideBarRenderer.scss diff --git a/shared/js/ui/frames/SideBarRenderer.tsx b/shared/js/ui/frames/SideBarRenderer.tsx index 0b379cde..81ad17b3 100644 --- a/shared/js/ui/frames/SideBarRenderer.tsx +++ b/shared/js/ui/frames/SideBarRenderer.tsx @@ -8,7 +8,7 @@ import {useContext, useState} from "react"; import {ClientInfoRenderer} from "tc-shared/ui/frames/side/ClientInfoRenderer"; import {PrivateConversationsPanel} from "tc-shared/ui/frames/side/PrivateConversationRenderer"; -const cssStyle = require("./SideBar.scss"); +const cssStyle = require("./SideBarRenderer.scss"); const EventContent = React.createContext>(undefined); @@ -109,7 +109,6 @@ const SideBarHeader = (props: { type: SideBarType, eventsHeader: Registry, eventsHeader: Registry }) => { diff --git a/shared/js/ui/frames/side/AbstractConversationController.ts b/shared/js/ui/frames/side/AbstractConversationController.ts index 80999ca4..bc9ffdaa 100644 --- a/shared/js/ui/frames/side/AbstractConversationController.ts +++ b/shared/js/ui/frames/side/AbstractConversationController.ts @@ -1,14 +1,14 @@ import { ChatHistoryState, - ConversationUIEvents -} from "../../../ui/frames/side/ConversationDefinitions"; + AbstractConversationUiEvents +} from "./AbstractConversationDefinitions"; import {EventHandler, Registry} from "../../../events"; import * as log from "../../../log"; import {LogCategory} from "../../../log"; import {tra, tr} from "../../../i18n/localize"; import { AbstractChat, - AbstractChatEvents, + AbstractConversationEvents, AbstractChatManager, AbstractChatManagerEvents } from "tc-shared/conversations/AbstractConversion"; @@ -16,50 +16,25 @@ import { export const kMaxChatFrameMessageSize = 50; /* max 100 messages, since the server does not support more than 100 messages queried at once */ export abstract class AbstractConversationController< - Events extends ConversationUIEvents, + Events extends AbstractConversationUiEvents, Manager extends AbstractChatManager, ManagerEvents extends AbstractChatManagerEvents, ConversationType extends AbstractChat, - ConversationEvents extends AbstractChatEvents + ConversationEvents extends AbstractConversationEvents > { protected readonly uiEvents: Registry; - protected readonly conversationManager: Manager; - protected readonly listenerManager: (() => void)[]; - - private historyUiStates: {[id: string]: { - executingUIHistoryQuery: boolean, - historyErrorMessage: string | undefined, - historyRetryTimestamp: number - }} = {}; + protected conversationManager: Manager | undefined; + protected listenerManager: (() => void)[]; protected currentSelectedConversation: ConversationType; protected currentSelectedListener: (() => void)[]; protected crossChannelChatSupported = true; - protected constructor(conversationManager: Manager) { + protected constructor() { this.uiEvents = new Registry(); this.currentSelectedListener = []; - this.conversationManager = conversationManager; - this.listenerManager = []; - - this.listenerManager.push(this.conversationManager.events.on("notify_selected_changed", event => { - this.currentSelectedListener.forEach(callback => callback()); - this.currentSelectedListener = []; - - this.currentSelectedConversation = event.newConversation; - this.uiEvents.fire_react("notify_selected_chat", { chatId: event.newConversation ? event.newConversation.getChatId() : "unselected" }); - - const conversation = event.newConversation; - if(conversation) { - this.registerConversationEvents(conversation); - } - })); - - this.listenerManager.push(this.conversationManager.events.on("notify_conversation_destroyed", event => { - delete this.historyUiStates[event.conversation.getChatId()]; - })); } destroy() { @@ -70,6 +45,31 @@ export abstract class AbstractConversationController< this.uiEvents.destroy(); } + getUiEvents() : Registry { + return this.uiEvents; + } + + protected setConversationManager(manager: Manager | undefined) { + if(this.conversationManager === manager) { + return; + } + + this.listenerManager.forEach(callback => callback()); + this.listenerManager = []; + this.conversationManager = manager; + + if(manager) { + this.registerConversationManagerEvents(manager); + this.setCurrentlySelected(manager.getSelectedConversation()); + } else { + this.setCurrentlySelected(undefined); + } + } + + protected registerConversationManagerEvents(manager: Manager) { + this.listenerManager.push(manager.events.on("notify_selected_changed", event => this.setCurrentlySelected(event.newConversation))); + } + protected registerConversationEvents(conversation: ConversationType) { this.currentSelectedListener.push(conversation.events.on("notify_unread_timestamp_changed", event => this.uiEvents.fire_react("notify_unread_timestamp_changed", { chatId: conversation.getChatId(), timestamp: event.timestamp }))); @@ -94,13 +94,30 @@ export abstract class AbstractConversationController< })); } + protected setCurrentlySelected(conversation: ConversationType | undefined) { + if(this.currentSelectedConversation === conversation) { + return; + } + + this.currentSelectedListener.forEach(callback => callback()); + this.currentSelectedListener = []; + + this.currentSelectedConversation = conversation; + this.uiEvents.fire_react("notify_selected_chat", { chatId: conversation ? conversation.getChatId() : "unselected" }); + + if(conversation) { + this.registerConversationEvents(conversation); + } + } + + /* TODO: Is this even a thing? */ handlePanelShow() { this.uiEvents.fire_react("notify_panel_show"); } protected reportStateToUI(conversation: AbstractChat) { let historyState: ChatHistoryState; - const localHistoryState = this.historyUiStates[conversation.getChatId()]; + const localHistoryState = this.conversationManager.historyUiStates[conversation.getChatId()]; if(!localHistoryState) { historyState = conversation.hasHistory() ? "available" : "none"; } else { @@ -171,7 +188,7 @@ export abstract class AbstractConversationController< } } public uiQueryHistory(conversation: AbstractChat, timestamp: number, enforce?: boolean) { - const localHistoryState = this.historyUiStates[conversation.getChatId()] || (this.historyUiStates[conversation.getChatId()] = { + const localHistoryState = this.conversationManager.historyUiStates[conversation.getChatId()] || (this.conversationManager.historyUiStates[conversation.getChatId()] = { executingUIHistoryQuery: false, historyErrorMessage: undefined, historyRetryTimestamp: 0 @@ -242,13 +259,13 @@ export abstract class AbstractConversationController< this.crossChannelChatSupported = flag; const currentConversation = this.getCurrentConversation(); if(currentConversation) { - this.reportStateToUI(this.getCurrentConversation()); + this.reportStateToUI(currentConversation); } } - @EventHandler("query_conversation_state") - protected handleQueryConversationState(event: ConversationUIEvents["query_conversation_state"]) { - const conversation = this.conversationManager.findConversationById(event.chatId); + @EventHandler("query_conversation_state") + protected handleQueryConversationState(event: AbstractConversationUiEvents["query_conversation_state"]) { + const conversation = this.conversationManager?.findConversationById(event.chatId); if(!conversation) { this.uiEvents.fire_react("notify_conversation_state", { state: "error", @@ -267,9 +284,9 @@ export abstract class AbstractConversationController< } } - @EventHandler("query_conversation_history") - protected handleQueryHistory(event: ConversationUIEvents["query_conversation_history"]) { - const conversation = this.conversationManager.findConversationById(event.chatId); + @EventHandler("query_conversation_history") + protected handleQueryHistory(event: AbstractConversationUiEvents["query_conversation_history"]) { + const conversation = this.conversationManager?.findConversationById(event.chatId); if(!conversation) { this.uiEvents.fire_react("notify_conversation_history", { state: "error", @@ -286,15 +303,15 @@ export abstract class AbstractConversationController< this.uiQueryHistory(conversation, event.timestamp); } - @EventHandler(["action_clear_unread_flag", "action_self_typing"]) - protected handleClearUnreadFlag(event: ConversationUIEvents["action_clear_unread_flag" | "action_self_typing"]) { - const conversation = this.conversationManager.findConversationById(event.chatId); + @EventHandler(["action_clear_unread_flag", "action_self_typing"]) + protected handleClearUnreadFlag(event: AbstractConversationUiEvents["action_clear_unread_flag" | "action_self_typing"]) { + const conversation = this.conversationManager?.findConversationById(event.chatId); conversation?.setUnreadTimestamp(Date.now()); } - @EventHandler("action_send_message") - protected handleSendMessage(event: ConversationUIEvents["action_send_message"]) { - const conversation = this.conversationManager.findConversationById(event.chatId); + @EventHandler("action_send_message") + protected handleSendMessage(event: AbstractConversationUiEvents["action_send_message"]) { + const conversation = this.conversationManager?.findConversationById(event.chatId); if(!conversation) { log.error(LogCategory.CLIENT, tr("Tried to send a chat message to an unknown conversation with id %s"), event.chatId); return; @@ -303,9 +320,9 @@ export abstract class AbstractConversationController< conversation.sendMessage(event.text); } - @EventHandler("action_jump_to_present") - protected handleJumpToPresent(event: ConversationUIEvents["action_jump_to_present"]) { - const conversation = this.conversationManager.findConversationById(event.chatId); + @EventHandler("action_jump_to_present") + protected handleJumpToPresent(event: AbstractConversationUiEvents["action_jump_to_present"]) { + const conversation = this.conversationManager?.findConversationById(event.chatId); if(!conversation) { log.error(LogCategory.CLIENT, tr("Tried to jump to present for an unknown conversation with id %s"), event.chatId); return; @@ -314,14 +331,14 @@ export abstract class AbstractConversationController< this.reportStateToUI(conversation); } - @EventHandler("query_selected_chat") + @EventHandler("query_selected_chat") private handleQuerySelectedChat() { - this.uiEvents.fire_react("notify_selected_chat", { chatId: this.currentSelectedConversation ? this.currentSelectedConversation.getChatId() : "unselected"}) + this.uiEvents.fire_react("notify_selected_chat", { chatId: this.currentSelectedConversation ? this.currentSelectedConversation.getChatId() : "unselected"}); } - @EventHandler("action_select_chat") - private handleActionSelectChat(event: ConversationUIEvents["action_select_chat"]) { - const conversation = this.conversationManager.findConversationById(event.chatId); + @EventHandler("action_select_chat") + private handleActionSelectChat(event: AbstractConversationUiEvents["action_select_chat"]) { + const conversation = this.conversationManager?.findConversationById(event.chatId); this.conversationManager.setSelectedConversation(conversation); } } \ No newline at end of file diff --git a/shared/js/ui/frames/side/ConversationDefinitions.ts b/shared/js/ui/frames/side/AbstractConversationDefinitions.ts similarity index 98% rename from shared/js/ui/frames/side/ConversationDefinitions.ts rename to shared/js/ui/frames/side/AbstractConversationDefinitions.ts index e7dd6a3e..a396fca0 100644 --- a/shared/js/ui/frames/side/ConversationDefinitions.ts +++ b/shared/js/ui/frames/side/AbstractConversationDefinitions.ts @@ -111,7 +111,7 @@ export interface ChatStatePrivate { export type ChatStateData = ChatStateNormal | ChatStateNoPermissions | ChatStateError | ChatStateLoading | ChatStatePrivate; -export interface ConversationUIEvents { +export interface AbstractConversationUiEvents { action_select_chat: { chatId: "unselected" | string }, action_clear_unread_flag: { chatId: string }, action_self_typing: { chatId: string }, diff --git a/shared/js/ui/frames/side/AbstractConversationRenderer.tsx b/shared/js/ui/frames/side/AbstractConversationRenderer.tsx index 3b517410..79357c25 100644 --- a/shared/js/ui/frames/side/AbstractConversationRenderer.tsx +++ b/shared/js/ui/frames/side/AbstractConversationRenderer.tsx @@ -15,8 +15,8 @@ import { ChatEventPartnerAction, ChatHistoryState, ChatMessage, - ConversationUIEvents, ChatEventModeChanged -} from "tc-shared/ui/frames/side/ConversationDefinitions"; + AbstractConversationUiEvents, ChatEventModeChanged +} from "./AbstractConversationDefinitions"; import {TimestampRenderer} from "tc-shared/ui/react-elements/TimestampRenderer"; import {BBCodeRenderer} from "tc-shared/text/bbcode"; import {getGlobalAvatarManagerFactory} from "tc-shared/file/Avatars"; @@ -34,7 +34,7 @@ const ChatMessageTextRenderer = React.memo((props: { text: string }) => { const ChatEventMessageRenderer = React.memo((props: { message: ChatMessage, callbackDelete?: () => void, - events: Registry, + events: Registry, handlerId: string, refHTMLElement?: Ref @@ -126,7 +126,7 @@ const UnreadEntry = (props: { refDiv: React.Ref }) => ( ); -const LoadOderMessages = (props: { events: Registry, chatId: string, state: ChatHistoryState | "error", errorMessage?: string, retryTimestamp?: number, timestamp: number | undefined }) => { +const LoadOderMessages = (props: { events: Registry, chatId: string, state: ChatHistoryState | "error", errorMessage?: string, retryTimestamp?: number, timestamp: number | undefined }) => { if(props.state === "none") return null; @@ -172,7 +172,7 @@ const LoadOderMessages = (props: { events: Registry, chatI ) }; -const JumpToPresent = (props: { events: Registry, chatId: string }) => ( +const JumpToPresent = (props: { events: Registry, chatId: string }) => (
props.events.fire("action_jump_to_present", { chatId: props.chatId })} @@ -305,7 +305,7 @@ const ChatEventModeChangedRenderer = (props: { event: ChatEventModeChanged, refH } } -const PartnerTypingIndicator = (props: { events: Registry, chatId: string, timeout?: number }) => { +const PartnerTypingIndicator = (props: { events: Registry, chatId: string, timeout?: number }) => { const kTypingTimeout = props.timeout || 5000; @@ -349,7 +349,7 @@ const PartnerTypingIndicator = (props: { events: Registry, }; interface ConversationMessagesProperties { - events: Registry; + events: Registry; handlerId: string; noFirstMessageOverlay?: boolean @@ -698,8 +698,8 @@ class ConversationMessages extends React.PureComponent("notify_selected_chat") - private handleNotifySelectedChat(event: ConversationUIEvents["notify_selected_chat"]) { + @EventHandler("notify_selected_chat") + private handleNotifySelectedChat(event: AbstractConversationUiEvents["notify_selected_chat"]) { if(this.currentChatId === event.chatId) { return; } @@ -718,8 +718,8 @@ class ConversationMessages extends React.PureComponent("notify_conversation_state") - private handleConversationStateUpdate(event: ConversationUIEvents["notify_conversation_state"]) { + @EventHandler("notify_conversation_state") + private handleConversationStateUpdate(event: AbstractConversationUiEvents["notify_conversation_state"]) { if(event.chatId !== this.currentChatId) return; @@ -771,8 +771,8 @@ class ConversationMessages extends React.PureComponent("notify_chat_event") - private handleChatEvent(event: ConversationUIEvents["notify_chat_event"]) { + @EventHandler("notify_chat_event") + private handleChatEvent(event: AbstractConversationUiEvents["notify_chat_event"]) { if(event.chatId !== this.currentChatId || this.state.isBrowsingHistory) return; @@ -793,8 +793,8 @@ class ConversationMessages extends React.PureComponent this.scrollToBottom()); } - @EventHandler("notify_chat_message_delete") - private handleMessageDeleted(event: ConversationUIEvents["notify_chat_message_delete"]) { + @EventHandler("notify_chat_message_delete") + private handleMessageDeleted(event: AbstractConversationUiEvents["notify_chat_message_delete"]) { if(event.chatId !== this.currentChatId) { return; } @@ -805,8 +805,8 @@ class ConversationMessages extends React.PureComponent this.scrollToBottom()); } - @EventHandler("notify_unread_timestamp_changed") - private handleUnreadTimestampChanged(event: ConversationUIEvents["notify_unread_timestamp_changed"]) { + @EventHandler("notify_unread_timestamp_changed") + private handleUnreadTimestampChanged(event: AbstractConversationUiEvents["notify_unread_timestamp_changed"]) { if (event.chatId !== this.currentChatId) return; @@ -823,13 +823,13 @@ class ConversationMessages extends React.PureComponent("notify_panel_show") + @EventHandler("notify_panel_show") private handlePanelShow() { this.fixScroll(); } - @EventHandler("query_conversation_history") - private handleQueryConversationHistory(event: ConversationUIEvents["query_conversation_history"]) { + @EventHandler("query_conversation_history") + private handleQueryConversationHistory(event: AbstractConversationUiEvents["query_conversation_history"]) { if (event.chatId !== this.currentChatId) return; @@ -838,8 +838,8 @@ class ConversationMessages extends React.PureComponent("notify_conversation_history") - private handleNotifyConversationHistory(event: ConversationUIEvents["notify_conversation_history"]) { + @EventHandler("notify_conversation_history") + private handleNotifyConversationHistory(event: AbstractConversationUiEvents["notify_conversation_history"]) { if (event.chatId !== this.currentChatId) return; @@ -881,7 +881,7 @@ class ConversationMessages extends React.PureComponent, handlerId: string, messagesDeletable: boolean, noFirstMessageOverlay: boolean }) => { +export const ConversationPanel = React.memo((props: { events: Registry, handlerId: string, messagesDeletable: boolean, noFirstMessageOverlay: boolean }) => { const currentChat = useRef({ id: "unselected" }); const chatEnabled = useRef(false); @@ -900,7 +900,7 @@ export const ConversationPanel = React.memo((props: { events: Registry { if(event.chatId !== currentChat.current.id) return; diff --git a/shared/js/ui/frames/side/ChannelConversationController.ts b/shared/js/ui/frames/side/ChannelConversationController.ts index 11d3fd5f..0ce07034 100644 --- a/shared/js/ui/frames/side/ChannelConversationController.ts +++ b/shared/js/ui/frames/side/ChannelConversationController.ts @@ -1,11 +1,9 @@ -import * as React from "react"; import {ConnectionHandler, ConnectionState} from "../../../ConnectionHandler"; import {EventHandler} from "../../../events"; import * as log from "../../../log"; import {LogCategory} from "../../../log"; import {tr} from "../../../i18n/localize"; -import {ConversationUIEvents} from "../../../ui/frames/side/ConversationDefinitions"; -import {ConversationPanel} from "./AbstractConversationRenderer"; +import {AbstractConversationUiEvents} from "./AbstractConversationDefinitions"; import {AbstractConversationController} from "./AbstractConversationController"; import { ChannelConversation, @@ -14,34 +12,22 @@ import { ChannelConversationManagerEvents } from "tc-shared/conversations/ChannelConversationManager"; import {ServerFeature} from "tc-shared/connection/ServerFeatures"; -import ReactDOM = require("react-dom"); +import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions"; export class ChannelConversationController extends AbstractConversationController< - ConversationUIEvents, + ChannelConversationUiEvents, ChannelConversationManager, ChannelConversationManagerEvents, ChannelConversation, ChannelConversationEvents > { - readonly connection: ConnectionHandler; - readonly htmlTag: HTMLDivElement; + private connection: ConnectionHandler; + private connectionListener: (() => void)[]; - constructor(connection: ConnectionHandler) { - super(connection.getChannelConversations() as any); - this.connection = connection; + constructor() { + super(); + this.connectionListener = []; - this.htmlTag = document.createElement("div"); - this.htmlTag.style.display = "flex"; - this.htmlTag.style.flexDirection = "column"; - this.htmlTag.style.justifyContent = "stretch"; - this.htmlTag.style.height = "100%"; - - ReactDOM.render(React.createElement(ConversationPanel, { - events: this.uiEvents, - handlerId: this.connection.handlerId, - noFirstMessageOverlay: false, - messagesDeletable: true - }), this.htmlTag); /* spawnExternalModal("conversation", this.uiEvents, { handlerId: this.connection.handlerId, @@ -52,7 +38,37 @@ export class ChannelConversationController extends AbstractConversationControlle }); */ - this.uiEvents.on("notify_destroy", connection.events().on("notify_visibility_changed", event => { + this.uiEvents.register_handler(this, true); + } + + destroy() { + this.connectionListener.forEach(callback => callback()); + this.connectionListener = []; + + this.uiEvents.unregister_handler(this); + super.destroy(); + } + + setConnectionHandler(connection: ConnectionHandler) { + if(this.connection === connection) { + return; + } + + this.connectionListener.forEach(callback => callback()); + this.connectionListener = []; + + this.connection = connection; + if(connection) { + this.initializeConnectionListener(connection); + /* FIXME: Update cross channel talk state! */ + this.setConversationManager(connection.getChannelConversations()); + } else { + this.setConversationManager(undefined); + } + } + + private initializeConnectionListener(connection: ConnectionHandler) { + this.connectionListener.push(connection.events().on("notify_visibility_changed", event => { if(!event.visible) { return; } @@ -60,9 +76,7 @@ export class ChannelConversationController extends AbstractConversationControlle this.handlePanelShow(); })); - this.uiEvents.register_handler(this, true); - - this.listenerManager.push(connection.events().on("notify_connection_state_changed", event => { + this.connectionListener.push(connection.events().on("notify_connection_state_changed", event => { if(event.newState === ConnectionState.CONNECTED) { connection.serverFeatures.awaitFeatures().then(success => { if(!success) { return; } @@ -75,17 +89,9 @@ export class ChannelConversationController extends AbstractConversationControlle })); } - destroy() { - ReactDOM.unmountComponentAtNode(this.htmlTag); - this.htmlTag.remove(); - - this.uiEvents.unregister_handler(this); - super.destroy(); - } - - @EventHandler("action_delete_message") - private handleMessageDelete(event: ConversationUIEvents["action_delete_message"]) { - const conversation = this.conversationManager.findConversationById(event.chatId); + @EventHandler("action_delete_message") + private handleMessageDelete(event: AbstractConversationUiEvents["action_delete_message"]) { + const conversation = this.conversationManager?.findConversationById(event.chatId); if(!conversation) { log.error(LogCategory.CLIENT, tr("Tried to delete a chat message from an unknown conversation with id %s"), event.chatId); return; diff --git a/shared/js/ui/frames/side/ChannelConversationDefinitions.ts b/shared/js/ui/frames/side/ChannelConversationDefinitions.ts new file mode 100644 index 00000000..7d7908e6 --- /dev/null +++ b/shared/js/ui/frames/side/ChannelConversationDefinitions.ts @@ -0,0 +1,3 @@ +import {AbstractConversationUiEvents} from "tc-shared/ui/frames/side/AbstractConversationDefinitions"; + +export interface ChannelConversationUiEvents extends AbstractConversationUiEvents {} \ No newline at end of file diff --git a/shared/js/ui/frames/side/ClientInfoController.ts b/shared/js/ui/frames/side/ClientInfoController.ts index 3ab6ae46..969b7418 100644 --- a/shared/js/ui/frames/side/ClientInfoController.ts +++ b/shared/js/ui/frames/side/ClientInfoController.ts @@ -1,107 +1,23 @@ -import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler"; -import {ClientEntry, ClientType, LocalClientEntry} from "tc-shared/tree/Client"; +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import { - ClientForumInfo, ClientGroupInfo, - ClientInfoEvents, ClientInfoType, - ClientStatusInfo, - ClientVersionInfo + ClientInfoEvents, } from "tc-shared/ui/frames/side/ClientInfoDefinitions"; import {Registry} from "tc-shared/events"; -import * as i18nc from "../../../i18n/country"; import {openClientInfo} from "tc-shared/ui/modal/ModalClientInfo"; -type CurrentClientInfo = { - type: ClientInfoType; - name: string, - uniqueId: string, - databaseId: number, - clientId: number, - - description: string, - joinTimestamp: number, - leaveTimestamp: number, - country: { name: string, flag: string }, - volume: { volume: number, muted: boolean }, - status: ClientStatusInfo, - forumAccount: ClientForumInfo | undefined, - channelGroup: number, - serverGroups: number[], - version: ClientVersionInfo -} - -export interface ClientInfoControllerEvents { - notify_client_changed: { - newClient: ClientEntry | undefined - } -} - export class ClientInfoController { - readonly events: Registry; - - private readonly connection: ConnectionHandler; - private readonly listenerConnection: (() => void)[]; - private readonly uiEvents: Registry; - private listenerClient: (() => void)[]; - private currentClient: ClientEntry | undefined; - private currentClientStatus: CurrentClientInfo | undefined; + private connection: ConnectionHandler; + private listenerConnection: (() => void)[]; - constructor(connection: ConnectionHandler) { - this.connection = connection; - this.events = new Registry(); + constructor() { this.uiEvents = new Registry(); this.uiEvents.enableDebug("client-info"); this.listenerConnection = []; - this.listenerClient = []; - - this.initialize(); - } - - private initialize() { - this.listenerConnection.push(this.connection.groups.events.on("notify_groups_updated", event => { - if(!this.currentClientStatus) { - return; - } - - for(const update of event.updates) { - if(update.group.id === this.currentClientStatus.channelGroup) { - this.sendChannelGroup(); - break; - } - } - - for(const update of event.updates) { - if(this.currentClientStatus.serverGroups.indexOf(update.group.id) !== -1) { - this.sendServerGroups(); - break; - } - } - })); - - this.listenerConnection.push(this.connection.channelTree.events.on("notify_client_leave_view", event => { - if(event.client !== this.currentClient) { - return; - } - - this.currentClientStatus.leaveTimestamp = Date.now() / 1000; - this.currentClientStatus.clientId = 0; - this.currentClient = undefined; - this.unregisterClientEvents(); - this.sendOnline(); - })); - - this.listenerConnection.push(this.connection.events().on("notify_connection_state_changed", event => { - if(event.newState !== ConnectionState.CONNECTED && this.currentClientStatus) { - this.currentClient = undefined; - this.currentClientStatus.leaveTimestamp = Date.now() / 1000; - this.sendOnline(); - } - })) - this.uiEvents.on("query_client", () => this.sendClient()); this.uiEvents.on("query_client_name", () => this.sendClientName()); this.uiEvents.on("query_client_description", () => this.sendClientDescription()); @@ -114,179 +30,105 @@ export class ClientInfoController { this.uiEvents.on("query_version", () => this.sendVersion()); this.uiEvents.on("query_forum", () => this.sendForum()); - this.uiEvents.on("action_edit_avatar", () => this.connection.update_avatar()); - this.uiEvents.on("action_show_full_info", () => this.currentClient && openClientInfo(this.currentClient)); + this.uiEvents.on("action_edit_avatar", () => this.connection?.update_avatar()); + this.uiEvents.on("action_show_full_info", () => { + const client = this.connection?.getSelectedClientInfo().getClient(); + if(client) { + openClientInfo(client); + } + }); } - private unregisterClientEvents() { - this.listenerClient.forEach(callback => callback()); - this.listenerClient = []; + destroy() { + this.listenerConnection.forEach(callback => callback()); + this.listenerConnection = []; } - private registerClientEvents(client: ClientEntry) { - const events = this.listenerClient; + setConnectionHandler(connection: ConnectionHandler) { + if(this.connection === connection) { + return; + } - events.push(client.events.on("notify_properties_updated", event => { - if('client_nickname' in event.updated_properties) { - this.currentClientStatus.name = event.client_properties.client_nickname; - this.sendClientName(); + this.listenerConnection.forEach(callback => callback()); + this.listenerConnection = []; + + this.connection = connection; + if(connection) { + this.initializeConnection(connection); + } + this.sendClient(); + } + + private initializeConnection(connection: ConnectionHandler) { + this.listenerConnection.push(connection.groups.events.on("notify_groups_updated", event => { + const info = this.connection?.getSelectedClientInfo().getInfo(); + if(!info) { + return; } - if('client_description' in event.updated_properties) { - this.currentClientStatus.description = event.client_properties.client_description; - this.sendClientDescription(); - } - - if('client_channel_group_id' in event.updated_properties) { - this.currentClientStatus.channelGroup = event.client_properties.client_channel_group_id; - this.sendChannelGroup(); - } - - if('client_servergroups' in event.updated_properties) { - this.currentClientStatus.serverGroups = client.assignedServerGroupIds(); - this.sendServerGroups(); - } - - /* Can happen since that variable isn't in view on client appearance */ - if('client_lastconnected' in event.updated_properties) { - this.currentClientStatus.joinTimestamp = event.client_properties.client_lastconnected; - this.sendOnline(); - } - - if('client_country' in event.updated_properties) { - this.updateCachedCountry(client); - this.sendCountry(); - } - - for(const key of ["client_away", "client_away_message", "client_input_muted", "client_input_hardware", "client_output_muted", "client_output_hardware"]) { - if(key in event.updated_properties) { - this.updateCachedClientStatus(client); - this.sendClientStatus(); + for(const update of event.updates) { + if(update.group.id === info.channelGroup) { + this.sendChannelGroup(); break; } } - if('client_platform' in event.updated_properties || 'client_version' in event.updated_properties) { - this.currentClientStatus.version = { - platform: client.properties.client_platform, - version: client.properties.client_version - }; - this.sendVersion(); - } - - if('client_teaforo_flags' in event.updated_properties || 'client_teaforo_name' in event.updated_properties || 'client_teaforo_id' in event.updated_properties) { - this.updateForumAccount(client); - this.sendForum(); + for(const update of event.updates) { + if(info.serverGroups.indexOf(update.group.id) !== -1) { + this.sendServerGroups(); + break; + } } })); - events.push(client.events.on("notify_audio_level_changed", () => { - this.updateCachedVolume(client); - this.sendVolume(); - })); + this.listenerConnection.push(connection.getSelectedClientInfo().events.on("notify_cache_changed", event => { + switch (event.category) { + case "name": + this.sendClientName(); + break; - events.push(client.events.on("notify_mute_state_change", () => { - this.updateCachedVolume(client); - this.sendVolume(); - })); - } + case "country": + this.sendCountry(); + break; - private updateCachedClientStatus(client: ClientEntry) { - this.currentClientStatus.status = { - away: client.properties.client_away ? client.properties.client_away_message ? client.properties.client_away_message : true : false, - microphoneMuted: client.properties.client_input_muted, - microphoneDisabled: !client.properties.client_input_hardware, - speakerMuted: client.properties.client_output_muted, - speakerDisabled: !client.properties.client_output_hardware - }; - } + case "description": + this.sendClientDescription(); + break; - private updateCachedCountry(client: ClientEntry) { - this.currentClientStatus.country = { - flag: client.properties.client_country, - name: i18nc.country_name(client.properties.client_country.toUpperCase()), - }; - } + case "forum-account": + this.sendForum(); + break; - private updateCachedVolume(client: ClientEntry) { - this.currentClientStatus.volume = { - volume: client.getAudioVolume(), - muted: client.isMuted() - } - } + case "group-channel": + this.sendChannelGroup(); + break; - private updateForumAccount(client: ClientEntry) { - if(client.properties.client_teaforo_id) { - this.currentClientStatus.forumAccount = { - flags: client.properties.client_teaforo_flags, - nickname: client.properties.client_teaforo_name, - userId: client.properties.client_teaforo_id - }; - } else { - this.currentClientStatus.forumAccount = undefined; - } - } + case "groups-server": + this.sendServerGroups(); + break; - private initializeClientInfo(client: ClientEntry) { - this.currentClientStatus = { - type: client instanceof LocalClientEntry ? "self" : client.properties.client_type === ClientType.CLIENT_QUERY ? "query" : "voice", - name: client.properties.client_nickname, - databaseId: client.properties.client_database_id, - uniqueId: client.properties.client_unique_identifier, - clientId: client.clientId(), + case "online-state": + this.sendOnline(); + break; - description: client.properties.client_description, - channelGroup: client.properties.client_channel_group_id, - serverGroups: client.assignedServerGroupIds(), - country: undefined, - forumAccount: undefined, - joinTimestamp: client.properties.client_lastconnected, - leaveTimestamp: 0, - status: undefined, - volume: undefined, - version: { - platform: client.properties.client_platform, - version: client.properties.client_version + case "status": + this.sendClientStatus(); + break; + + case "version": + this.sendVolume(); + break; + + case "volume": + this.sendVolume(); + break; } - }; - this.updateCachedClientStatus(client); - this.updateCachedCountry(client); - this.updateCachedVolume(client); - this.updateForumAccount(client); - } - - destroy() { - this.listenerClient.forEach(callback => callback()); - this.listenerClient = []; - - this.listenerConnection.forEach(callback => callback()); - this.listenerConnection.splice(0, this.listenerConnection.length); - } - - setClient(client: ClientEntry | undefined) { - if(this.currentClient === client) { - return; - } - - this.unregisterClientEvents(); - this.currentClient = client; - this.currentClientStatus = undefined; - if(this.currentClient) { - this.currentClient.updateClientVariables().then(undefined); - this.registerClientEvents(this.currentClient); - this.initializeClientInfo(this.currentClient); - } - this.sendClient(); - this.events.fire("notify_client_changed", { newClient: client }); - } - - getClient() : ClientEntry | undefined { - return this.currentClient; + })); } private generateGroupInfo(groupId: number, type: "channel" | "server") : ClientGroupInfo { - const uniqueServerId = this.connection.channelTree.server.properties.virtualserver_unique_identifier; - const group = type === "channel" ? this.connection.groups.findChannelGroup(groupId) : this.connection.groups.findServerGroup(groupId); + const uniqueServerId = this.connection?.channelTree.server.properties.virtualserver_unique_identifier; + const group = type === "channel" ? this.connection?.groups.findChannelGroup(groupId) : this.connection?.groups.findServerGroup(groupId); if(!group) { return { @@ -310,14 +152,15 @@ export class ClientInfoController { } private sendClient() { - if(this.currentClientStatus) { + const info = this.connection?.getSelectedClientInfo().getInfo(); + if(info) { this.uiEvents.fire_react("notify_client", { info: { handlerId: this.connection.handlerId, - type: this.currentClientStatus.type, - clientDatabaseId: this.currentClientStatus.databaseId, - clientId: this.currentClientStatus.clientId, - clientUniqueId: this.currentClientStatus.uniqueId + type: info.type, + clientDatabaseId: info.databaseId, + clientId: info.clientId, + clientUniqueId: info.uniqueId } }); } else { @@ -328,19 +171,21 @@ export class ClientInfoController { } private sendChannelGroup() { - if(typeof this.currentClientStatus === "undefined") { + const info = this.connection?.getSelectedClientInfo().getInfo(); + if(typeof info === "undefined") { this.uiEvents.fire_react("notify_channel_group", { group: undefined }); } else { - this.uiEvents.fire_react("notify_channel_group", { group: this.generateGroupInfo(this.currentClientStatus.channelGroup, "channel") }); + this.uiEvents.fire_react("notify_channel_group", { group: this.generateGroupInfo(info.channelGroup, "channel") }); } } private sendServerGroups() { - if(this.currentClientStatus === undefined) { + const info = this.connection?.getSelectedClientInfo().getInfo(); + if(info === undefined) { this.uiEvents.fire_react("notify_server_groups", { groups: [] }); } else { this.uiEvents.fire_react("notify_server_groups", { - groups: this.currentClientStatus.serverGroups.map(group => this.generateGroupInfo(group, "server")) + groups: info.serverGroups.map(group => this.generateGroupInfo(group, "server")) .sort((a, b) => { if (a.groupSortOrder < b.groupSortOrder) return 1; @@ -361,8 +206,9 @@ export class ClientInfoController { } private sendClientStatus() { + const info = this.connection?.getSelectedClientInfo().getInfo(); this.uiEvents.fire_react("notify_status", { - status: this.currentClientStatus?.status || { + status: info?.status || { away: false, speakerDisabled: false, speakerMuted: false, @@ -373,27 +219,31 @@ export class ClientInfoController { } private sendClientName() { - this.uiEvents.fire_react("notify_client_name", { name: this.currentClientStatus?.name }); + const info = this.connection?.getSelectedClientInfo().getInfo(); + this.uiEvents.fire_react("notify_client_name", { name: info?.name }); } private sendClientDescription() { - this.uiEvents.fire_react("notify_client_description", { description: this.currentClientStatus?.description }); + const info = this.connection?.getSelectedClientInfo().getInfo(); + this.uiEvents.fire_react("notify_client_description", { description: info?.description }); } private sendOnline() { + const info = this.connection?.getSelectedClientInfo().getInfo(); this.uiEvents.fire_react("notify_online", { status: { - leaveTimestamp: this.currentClientStatus ? this.currentClientStatus.leaveTimestamp : 0, - joinTimestamp: this.currentClientStatus ? this.currentClientStatus.joinTimestamp : 0 + leaveTimestamp: info ? info.leaveTimestamp : 0, + joinTimestamp: info ? info.joinTimestamp : 0 } }); } private sendCountry() { + const info = this.connection?.getSelectedClientInfo().getInfo(); this.uiEvents.fire_react("notify_country", { - country: this.currentClientStatus ? { - name: this.currentClientStatus.country.name, - flag: this.currentClientStatus.country.flag + country: info ? { + name: info.country.name, + flag: info.country.flag } : { name: tr("Unknown"), flag: "xx" @@ -402,10 +252,11 @@ export class ClientInfoController { } private sendVolume() { + const info = this.connection?.getSelectedClientInfo().getInfo(); this.uiEvents.fire_react("notify_volume", { - volume: this.currentClientStatus ? { - volume: this.currentClientStatus.volume.volume, - muted: this.currentClientStatus.volume.muted + volume: info ? { + volume: info.volume.volume, + muted: info.volume.muted } : { volume: -1, muted: false @@ -414,10 +265,11 @@ export class ClientInfoController { } private sendVersion() { + const info = this.connection?.getSelectedClientInfo().getInfo(); this.uiEvents.fire_react("notify_version", { - version: this.currentClientStatus ? { - platform: this.currentClientStatus.version.platform, - version: this.currentClientStatus.version.version + version: info ? { + platform: info.version.platform, + version: info.version.version } : { platform: tr("Unknown"), version: tr("Unknown") @@ -426,6 +278,7 @@ export class ClientInfoController { } private sendForum() { - this.uiEvents.fire_react("notify_forum", { forum: this.currentClientStatus?.forumAccount }) + const info = this.connection?.getSelectedClientInfo().getInfo(); + this.uiEvents.fire_react("notify_forum", { forum: info?.forumAccount }) } } \ No newline at end of file diff --git a/shared/js/ui/frames/side/HeaderController.ts b/shared/js/ui/frames/side/HeaderController.ts index 5b89dc8a..2f9571ef 100644 --- a/shared/js/ui/frames/side/HeaderController.ts +++ b/shared/js/ui/frames/side/HeaderController.ts @@ -18,7 +18,7 @@ const ChannelInfoUpdateProperties: (keyof ChannelProperties)[] = [ ]; /* TODO: Remove the ping interval handler. It's currently still there since the clients are not emitting the event yet */ -export class SideHeader { +export class SideHeaderController { private readonly uiEvents: Registry; private connection: ConnectionHandler; @@ -43,26 +43,32 @@ export class SideHeader { private initialize() { this.uiEvents.on("action_open_conversation", () => { - const selectedClient = this.connection.side_bar.getClientInfo().getClient() + const selectedClient = this.connection.getSelectedClientInfo().getClient() if(selectedClient) { const conversations = this.connection.getPrivateConversations(); conversations.setSelectedConversation(conversations.findOrCreateConversation(selectedClient)); } - this.connection.side_bar.showPrivateConversations(); + this.connection.getSideBar().showPrivateConversations(); }); this.uiEvents.on("action_switch_channel_chat", () => { - this.connection.side_bar.showChannelConversations(); + this.connection.getSideBar().showChannelConversations(); }); this.uiEvents.on("action_bot_manage", () => { - const bot = this.connection.side_bar.music_info().current_bot(); + /* FIXME: TODO! */ + /* + const bot = this.connection.getSideBar().music_info().current_bot(); if(!bot) return; openMusicManage(this.connection, bot); + */ }); - this.uiEvents.on("action_bot_add_song", () => this.connection.side_bar.music_info().events.fire("action_song_add")); + this.uiEvents.on("action_bot_add_song", () => { + /* FIXME: TODO! */ + //this.connection.side_bar.music_info().events.fire("action_song_add") + }); this.uiEvents.on("query_client_info_own_client", () => this.sendClientInfoOwnClient()); this.uiEvents.on("query_current_channel_state", event => this.sendChannelState(event.mode)); @@ -98,7 +104,7 @@ export class SideHeader { this.listenerConnection.push(this.connection.serverConnection.events.on("notify_ping_updated", () => this.sendPing())); this.listenerConnection.push(this.connection.getPrivateConversations().events.on("notify_unread_count_changed", () => this.sendPrivateConversationInfo())); this.listenerConnection.push(this.connection.getPrivateConversations().events.on(["notify_conversation_destroyed", "notify_conversation_destroyed"], () => this.sendPrivateConversationInfo())); - this.listenerConnection.push(this.connection.side_bar.getClientInfo().events.on("notify_client_changed", () => this.sendClientInfoOwnClient())); + this.listenerConnection.push(this.connection.getSelectedClientInfo().events.on("notify_client_changed", () => this.sendClientInfoOwnClient())); } setConnectionHandler(connection: ConnectionHandler) { @@ -253,7 +259,7 @@ export class SideHeader { private sendClientInfoOwnClient() { if(this.connection) { - this.uiEvents.fire_react("notify_client_info_own_client", { isOwnClient: this.connection.side_bar.getClientInfo().getClient() instanceof LocalClientEntry }); + this.uiEvents.fire_react("notify_client_info_own_client", { isOwnClient: this.connection.getSelectedClientInfo().getClient() instanceof LocalClientEntry }); } else { this.uiEvents.fire_react("notify_client_info_own_client", { isOwnClient: false }); } diff --git a/shared/js/ui/frames/side/PopoutConversationRenderer.tsx b/shared/js/ui/frames/side/PopoutConversationRenderer.tsx index 72d92f6a..79ff39d8 100644 --- a/shared/js/ui/frames/side/PopoutConversationRenderer.tsx +++ b/shared/js/ui/frames/side/PopoutConversationRenderer.tsx @@ -1,11 +1,11 @@ import {Registry, RegistryMap} from "tc-shared/events"; -import {ConversationUIEvents} from "tc-shared/ui/frames/side/ConversationDefinitions"; +import {AbstractConversationUiEvents} from "./AbstractConversationDefinitions"; import {ConversationPanel} from "./AbstractConversationRenderer"; import * as React from "react"; import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions"; class PopoutConversationRenderer extends AbstractModal { - private readonly events: Registry; + private readonly events: Registry; private readonly userData: any; constructor(registryMap: RegistryMap, userData: any) { diff --git a/shared/js/ui/frames/side/PrivateConversationController.ts b/shared/js/ui/frames/side/PrivateConversationController.ts index 262bb8f1..60f9708a 100644 --- a/shared/js/ui/frames/side/PrivateConversationController.ts +++ b/shared/js/ui/frames/side/PrivateConversationController.ts @@ -4,12 +4,9 @@ import { PrivateConversationInfo, PrivateConversationUIEvents } from "../../../ui/frames/side/PrivateConversationDefinitions"; -import * as ReactDOM from "react-dom"; -import * as React from "react"; -import {PrivateConversationsPanel} from "./PrivateConversationRenderer"; import { - ConversationUIEvents -} from "../../../ui/frames/side/ConversationDefinitions"; + AbstractConversationUiEvents +} from "./AbstractConversationDefinitions"; import * as log from "../../../log"; import {LogCategory} from "../../../log"; import {AbstractConversationController} from "./AbstractConversationController"; @@ -48,35 +45,57 @@ export class PrivateConversationController extends AbstractConversationControlle PrivateConversation, PrivateConversationEvents > { - public readonly htmlTag: HTMLDivElement; - public readonly connection: ConnectionHandler; + private connection: ConnectionHandler; + private connectionListener: (() => void)[]; private listenerConversation: {[key: string]:(() => void)[]}; - constructor(connection: ConnectionHandler) { - super(connection.getPrivateConversations()); - this.connection = connection; + constructor() { + super(); + this.connectionListener = []; this.listenerConversation = {}; - this.htmlTag = document.createElement("div"); - this.htmlTag.style.display = "flex"; - this.htmlTag.style.flexDirection = "row"; - this.htmlTag.style.justifyContent = "stretch"; - this.htmlTag.style.height = "100%"; - this.uiEvents.register_handler(this, true); this.uiEvents.enableDebug("private-conversations"); + } - ReactDOM.render(React.createElement(PrivateConversationsPanel, { events: this.uiEvents, handler: this.connection }), this.htmlTag); + destroy() { + /* listenerConversation will be cleaned up via the listenerManager callbacks */ - this.uiEvents.on("notify_destroy", connection.events().on("notify_visibility_changed", event => { + this.uiEvents.unregister_handler(this); + super.destroy(); + } + + setConnectionHandler(connection: ConnectionHandler) { + if(this.connection === connection) { + return; + } + + this.connectionListener.forEach(callback => callback()); + this.connectionListener = []; + + this.connection = connection; + if(connection) { + this.initializeConnectionListener(connection); + this.setConversationManager(connection.getPrivateConversations()); + } else { + this.setConversationManager(undefined); + } + } + + private initializeConnectionListener(connection: ConnectionHandler) { + this.connectionListener.push(connection.events().on("notify_visibility_changed", event => { if(!event.visible) return; this.handlePanelShow(); })); + } - this.listenerManager.push(this.conversationManager.events.on("notify_conversation_created", event => { + protected registerConversationManagerEvents(manager: PrivateConversationManager) { + super.registerConversationManagerEvents(manager); + + this.listenerManager.push(manager.events.on("notify_conversation_created", event => { const conversation = event.conversation; const events = this.listenerConversation[conversation.getChatId()] = []; events.push(conversation.events.on("notify_partner_changed", event => { @@ -94,21 +113,17 @@ export class PrivateConversationController extends AbstractConversationControlle this.reportConversationList(); })); - this.listenerManager.push(this.conversationManager.events.on("notify_conversation_destroyed", event => { + this.listenerManager.push(manager.events.on("notify_conversation_destroyed", event => { this.listenerConversation[event.conversation.getChatId()]?.forEach(callback => callback()); delete this.listenerConversation[event.conversation.getChatId()]; this.reportConversationList(); })); - this.listenerManager.push(this.conversationManager.events.on("notify_selected_changed", () => this.reportConversationList())); - } - - destroy() { - ReactDOM.unmountComponentAtNode(this.htmlTag); - this.htmlTag.remove(); - - this.uiEvents.unregister_handler(this); - super.destroy(); + this.listenerManager.push(manager.events.on("notify_selected_changed", () => this.reportConversationList())); + this.listenerManager.push(() => { + Object.values(this.listenerConversation).forEach(callbacks => callbacks.forEach(callback => callback())); + this.listenerConversation = {}; + }); } focusInput() { @@ -117,8 +132,8 @@ export class PrivateConversationController extends AbstractConversationControlle private reportConversationList() { this.uiEvents.fire_react("notify_private_conversations", { - conversations: this.conversationManager.getConversations().map(generateConversationUiInfo), - selected: this.conversationManager.getSelectedConversation()?.clientUniqueId || "unselected" + conversations: this.conversationManager ? this.conversationManager.getConversations().map(generateConversationUiInfo) : [], + selected: this.conversationManager?.getSelectedConversation()?.clientUniqueId || "unselected" }); } @@ -129,7 +144,7 @@ export class PrivateConversationController extends AbstractConversationControlle @EventHandler("action_close_chat") private handleConversationClose(event: PrivateConversationUIEvents["action_close_chat"]) { - const conversation = this.conversationManager.findConversation(event.chatId); + const conversation = this.conversationManager?.findConversation(event.chatId); if(!conversation) { log.error(LogCategory.CLIENT, tr("Tried to close a not existing private conversation with id %s"), event.chatId); return; @@ -138,13 +153,8 @@ export class PrivateConversationController extends AbstractConversationControlle this.conversationManager.closeConversation(conversation); } - @EventHandler("notify_partner_typing") - private handleNotifySelectChat(event: PrivateConversationUIEvents["notify_partner_typing"]) { - /* TODO, set active chat? MH 9/12/20: What?? */ - } - - @EventHandler("action_self_typing") - protected handleActionSelfTyping1(_event: ConversationUIEvents["action_self_typing"]) { + @EventHandler("action_self_typing") + protected handleActionSelfTyping1(_event: AbstractConversationUiEvents["action_self_typing"]) { const conversation = this.getCurrentConversation(); if(!conversation) { return; diff --git a/shared/js/ui/frames/side/PrivateConversationDefinitions.ts b/shared/js/ui/frames/side/PrivateConversationDefinitions.ts index 8c98cfc6..2e083379 100644 --- a/shared/js/ui/frames/side/PrivateConversationDefinitions.ts +++ b/shared/js/ui/frames/side/PrivateConversationDefinitions.ts @@ -1,4 +1,4 @@ -import {ConversationUIEvents} from "../../../ui/frames/side/ConversationDefinitions"; +import {AbstractConversationUiEvents} from "./AbstractConversationDefinitions"; export type PrivateConversationInfo = { nickname: string; @@ -11,7 +11,7 @@ export type PrivateConversationInfo = { unreadMessages: boolean; }; -export interface PrivateConversationUIEvents extends ConversationUIEvents { +export interface PrivateConversationUIEvents extends AbstractConversationUiEvents { action_close_chat: { chatId: string }, query_private_conversations: {}, diff --git a/shared/js/ui/frames/side/PrivateConversationRenderer.scss b/shared/js/ui/frames/side/PrivateConversationRenderer.scss index 9794bf7b..8f88b7c5 100644 --- a/shared/js/ui/frames/side/PrivateConversationRenderer.scss +++ b/shared/js/ui/frames/side/PrivateConversationRenderer.scss @@ -17,6 +17,13 @@ html:root { --chat-private-selected-background: #2c2c2c; } +.dividerContainer { + display: flex; + flex-direction: row; + justify-content: stretch; + height: 100%; +} + .divider { width: 2px!important; min-width: 2px!important; diff --git a/shared/js/ui/frames/side/PrivateConversationRenderer.tsx b/shared/js/ui/frames/side/PrivateConversationRenderer.tsx index ad013c0f..cc87a6f5 100644 --- a/shared/js/ui/frames/side/PrivateConversationRenderer.tsx +++ b/shared/js/ui/frames/side/PrivateConversationRenderer.tsx @@ -218,10 +218,11 @@ const OpenConversationsPanel = React.memo(() => { export const PrivateConversationsPanel = (props: { events: Registry, handlerId: string }) => ( - +
+ - +
); \ No newline at end of file diff --git a/shared/js/ui/frames/side/music_info.ts b/shared/js/ui/frames/side/music_info.ts index 60f1eaf7..1ef3f2fa 100644 --- a/shared/js/ui/frames/side/music_info.ts +++ b/shared/js/ui/frames/side/music_info.ts @@ -1,4 +1,4 @@ -import {Frame, FrameContent} from "../../../ui/frames/chat_frame"; +import {SideBarController, FrameContent} from "../SideBarController"; import {LogCategory} from "../../../log"; import {CommandResult, PlaylistSong} from "../../../connection/ServerConnectionDeclaration"; import {createErrorModal, createInputModal} from "../../../ui/elements/Modal"; @@ -67,7 +67,7 @@ interface LoadedSongData { export class MusicInfo { readonly events: Registry; - readonly handle: Frame; + readonly handle: SideBarController; private _html_tag: JQuery; private _container_playlist: JQuery; @@ -91,7 +91,7 @@ export class MusicInfo { previous_frame_content: FrameContent; - constructor(handle: Frame) { + constructor(handle: SideBarController) { this.events = new Registry(); this.handle = handle; diff --git a/shared/js/ui/react-elements/ContextDivider.tsx b/shared/js/ui/react-elements/ContextDivider.tsx index 4b94f28f..bbbfc206 100644 --- a/shared/js/ui/react-elements/ContextDivider.tsx +++ b/shared/js/ui/react-elements/ContextDivider.tsx @@ -11,7 +11,7 @@ export interface ContextDividerProperties { separatorClassName?: string; separatorActiveClassName?: string; - children: [React.ReactElement, React.ReactElement]; + children?: never; } export interface ContextDividerState { @@ -99,11 +99,9 @@ export class ContextDivider extends React.Component this.startMovement(e)} onTouchStart={e => this.startMovement(e)} />, - this.props.children[1] - ]; + return ( +
this.startMovement(e)} onTouchStart={e => this.startMovement(e)} /> + ) } componentDidMount(): void {