The sidebar now is completely disconnected from the connection handler itself and only renders what's needed.

master
WolverinDEV 2020-12-09 20:44:33 +01:00 committed by WolverinDEV
parent cb505d39cf
commit 787de619de
31 changed files with 777 additions and 566 deletions

View File

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

View File

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

View File

@ -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<ConnectionManagerEvents>();
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", {

View File

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

View File

@ -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<SideBarManagerEvents>;
private readonly connection: ConnectionHandler;
private currentType: SideBarType;
constructor(connection: ConnectionHandler) {
this.events = new Registry<SideBarManagerEvents>();
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");
}
}

View File

@ -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<Events extends AbstractChatEvents> {
export abstract class AbstractChat<Events extends AbstractConversationEvents> {
readonly events: Registry<Events>;
protected readonly connection: ConnectionHandler;
@ -341,7 +341,7 @@ export interface AbstractChatManagerEvents<ConversationType> {
}
}
export abstract class AbstractChatManager<ManagerEvents extends AbstractChatManagerEvents<ConversationType>, ConversationType extends AbstractChat<ConversationEvents>, ConversationEvents extends AbstractChatEvents> {
export abstract class AbstractChatManager<ManagerEvents extends AbstractChatManagerEvents<ConversationType>, ConversationType extends AbstractChat<ConversationEvents>, ConversationEvents extends AbstractConversationEvents> {
readonly events: Registry<ManagerEvents>;
readonly connection: ConnectionHandler;
protected readonly listenerConnection: (() => void)[];
@ -351,6 +351,12 @@ export abstract class AbstractChatManager<ManagerEvents extends AbstractChatMana
private selectedConversation: ConversationType;
private currentUnreadCount: number;
/* FIXME: Access modifier */
public historyUiStates: {[id: string]: {
executingUIHistoryQuery: boolean,
historyErrorMessage: string | undefined,
historyRetryTimestamp: number
}} = {};
protected constructor(connection: ConnectionHandler) {
this.events = new Registry<ManagerEvents>();
@ -418,6 +424,8 @@ export abstract class AbstractChatManager<ManagerEvents extends AbstractChatMana
this.setSelectedConversation(undefined);
}
delete this.historyUiStates[conversation.getChatId()];
conversation.events.off("notify_unread_state_changed", this.listenerUnreadTimestamp);
delete this.conversations[conversation.getChatId()];
this.events.fire("notify_conversation_destroyed", { conversation: conversation });

View File

@ -1,11 +1,11 @@
import {
AbstractChat,
AbstractChatEvents,
AbstractConversationEvents,
AbstractChatManager,
AbstractChatManagerEvents,
kMaxChatFrameMessageSize
} from "./AbstractConversion";
import {ChatMessage, ConversationHistoryResponse} from "tc-shared/ui/frames/side/ConversationDefinitions";
import {ChatMessage, ConversationHistoryResponse} from "../ui/frames/side/AbstractConversationDefinitions";
import {Settings} from "tc-shared/settings";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {ErrorCode} from "tc-shared/connection/ErrorCode";
@ -17,7 +17,7 @@ import {LocalClientEntry} from "tc-shared/tree/Client";
import {ServerCommand} from "tc-shared/connection/ConnectionBase";
import {ChannelConversationMode} from "tc-shared/tree/Channel";
export interface ChannelConversationEvents extends AbstractChatEvents {
export interface ChannelConversationEvents extends AbstractConversationEvents {
notify_messages_deleted: { messages: string[] },
notify_messages_loaded: {}
}

View File

@ -2,7 +2,7 @@ import * as loader from "tc-loader";
import {Stage} from "tc-loader";
import { tr } from "tc-shared/i18n/localize";
import {LogCategory, logDebug, logError, logInfo, logWarn} from "tc-shared/log";
import {ChatEvent} from "tc-shared/ui/frames/side/ConversationDefinitions";
import {ChatEvent} from "../ui/frames/side/AbstractConversationDefinitions";
const clientUniqueId2StoreName = uniqueId => "conversation-" + uniqueId;

View File

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

View File

@ -412,7 +412,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
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)
}, {

View File

@ -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()
}
}
}

View File

@ -382,7 +382,7 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
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<ClientEvents> {
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) {

View File

@ -193,7 +193,7 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
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)
}, {

View File

@ -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<SideBarEvents>;
export class Frame {
readonly handle: ConnectionHandler;
private htmlTag: HTMLDivElement;
private currentConnection: ConnectionHandler;
private listenerConnection: (() => void)[];
private currentType: SideBarType;
private uiEvents: Registry<SideBarEvents>;
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<SideBarEvents>();
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");
}
}

View File

@ -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<ConversationUIEvents>,
events: Registry<AbstractConversationUiEvents>,
handlerId: string
},
"private-chat": {
@ -32,7 +33,9 @@ export type SideBarNotifyContentData<T extends SideBarType> = {
export interface SideBarEvents {
query_content: {},
query_content_data: { content: SideBarType },
query_header_data: {},
notify_content: { content: SideBarType },
notify_content_data: SideBarNotifyContentData<SideBarType>
notify_content_data: SideBarNotifyContentData<SideBarType>,
notify_header_data: { events: Registry<SideHeaderEvents> }
}

View File

@ -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<Registry<SideBarEvents>>(undefined);
@ -109,7 +109,6 @@ const SideBarHeader = (props: { type: SideBarType, eventsHeader: Registry<SideHe
}
export const SideBarRenderer = (props: {
handlerId: string,
events: Registry<SideBarEvents>,
eventsHeader: Registry<SideHeaderEvents>
}) => {

View File

@ -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, ConversationType, ConversationEvents>,
ManagerEvents extends AbstractChatManagerEvents<ConversationType>,
ConversationType extends AbstractChat<ConversationEvents>,
ConversationEvents extends AbstractChatEvents
ConversationEvents extends AbstractConversationEvents
> {
protected readonly uiEvents: Registry<Events>;
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<Events>();
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<Events> {
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<any>) {
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<any>, 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<ConversationUIEvents>("query_conversation_state")
protected handleQueryConversationState(event: ConversationUIEvents["query_conversation_state"]) {
const conversation = this.conversationManager.findConversationById(event.chatId);
@EventHandler<AbstractConversationUiEvents>("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<ConversationUIEvents>("query_conversation_history")
protected handleQueryHistory(event: ConversationUIEvents["query_conversation_history"]) {
const conversation = this.conversationManager.findConversationById(event.chatId);
@EventHandler<AbstractConversationUiEvents>("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<ConversationUIEvents>(["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<AbstractConversationUiEvents>(["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<ConversationUIEvents>("action_send_message")
protected handleSendMessage(event: ConversationUIEvents["action_send_message"]) {
const conversation = this.conversationManager.findConversationById(event.chatId);
@EventHandler<AbstractConversationUiEvents>("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<ConversationUIEvents>("action_jump_to_present")
protected handleJumpToPresent(event: ConversationUIEvents["action_jump_to_present"]) {
const conversation = this.conversationManager.findConversationById(event.chatId);
@EventHandler<AbstractConversationUiEvents>("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<ConversationUIEvents>("query_selected_chat")
@EventHandler<AbstractConversationUiEvents>("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<ConversationUIEvents>("action_select_chat")
private handleActionSelectChat(event: ConversationUIEvents["action_select_chat"]) {
const conversation = this.conversationManager.findConversationById(event.chatId);
@EventHandler<AbstractConversationUiEvents>("action_select_chat")
private handleActionSelectChat(event: AbstractConversationUiEvents["action_select_chat"]) {
const conversation = this.conversationManager?.findConversationById(event.chatId);
this.conversationManager.setSelectedConversation(conversation);
}
}

View File

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

View File

@ -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<ConversationUIEvents>,
events: Registry<AbstractConversationUiEvents>,
handlerId: string,
refHTMLElement?: Ref<HTMLDivElement>
@ -126,7 +126,7 @@ const UnreadEntry = (props: { refDiv: React.Ref<HTMLDivElement> }) => (
</div>
);
const LoadOderMessages = (props: { events: Registry<ConversationUIEvents>, chatId: string, state: ChatHistoryState | "error", errorMessage?: string, retryTimestamp?: number, timestamp: number | undefined }) => {
const LoadOderMessages = (props: { events: Registry<AbstractConversationUiEvents>, 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<ConversationUIEvents>, chatI
)
};
const JumpToPresent = (props: { events: Registry<ConversationUIEvents>, chatId: string }) => (
const JumpToPresent = (props: { events: Registry<AbstractConversationUiEvents>, chatId: string }) => (
<div
className={cssStyle.containerLoadMessages + " " + cssStyle.present}
onClick={() => 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<ConversationUIEvents>, chatId: string, timeout?: number }) => {
const PartnerTypingIndicator = (props: { events: Registry<AbstractConversationUiEvents>, chatId: string, timeout?: number }) => {
const kTypingTimeout = props.timeout || 5000;
@ -349,7 +349,7 @@ const PartnerTypingIndicator = (props: { events: Registry<ConversationUIEvents>,
};
interface ConversationMessagesProperties {
events: Registry<ConversationUIEvents>;
events: Registry<AbstractConversationUiEvents>;
handlerId: string;
noFirstMessageOverlay?: boolean
@ -698,8 +698,8 @@ class ConversationMessages extends React.PureComponent<ConversationMessagesPrope
}
}
@EventHandler<ConversationUIEvents>("notify_selected_chat")
private handleNotifySelectedChat(event: ConversationUIEvents["notify_selected_chat"]) {
@EventHandler<AbstractConversationUiEvents>("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<ConversationMessagesPrope
}
}
@EventHandler<ConversationUIEvents>("notify_conversation_state")
private handleConversationStateUpdate(event: ConversationUIEvents["notify_conversation_state"]) {
@EventHandler<AbstractConversationUiEvents>("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<ConversationMessagesPrope
}
}
@EventHandler<ConversationUIEvents>("notify_chat_event")
private handleChatEvent(event: ConversationUIEvents["notify_chat_event"]) {
@EventHandler<AbstractConversationUiEvents>("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<ConversationMessagesPrope
this.forceUpdate(() => this.scrollToBottom());
}
@EventHandler<ConversationUIEvents>("notify_chat_message_delete")
private handleMessageDeleted(event: ConversationUIEvents["notify_chat_message_delete"]) {
@EventHandler<AbstractConversationUiEvents>("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<ConversationMessagesPrope
this.forceUpdate(() => this.scrollToBottom());
}
@EventHandler<ConversationUIEvents>("notify_unread_timestamp_changed")
private handleUnreadTimestampChanged(event: ConversationUIEvents["notify_unread_timestamp_changed"]) {
@EventHandler<AbstractConversationUiEvents>("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<ConversationMessagesPrope
}
}
@EventHandler<ConversationUIEvents>("notify_panel_show")
@EventHandler<AbstractConversationUiEvents>("notify_panel_show")
private handlePanelShow() {
this.fixScroll();
}
@EventHandler<ConversationUIEvents>("query_conversation_history")
private handleQueryConversationHistory(event: ConversationUIEvents["query_conversation_history"]) {
@EventHandler<AbstractConversationUiEvents>("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<ConversationMessagesPrope
});
}
@EventHandler<ConversationUIEvents>("notify_conversation_history")
private handleNotifyConversationHistory(event: ConversationUIEvents["notify_conversation_history"]) {
@EventHandler<AbstractConversationUiEvents>("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<ConversationMessagesPrope
}
}
export const ConversationPanel = React.memo((props: { events: Registry<ConversationUIEvents>, handlerId: string, messagesDeletable: boolean, noFirstMessageOverlay: boolean }) => {
export const ConversationPanel = React.memo((props: { events: Registry<AbstractConversationUiEvents>, 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<Conversat
chatEnabled.current = event.state === "normal" && event.sendEnabled;
updateChatBox();
});
props.events.reactUse("notify_send_enabled", event => {
if(event.chatId !== currentChat.current.id)
return;

View File

@ -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<ConversationUIEvents>("action_delete_message")
private handleMessageDelete(event: ConversationUIEvents["action_delete_message"]) {
const conversation = this.conversationManager.findConversationById(event.chatId);
@EventHandler<AbstractConversationUiEvents>("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;

View File

@ -0,0 +1,3 @@
import {AbstractConversationUiEvents} from "tc-shared/ui/frames/side/AbstractConversationDefinitions";
export interface ChannelConversationUiEvents extends AbstractConversationUiEvents {}

View File

@ -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<ClientInfoControllerEvents>;
private readonly connection: ConnectionHandler;
private readonly listenerConnection: (() => void)[];
private readonly uiEvents: Registry<ClientInfoEvents>;
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<ClientInfoControllerEvents>();
constructor() {
this.uiEvents = new Registry<ClientInfoEvents>();
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 })
}
}

View File

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

View File

@ -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<ConversationUIEvents>;
private readonly events: Registry<AbstractConversationUiEvents>;
private readonly userData: any;
constructor(registryMap: RegistryMap, userData: any) {

View File

@ -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<PrivateConversationUIEvents>("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<PrivateConversationUIEvents>("notify_partner_typing")
private handleNotifySelectChat(event: PrivateConversationUIEvents["notify_partner_typing"]) {
/* TODO, set active chat? MH 9/12/20: What?? */
}
@EventHandler<ConversationUIEvents>("action_self_typing")
protected handleActionSelfTyping1(_event: ConversationUIEvents["action_self_typing"]) {
@EventHandler<AbstractConversationUiEvents>("action_self_typing")
protected handleActionSelfTyping1(_event: AbstractConversationUiEvents["action_self_typing"]) {
const conversation = this.getCurrentConversation();
if(!conversation) {
return;

View File

@ -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: {},

View File

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

View File

@ -218,10 +218,11 @@ const OpenConversationsPanel = React.memo(() => {
export const PrivateConversationsPanel = (props: { events: Registry<PrivateConversationUIEvents>, handlerId: string }) => (
<HandlerIdContext.Provider value={props.handlerId}>
<EventContext.Provider value={props.events}>
<ContextDivider id={"seperator-conversation-list-messages"} direction={"horizontal"} defaultValue={25} separatorClassName={cssStyle.divider}>
<div className={cssStyle.dividerContainer}>
<OpenConversationsPanel />
<ContextDivider id={"seperator-conversation-list-messages"} direction={"horizontal"} defaultValue={25} separatorClassName={cssStyle.divider} />
<ConversationPanel events={props.events as any} handlerId={props.handlerId} noFirstMessageOverlay={true} messagesDeletable={false} />
</ContextDivider>
</div>
</EventContext.Provider>
</HandlerIdContext.Provider>
);

View File

@ -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<MusicSidebarEvents>;
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<MusicSidebarEvents>();
this.handle = handle;

View File

@ -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<ContextDividerProperties, Co
if(this.state.active && this.props.separatorClassName)
separatorClassNames += " " + this.props.separatorClassName;
return [
this.props.children[0],
<div key={"context-separator"} ref={this.refSeparator} className={separatorClassNames} onMouseDown={e => this.startMovement(e)} onTouchStart={e => this.startMovement(e)} />,
this.props.children[1]
];
return (
<div key={"context-separator"} ref={this.refSeparator} className={separatorClassNames} onMouseDown={e => this.startMovement(e)} onTouchStart={e => this.startMovement(e)} />
)
}
componentDidMount(): void {