Some minor chat related bugfixing and separated the chat controller form the chat modal
parent
c144af0879
commit
a05b795859
|
@ -1,8 +1,14 @@
|
|||
# Changelog:
|
||||
* **09.12.20**
|
||||
- Fixed the private messages unread indicator
|
||||
- Properly updating the private message unread count
|
||||
|
||||
* **08.12.20**
|
||||
- Fixed the permission editor not resolving unique ids
|
||||
- Fixed client database info resolve
|
||||
|
||||
- Improved the side header bar
|
||||
All values are not updating accordingly to the connection state
|
||||
|
||||
* **07.12.20**
|
||||
- Fixed the Markdown to BBCode transpiler falsely emitting empty lines
|
||||
- Fixed invalid BBCode escaping
|
||||
|
|
|
@ -38,214 +38,6 @@ html:root {
|
|||
|
||||
min-height: 200px;
|
||||
|
||||
.container-info {
|
||||
user-select: none;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
height: 9em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
|
||||
background-color: var(--side-info-background);
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
|
||||
-moz-box-shadow: inset 0 0 5px var(--side-info-shadow);
|
||||
-webkit-box-shadow: inset 0 0 5px var(--side-info-shadow);
|
||||
box-shadow: inset 0 0 5px var(--side-info-shadow);
|
||||
|
||||
.lane {
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
height: 3.25em;
|
||||
|
||||
.block, .button {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.block {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 0;
|
||||
|
||||
&.right {
|
||||
text-align: right;
|
||||
|
||||
&.mode-client_info {
|
||||
max-width: calc(50% - #{$client_info_avatar_size / 2});
|
||||
margin-left: calc(#{$client_info_avatar_size / 2});
|
||||
}
|
||||
}
|
||||
|
||||
&.left {
|
||||
margin-right: .5em;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
|
||||
&.mode-client_info {
|
||||
max-width: calc(50% - #{$client_info_avatar_size / 2});
|
||||
margin-right: calc(#{$client_info_avatar_size} / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.title, .value, .small-value {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
color: var(--side-info-title);
|
||||
|
||||
.container-indicator {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
background: var(--side-info-indicator-background);
|
||||
border: 1px solid var(--side-info-indicator-border);
|
||||
border-radius: 4px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
vertical-align: text-top;
|
||||
|
||||
color: var(--side-info-indicator);
|
||||
|
||||
font-size: .66em;
|
||||
height: 1.3em;
|
||||
min-width: .9em;
|
||||
|
||||
padding-right: 2px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--side-info-value);
|
||||
background-color: var(--side-info-value-background);
|
||||
|
||||
display: inline-block;
|
||||
|
||||
border-radius: .18em;
|
||||
padding-right: .31em;
|
||||
padding-left: .31em;
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.icon-container, .icon {
|
||||
vertical-align: middle;
|
||||
margin-right: .25em;
|
||||
}
|
||||
|
||||
&.value-ping {
|
||||
//very-good good medium poor very-poor
|
||||
&.very-good {
|
||||
color: var(--side-info-ping-very-good);
|
||||
}
|
||||
&.good {
|
||||
color: var(--side-info-ping-good);
|
||||
}
|
||||
&.medium {
|
||||
color: var(--side-info-ping-medium);
|
||||
}
|
||||
&.poor {
|
||||
color: var(--side-info-ping-poor);
|
||||
}
|
||||
&.very-poor {
|
||||
color: var(--side-info-ping-very-poor);
|
||||
}
|
||||
}
|
||||
|
||||
&.chat-counter {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.bot-add-song {
|
||||
color: var(--side-info-bot-add-song);
|
||||
}
|
||||
}
|
||||
|
||||
.small-value {
|
||||
display: inline-block;
|
||||
color: var(--side-info-value);
|
||||
font-size: .66em;
|
||||
vertical-align: top;
|
||||
margin-top: -.2em;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: var(--side-info-value);
|
||||
background-color: var(--side-info-value-background);
|
||||
|
||||
display: inline-block;
|
||||
|
||||
&:not(.value) {
|
||||
border-radius: .18em;
|
||||
padding-right: .31em;
|
||||
padding-left: .31em;
|
||||
|
||||
margin-top: 1.5em; /* because we've no title */
|
||||
}
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #4e4e4e; /* TODO: Evaluate color */
|
||||
}
|
||||
@include transition(background-color $button_hover_animation_time ease-in-out);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.mode-channel_chat) {
|
||||
.mode-channel_chat { display: none; }
|
||||
}
|
||||
|
||||
&:not(.mode-private_chat) {
|
||||
.mode-private_chat { display: none; }
|
||||
}
|
||||
|
||||
&:not(.mode-client_info) {
|
||||
.mode-client_info { display: none; }
|
||||
}
|
||||
|
||||
&:not(.mode-music_bot) {
|
||||
.mode-music_bot { display: none; }
|
||||
}
|
||||
|
||||
&.mode-music_bot {
|
||||
.mode-music_bot {
|
||||
&.right {
|
||||
margin-left: 8.5em;
|
||||
}
|
||||
&.left {
|
||||
margin-right: 8.5em;
|
||||
}
|
||||
|
||||
width: 60em; /* same width so flex-shrik applies equaly */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-chat {
|
||||
width: 100%;
|
||||
|
||||
|
|
|
@ -38,6 +38,9 @@ import {LocalClientEntry} from "./tree/Client";
|
|||
import {ServerAddress} from "./tree/Server";
|
||||
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";
|
||||
|
||||
export enum InputHardwareState {
|
||||
MISSING,
|
||||
|
@ -154,6 +157,9 @@ export class ConnectionHandler {
|
|||
|
||||
serverFeatures: ServerFeatures;
|
||||
|
||||
private channelConversations: ChannelConversationManager;
|
||||
private privateConversations: PrivateConversationManager;
|
||||
|
||||
private _clientId: number = 0;
|
||||
private localClient: LocalClientEntry;
|
||||
|
||||
|
@ -205,6 +211,9 @@ export class ConnectionHandler {
|
|||
this.fileManager = new FileManager(this);
|
||||
this.permissions = new PermissionManager(this);
|
||||
|
||||
this.privateConversations = new PrivateConversationManager(this);
|
||||
this.channelConversations = new ChannelConversationManager(this);
|
||||
|
||||
this.pluginCmdRegistry = new PluginCmdRegistry(this);
|
||||
this.video_frame = new ChannelVideoFrame(this);
|
||||
|
||||
|
@ -217,9 +226,9 @@ export class ConnectionHandler {
|
|||
this.localClient.channelTree = this.channelTree;
|
||||
|
||||
this.event_registry.register_handler(this);
|
||||
this.events().fire("notify_handler_initialized");
|
||||
|
||||
this.pluginCmdRegistry.registerHandler(new W2GPluginCmdHandler());
|
||||
|
||||
this.events().fire("notify_handler_initialized");
|
||||
}
|
||||
|
||||
initialize_client_state(source?: ConnectionHandler) {
|
||||
|
@ -341,6 +350,14 @@ export class ConnectionHandler {
|
|||
getClient() : LocalClientEntry { return this.localClient; }
|
||||
getClientId() { return this._clientId; }
|
||||
|
||||
getPrivateConversations() : PrivateConversationManager {
|
||||
return this.privateConversations;
|
||||
}
|
||||
|
||||
getChannelConversations() : ChannelConversationManager {
|
||||
return this.channelConversations;
|
||||
}
|
||||
|
||||
initializeLocalClient(clientId: number, acceptedName: string) {
|
||||
this._clientId = clientId;
|
||||
this.localClient["_clientId"] = clientId;
|
||||
|
@ -354,8 +371,8 @@ export class ConnectionHandler {
|
|||
|
||||
@EventHandler<ConnectionEvents>("notify_connection_state_changed")
|
||||
private handleConnectionStateChanged(event: ConnectionEvents["notify_connection_state_changed"]) {
|
||||
this.connection_state = event.new_state;
|
||||
if(event.new_state === ConnectionState.CONNECTED) {
|
||||
this.connection_state = event.newState;
|
||||
if(event.newState === ConnectionState.CONNECTED) {
|
||||
log.info(LogCategory.CLIENT, tr("Client connected"));
|
||||
this.log.log(EventType.CONNECTION_CONNECTED, {
|
||||
serverAddress: {
|
||||
|
@ -679,8 +696,8 @@ export class ConnectionHandler {
|
|||
private on_connection_state_changed(old_state: ConnectionState, new_state: ConnectionState) {
|
||||
console.log("From %s to %s", ConnectionState[old_state], ConnectionState[new_state]);
|
||||
this.event_registry.fire("notify_connection_state_changed", {
|
||||
old_state: old_state,
|
||||
new_state: new_state
|
||||
oldState: old_state,
|
||||
newState: new_state
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1016,6 +1033,12 @@ export class ConnectionHandler {
|
|||
}
|
||||
this.localClient = undefined;
|
||||
|
||||
this.privateConversations?.destroy();
|
||||
this.privateConversations = undefined;
|
||||
|
||||
this.channelConversations?.destroy();
|
||||
this.channelConversations = undefined;
|
||||
|
||||
this.channelTree?.destroy();
|
||||
this.channelTree = undefined;
|
||||
|
||||
|
@ -1194,8 +1217,8 @@ export interface ConnectionEvents {
|
|||
}
|
||||
|
||||
notify_connection_state_changed: {
|
||||
old_state: ConnectionState,
|
||||
new_state: ConnectionState
|
||||
oldState: ConnectionState,
|
||||
newState: ConnectionState
|
||||
},
|
||||
|
||||
/* the handler has become visible/invisible for the client */
|
||||
|
|
|
@ -17,7 +17,7 @@ import {formatMessage} from "../ui/frames/chat";
|
|||
import {spawnPoke} from "../ui/modal/ModalPoke";
|
||||
import {AbstractCommandHandler, AbstractCommandHandlerBoss} from "../connection/AbstractCommandHandler";
|
||||
import {batch_updates, BatchUpdateType, flush_batched_updates} from "../ui/react-elements/ReactComponentBase";
|
||||
import {OutOfViewClient} from "../ui/frames/side/PrivateConversationManager";
|
||||
import {OutOfViewClient} from "../ui/frames/side/PrivateConversationController";
|
||||
import {renderBBCodeAsJQuery} from "../text/bbcode";
|
||||
import {tr} from "../i18n/localize";
|
||||
import {EventClient, EventType} from "../ui/frames/log/Definitions";
|
||||
|
@ -74,9 +74,6 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
this["notifychannelsubscribed"] = this.handleNotifyChannelSubscribed;
|
||||
this["notifychannelunsubscribed"] = this.handleNotifyChannelUnsubscribed;
|
||||
|
||||
//this["notifyconversationhistory"] = this.handleNotifyConversationHistory;
|
||||
//this["notifyconversationmessagedelete"] = this.handleNotifyConversationMessageDelete;
|
||||
|
||||
this["notifymusicstatusupdate"] = this.handleNotifyMusicStatusUpdate;
|
||||
this["notifymusicplayersongchange"] = this.handleMusicPlayerSongChange;
|
||||
|
||||
|
@ -413,7 +410,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
|
||||
handleCommandChannelDelete(json) {
|
||||
let tree = this.connection.client.channelTree;
|
||||
const conversations = this.connection.client.side_bar.channel_conversations();
|
||||
const conversations = this.connection.client.getChannelConversations();
|
||||
|
||||
let playSound = false;
|
||||
|
||||
|
@ -448,7 +445,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
|
||||
handleCommandChannelHide(json) {
|
||||
let tree = this.connection.client.channelTree;
|
||||
const conversations = this.connection.client.side_bar.channel_conversations();
|
||||
const conversations = this.connection.client.getChannelConversations();
|
||||
|
||||
log.info(LogCategory.NETWORKING, tr("Got %d channel hides"), json.length);
|
||||
for(let index = 0; index < json.length; index++) {
|
||||
|
@ -556,9 +553,8 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
if(client instanceof LocalClientEntry) {
|
||||
client.initializeListener();
|
||||
this.connection_handler.update_voice_status();
|
||||
this.connection_handler.side_bar.info_frame().update_channel_talk();
|
||||
const conversations = this.connection.client.side_bar.channel_conversations();
|
||||
conversations.setSelectedConversation(client.currentChannel().channelId);
|
||||
const conversations = this.connection.client.getChannelConversations();
|
||||
conversations.setSelectedConversation(conversations.findOrCreateConversation(client.currentChannel().channelId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -586,7 +582,6 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
} else {
|
||||
this.connection.client.handleDisconnect(DisconnectReason.UNKNOWN, entry);
|
||||
}
|
||||
this.connection_handler.side_bar.info_frame().update_channel_talk();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -673,9 +668,6 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
for(const entry of client.channelTree.clientsByChannel(channelFrom)) {
|
||||
entry.getVoiceClient()?.abortReplay();
|
||||
}
|
||||
|
||||
const side_bar = this.connection_handler.side_bar;
|
||||
side_bar.info_frame().update_channel_talk();
|
||||
} else {
|
||||
client.speaking = false;
|
||||
}
|
||||
|
@ -810,8 +802,8 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
uniqueId: targetIsOwn ? json["invokeruid"] : undefined
|
||||
} as OutOfViewClient;
|
||||
|
||||
const conversation_manager = this.connection_handler.side_bar.private_conversations();
|
||||
const conversation = conversation_manager.findOrCreateConversation(chatPartner);
|
||||
const conversationManager = this.connection_handler.getPrivateConversations();
|
||||
const conversation = conversationManager.findOrCreateConversation(chatPartner);
|
||||
|
||||
conversation.handleIncomingMessage(chatPartner, !targetIsOwn, {
|
||||
sender_database_id: targetClientEntry ? targetClientEntry.properties.client_database_id : 0,
|
||||
|
@ -842,7 +834,6 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
}
|
||||
});
|
||||
}
|
||||
this.connection_handler.side_bar.info_frame().update_chat_counter();
|
||||
} else if(mode == 2) {
|
||||
const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"]));
|
||||
const own_channel_id = this.connection.client.getClient().currentChannel().channelId;
|
||||
|
@ -854,7 +845,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
this.connection_handler.sound.play(Sound.MESSAGE_RECEIVED, {default_volume: .5});
|
||||
}
|
||||
|
||||
const conversations = this.connection_handler.side_bar.channel_conversations();
|
||||
const conversations = this.connection_handler.getChannelConversations();
|
||||
conversations.findOrCreateConversation(channel_id).handleIncomingMessage({
|
||||
sender_database_id: invoker ? invoker.properties.client_database_id : 0,
|
||||
sender_name: json["invokername"],
|
||||
|
@ -865,7 +856,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
}, invoker instanceof LocalClientEntry);
|
||||
} else if(mode == 3) {
|
||||
const invoker = this.connection_handler.channelTree.findClient(parseInt(json["invokerid"]));
|
||||
const conversations = this.connection_handler.side_bar.channel_conversations();
|
||||
const conversations = this.connection_handler.getChannelConversations();
|
||||
|
||||
this.connection_handler.log.log(EventType.GLOBAL_MESSAGE, {
|
||||
isOwnMessage: invoker instanceof LocalClientEntry,
|
||||
|
@ -891,7 +882,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
notifyClientChatComposing(json) {
|
||||
json = json[0];
|
||||
|
||||
const conversation_manager = this.connection_handler.side_bar.private_conversations();
|
||||
const conversation_manager = this.connection_handler.getPrivateConversations();
|
||||
const conversation = conversation_manager.findConversation(json["cluid"]);
|
||||
conversation?.handleRemoteComposing(parseInt(json["clid"]));
|
||||
}
|
||||
|
@ -899,8 +890,8 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
handleNotifyClientChatClosed(json) {
|
||||
json = json[0]; //Only one bulk
|
||||
|
||||
const conversation_manager = this.connection_handler.side_bar.private_conversations();
|
||||
const conversation = conversation_manager.findConversation(json["cluid"]);
|
||||
const conversationManager = this.connection_handler.getPrivateConversations();
|
||||
const conversation = conversationManager.findConversation(json["cluid"]);
|
||||
if(!conversation) {
|
||||
log.warn(LogCategory.GENERAL, tr("Received chat close for client, but we haven't a chat open."));
|
||||
return;
|
||||
|
@ -1054,22 +1045,6 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
handleNotifyConversationMessageDelete(json: any[]) {
|
||||
let conversation: Conversation;
|
||||
const conversations = this.connection.client.side_bar.channel_conversations();
|
||||
for(const entry of json) {
|
||||
if(typeof(entry["cid"]) !== "undefined")
|
||||
conversation = conversations.conversation(parseInt(entry["cid"]), false);
|
||||
|
||||
if(!conversation)
|
||||
continue;
|
||||
|
||||
conversation.delete_messages(parseInt(entry["timestamp_begin"]), parseInt(entry["timestamp_end"]), parseInt(entry["cldbid"]), parseInt(entry["limit"]));
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
handleNotifyMusicStatusUpdate(json: any[]) {
|
||||
json = json[0];
|
||||
|
||||
|
|
|
@ -20,10 +20,18 @@ export const CommandOptionDefaults: CommandOptions = {
|
|||
timeout: 10_000
|
||||
};
|
||||
|
||||
export type ConnectionPing = {
|
||||
javascript: number | undefined,
|
||||
native: number
|
||||
};
|
||||
|
||||
export interface ServerConnectionEvents {
|
||||
notify_connection_state_changed: {
|
||||
oldState: ConnectionState,
|
||||
newState: ConnectionState
|
||||
},
|
||||
notify_ping_updated: {
|
||||
newPing: ConnectionPing
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,10 +81,7 @@ export abstract class AbstractServerConnection {
|
|||
|
||||
getConnectionState() { return this.connectionState; }
|
||||
|
||||
abstract ping() : {
|
||||
native: number,
|
||||
javascript?: number
|
||||
};
|
||||
abstract ping() : ConnectionPing;
|
||||
}
|
||||
|
||||
export class ServerCommand {
|
||||
|
|
|
@ -91,8 +91,9 @@ export class PluginCmdRegistry {
|
|||
}
|
||||
|
||||
registerHandler(handler: PluginCmdHandler) {
|
||||
if(this.handlerMap[handler.getChannel()] !== undefined)
|
||||
if(this.handlerMap[handler.getChannel()] !== undefined) {
|
||||
throw tra("A handler for channel {} already exists", handler.getChannel());
|
||||
}
|
||||
|
||||
this.handlerMap[handler.getChannel()] = handler;
|
||||
handler["currentServerConnection"] = this.connection.serverConnection;
|
||||
|
@ -100,8 +101,9 @@ export class PluginCmdRegistry {
|
|||
}
|
||||
|
||||
unregisterHandler(handler: PluginCmdHandler) {
|
||||
if(this.handlerMap[handler.getChannel()] !== handler)
|
||||
if(this.handlerMap[handler.getChannel()] !== handler) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler["currentServerConnection"] = undefined;
|
||||
handler.handleHandlerUnregistered();
|
||||
|
|
|
@ -68,7 +68,7 @@ export class ServerFeatures {
|
|||
});
|
||||
|
||||
this.connection.events().on("notify_connection_state_changed", this.stateChangeListener = event => {
|
||||
if(event.new_state === ConnectionState.CONNECTED) {
|
||||
if(event.newState === ConnectionState.CONNECTED) {
|
||||
this.connection.getServerConnection().send_command("listfeaturesupport").catch(error => {
|
||||
this.disableAllFeatures();
|
||||
if(error instanceof CommandResult) {
|
||||
|
@ -84,7 +84,7 @@ export class ServerFeatures {
|
|||
this.featureAwaitCallback(true);
|
||||
}
|
||||
});
|
||||
} else if(event.new_state === ConnectionState.DISCONNECTING || event.new_state === ConnectionState.UNCONNECTED) {
|
||||
} else if(event.newState === ConnectionState.DISCONNECTING || event.newState === ConnectionState.UNCONNECTED) {
|
||||
this.disableAllFeatures();
|
||||
this.featureAwait = undefined;
|
||||
this.featureAwaitCallback = undefined;
|
||||
|
|
|
@ -0,0 +1,383 @@
|
|||
import {
|
||||
ChatEvent,
|
||||
ChatEventMessage,
|
||||
ChatMessage,
|
||||
ChatState, ConversationHistoryResponse
|
||||
} from "tc-shared/ui/frames/side/ConversationDefinitions";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
||||
import {preprocessChatMessageForSend} from "tc-shared/text/chat";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
import {ChannelConversation} from "tc-shared/conversations/ChannelConversationManager";
|
||||
|
||||
export const kMaxChatFrameMessageSize = 50; /* max 100 messages, since the server does not support more than 100 messages queried at once */
|
||||
|
||||
export interface AbstractChatEvents {
|
||||
notify_chat_event: {
|
||||
triggerUnread: boolean,
|
||||
event: ChatEvent
|
||||
},
|
||||
notify_unread_timestamp_changed: {
|
||||
timestamp: number
|
||||
},
|
||||
notify_unread_state_changed: {
|
||||
unread: boolean
|
||||
},
|
||||
notify_send_toggle: {
|
||||
enabled: boolean
|
||||
},
|
||||
notify_state_changed: {
|
||||
oldSTate: ChatState,
|
||||
newState: ChatState
|
||||
},
|
||||
notify_history_state_changed: {
|
||||
hasHistory: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class AbstractChat<Events extends AbstractChatEvents> {
|
||||
readonly events: Registry<Events>;
|
||||
|
||||
protected readonly connection: ConnectionHandler;
|
||||
protected readonly chatId: string;
|
||||
|
||||
protected presentMessages: ChatEvent[] = [];
|
||||
protected presentEvents: Exclude<ChatEvent, ChatEventMessage>[] = []; /* everything excluding chat messages */
|
||||
|
||||
private mode: ChatState = "unloaded";
|
||||
protected failedPermission: string;
|
||||
protected errorMessage: string;
|
||||
|
||||
protected conversationPrivate: boolean = false;
|
||||
protected crossChannelChatSupported: boolean = true;
|
||||
|
||||
protected unreadTimestamp: number;
|
||||
protected unreadState: boolean = false;
|
||||
|
||||
protected messageSendEnabled: boolean = true;
|
||||
|
||||
private history = false;
|
||||
|
||||
protected constructor(connection: ConnectionHandler, chatId: string) {
|
||||
this.events = new Registry<Events>();
|
||||
this.connection = connection;
|
||||
this.chatId = chatId;
|
||||
this.unreadTimestamp = Date.now();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.events.destroy();
|
||||
}
|
||||
|
||||
public getCurrentMode() : ChatState { return this.mode; };
|
||||
|
||||
protected setCurrentMode(mode: ChatState) {
|
||||
if(this.mode === mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldState = this.mode;
|
||||
this.mode = mode;
|
||||
this.events.fire("notify_state_changed", { oldSTate: oldState, newState: mode });
|
||||
}
|
||||
|
||||
public registerChatEvent(event: ChatEvent, triggerUnread: boolean) {
|
||||
if(event.type === "message") {
|
||||
let index = 0;
|
||||
while(index < this.presentMessages.length && this.presentMessages[index].timestamp <= event.timestamp) {
|
||||
index++;
|
||||
}
|
||||
|
||||
this.presentMessages.splice(index, 0, event);
|
||||
|
||||
const deleteMessageCount = Math.max(0, this.presentMessages.length - kMaxChatFrameMessageSize);
|
||||
this.presentMessages.splice(0, deleteMessageCount);
|
||||
if(deleteMessageCount > 0) {
|
||||
this.setHistory(true);
|
||||
}
|
||||
index -= deleteMessageCount;
|
||||
|
||||
if(event.isOwnMessage) {
|
||||
this.setUnreadTimestamp(Date.now());
|
||||
} else if(!this.isUnread() && triggerUnread) {
|
||||
this.setUnreadTimestamp(event.message.timestamp - 1);
|
||||
} else {
|
||||
/* mark the last message as read */
|
||||
this.setUnreadTimestamp(event.message.timestamp);
|
||||
}
|
||||
|
||||
this.events.fire("notify_chat_event", {
|
||||
triggerUnread: triggerUnread,
|
||||
event: event
|
||||
});
|
||||
} else {
|
||||
this.presentEvents.push(event);
|
||||
this.presentEvents.sort((a, b) => a.timestamp - b.timestamp);
|
||||
/* TODO: Cutoff too old events! */
|
||||
|
||||
if(!this.isUnread() && triggerUnread) {
|
||||
this.setUnreadTimestamp(event.timestamp - 1);
|
||||
} else {
|
||||
/* mark the last message as read */
|
||||
this.setUnreadTimestamp(event.timestamp);
|
||||
}
|
||||
|
||||
this.events.fire("notify_chat_event", {
|
||||
triggerUnread: triggerUnread,
|
||||
event: event
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected registerIncomingMessage(message: ChatMessage, isOwnMessage: boolean, uniqueId: string) {
|
||||
this.registerChatEvent({
|
||||
type: "message",
|
||||
isOwnMessage: isOwnMessage,
|
||||
uniqueId: uniqueId,
|
||||
timestamp: message.timestamp,
|
||||
message: message
|
||||
}, !isOwnMessage);
|
||||
}
|
||||
|
||||
protected doSendMessage(message: string, targetMode: number, target: number) : Promise<boolean> {
|
||||
let msg = preprocessChatMessageForSend(message);
|
||||
return this.connection.serverConnection.send_command("sendtextmessage", {
|
||||
targetmode: targetMode,
|
||||
cid: target,
|
||||
target: target,
|
||||
msg: msg
|
||||
}, { process_result: false }).then(async () => true).catch(error => {
|
||||
if(error instanceof CommandResult) {
|
||||
if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
||||
this.registerChatEvent({
|
||||
type: "message-failed",
|
||||
uniqueId: "msf-" + this.chatId + "-" + Date.now(),
|
||||
timestamp: Date.now(),
|
||||
error: "permission",
|
||||
failedPermission: this.connection.permissions.resolveInfo(parseInt(error.json["failed_permid"]))?.name || tr("unknown")
|
||||
}, false);
|
||||
} else {
|
||||
this.registerChatEvent({
|
||||
type: "message-failed",
|
||||
uniqueId: "msf-" + this.chatId + "-" + Date.now(),
|
||||
timestamp: Date.now(),
|
||||
error: "error",
|
||||
errorMessage: error.formattedMessage()
|
||||
}, false);
|
||||
}
|
||||
} else if(typeof error === "string") {
|
||||
this.registerChatEvent({
|
||||
type: "message-failed",
|
||||
uniqueId: "msf-" + this.chatId + "-" + Date.now(),
|
||||
timestamp: Date.now(),
|
||||
error: "error",
|
||||
errorMessage: error
|
||||
}, false);
|
||||
} else {
|
||||
logWarn(LogCategory.CHAT, tr("Failed to send channel chat message to %s: %o"), this.chatId, error);
|
||||
this.registerChatEvent({
|
||||
type: "message-failed",
|
||||
uniqueId: "msf-" + this.chatId + "-" + Date.now(),
|
||||
timestamp: Date.now(),
|
||||
error: "error",
|
||||
errorMessage: tr("lookup the console")
|
||||
}, false);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public getChatId() : string {
|
||||
return this.chatId;
|
||||
}
|
||||
|
||||
public isUnread() : boolean {
|
||||
return this.unreadState;
|
||||
}
|
||||
|
||||
public isPrivate() : boolean {
|
||||
return this.conversationPrivate;
|
||||
}
|
||||
|
||||
public isSendEnabled() : boolean {
|
||||
return this.messageSendEnabled;
|
||||
}
|
||||
|
||||
public getUnreadTimestamp() : number | undefined {
|
||||
return this.unreadTimestamp;
|
||||
}
|
||||
|
||||
public getPresentMessages() : ChatEvent[] {
|
||||
return this.presentMessages;
|
||||
}
|
||||
|
||||
public getPresentEvents() : ChatEvent[] {
|
||||
return this.presentEvents;
|
||||
}
|
||||
|
||||
public getErrorMessage() : string | undefined {
|
||||
return this.errorMessage;
|
||||
}
|
||||
|
||||
public getFailedPermission() : string | undefined {
|
||||
return this.failedPermission;
|
||||
}
|
||||
|
||||
public setUnreadTimestamp(timestamp: number) {
|
||||
if(this.unreadTimestamp !== timestamp) {
|
||||
this.unreadTimestamp = timestamp;
|
||||
this.events.fire("notify_unread_timestamp_changed", { timestamp: timestamp });
|
||||
}
|
||||
|
||||
/* do update the unread state anyways since setUnreadTimestamp will be called when new messages arrive */
|
||||
this.updateUnreadState();
|
||||
}
|
||||
|
||||
protected updateUnreadState() {
|
||||
const newState = this.unreadTimestamp < this.lastEvent()?.timestamp;
|
||||
if(this.unreadState !== newState) {
|
||||
this.unreadState = newState;
|
||||
this.events.fire("notify_unread_state_changed", { unread: newState });
|
||||
}
|
||||
}
|
||||
|
||||
public hasHistory() : boolean {
|
||||
return this.history;
|
||||
}
|
||||
|
||||
protected setHistory(hasHistory: boolean) {
|
||||
if(this.history === hasHistory) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.history = hasHistory;
|
||||
this.events.fire("notify_history_state_changed", { hasHistory: hasHistory });
|
||||
}
|
||||
|
||||
protected lastEvent() : ChatEvent | undefined {
|
||||
if(this.presentMessages.length === 0) {
|
||||
return this.presentEvents.last();
|
||||
} else if(this.presentEvents.length === 0 || this.presentMessages.last().timestamp > this.presentEvents.last().timestamp) {
|
||||
return this.presentMessages.last();
|
||||
} else {
|
||||
return this.presentEvents.last();
|
||||
}
|
||||
}
|
||||
|
||||
protected sendMessageSendingEnabled(enabled: boolean) {
|
||||
if(this.messageSendEnabled === enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageSendEnabled = enabled;
|
||||
this.events.fire("notify_send_toggle", { enabled: enabled });
|
||||
}
|
||||
|
||||
public abstract canClientAccessChat() : boolean;
|
||||
public abstract queryHistory(criteria: { begin?: number, end?: number, limit?: number }) : Promise<ConversationHistoryResponse>;
|
||||
public abstract queryCurrentMessages();
|
||||
public abstract sendMessage(text: string);
|
||||
}
|
||||
|
||||
export interface AbstractChatManagerEvents<ConversationType> {
|
||||
notify_selected_changed: {
|
||||
oldConversation: ConversationType,
|
||||
newConversation: ConversationType
|
||||
},
|
||||
notify_conversation_destroyed: {
|
||||
conversation: ConversationType
|
||||
},
|
||||
notify_conversation_created: {
|
||||
conversation: ConversationType
|
||||
},
|
||||
notify_unread_count_changed: {
|
||||
unreadConversations: number
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class AbstractChatManager<ManagerEvents extends AbstractChatManagerEvents<ConversationType>, ConversationType extends AbstractChat<ConversationEvents>, ConversationEvents extends AbstractChatEvents> {
|
||||
readonly events: Registry<ManagerEvents>;
|
||||
readonly connection: ConnectionHandler;
|
||||
protected readonly listenerConnection: (() => void)[];
|
||||
private readonly listenerUnreadTimestamp: () => void;
|
||||
|
||||
private conversations: {[key: string]: ConversationType} = {};
|
||||
private selectedConversation: ConversationType;
|
||||
|
||||
private currentUnreadCount: number;
|
||||
|
||||
protected constructor(connection: ConnectionHandler) {
|
||||
this.events = new Registry<ManagerEvents>();
|
||||
this.listenerConnection = [];
|
||||
this.currentUnreadCount = 0;
|
||||
|
||||
this.listenerUnreadTimestamp = () => {
|
||||
let count = this.getConversations().filter(conversation => conversation.isUnread()).length;
|
||||
if(count === this.currentUnreadCount) { return; }
|
||||
|
||||
this.currentUnreadCount = count;
|
||||
this.events.fire("notify_unread_count_changed", { unreadConversations: count });
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.events.destroy();
|
||||
|
||||
this.listenerConnection.forEach(callback => callback());
|
||||
this.listenerConnection.splice(0, this.listenerConnection.length);
|
||||
}
|
||||
|
||||
getConversations() : ConversationType[] {
|
||||
return Object.values(this.conversations);
|
||||
}
|
||||
|
||||
getUnreadCount() : number {
|
||||
return this.currentUnreadCount;
|
||||
}
|
||||
|
||||
getSelectedConversation() : ConversationType {
|
||||
return this.selectedConversation;
|
||||
}
|
||||
|
||||
setSelectedConversation(conversation: ConversationType | undefined) {
|
||||
if(this.selectedConversation === conversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldConversation = this.selectedConversation;
|
||||
this.selectedConversation = conversation;
|
||||
|
||||
this.events.fire("notify_selected_changed", {
|
||||
oldConversation: oldConversation,
|
||||
newConversation: conversation
|
||||
});
|
||||
}
|
||||
|
||||
findConversationById(id: string) : ConversationType {
|
||||
return this.conversations[id];
|
||||
}
|
||||
|
||||
protected registerConversation(conversation: ConversationType) {
|
||||
conversation.events.on("notify_unread_state_changed", this.listenerUnreadTimestamp);
|
||||
this.conversations[conversation.getChatId()] = conversation;
|
||||
this.events.fire("notify_conversation_created", { conversation: conversation });
|
||||
this.listenerUnreadTimestamp();
|
||||
}
|
||||
|
||||
protected unregisterConversation(conversation: ConversationType) {
|
||||
conversation = this.conversations[conversation.getChatId()];
|
||||
if(!conversation) { return; }
|
||||
|
||||
if(conversation === this.selectedConversation) {
|
||||
this.setSelectedConversation(undefined);
|
||||
}
|
||||
|
||||
conversation.events.off("notify_unread_state_changed", this.listenerUnreadTimestamp);
|
||||
delete this.conversations[conversation.getChatId()];
|
||||
this.events.fire("notify_conversation_destroyed", { conversation: conversation });
|
||||
|
||||
this.listenerUnreadTimestamp();
|
||||
}
|
||||
}
|
|
@ -1,44 +1,58 @@
|
|||
import * as React from "react";
|
||||
import {ConnectionHandler, ConnectionState} from "../../../ConnectionHandler";
|
||||
import {EventHandler, Registry} from "../../../events";
|
||||
import * as log from "../../../log";
|
||||
import {LogCategory} from "../../../log";
|
||||
import {CommandResult} from "../../../connection/ServerConnectionDeclaration";
|
||||
import {ServerCommand} from "../../../connection/ConnectionBase";
|
||||
import {Settings} from "../../../settings";
|
||||
import {traj, tr} from "../../../i18n/localize";
|
||||
import {createErrorModal} from "../../../ui/elements/Modal";
|
||||
import ReactDOM = require("react-dom");
|
||||
import {
|
||||
ChatMessage, ConversationHistoryResponse,
|
||||
ConversationUIEvents
|
||||
} from "../../../ui/frames/side/ConversationDefinitions";
|
||||
import {ConversationPanel} from "../../../ui/frames/side/ConversationUI";
|
||||
import {AbstractChat, AbstractChatManager, kMaxChatFrameMessageSize} from "./AbstractConversion";
|
||||
import {ErrorCode} from "../../../connection/ErrorCode";
|
||||
AbstractChat,
|
||||
AbstractChatEvents,
|
||||
AbstractChatManager,
|
||||
AbstractChatManagerEvents,
|
||||
kMaxChatFrameMessageSize
|
||||
} from "./AbstractConversion";
|
||||
import {ChatMessage, ConversationHistoryResponse} from "tc-shared/ui/frames/side/ConversationDefinitions";
|
||||
import {Settings} from "tc-shared/settings";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||
import {traj} from "tc-shared/i18n/localize";
|
||||
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
||||
import {LocalClientEntry} from "tc-shared/tree/Client";
|
||||
import {ServerCommand} from "tc-shared/connection/ConnectionBase";
|
||||
|
||||
export interface ChannelConversationEvents extends AbstractChatEvents {
|
||||
notify_messages_deleted: { messages: string[] },
|
||||
notify_messages_loaded: {}
|
||||
}
|
||||
|
||||
const kSuccessQueryThrottle = 5 * 1000;
|
||||
const kErrorQueryThrottle = 30 * 1000;
|
||||
export class Conversation extends AbstractChat<ConversationUIEvents> {
|
||||
private readonly handle: ConversationManager;
|
||||
export class ChannelConversation extends AbstractChat<ChannelConversationEvents> {
|
||||
private readonly handle: ChannelConversationManager;
|
||||
public readonly conversationId: number;
|
||||
|
||||
private conversationVolatile: boolean = false;
|
||||
private preventUnreadUpdate = false;
|
||||
|
||||
private executingHistoryQueries = false;
|
||||
private pendingHistoryQueries: (() => Promise<any>)[] = [];
|
||||
public historyQueryResponse: ChatMessage[] = [];
|
||||
|
||||
constructor(handle: ConversationManager, events: Registry<ConversationUIEvents>, id: number) {
|
||||
super(handle.connection, id.toString(), events);
|
||||
constructor(handle: ChannelConversationManager, id: number) {
|
||||
super(handle.connection, id.toString());
|
||||
this.handle = handle;
|
||||
this.conversationId = id;
|
||||
|
||||
this.lastReadMessage = handle.connection.settings.server(Settings.FN_CHANNEL_CHAT_READ(id), Date.now());
|
||||
this.preventUnreadUpdate = true;
|
||||
const dateNow = Date.now();
|
||||
const unreadTimestamp = handle.connection.settings.server(Settings.FN_CHANNEL_CHAT_READ(id), Date.now());
|
||||
this.setUnreadTimestamp(unreadTimestamp);
|
||||
this.preventUnreadUpdate = false;
|
||||
|
||||
this.events.on("notify_unread_state_changed", event => {
|
||||
this.handle.connection.channelTree.findChannel(this.conversationId)?.setUnread(event.unread);
|
||||
});
|
||||
}
|
||||
|
||||
destroy() { }
|
||||
destroy() {
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
queryHistory(criteria: { begin?: number, end?: number, limit?: number }) : Promise<ConversationHistoryResponse> {
|
||||
return new Promise<ConversationHistoryResponse>(resolve => {
|
||||
|
@ -108,7 +122,7 @@ export class Conversation extends AbstractChat<ConversationUIEvents> {
|
|||
errorMessage = error.formattedMessage();
|
||||
}
|
||||
} else {
|
||||
log.error(LogCategory.CHAT, tr("Failed to fetch conversation history. %o"), error);
|
||||
logError(LogCategory.CHAT, tr("Failed to fetch conversation history. %o"), error);
|
||||
errorMessage = tr("lookup the console");
|
||||
}
|
||||
resolve({
|
||||
|
@ -125,56 +139,54 @@ export class Conversation extends AbstractChat<ConversationUIEvents> {
|
|||
|
||||
queryCurrentMessages() {
|
||||
this.presentMessages = [];
|
||||
this.mode = "loading";
|
||||
this.setCurrentMode("loading");
|
||||
|
||||
this.reportStateToUI();
|
||||
this.queryHistory({ end: 1, limit: kMaxChatFrameMessageSize }).then(history => {
|
||||
this.conversationPrivate = false;
|
||||
this.conversationVolatile = false;
|
||||
this.failedPermission = undefined;
|
||||
this.errorMessage = undefined;
|
||||
this.hasHistory = !!history.moreEvents;
|
||||
this.setHistory(!!history.moreEvents);
|
||||
this.presentMessages = history.events?.map(e => Object.assign({ uniqueId: "m-" + this.conversationId + "-" + e.timestamp }, e)) || [];
|
||||
|
||||
switch (history.status) {
|
||||
case "error":
|
||||
this.mode = "normal";
|
||||
this.presentEvents.push({
|
||||
this.setCurrentMode("normal");
|
||||
this.registerChatEvent({
|
||||
type: "query-failed",
|
||||
timestamp: Date.now(),
|
||||
uniqueId: "qf-" + this.conversationId + "-" + Date.now() + "-" + Math.random(),
|
||||
message: history.errorMessage
|
||||
});
|
||||
}, false);
|
||||
break;
|
||||
|
||||
case "no-permission":
|
||||
this.mode = "no-permissions";
|
||||
this.setCurrentMode("no-permissions");
|
||||
this.failedPermission = history.failedPermission;
|
||||
break;
|
||||
|
||||
case "private":
|
||||
this.conversationPrivate = true;
|
||||
this.mode = "normal";
|
||||
this.setCurrentMode("normal");
|
||||
break;
|
||||
|
||||
case "success":
|
||||
this.mode = "normal";
|
||||
this.setCurrentMode("normal");
|
||||
break;
|
||||
|
||||
case "unsupported":
|
||||
this.crossChannelChatSupported = false;
|
||||
this.conversationPrivate = true;
|
||||
this.mode = "normal";
|
||||
this.setCurrentMode("normal");
|
||||
break;
|
||||
}
|
||||
|
||||
/* only update the UI if needed */
|
||||
if(this.handle.selectedConversation() === this.conversationId)
|
||||
this.reportStateToUI();
|
||||
this.events.fire("notify_messages_loaded");
|
||||
});
|
||||
}
|
||||
|
||||
protected canClientAccessChat() {
|
||||
/* TODO: Query this state and if changed notify state */
|
||||
public canClientAccessChat() {
|
||||
return this.conversationId === 0 || this.handle.connection.getClient().currentChannel()?.channelId === this.conversationId;
|
||||
}
|
||||
|
||||
|
@ -186,7 +198,7 @@ export class Conversation extends AbstractChat<ConversationUIEvents> {
|
|||
try {
|
||||
const promise = this.pendingHistoryQueries.pop_front()();
|
||||
promise
|
||||
.catch(error => log.error(LogCategory.CLIENT, tr("Conversation history query task threw an error; this should never happen: %o"), error))
|
||||
.catch(error => logError(LogCategory.CLIENT, tr("Conversation history query task threw an error; this should never happen: %o"), error))
|
||||
.then(() => { this.executingHistoryQueries = false; this.executeHistoryQuery(); });
|
||||
} catch (e) {
|
||||
this.executingHistoryQueries = false;
|
||||
|
@ -205,8 +217,12 @@ export class Conversation extends AbstractChat<ConversationUIEvents> {
|
|||
return;
|
||||
}
|
||||
|
||||
if(timestamp > this.lastReadMessage) {
|
||||
this.setUnreadTimestamp(this.lastReadMessage);
|
||||
if(this.unreadTimestamp < timestamp) {
|
||||
this.registerChatEvent({
|
||||
type: "unread-trigger",
|
||||
timestamp: timestamp,
|
||||
uniqueId: "unread-trigger-" + Date.now() + " - " + timestamp
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -217,29 +233,36 @@ export class Conversation extends AbstractChat<ConversationUIEvents> {
|
|||
public handleDeleteMessages(criteria: { begin: number, end: number, cldbid: number, limit: number }) {
|
||||
let limit = { current: criteria.limit };
|
||||
|
||||
this.presentMessages = this.presentMessages.filter(message => {
|
||||
if(message.type !== "message")
|
||||
return;
|
||||
const deletedMessages = this.presentMessages.filter(message => {
|
||||
if(message.type !== "message") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(message.message.sender_database_id !== criteria.cldbid)
|
||||
return true;
|
||||
if(message.message.sender_database_id !== criteria.cldbid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(criteria.end != 0 && message.timestamp > criteria.end)
|
||||
return true;
|
||||
if(criteria.end != 0 && message.timestamp > criteria.end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(criteria.begin != 0 && message.timestamp < criteria.begin)
|
||||
return true;
|
||||
if(criteria.begin != 0 && message.timestamp < criteria.begin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return --limit.current < 0;
|
||||
/* if the limit is zero it means all messages */
|
||||
return --limit.current >= 0;
|
||||
});
|
||||
|
||||
this.events.fire("notify_chat_message_delete", { chatId: this.conversationId.toString(), criteria: criteria });
|
||||
this.presentMessages = this.presentMessages.filter(message => deletedMessages.indexOf(message) === -1);
|
||||
this.events.fire("notify_messages_deleted", { messages: deletedMessages.map(message => message.uniqueId) });
|
||||
this.updateUnreadState();
|
||||
}
|
||||
|
||||
public deleteMessage(messageUniqueId: string) {
|
||||
const message = this.presentMessages.find(e => e.uniqueId === messageUniqueId);
|
||||
if(!message) {
|
||||
log.warn(LogCategory.CHAT, tr("Tried to delete an unknown message (id: %s)"), messageUniqueId);
|
||||
logWarn(LogCategory.CHAT, tr("Tried to delete an unknown message (id: %s)"), messageUniqueId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -253,32 +276,34 @@ export class Conversation extends AbstractChat<ConversationUIEvents> {
|
|||
limit: 1,
|
||||
cldbid: message.message.sender_database_id
|
||||
}, { process_result: false }).catch(error => {
|
||||
log.error(LogCategory.CHAT, tr("Failed to delete conversation message for conversation %d: %o"), this.conversationId, error);
|
||||
if(error instanceof CommandResult)
|
||||
logError(LogCategory.CHAT, tr("Failed to delete conversation message for conversation %d: %o"), this.conversationId, error);
|
||||
if(error instanceof CommandResult) {
|
||||
error = error.extra_message || error.message;
|
||||
}
|
||||
|
||||
createErrorModal(tr("Failed to delete message"), traj("Failed to delete conversation message{:br:}Error: {}", error)).open();
|
||||
});
|
||||
}
|
||||
|
||||
setUnreadTimestamp(timestamp: number | undefined) {
|
||||
setUnreadTimestamp(timestamp: number) {
|
||||
super.setUnreadTimestamp(timestamp);
|
||||
|
||||
/* we've to update the last read timestamp regardless of if we're having actual unread stuff */
|
||||
this.handle.connection.settings.changeServer(Settings.FN_CHANNEL_CHAT_READ(this.conversationId), typeof timestamp === "number" ? timestamp : Date.now());
|
||||
this.handle.connection.channelTree.findChannel(this.conversationId)?.setUnread(timestamp !== undefined);
|
||||
if(this.preventUnreadUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handle.connection.settings.changeServer(Settings.FN_CHANNEL_CHAT_READ(this.conversationId), timestamp);
|
||||
}
|
||||
|
||||
public localClientSwitchedChannel(type: "join" | "leave") {
|
||||
this.presentEvents.push({
|
||||
this.registerChatEvent({
|
||||
type: "local-user-switch",
|
||||
uniqueId: "us-" + this.conversationId + "-" + Date.now() + "-" + Math.random(),
|
||||
timestamp: Date.now(),
|
||||
mode: type
|
||||
});
|
||||
}, false);
|
||||
|
||||
if(this.conversationId === this.handle.selectedConversation())
|
||||
this.reportStateToUI();
|
||||
/* TODO: Update can access state! */
|
||||
}
|
||||
|
||||
sendMessage(text: string) {
|
||||
|
@ -286,138 +311,84 @@ export class Conversation extends AbstractChat<ConversationUIEvents> {
|
|||
}
|
||||
}
|
||||
|
||||
export class ConversationManager extends AbstractChatManager<ConversationUIEvents> {
|
||||
readonly connection: ConnectionHandler;
|
||||
readonly htmlTag: HTMLDivElement;
|
||||
export interface ChannelConversationManagerEvents extends AbstractChatManagerEvents<ChannelConversation> { }
|
||||
|
||||
private conversations: {[key: number]: Conversation} = {};
|
||||
private selectedConversation_: number;
|
||||
export class ChannelConversationManager extends AbstractChatManager<ChannelConversationManagerEvents, ChannelConversation, ChannelConversationEvents> {
|
||||
readonly connection: ConnectionHandler;
|
||||
|
||||
constructor(connection: ConnectionHandler) {
|
||||
super();
|
||||
super(connection);
|
||||
this.connection = connection;
|
||||
|
||||
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,
|
||||
noFirstMessageOverlay: false,
|
||||
messagesDeletable: true
|
||||
}).open().then(() => {
|
||||
console.error("Opened");
|
||||
});
|
||||
*/
|
||||
|
||||
this.uiEvents.on("action_select_chat", event => this.selectedConversation_ = parseInt(event.chatId));
|
||||
this.uiEvents.on("notify_destroy", connection.events().on("notify_connection_state_changed", event => {
|
||||
if(ConnectionState.socketConnected(event.old_state) !== ConnectionState.socketConnected(event.new_state)) {
|
||||
this.conversations = {};
|
||||
this.setSelectedConversation(-1);
|
||||
}
|
||||
}));
|
||||
this.uiEvents.on("notify_destroy", connection.events().on("notify_visibility_changed", event => {
|
||||
if(!event.visible)
|
||||
return;
|
||||
|
||||
this.handlePanelShow();
|
||||
}));
|
||||
|
||||
connection.events().one("notify_handler_initialized", () => this.uiEvents.on("notify_destroy", connection.channelTree.events.on("notify_client_moved", event => {
|
||||
connection.events().one("notify_handler_initialized", () => this.listenerConnection.push(connection.channelTree.events.on("notify_client_moved", event => {
|
||||
if(event.client instanceof LocalClientEntry) {
|
||||
event.oldChannel && this.findOrCreateConversation(event.oldChannel.channelId).localClientSwitchedChannel("leave");
|
||||
this.findOrCreateConversation(event.newChannel.channelId).localClientSwitchedChannel("join");
|
||||
}
|
||||
})));
|
||||
|
||||
this.uiEvents.register_handler(this, true);
|
||||
this.uiEvents.on("notify_destroy", connection.serverConnection.command_handler_boss().register_explicit_handler("notifyconversationhistory", this.handleConversationHistory.bind(this)));
|
||||
this.uiEvents.on("notify_destroy", connection.serverConnection.command_handler_boss().register_explicit_handler("notifyconversationindex", this.handleConversationIndex.bind(this)));
|
||||
this.uiEvents.on("notify_destroy", connection.serverConnection.command_handler_boss().register_explicit_handler("notifyconversationmessagedelete", this.handleConversationMessageDelete.bind(this)));
|
||||
this.listenerConnection.push(connection.events().on("notify_connection_state_changed", event => {
|
||||
if(ConnectionState.socketConnected(event.oldState) !== ConnectionState.socketConnected(event.newState)) {
|
||||
this.setSelectedConversation(undefined);
|
||||
this.getConversations().forEach(conversation => {
|
||||
this.unregisterConversation(conversation);
|
||||
conversation.destroy();
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
this.uiEvents.on("notify_destroy", this.connection.channelTree.events.on("notify_channel_list_received", () => {
|
||||
this.listenerConnection.push(connection.serverConnection.command_handler_boss().register_explicit_handler("notifyconversationhistory", this.handleConversationHistory.bind(this)));
|
||||
this.listenerConnection.push(connection.serverConnection.command_handler_boss().register_explicit_handler("notifyconversationindex", this.handleConversationIndex.bind(this)));
|
||||
this.listenerConnection.push(connection.serverConnection.command_handler_boss().register_explicit_handler("notifyconversationmessagedelete", this.handleConversationMessageDelete.bind(this)));
|
||||
|
||||
this.listenerConnection.push(this.connection.channelTree.events.on("notify_channel_list_received", () => {
|
||||
this.queryUnreadFlags();
|
||||
}));
|
||||
|
||||
this.uiEvents.on("notify_destroy", this.connection.channelTree.events.on("notify_channel_updated", _event => {
|
||||
this.listenerConnection.push(this.connection.channelTree.events.on("notify_channel_updated", () => {
|
||||
/* TODO private flag! */
|
||||
}));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
ReactDOM.unmountComponentAtNode(this.htmlTag);
|
||||
this.htmlTag.remove();
|
||||
|
||||
this.uiEvents.unregister_handler(this);
|
||||
this.uiEvents.fire("notify_destroy");
|
||||
this.uiEvents.destroy();
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
selectedConversation() {
|
||||
return this.selectedConversation_;
|
||||
findConversation(channelId: number) : ChannelConversation {
|
||||
return this.findConversationById(channelId.toString());
|
||||
}
|
||||
|
||||
setSelectedConversation(id: number) {
|
||||
if(id >= 0)
|
||||
this.findOrCreateConversation(id);
|
||||
|
||||
this.uiEvents.fire("notify_selected_chat", { chatId: id.toString() });
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("action_select_chat")
|
||||
private handleActionSelectChat(event: ConversationUIEvents["action_select_chat"]) {
|
||||
this.setSelectedConversation(parseInt(event.chatId));
|
||||
}
|
||||
|
||||
findConversation(id: number) : Conversation {
|
||||
for(const conversation of Object.values(this.conversations))
|
||||
if(conversation.conversationId === id)
|
||||
return conversation;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected findChat(id: string): AbstractChat<ConversationUIEvents> {
|
||||
return this.findConversation(parseInt(id));
|
||||
}
|
||||
|
||||
findOrCreateConversation(id: number) {
|
||||
let conversation = this.findConversation(id);
|
||||
findOrCreateConversation(channelId: number) {
|
||||
let conversation = this.findConversation(channelId);
|
||||
if(!conversation) {
|
||||
conversation = new Conversation(this, this.uiEvents, id);
|
||||
this.conversations[id] = conversation;
|
||||
conversation = new ChannelConversation(this, channelId);
|
||||
this.registerConversation(conversation);
|
||||
}
|
||||
|
||||
return conversation;
|
||||
}
|
||||
|
||||
destroyConversation(id: number) {
|
||||
delete this.conversations[id];
|
||||
const conversation = this.findConversation(id);
|
||||
if(!conversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(id === this.selectedConversation_)
|
||||
this.uiEvents.fire("action_select_chat", { chatId: "unselected" });
|
||||
this.unregisterConversation(conversation);
|
||||
conversation.destroy();
|
||||
}
|
||||
|
||||
queryUnreadFlags() {
|
||||
const commandData = this.connection.channelTree.channels.map(e => { return { cid: e.channelId, cpw: e.cached_password() }});
|
||||
this.connection.serverConnection.send_command("conversationfetch", commandData).catch(error => {
|
||||
log.warn(LogCategory.CHAT, tr("Failed to query conversation indexes: %o"), error);
|
||||
logWarn(LogCategory.CHAT, tr("Failed to query conversation indexes: %o"), error);
|
||||
});
|
||||
}
|
||||
|
||||
private handleConversationHistory(command: ServerCommand) {
|
||||
const conversation = this.findConversation(parseInt(command.arguments[0]["cid"]));
|
||||
if(!conversation) {
|
||||
log.warn(LogCategory.NETWORKING, tr("Received conversation history for an unknown conversation: %o"), command.arguments[0]["cid"]);
|
||||
logWarn(LogCategory.NETWORKING, tr("Received conversation history for an unknown conversation: %o"), command.arguments[0]["cid"]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -444,8 +415,9 @@ export class ConversationManager extends AbstractChatManager<ConversationUIEvent
|
|||
private handleConversationMessageDelete(command: ServerCommand) {
|
||||
const data = command.arguments[0];
|
||||
const conversation = this.findConversation(parseInt(data["cid"]));
|
||||
if(!conversation)
|
||||
if(!conversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
conversation.handleDeleteMessages({
|
||||
limit: parseInt(data["limit"]),
|
||||
|
@ -454,30 +426,4 @@ export class ConversationManager extends AbstractChatManager<ConversationUIEvent
|
|||
cldbid: parseInt(data["cldbid"])
|
||||
})
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("action_delete_message")
|
||||
private handleMessageDelete(event: ConversationUIEvents["action_delete_message"]) {
|
||||
const conversation = this.findConversation(parseInt(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;
|
||||
}
|
||||
|
||||
conversation.deleteMessage(event.uniqueId);
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("query_selected_chat")
|
||||
private handleQuerySelectedChat() {
|
||||
this.uiEvents.fire_react("notify_selected_chat", { chatId: isNaN(this.selectedConversation_) ? "unselected" : this.selectedConversation_ + ""})
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("notify_selected_chat")
|
||||
private handleNotifySelectedChat(event: ConversationUIEvents["notify_selected_chat"]) {
|
||||
this.selectedConversation_ = parseInt(event.chatId);
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("action_clear_unread_flag")
|
||||
protected handleClearUnreadFlag1(event: ConversationUIEvents["action_clear_unread_flag"]) {
|
||||
this.connection.channelTree.findChannel(parseInt(event.chatId))?.setUnread(false);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import * as log from "../../../log";
|
||||
import {LogCategory} from "../../../log";
|
||||
import {ChatEvent} from "../../../ui/frames/side/ConversationDefinitions";
|
||||
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";
|
||||
|
||||
const clientUniqueId2StoreName = uniqueId => "conversation-" + uniqueId;
|
||||
|
||||
|
@ -65,17 +64,18 @@ function fireDatabaseStateChanged() {
|
|||
try {
|
||||
databaseStateChangedCallbacks.pop()();
|
||||
} catch (error) {
|
||||
log.error(LogCategory.CHAT, tr("Database ready callback throw an unexpected exception: %o"), error);
|
||||
logError(LogCategory.CHAT, tr("Database ready callback throw an unexpected exception: %o"), error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cacheImportUniqueKeyId = 0;
|
||||
async function importChatsFromCacheStorage(database: IDBDatabase) {
|
||||
if(!(await caches.has("chat_history")))
|
||||
if(!(await caches.has("chat_history"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(LogCategory.CHAT, tr("Importing old chats saved via cache storage. This may take some moments."));
|
||||
logInfo(LogCategory.CHAT, tr("Importing old chats saved via cache storage. This may take some moments."));
|
||||
|
||||
let chatEvents = {};
|
||||
const cache = await caches.open("chat_history");
|
||||
|
@ -83,7 +83,7 @@ async function importChatsFromCacheStorage(database: IDBDatabase) {
|
|||
for(const chat of await cache.keys()) {
|
||||
try {
|
||||
if(!chat.url.startsWith("https://_local_cache/cache_request_")) {
|
||||
log.warn(LogCategory.CHAT, tr("Skipping importing chat %s because URL does not match."), chat.url);
|
||||
logWarn(LogCategory.CHAT, tr("Skipping importing chat %s because URL does not match."), chat.url);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -91,8 +91,9 @@ async function importChatsFromCacheStorage(database: IDBDatabase) {
|
|||
const events: ChatEvent[] = chatEvents[clientUniqueId] || (chatEvents[clientUniqueId] = []);
|
||||
|
||||
const data = await (await cache.match(chat)).json();
|
||||
if(!Array.isArray(data))
|
||||
if(!Array.isArray(data)) {
|
||||
throw tr("array expected");
|
||||
}
|
||||
|
||||
for(const event of data) {
|
||||
events.push({
|
||||
|
@ -110,18 +111,20 @@ async function importChatsFromCacheStorage(database: IDBDatabase) {
|
|||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn(LogCategory.CHAT, tr("Skipping importing chat %s because of an error: %o"), chat?.url, error);
|
||||
logWarn(LogCategory.CHAT, tr("Skipping importing chat %s because of an error: %o"), chat?.url, error);
|
||||
}
|
||||
}
|
||||
|
||||
const clientUniqueIds = Object.keys(chatEvents);
|
||||
if(clientUniqueIds.length === 0)
|
||||
if(clientUniqueIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(LogCategory.CHAT, tr("Found %d old chats. Importing."), clientUniqueIds.length);
|
||||
logInfo(LogCategory.CHAT, tr("Found %d old chats. Importing."), clientUniqueIds.length);
|
||||
await requestDatabaseUpdate(database => {
|
||||
for(const uniqueId of clientUniqueIds)
|
||||
for(const uniqueId of clientUniqueIds) {
|
||||
doInitializeUser(uniqueId, database);
|
||||
}
|
||||
});
|
||||
await requestDatabase();
|
||||
|
||||
|
@ -134,7 +137,8 @@ async function importChatsFromCacheStorage(database: IDBDatabase) {
|
|||
});
|
||||
await new Promise(resolve => store.transaction.oncomplete = resolve);
|
||||
}
|
||||
log.info(LogCategory.CHAT, tr("All old chats have been imported. Deleting old data."));
|
||||
|
||||
logInfo(LogCategory.CHAT, tr("All old chats have been imported. Deleting old data."));
|
||||
await caches.delete("chat_history");
|
||||
}
|
||||
|
||||
|
@ -153,7 +157,7 @@ async function doOpenDatabase(forceUpgrade: boolean) {
|
|||
if(event.oldVersion === 0) {
|
||||
/* database newly created */
|
||||
importChatsFromCacheStorage(openRequest.result).catch(error => {
|
||||
log.warn(LogCategory.CHAT, tr("Failed to import old chats from cache storage: %o"), error);
|
||||
logWarn(LogCategory.CHAT, tr("Failed to import old chats from cache storage: %o"), error);
|
||||
});
|
||||
}
|
||||
upgradePerformed = true;
|
||||
|
@ -161,7 +165,7 @@ async function doOpenDatabase(forceUpgrade: boolean) {
|
|||
try {
|
||||
databaseUpdateRequests.pop()(openRequest.result);
|
||||
} catch (error) {
|
||||
log.error(LogCategory.CHAT, tr("Database update callback throw an unexpected exception: %o"), error);
|
||||
logError(LogCategory.CHAT, tr("Database update callback throw an unexpected exception: %o"), error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -181,14 +185,14 @@ async function doOpenDatabase(forceUpgrade: boolean) {
|
|||
|
||||
localStorage.setItem("indexeddb-private-conversations-version", database.version.toString());
|
||||
if(!upgradePerformed && forceUpgrade) {
|
||||
log.warn(LogCategory.CHAT, tr("Opened private conversations database, with an update, but update didn't happened. Trying again."));
|
||||
logWarn(LogCategory.CHAT, tr("Opened private conversations database, with an update, but update didn't happened. Trying again."));
|
||||
database.close();
|
||||
await new Promise(resolve => database.onclose = resolve);
|
||||
continue;
|
||||
}
|
||||
|
||||
database.onversionchange = () => {
|
||||
log.debug(LogCategory.CHAT, tr("Received external database version change. Closing database."));
|
||||
logDebug(LogCategory.CHAT, tr("Received external database version change. Closing database."));
|
||||
databaseMode = "closed";
|
||||
executeClose();
|
||||
};
|
||||
|
@ -233,10 +237,10 @@ loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
|||
|
||||
try {
|
||||
await doOpenDatabase(false);
|
||||
log.debug(LogCategory.CHAT, tr("Successfully initialized private conversation history database"));
|
||||
logDebug(LogCategory.CHAT, tr("Successfully initialized private conversation history database"));
|
||||
} catch (error) {
|
||||
log.error(LogCategory.CHAT, tr("Failed to initialize private conversation history database: %o"), error);
|
||||
log.error(LogCategory.CHAT, tr("Do not saving the private conversation chat."));
|
||||
logError(LogCategory.CHAT, tr("Failed to initialize private conversation history database: %o"), error);
|
||||
logError(LogCategory.CHAT, tr("Do not saving the private conversation chat."));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,25 +1,15 @@
|
|||
import {ClientEntry} from "../../../tree/Client";
|
||||
import {ConnectionHandler, ConnectionState} from "../../../ConnectionHandler";
|
||||
import {EventHandler, Registry} from "../../../events";
|
||||
import {
|
||||
PrivateConversationInfo,
|
||||
PrivateConversationUIEvents
|
||||
} from "../../../ui/frames/side/PrivateConversationDefinitions";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import * as React from "react";
|
||||
import {PrivateConversationsPanel} from "../../../ui/frames/side/PrivateConversationUI";
|
||||
import {
|
||||
ChatEvent,
|
||||
ChatMessage,
|
||||
ConversationHistoryResponse,
|
||||
ConversationUIEvents
|
||||
} from "../../../ui/frames/side/ConversationDefinitions";
|
||||
import * as log from "../../../log";
|
||||
import {LogCategory} from "../../../log";
|
||||
import {queryConversationEvents, registerConversationEvent} from "../../../ui/frames/side/PrivateConversationHistory";
|
||||
import {AbstractChat, AbstractChatManager} from "../../../ui/frames/side/AbstractConversion";
|
||||
AbstractChat,
|
||||
AbstractChatEvents,
|
||||
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 {ChannelTreeEvents} from "tc-shared/tree/ChannelTree";
|
||||
import { tr } from "tc-shared/i18n/localize";
|
||||
import {queryConversationEvents, registerConversationEvent} from "tc-shared/conversations/PrivateConversationHistory";
|
||||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
|
||||
|
||||
export type OutOfViewClient = {
|
||||
nickname: string,
|
||||
|
@ -29,16 +19,29 @@ export type OutOfViewClient = {
|
|||
|
||||
let receivingEventUniqueIdIndex = 0;
|
||||
|
||||
export class PrivateConversation extends AbstractChat<PrivateConversationUIEvents> {
|
||||
export interface PrivateConversationEvents extends AbstractChatEvents {
|
||||
notify_partner_typing: {},
|
||||
notify_partner_changed: {
|
||||
chatId: string,
|
||||
clientId: number,
|
||||
name: string
|
||||
},
|
||||
notify_partner_name_changed: {
|
||||
chatId: string,
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export class PrivateConversation extends AbstractChat<PrivateConversationEvents> {
|
||||
public readonly clientUniqueId: string;
|
||||
|
||||
private activeClientListener: (() => void)[] | undefined = undefined;
|
||||
private activeClient: ClientEntry | OutOfViewClient | undefined = undefined;
|
||||
private lastClientInfo: OutOfViewClient = undefined;
|
||||
private lastClientInfo: OutOfViewClient;
|
||||
private conversationOpen: boolean = false;
|
||||
|
||||
constructor(manager: PrivateConversationManager, events: Registry<PrivateConversationUIEvents>, client: ClientEntry | OutOfViewClient) {
|
||||
super(manager.connection, client instanceof ClientEntry ? client.clientUid() : client.uniqueId, events);
|
||||
constructor(manager: PrivateConversationManager, client: ClientEntry | OutOfViewClient) {
|
||||
super(manager.connection, client instanceof ClientEntry ? client.clientUid() : client.uniqueId);
|
||||
|
||||
this.activeClient = client;
|
||||
if(client instanceof ClientEntry) {
|
||||
|
@ -48,11 +51,10 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
|||
this.clientUniqueId = client.uniqueId;
|
||||
}
|
||||
this.updateClientInfo();
|
||||
|
||||
this.events.on("notify_destroy", () => this.unregisterClientEvents());
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
this.unregisterClientEvents();
|
||||
}
|
||||
|
||||
|
@ -62,10 +64,15 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
|||
return this.lastClientInfo.clientId;
|
||||
}
|
||||
|
||||
getLastClientInfo() : OutOfViewClient {
|
||||
return this.lastClientInfo;
|
||||
}
|
||||
|
||||
/* A value of undefined means that the remote client has disconnected */
|
||||
setActiveClientEntry(client: ClientEntry | OutOfViewClient | undefined) {
|
||||
if(this.activeClient === client)
|
||||
if(this.activeClient === client) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.activeClient instanceof ClientEntry) {
|
||||
this.activeClient.setUnread(false); /* clear the unread flag */
|
||||
|
@ -83,8 +90,9 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
|||
|
||||
this.unregisterClientEvents();
|
||||
this.activeClient = client;
|
||||
if(this.activeClient instanceof ClientEntry)
|
||||
if(this.activeClient instanceof ClientEntry) {
|
||||
this.registerClientEvents(this.activeClient);
|
||||
}
|
||||
|
||||
this.updateClientInfo();
|
||||
}
|
||||
|
@ -100,11 +108,13 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
|||
|
||||
this.conversationOpen = true;
|
||||
this.registerIncomingMessage(message, isOwnMessage, "m-" + this.clientUniqueId + "-" + message.timestamp + "-" + (++receivingEventUniqueIdIndex));
|
||||
/* FIXME: notify_unread_count_changed */
|
||||
}
|
||||
|
||||
handleChatRemotelyClosed(clientId: number) {
|
||||
if(clientId !== this.lastClientInfo.clientId)
|
||||
if(clientId !== this.lastClientInfo.clientId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerChatEvent({
|
||||
type: "partner-action",
|
||||
|
@ -133,47 +143,30 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
|||
this.setActiveClientEntry(client);
|
||||
}
|
||||
|
||||
handleRemoteComposing(clientId: number) {
|
||||
this.events.fire("notify_partner_typing", { chatId: this.chatId });
|
||||
}
|
||||
|
||||
generateUIInfo() : PrivateConversationInfo {
|
||||
const lastMessage = this.presentEvents.last();
|
||||
return {
|
||||
nickname: this.lastClientInfo.nickname,
|
||||
uniqueId: this.lastClientInfo.uniqueId,
|
||||
clientId: this.lastClientInfo.clientId,
|
||||
chatId: this.clientUniqueId,
|
||||
|
||||
lastMessage: lastMessage ? lastMessage.timestamp : 0,
|
||||
unreadMessages: this.unreadTimestamp !== undefined
|
||||
};
|
||||
handleRemoteComposing(_clientId: number) {
|
||||
this.events.fire("notify_partner_typing", { });
|
||||
}
|
||||
|
||||
sendMessage(text: string) {
|
||||
if(this.activeClient instanceof ClientEntry)
|
||||
if(this.activeClient instanceof ClientEntry) {
|
||||
this.doSendMessage(text, 1, this.activeClient.clientId()).then(succeeded => succeeded && (this.conversationOpen = true));
|
||||
else if(this.activeClient !== undefined && this.activeClient.clientId > 0)
|
||||
} else if(this.activeClient !== undefined && this.activeClient.clientId > 0) {
|
||||
this.doSendMessage(text, 1, this.activeClient.clientId).then(succeeded => succeeded && (this.conversationOpen = true));
|
||||
else {
|
||||
this.presentEvents.push({
|
||||
} else {
|
||||
this.registerChatEvent({
|
||||
type: "message-failed",
|
||||
uniqueId: "msf-" + this.chatId + "-" + Date.now(),
|
||||
timestamp: Date.now(),
|
||||
error: "error",
|
||||
errorMessage: tr("target client is offline/invisible")
|
||||
});
|
||||
this.events.fire_react("notify_chat_event", {
|
||||
chatId: this.chatId,
|
||||
triggerUnread: false,
|
||||
event: this.presentEvents.last()
|
||||
});
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
|
||||
sendChatClose() {
|
||||
if(!this.conversationOpen)
|
||||
if(!this.conversationOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.conversationOpen = false;
|
||||
if(this.lastClientInfo.clientId > 0 && this.connection.connected) {
|
||||
|
@ -208,14 +201,16 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
|||
private registerClientEvents(client: ClientEntry) {
|
||||
this.activeClientListener = [];
|
||||
this.activeClientListener.push(client.events.on("notify_properties_updated", event => {
|
||||
if('client_nickname' in event.updated_properties)
|
||||
if('client_nickname' in event.updated_properties) {
|
||||
this.updateClientInfo();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private unregisterClientEvents() {
|
||||
if(this.activeClientListener === undefined)
|
||||
if(this.activeClientListener === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeClientListener.forEach(e => e());
|
||||
this.activeClientListener = undefined;
|
||||
|
@ -253,18 +248,16 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
|||
this.sendMessageSendingEnabled(this.lastClientInfo.clientId !== 0);
|
||||
}
|
||||
|
||||
setUnreadTimestamp(timestamp: number | undefined) {
|
||||
setUnreadTimestamp(timestamp: number) {
|
||||
super.setUnreadTimestamp(timestamp);
|
||||
|
||||
/* TODO: Move this somehow to the client itself? */
|
||||
if(this.activeClient instanceof ClientEntry)
|
||||
this.activeClient.setUnread(timestamp !== undefined);
|
||||
|
||||
/* TODO: Eliminate this cross reference? */
|
||||
this.connection.side_bar.info_frame().update_chat_counter();
|
||||
if(this.activeClient instanceof ClientEntry) {
|
||||
this.activeClient.setUnread(this.isUnread());
|
||||
}
|
||||
}
|
||||
|
||||
protected canClientAccessChat(): boolean {
|
||||
public canClientAccessChat(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -282,24 +275,22 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
|||
}
|
||||
|
||||
queryCurrentMessages() {
|
||||
this.mode = "loading";
|
||||
this.reportStateToUI();
|
||||
this.setCurrentMode("loading");
|
||||
|
||||
queryConversationEvents(this.clientUniqueId, { limit: 50, begin: Date.now(), end: 0, direction: "backwards" }).then(result => {
|
||||
this.presentEvents = result.events.filter(e => e.type !== "message") as any;
|
||||
this.presentMessages = result.events.filter(e => e.type === "message");
|
||||
this.hasHistory = result.hasMore;
|
||||
this.mode = "normal";
|
||||
this.setHistory(!!result.hasMore);
|
||||
|
||||
this.reportStateToUI();
|
||||
this.setCurrentMode("normal");
|
||||
});
|
||||
}
|
||||
|
||||
protected registerChatEvent(event: ChatEvent, triggerUnread: boolean) {
|
||||
public registerChatEvent(event: ChatEvent, triggerUnread: boolean) {
|
||||
super.registerChatEvent(event, triggerUnread);
|
||||
|
||||
registerConversationEvent(this.clientUniqueId, event).catch(error => {
|
||||
log.warn(LogCategory.CHAT, tr("Failed to register private conversation chat event for %s: %o"), this.clientUniqueId, error);
|
||||
logWarn(LogCategory.CHAT, tr("Failed to register private conversation chat event for %s: %o"), this.clientUniqueId, error);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -320,164 +311,65 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
|||
}
|
||||
}
|
||||
|
||||
export class PrivateConversationManager extends AbstractChatManager<PrivateConversationUIEvents> {
|
||||
public readonly htmlTag: HTMLDivElement;
|
||||
export interface PrivateConversationManagerEvents extends AbstractChatManagerEvents<PrivateConversation> { }
|
||||
|
||||
export class PrivateConversationManager extends AbstractChatManager<PrivateConversationManagerEvents, PrivateConversation, PrivateConversationEvents> {
|
||||
public readonly connection: ConnectionHandler;
|
||||
|
||||
private activeConversation: PrivateConversation | undefined = undefined;
|
||||
private conversations: PrivateConversation[] = [];
|
||||
|
||||
private channelTreeInitialized = false;
|
||||
|
||||
constructor(connection: ConnectionHandler) {
|
||||
super();
|
||||
super(connection);
|
||||
this.connection = connection;
|
||||
|
||||
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);
|
||||
|
||||
this.uiEvents.on("notify_destroy", connection.events().on("notify_visibility_changed", event => {
|
||||
if(!event.visible)
|
||||
return;
|
||||
|
||||
this.handlePanelShow();
|
||||
}));
|
||||
|
||||
this.uiEvents.on("notify_destroy", connection.events().on("notify_connection_state_changed", event => {
|
||||
if(ConnectionState.socketConnected(event.old_state) !== ConnectionState.socketConnected(event.new_state)) {
|
||||
for(const chat of this.conversations) {
|
||||
chat.handleLocalClientDisconnect(event.old_state === ConnectionState.CONNECTED);
|
||||
}
|
||||
this.listenerConnection.push(connection.events().on("notify_connection_state_changed", event => {
|
||||
if(ConnectionState.socketConnected(event.oldState) !== ConnectionState.socketConnected(event.newState)) {
|
||||
this.getConversations().forEach(conversation => {
|
||||
conversation.handleLocalClientDisconnect(event.oldState === ConnectionState.CONNECTED);
|
||||
});
|
||||
|
||||
this.channelTreeInitialized = false;
|
||||
}
|
||||
}));
|
||||
|
||||
this.uiEvents.on("notify_destroy", connection.channelTree.events.on("notify_client_enter_view", event => {
|
||||
this.listenerConnection.push(connection.channelTree.events.on("notify_client_enter_view", event => {
|
||||
const conversation = this.findConversation(event.client);
|
||||
if(!conversation) return;
|
||||
|
||||
conversation.handleClientEnteredView(event.client, this.channelTreeInitialized ? event.isServerJoin ? "server-join" : "appear" : "local-reconnect");
|
||||
}));
|
||||
|
||||
this.uiEvents.on("notify_destroy", connection.channelTree.events.on("notify_channel_list_received", event => {
|
||||
this.listenerConnection.push(connection.channelTree.events.on("notify_channel_list_received", _event => {
|
||||
this.channelTreeInitialized = true;
|
||||
}));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
ReactDOM.unmountComponentAtNode(this.htmlTag);
|
||||
this.htmlTag.remove();
|
||||
super.destroy();
|
||||
|
||||
this.uiEvents.unregister_handler(this);
|
||||
this.uiEvents.fire("notify_destroy");
|
||||
this.uiEvents.destroy();
|
||||
this.listenerConnection.forEach(callback => callback());
|
||||
this.listenerConnection.splice(0, this.listenerConnection.length);
|
||||
}
|
||||
|
||||
findConversation(client: ClientEntry | string) {
|
||||
const uniqueId = client instanceof ClientEntry ? client.clientUid() : client;
|
||||
return this.conversations.find(e => e.clientUniqueId === uniqueId);
|
||||
}
|
||||
|
||||
protected findChat(id: string): AbstractChat<PrivateConversationUIEvents> {
|
||||
return this.findConversation(id);
|
||||
return this.getConversations().find(e => e.clientUniqueId === uniqueId);
|
||||
}
|
||||
|
||||
findOrCreateConversation(client: ClientEntry | OutOfViewClient) {
|
||||
let conversation = this.findConversation(client instanceof ClientEntry ? client : client.uniqueId);
|
||||
if(!conversation) {
|
||||
this.conversations.push(conversation = new PrivateConversation(this, this.uiEvents, client));
|
||||
this.reportConversationList();
|
||||
conversation = new PrivateConversation(this, client);
|
||||
this.registerConversation(conversation);
|
||||
}
|
||||
|
||||
return conversation;
|
||||
}
|
||||
|
||||
setActiveConversation(conversation: PrivateConversation | undefined) {
|
||||
if(conversation === this.activeConversation)
|
||||
return;
|
||||
|
||||
this.activeConversation = conversation;
|
||||
/* fire this after all other events have been processed, maybe reportConversationList has been called before */
|
||||
this.uiEvents.fire_react("notify_selected_chat", { chatId: this.activeConversation ? this.activeConversation.clientUniqueId : "unselected" });
|
||||
}
|
||||
|
||||
@EventHandler<PrivateConversationUIEvents>("action_select_chat")
|
||||
private handleActionSelectChat(event: PrivateConversationUIEvents["action_select_chat"]) {
|
||||
this.setActiveConversation(this.findConversation(event.chatId));
|
||||
}
|
||||
|
||||
getActiveConversation() {
|
||||
return this.activeConversation;
|
||||
}
|
||||
|
||||
getConversations() {
|
||||
return this.conversations;
|
||||
}
|
||||
|
||||
focusInput() {
|
||||
this.uiEvents.fire("action_focus_chat");
|
||||
}
|
||||
|
||||
closeConversation(...conversations: PrivateConversation[]) {
|
||||
for(const conversation of conversations) {
|
||||
conversation.sendChatClose();
|
||||
this.conversations.remove(conversation);
|
||||
this.unregisterConversation(conversation);
|
||||
conversation.destroy();
|
||||
|
||||
if(this.activeConversation === conversation)
|
||||
this.setActiveConversation(undefined);
|
||||
}
|
||||
this.reportConversationList();
|
||||
}
|
||||
|
||||
private reportConversationList() {
|
||||
this.uiEvents.fire_react("notify_private_conversations", {
|
||||
conversations: this.conversations.map(conversation => conversation.generateUIInfo()),
|
||||
selected: this.activeConversation?.clientUniqueId || "unselected"
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler<PrivateConversationUIEvents>("query_private_conversations")
|
||||
private handleQueryPrivateConversations() {
|
||||
this.reportConversationList();
|
||||
}
|
||||
|
||||
@EventHandler<PrivateConversationUIEvents>("action_close_chat")
|
||||
private handleConversationClose(event: PrivateConversationUIEvents["action_close_chat"]) {
|
||||
const conversation = this.findConversation(event.chatId);
|
||||
if(!conversation) {
|
||||
log.error(LogCategory.CLIENT, tr("Tried to close a not existing private conversation with id %s"), event.chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeConversation(conversation);
|
||||
}
|
||||
|
||||
@EventHandler<PrivateConversationUIEvents>("notify_partner_typing")
|
||||
private handleNotifySelectChat(event: PrivateConversationUIEvents["notify_selected_chat"]) {
|
||||
/* TODO, set active chat? */
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("action_self_typing")
|
||||
protected handleActionSelfTyping1(event: ConversationUIEvents["action_self_typing"]) {
|
||||
if(!this.activeConversation)
|
||||
return;
|
||||
|
||||
const clientId = this.activeConversation.currentClientId();
|
||||
if(!clientId)
|
||||
return;
|
||||
|
||||
this.connection.serverConnection.send_command("clientchatcomposing", { clid: clientId }).catch(error => {
|
||||
log.warn(LogCategory.CHAT, tr("Failed to send chat composing to server for chat %d: %o"), clientId, error);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -99,7 +99,7 @@ class IconManager extends AbstractIconManager {
|
|||
return;
|
||||
}
|
||||
|
||||
if(event.new_state !== ConnectionState.CONNECTED) {
|
||||
if(event.newState !== ConnectionState.CONNECTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -164,7 +164,7 @@ export class GroupManager extends AbstractCommandHandler {
|
|||
this.connectionHandler = client;
|
||||
|
||||
this.connectionStateListener = (event: ConnectionEvents["notify_connection_state_changed"]) => {
|
||||
if(event.new_state === ConnectionState.DISCONNECTING || event.new_state === ConnectionState.UNCONNECTED || event.new_state === ConnectionState.CONNECTING) {
|
||||
if(event.newState === ConnectionState.DISCONNECTING || event.newState === ConnectionState.UNCONNECTED || event.newState === ConnectionState.CONNECTING) {
|
||||
this.reset();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -404,8 +404,9 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
icon_class: "client-channel_switch",
|
||||
name: bold(tr("Join text channel")),
|
||||
callback: () => {
|
||||
this.channelTree.client.side_bar.channel_conversations().setSelectedConversation(this.getChannelId());
|
||||
this.channelTree.client.side_bar.show_channel_conversations();
|
||||
const conversation = this.channelTree.client.getChannelConversations().findOrCreateConversation(this.getChannelId());
|
||||
this.channelTree.client.getChannelConversations().setSelectedConversation(conversation);
|
||||
this.channelTree.client.side_bar.showChannelConversations();
|
||||
},
|
||||
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)
|
||||
}, {
|
||||
|
@ -563,7 +564,6 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
}
|
||||
/* devel-block-end */
|
||||
|
||||
let info_update = false;
|
||||
for(let variable of variables) {
|
||||
let key = variable.key;
|
||||
let value = variable.value;
|
||||
|
@ -571,7 +571,6 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
|
||||
if(key == "channel_name") {
|
||||
this.parsed_channel_name = new ParsedChannelName(value, this.hasParent());
|
||||
info_update = true;
|
||||
} else if(key == "channel_order") {
|
||||
let order = this.channelTree.findChannel(this.properties.channel_order);
|
||||
this.channelTree.moveChannel(this, order, this.parent, true);
|
||||
|
@ -599,13 +598,6 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
properties[property.key] = this.properties[property.key];
|
||||
this.events.fire("notify_properties_updated", { updated_properties: properties as any, channel_properties: this.properties });
|
||||
}
|
||||
|
||||
if(info_update) {
|
||||
const _client = this.channelTree.client.getClient();
|
||||
if(_client.currentChannel() === this)
|
||||
this.channelTree.client.side_bar.info_frame().update_channel_talk();
|
||||
//TODO chat channel!
|
||||
}
|
||||
}
|
||||
|
||||
generate_bbcode() {
|
||||
|
|
|
@ -166,22 +166,23 @@ 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.show_music_player(this.selectedEntry);
|
||||
this.client.side_bar.showMusicPlayer(this.selectedEntry);
|
||||
} else {
|
||||
this.client.side_bar.show_client_info(this.selectedEntry);
|
||||
this.client.side_bar.showClientInfo(this.selectedEntry);
|
||||
}
|
||||
}
|
||||
} else if(this.selectedEntry instanceof ChannelEntry) {
|
||||
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||
this.client.side_bar.channel_conversations().setSelectedConversation(this.selectedEntry.channelId);
|
||||
this.client.side_bar.show_channel_conversations();
|
||||
const conversation = this.client.getChannelConversations().findOrCreateConversation(this.selectedEntry.channelId);
|
||||
this.client.getChannelConversations().setSelectedConversation(conversation);
|
||||
this.client.side_bar.showChannelConversations();
|
||||
}
|
||||
} else if(this.selectedEntry instanceof ServerEntry) {
|
||||
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||
const sidebar = this.client.side_bar;
|
||||
sidebar.channel_conversations().findOrCreateConversation(0);
|
||||
sidebar.channel_conversations().setSelectedConversation(0);
|
||||
sidebar.show_channel_conversations();
|
||||
const conversation = this.client.getChannelConversations().findOrCreateConversation(0);
|
||||
this.client.getChannelConversations().setSelectedConversation(conversation);
|
||||
sidebar.showChannelConversations();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -373,7 +374,6 @@ export class ChannelTree {
|
|||
|
||||
if(oldChannel) {
|
||||
this.events.fire("notify_client_leave_view", { client: client, message: reason.message, reason: reason.reason, isServerLeave: reason.serverLeave, sourceChannel: oldChannel });
|
||||
this.client.side_bar.info_frame().update_channel_client_count(oldChannel);
|
||||
} else {
|
||||
logWarn(LogCategory.CHANNEL, tr("Deleting client %s from channel tree which hasn't a channel."), client.clientId());
|
||||
}
|
||||
|
@ -458,14 +458,6 @@ export class ChannelTree {
|
|||
client["_channel"] = targetChannel;
|
||||
targetChannel?.registerClient(client);
|
||||
|
||||
if(oldChannel) {
|
||||
this.client.side_bar.info_frame().update_channel_client_count(oldChannel);
|
||||
}
|
||||
|
||||
if(targetChannel) {
|
||||
this.client.side_bar.info_frame().update_channel_client_count(targetChannel);
|
||||
}
|
||||
|
||||
this.events.fire("notify_client_moved", { oldChannel: oldChannel, newChannel: targetChannel, client: client });
|
||||
} finally {
|
||||
flush_batched_updates(BatchUpdateType.CHANNEL_TREE);
|
||||
|
|
|
@ -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.show_client_info(this);
|
||||
this.channelTree.client.side_bar.showClientInfo(this);
|
||||
},
|
||||
icon_class: "client-about",
|
||||
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)
|
||||
|
@ -518,12 +518,13 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
}
|
||||
|
||||
open_text_chat() {
|
||||
const chat = this.channelTree.client.side_bar;
|
||||
const conversation = chat.private_conversations().findOrCreateConversation(this);
|
||||
const privateConversations = this.channelTree.client.getPrivateConversations();
|
||||
const sideBar = this.channelTree.client.side_bar;
|
||||
const conversation = privateConversations.findOrCreateConversation(this);
|
||||
conversation.setActiveClientEntry(this);
|
||||
chat.private_conversations().setActiveConversation(conversation);
|
||||
chat.show_private_conversations();
|
||||
chat.private_conversations().focusInput();
|
||||
privateConversations.setSelectedConversation(conversation);
|
||||
sideBar.showPrivateConversations();
|
||||
sideBar.private_conversations().focusInput();
|
||||
}
|
||||
|
||||
showContextMenu(x: number, y: number, on_close: () => void = undefined) {
|
||||
|
|
|
@ -192,8 +192,8 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
|||
icon_class: "client-channel_switch",
|
||||
name: tr("Join server text channel"),
|
||||
callback: () => {
|
||||
this.channelTree.client.side_bar.channel_conversations().setSelectedConversation(0);
|
||||
this.channelTree.client.side_bar.show_channel_conversations();
|
||||
this.channelTree.client.getChannelConversations().setSelectedConversation(0);
|
||||
this.channelTree.client.side_bar.showChannelConversations();
|
||||
},
|
||||
visible: !settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)
|
||||
}, {
|
||||
|
|
|
@ -1,269 +1,10 @@
|
|||
/* the bar on the right with the chats (Channel & Client) */
|
||||
import {ClientEntry, MusicClientEntry} from "../../tree/Client";
|
||||
import {ClientEntry, LocalClientEntry, MusicClientEntry} from "../../tree/Client";
|
||||
import {ConnectionHandler} from "../../ConnectionHandler";
|
||||
import {ChannelEntry} from "../../tree/Channel";
|
||||
import {ServerEntry} from "../../tree/Server";
|
||||
import {openMusicManage} from "../../ui/modal/ModalMusicManage";
|
||||
import {formatMessage} from "../../ui/frames/chat";
|
||||
import {MusicInfo} from "../../ui/frames/side/music_info";
|
||||
import {ConversationManager} from "../../ui/frames/side/ConversationManager";
|
||||
import {PrivateConversationManager} from "../../ui/frames/side/PrivateConversationManager";
|
||||
import {generateIconJQueryTag, getIconManager} from "tc-shared/file/Icons";
|
||||
import { tr } from "tc-shared/i18n/localize";
|
||||
import {ChannelConversationController} from "./side/ChannelConversationController";
|
||||
import {PrivateConversationController} from "./side/PrivateConversationController";
|
||||
import {ClientInfoController} from "tc-shared/ui/frames/side/ClientInfoController";
|
||||
|
||||
export enum InfoFrameMode {
|
||||
NONE = "none",
|
||||
CHANNEL_CHAT = "channel_chat",
|
||||
PRIVATE_CHAT = "private_chat",
|
||||
CLIENT_INFO = "client_info",
|
||||
MUSIC_BOT = "music_bot"
|
||||
}
|
||||
export class InfoFrame {
|
||||
private readonly handle: Frame;
|
||||
private _html_tag: JQuery;
|
||||
private _mode: InfoFrameMode;
|
||||
|
||||
private _value_ping: JQuery;
|
||||
private _ping_updater: number;
|
||||
|
||||
private _channel_text: ChannelEntry;
|
||||
private _channel_voice: ChannelEntry;
|
||||
|
||||
private _button_conversation: HTMLElement;
|
||||
|
||||
private _button_bot_manage: JQuery;
|
||||
private _button_song_add: JQuery;
|
||||
|
||||
constructor(handle: Frame) {
|
||||
this.handle = handle;
|
||||
this._build_html_tag();
|
||||
this.update_channel_talk();
|
||||
this.update_channel_text();
|
||||
this.set_mode(InfoFrameMode.CHANNEL_CHAT);
|
||||
this._ping_updater = setInterval(() => this.update_ping(), 2000);
|
||||
this.update_ping();
|
||||
}
|
||||
|
||||
html_tag() : JQuery { return this._html_tag; }
|
||||
destroy() {
|
||||
clearInterval(this._ping_updater);
|
||||
|
||||
this._html_tag && this._html_tag.remove();
|
||||
this._html_tag = undefined;
|
||||
this._value_ping = undefined;
|
||||
}
|
||||
|
||||
private _build_html_tag() {
|
||||
this._html_tag = $("#tmpl_frame_chat_info").renderTag();
|
||||
this._html_tag.find(".button-switch-chat-channel").on('click', () => this.handle.show_channel_conversations());
|
||||
this._value_ping = this._html_tag.find(".value-ping");
|
||||
this._html_tag.find(".chat-counter").on('click', event => this.handle.show_private_conversations());
|
||||
this._button_conversation = this._html_tag.find(".button.open-conversation").on('click', event => {
|
||||
const selected_client = this.handle.getClientInfo().getClient();
|
||||
if(!selected_client) return;
|
||||
|
||||
const conversation = selected_client ? this.handle.private_conversations().findOrCreateConversation(selected_client) : undefined;
|
||||
if(!conversation) return;
|
||||
|
||||
this.handle.private_conversations().setActiveConversation(conversation);
|
||||
this.handle.show_private_conversations();
|
||||
})[0];
|
||||
|
||||
this._button_bot_manage = this._html_tag.find(".bot-manage").on('click', event => {
|
||||
const bot = this.handle.music_info().current_bot();
|
||||
if(!bot) return;
|
||||
|
||||
openMusicManage(this.handle.handle, bot);
|
||||
});
|
||||
this._button_song_add = this._html_tag.find(".bot-add-song").on('click', event => {
|
||||
this.handle.music_info().events.fire("action_song_add");
|
||||
});
|
||||
}
|
||||
|
||||
update_ping() {
|
||||
this._value_ping.removeClass("very-good good medium poor very-poor");
|
||||
const connection = this.handle.handle.serverConnection;
|
||||
if(!this.handle.handle.connected || !connection) {
|
||||
this._value_ping.text("Not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
const ping = connection.ping();
|
||||
if(!ping || typeof(ping.native) !== "number") {
|
||||
this._value_ping.text("Not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let value;
|
||||
if(typeof(ping.javascript) !== "undefined") {
|
||||
value = ping.javascript;
|
||||
this._value_ping.text(ping.javascript.toFixed(0) + "ms").attr('title', 'Native: ' + ping.native.toFixed(3) + "ms \nJavascript: " + ping.javascript.toFixed(3) + "ms");
|
||||
} else {
|
||||
value = ping.native;
|
||||
this._value_ping.text(ping.native.toFixed(0) + "ms").attr('title', "Ping: " + ping.native.toFixed(3) + "ms");
|
||||
}
|
||||
|
||||
if(value <= 10)
|
||||
this._value_ping.addClass("very-good");
|
||||
else if(value <= 30)
|
||||
this._value_ping.addClass("good");
|
||||
else if(value <= 60)
|
||||
this._value_ping.addClass("medium");
|
||||
else if(value <= 150)
|
||||
this._value_ping.addClass("poor");
|
||||
else
|
||||
this._value_ping.addClass("very-poor");
|
||||
}
|
||||
|
||||
update_channel_talk() {
|
||||
const client = this.handle.handle.getClient();
|
||||
const channel = client ? client.currentChannel() : undefined;
|
||||
this._channel_voice = channel;
|
||||
|
||||
const html_tag = this._html_tag.find(".value-voice-channel");
|
||||
const html_limit_tag = this._html_tag.find(".value-voice-limit");
|
||||
|
||||
html_limit_tag.text("");
|
||||
html_tag.children().remove();
|
||||
|
||||
if(channel) {
|
||||
if(channel.properties.channel_icon_id != 0) {
|
||||
const connection = channel.channelTree.client;
|
||||
generateIconJQueryTag(getIconManager().resolveIcon(channel.properties.channel_icon_id, connection.getCurrentServerUniqueId(), connection.handlerId)).appendTo(html_tag);
|
||||
}
|
||||
$.spawn("div").text(channel.formattedChannelName()).appendTo(html_tag);
|
||||
|
||||
this.update_channel_limit(channel, html_limit_tag);
|
||||
} else {
|
||||
$.spawn("div").text("Not connected").appendTo(html_tag);
|
||||
}
|
||||
}
|
||||
|
||||
update_channel_text() {
|
||||
const channel_tree = this.handle.handle.connected ? this.handle.handle.channelTree : undefined;
|
||||
const current_channel_id = channel_tree ? this.handle.channel_conversations().selectedConversation() : 0;
|
||||
const channel = channel_tree ? channel_tree.findChannel(current_channel_id) : undefined;
|
||||
this._channel_text = channel;
|
||||
|
||||
const tag_container = this._html_tag.find(".mode-channel_chat.channel");
|
||||
const html_tag_title = tag_container.find(".title");
|
||||
const html_tag = tag_container.find(".value-text-channel");
|
||||
const html_limit_tag = tag_container.find(".value-text-limit");
|
||||
|
||||
/* reset */
|
||||
html_tag_title.text(tr("You're chatting in Channel"));
|
||||
html_limit_tag.text("");
|
||||
html_tag.children().detach();
|
||||
|
||||
/* initialize */
|
||||
if(channel) {
|
||||
if(channel.properties.channel_icon_id != 0) {
|
||||
const connection = channel.channelTree.client;
|
||||
generateIconJQueryTag(getIconManager().resolveIcon(channel.properties.channel_icon_id, connection.getCurrentServerUniqueId(), connection.handlerId)).appendTo(html_tag);
|
||||
}
|
||||
$.spawn("div").text(channel.formattedChannelName()).appendTo(html_tag);
|
||||
|
||||
this.update_channel_limit(channel, html_limit_tag);
|
||||
} else if(channel_tree && current_channel_id > 0) {
|
||||
html_tag.append(formatMessage(tr("Unknown channel id {}"), current_channel_id));
|
||||
} else if(channel_tree && current_channel_id == 0) {
|
||||
const server = this.handle.handle.channelTree.server;
|
||||
if(server.properties.virtualserver_icon_id != 0) {
|
||||
const connection = server.channelTree.client;
|
||||
generateIconJQueryTag(getIconManager().resolveIcon(server.properties.virtualserver_icon_id, connection.getCurrentServerUniqueId(), connection.handlerId)).appendTo(html_tag);
|
||||
}
|
||||
$.spawn("div").text(server.properties.virtualserver_name).appendTo(html_tag);
|
||||
html_tag_title.text(tr("You're chatting in Server"));
|
||||
|
||||
this.update_server_limit(server, html_limit_tag);
|
||||
} else if(this.handle.handle.connected) {
|
||||
$.spawn("div").text("No channel selected").appendTo(html_tag);
|
||||
} else {
|
||||
$.spawn("div").text("Not connected").appendTo(html_tag);
|
||||
}
|
||||
}
|
||||
|
||||
update_channel_client_count(channel: ChannelEntry) {
|
||||
if(channel === this._channel_text) {
|
||||
this.update_channel_limit(channel, this._html_tag.find(".value-text-limit"));
|
||||
}
|
||||
|
||||
if(channel === this._channel_voice) {
|
||||
this.update_channel_limit(channel, this._html_tag.find(".value-voice-limit"));
|
||||
}
|
||||
}
|
||||
|
||||
private update_channel_limit(channel: ChannelEntry, tag: JQuery) {
|
||||
let channel_limit = tr("Unlimited");
|
||||
if(!channel.properties.channel_flag_maxclients_unlimited)
|
||||
channel_limit = "" + channel.properties.channel_maxclients;
|
||||
else if(!channel.properties.channel_flag_maxfamilyclients_unlimited) {
|
||||
if(channel.properties.channel_maxfamilyclients >= 0)
|
||||
channel_limit = "" + channel.properties.channel_maxfamilyclients;
|
||||
}
|
||||
tag.text(channel.clients(false).length + " / " + channel_limit);
|
||||
}
|
||||
|
||||
private update_server_limit(server: ServerEntry, tag: JQuery) {
|
||||
const fn = () => {
|
||||
let text = server.properties.virtualserver_clientsonline + " / " + server.properties.virtualserver_maxclients;
|
||||
if(server.properties.virtualserver_reserved_slots)
|
||||
text += " (" + server.properties.virtualserver_reserved_slots + " " + tr("Reserved") + ")";
|
||||
tag.text(text);
|
||||
};
|
||||
|
||||
server.updateProperties().then(fn).catch(error => tag.text(tr("Failed to update info")));
|
||||
fn();
|
||||
}
|
||||
|
||||
update_chat_counter() {
|
||||
const privateConversations = this.handle.private_conversations().getConversations();
|
||||
{
|
||||
const count = privateConversations.filter(e => e.hasUnreadMessages()).length;
|
||||
const count_container = this._html_tag.find(".container-indicator");
|
||||
const count_tag = count_container.find(".chat-unread-counter");
|
||||
count_container.toggle(count > 0);
|
||||
count_tag.text(count);
|
||||
}
|
||||
{
|
||||
const count_tag = this._html_tag.find(".chat-counter");
|
||||
if(privateConversations.length == 0)
|
||||
count_tag.text(tr("No conversations"));
|
||||
else if(privateConversations.length == 1)
|
||||
count_tag.text(tr("One conversation"));
|
||||
else
|
||||
count_tag.text(privateConversations.length + " " + tr("conversations"));
|
||||
}
|
||||
}
|
||||
|
||||
current_mode() : InfoFrameMode {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
set_mode(mode: InfoFrameMode) {
|
||||
for(const mode in InfoFrameMode)
|
||||
this._html_tag.removeClass("mode-" + InfoFrameMode[mode]);
|
||||
this._html_tag.addClass("mode-" + mode);
|
||||
|
||||
if(mode === InfoFrameMode.CLIENT_INFO && this._button_conversation) {
|
||||
//Will be called every time a client is shown
|
||||
const selected_client = this.handle.getClientInfo().getClient();
|
||||
const conversation = selected_client ? this.handle.private_conversations().findConversation(selected_client) : undefined;
|
||||
|
||||
const visibility = (selected_client && selected_client.clientId() !== this.handle.handle.getClientId()) ? "visible" : "hidden";
|
||||
if(this._button_conversation.style.visibility !== visibility)
|
||||
this._button_conversation.style.visibility = visibility;
|
||||
if(conversation) {
|
||||
this._button_conversation.innerText = tr("Open conversation");
|
||||
} else {
|
||||
this._button_conversation.innerText = tr("Start a conversation");
|
||||
}
|
||||
} else if(mode === InfoFrameMode.MUSIC_BOT) {
|
||||
//TODO?
|
||||
}
|
||||
}
|
||||
}
|
||||
import {SideHeader} from "tc-shared/ui/frames/side/HeaderController";
|
||||
|
||||
export enum FrameContent {
|
||||
NONE,
|
||||
|
@ -275,44 +16,43 @@ export enum FrameContent {
|
|||
|
||||
export class Frame {
|
||||
readonly handle: ConnectionHandler;
|
||||
private infoFrame: InfoFrame;
|
||||
private htmlTag: JQuery;
|
||||
private containerInfo: JQuery;
|
||||
private containerChannelChat: JQuery;
|
||||
private _content_type: FrameContent;
|
||||
|
||||
private header: SideHeader;
|
||||
private clientInfo: ClientInfoController;
|
||||
private musicInfo: MusicInfo;
|
||||
private channelConversations: ConversationManager;
|
||||
private privateConversations: PrivateConversationManager;
|
||||
private channelConversations: ChannelConversationController;
|
||||
private privateConversations: PrivateConversationController;
|
||||
|
||||
constructor(handle: ConnectionHandler) {
|
||||
this.handle = handle;
|
||||
|
||||
this._content_type = FrameContent.NONE;
|
||||
this.infoFrame = new InfoFrame(this);
|
||||
this.privateConversations = new PrivateConversationManager(handle);
|
||||
this.channelConversations = new ConversationManager(handle);
|
||||
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._build_html_tag();
|
||||
this.show_channel_conversations();
|
||||
this.info_frame().update_chat_counter();
|
||||
this.handle.events().one("notify_handler_initialized", () => this.header.setConnectionHandler(handle));
|
||||
|
||||
this.createHtmlTag();
|
||||
this.showChannelConversations();
|
||||
}
|
||||
|
||||
html_tag() : JQuery { return this.htmlTag; }
|
||||
info_frame() : InfoFrame { return this.infoFrame; }
|
||||
|
||||
content_type() : FrameContent { return this._content_type; }
|
||||
|
||||
destroy() {
|
||||
this.header?.destroy();
|
||||
this.header = undefined;
|
||||
|
||||
this.htmlTag && this.htmlTag.remove();
|
||||
this.htmlTag = undefined;
|
||||
|
||||
this.infoFrame && this.infoFrame.destroy();
|
||||
this.infoFrame = undefined;
|
||||
|
||||
this.clientInfo?.destroy();
|
||||
this.clientInfo = undefined;
|
||||
|
||||
|
@ -325,30 +65,21 @@ export class Frame {
|
|||
this.channelConversations && this.channelConversations.destroy();
|
||||
this.channelConversations = undefined;
|
||||
|
||||
this.containerInfo && this.containerInfo.remove();
|
||||
this.containerInfo = undefined;
|
||||
|
||||
this.containerChannelChat && this.containerChannelChat.remove();
|
||||
this.containerChannelChat = undefined;
|
||||
}
|
||||
|
||||
private _build_html_tag() {
|
||||
private createHtmlTag() {
|
||||
this.htmlTag = $("#tmpl_frame_chat").renderTag();
|
||||
this.containerInfo = this.htmlTag.find(".container-info");
|
||||
this.htmlTag.find(".container-info").replaceWith(this.header.getHtmlTag());
|
||||
this.containerChannelChat = this.htmlTag.find(".container-chat");
|
||||
|
||||
this.infoFrame.html_tag().appendTo(this.containerInfo);
|
||||
}
|
||||
|
||||
|
||||
private_conversations() : PrivateConversationManager {
|
||||
private_conversations() : PrivateConversationController {
|
||||
return this.privateConversations;
|
||||
}
|
||||
|
||||
channel_conversations() : ConversationManager {
|
||||
return this.channelConversations;
|
||||
}
|
||||
|
||||
getClientInfo() : ClientInfoController {
|
||||
return this.clientInfo;
|
||||
}
|
||||
|
@ -357,71 +88,73 @@ export class Frame {
|
|||
return this.musicInfo;
|
||||
}
|
||||
|
||||
private _clear() {
|
||||
private clearSideBar() {
|
||||
this._content_type = FrameContent.NONE;
|
||||
this.containerChannelChat.children().detach();
|
||||
}
|
||||
|
||||
show_private_conversations() {
|
||||
showPrivateConversations() {
|
||||
if(this._content_type === FrameContent.PRIVATE_CHAT)
|
||||
return;
|
||||
|
||||
this._clear();
|
||||
this.header.setState({ state: "conversation", mode: "private" });
|
||||
|
||||
this.clearSideBar();
|
||||
this._content_type = FrameContent.PRIVATE_CHAT;
|
||||
this.containerChannelChat.append(this.privateConversations.htmlTag);
|
||||
this.privateConversations.handlePanelShow();
|
||||
this.infoFrame.set_mode(InfoFrameMode.PRIVATE_CHAT);
|
||||
}
|
||||
|
||||
show_channel_conversations() {
|
||||
showChannelConversations() {
|
||||
if(this._content_type === FrameContent.CHANNEL_CHAT)
|
||||
return;
|
||||
|
||||
this._clear();
|
||||
this.header.setState({ state: "conversation", mode: "channel" });
|
||||
|
||||
this.clearSideBar();
|
||||
this._content_type = FrameContent.CHANNEL_CHAT;
|
||||
this.containerChannelChat.append(this.channelConversations.htmlTag);
|
||||
this.channelConversations.handlePanelShow();
|
||||
|
||||
this.infoFrame.set_mode(InfoFrameMode.CHANNEL_CHAT);
|
||||
}
|
||||
|
||||
show_client_info(client: ClientEntry) {
|
||||
showClientInfo(client: ClientEntry) {
|
||||
this.clientInfo.setClient(client);
|
||||
this.infoFrame.set_mode(InfoFrameMode.CLIENT_INFO); /* specially needs an update here to update the conversation button */
|
||||
this.header.setState({ state: "client", ownClient: client instanceof LocalClientEntry });
|
||||
|
||||
if(this._content_type === FrameContent.CLIENT_INFO)
|
||||
return;
|
||||
|
||||
this._clear();
|
||||
this.clearSideBar();
|
||||
this._content_type = FrameContent.CLIENT_INFO;
|
||||
this.containerChannelChat.append(this.clientInfo.getHtmlTag());
|
||||
}
|
||||
|
||||
show_music_player(client: MusicClientEntry) {
|
||||
showMusicPlayer(client: MusicClientEntry) {
|
||||
this.musicInfo.set_current_bot(client);
|
||||
|
||||
if(this._content_type === FrameContent.MUSIC_BOT)
|
||||
return;
|
||||
|
||||
this.infoFrame.set_mode(InfoFrameMode.MUSIC_BOT);
|
||||
this.header.setState({ state: "music-bot" });
|
||||
this.musicInfo.previous_frame_content = this._content_type;
|
||||
this._clear();
|
||||
this.clearSideBar();
|
||||
this._content_type = FrameContent.MUSIC_BOT;
|
||||
this.containerChannelChat.append(this.musicInfo.html_tag());
|
||||
}
|
||||
|
||||
set_content(type: FrameContent) {
|
||||
if(this._content_type === type)
|
||||
if(this._content_type === type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(type === FrameContent.CHANNEL_CHAT)
|
||||
this.show_channel_conversations();
|
||||
else if(type === FrameContent.PRIVATE_CHAT)
|
||||
this.show_private_conversations();
|
||||
else {
|
||||
this._clear();
|
||||
if(type === FrameContent.CHANNEL_CHAT) {
|
||||
this.showChannelConversations();
|
||||
} else if(type === FrameContent.PRIVATE_CHAT) {
|
||||
this.showPrivateConversations();
|
||||
} else {
|
||||
this.header.setState({ state: "none" });
|
||||
this.clearSideBar();
|
||||
this._content_type = FrameContent.NONE;
|
||||
this.infoFrame.set_mode(InfoFrameMode.NONE);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -98,7 +98,7 @@ class InfoController {
|
|||
const events = this.handlerRegisteredEvents;
|
||||
|
||||
events.push(handler.events().on("notify_connection_state_changed", event => {
|
||||
if(event.old_state === ConnectionState.CONNECTED || event.new_state === ConnectionState.CONNECTED) {
|
||||
if(event.oldState === ConnectionState.CONNECTED || event.newState === ConnectionState.CONNECTED) {
|
||||
this.sendHostButton();
|
||||
this.sendVideoState("screen");
|
||||
this.sendVideoState("camera");
|
||||
|
|
|
@ -0,0 +1,311 @@
|
|||
import {
|
||||
ChatHistoryState,
|
||||
ConversationUIEvents
|
||||
} from "../../../ui/frames/side/ConversationDefinitions";
|
||||
import {EventHandler, Registry} from "../../../events";
|
||||
import * as log from "../../../log";
|
||||
import {LogCategory} from "../../../log";
|
||||
import {tra, tr} from "../../../i18n/localize";
|
||||
import {
|
||||
AbstractChat,
|
||||
AbstractChatEvents,
|
||||
AbstractChatManager,
|
||||
AbstractChatManagerEvents
|
||||
} from "tc-shared/conversations/AbstractConversion";
|
||||
|
||||
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,
|
||||
Manager extends AbstractChatManager<ManagerEvents, ConversationType, ConversationEvents>,
|
||||
ManagerEvents extends AbstractChatManagerEvents<ConversationType>,
|
||||
ConversationType extends AbstractChat<ConversationEvents>,
|
||||
ConversationEvents extends AbstractChatEvents
|
||||
> {
|
||||
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 currentSelectedConversation: ConversationType;
|
||||
protected currentSelectedListener: (() => void)[];
|
||||
|
||||
protected constructor(conversationManager: Manager) {
|
||||
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() {
|
||||
this.listenerManager.forEach(callback => callback());
|
||||
this.listenerManager.splice(0, this.listenerManager.length);
|
||||
|
||||
this.uiEvents.fire("notify_destroy");
|
||||
this.uiEvents.destroy();
|
||||
}
|
||||
|
||||
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 })));
|
||||
|
||||
this.currentSelectedListener.push(conversation.events.on("notify_send_toggle", event =>
|
||||
this.uiEvents.fire_react("notify_send_enabled", { chatId: conversation.getChatId(), enabled: event.enabled })));
|
||||
|
||||
this.currentSelectedListener.push(conversation.events.on("notify_chat_event", event => {
|
||||
this.uiEvents.fire_react("notify_chat_event", { chatId: conversation.getChatId(), event: event.event, triggerUnread: event.triggerUnread });
|
||||
}));
|
||||
|
||||
this.currentSelectedListener.push(conversation.events.on("notify_state_changed", () => {
|
||||
this.reportStateToUI(conversation);
|
||||
}));
|
||||
|
||||
this.currentSelectedListener.push(conversation.events.on("notify_history_state_changed", () => {
|
||||
this.reportStateToUI(conversation);
|
||||
}));
|
||||
}
|
||||
|
||||
handlePanelShow() {
|
||||
this.uiEvents.fire_react("notify_panel_show");
|
||||
}
|
||||
|
||||
protected reportStateToUI(conversation: AbstractChat<any>) {
|
||||
const crossChannelChatSupported = true; /* FIXME: Determine this form the server! */
|
||||
|
||||
let historyState: ChatHistoryState;
|
||||
const localHistoryState = this.historyUiStates[conversation.getChatId()];
|
||||
if(!localHistoryState) {
|
||||
historyState = conversation.hasHistory() ? "available" : "none";
|
||||
} else {
|
||||
if(Date.now() < localHistoryState.historyRetryTimestamp && localHistoryState.historyErrorMessage) {
|
||||
historyState = "error";
|
||||
} else if(localHistoryState.executingUIHistoryQuery) {
|
||||
historyState = "loading";
|
||||
} else if(conversation.hasHistory()) {
|
||||
historyState = "available";
|
||||
} else {
|
||||
historyState = "none";
|
||||
}
|
||||
}
|
||||
|
||||
switch (conversation.getCurrentMode()) {
|
||||
case "normal":
|
||||
if(conversation.isPrivate() && !conversation.canClientAccessChat()) {
|
||||
this.uiEvents.fire_react("notify_conversation_state", {
|
||||
chatId: conversation.getChatId(),
|
||||
state: "private",
|
||||
crossChannelChatSupported: crossChannelChatSupported
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiEvents.fire_react("notify_conversation_state", {
|
||||
chatId: conversation.getChatId(),
|
||||
state: "normal",
|
||||
|
||||
historyState: historyState,
|
||||
historyErrorMessage: localHistoryState?.historyErrorMessage,
|
||||
historyRetryTimestamp: localHistoryState ? localHistoryState.historyRetryTimestamp : 0,
|
||||
|
||||
chatFrameMaxMessageCount: kMaxChatFrameMessageSize,
|
||||
unreadTimestamp: conversation.getUnreadTimestamp(),
|
||||
|
||||
showUserSwitchEvents: conversation.isPrivate() || !crossChannelChatSupported,
|
||||
sendEnabled: conversation.isSendEnabled(),
|
||||
|
||||
events: [...conversation.getPresentEvents(), ...conversation.getPresentMessages()]
|
||||
});
|
||||
break;
|
||||
|
||||
case "loading":
|
||||
case "unloaded":
|
||||
this.uiEvents.fire_react("notify_conversation_state", {
|
||||
chatId: conversation.getChatId(),
|
||||
state: "loading"
|
||||
});
|
||||
break;
|
||||
|
||||
case "error":
|
||||
this.uiEvents.fire_react("notify_conversation_state", {
|
||||
chatId: conversation.getChatId(),
|
||||
state: "error",
|
||||
errorMessage: conversation.getErrorMessage()
|
||||
});
|
||||
break;
|
||||
|
||||
case "no-permissions":
|
||||
this.uiEvents.fire_react("notify_conversation_state", {
|
||||
chatId: conversation.getChatId(),
|
||||
state: "no-permissions",
|
||||
failedPermission: conversation.getFailedPermission()
|
||||
});
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
public uiQueryHistory(conversation: AbstractChat<any>, timestamp: number, enforce?: boolean) {
|
||||
const localHistoryState = this.historyUiStates[conversation.getChatId()] || (this.historyUiStates[conversation.getChatId()] = {
|
||||
executingUIHistoryQuery: false,
|
||||
historyErrorMessage: undefined,
|
||||
historyRetryTimestamp: 0
|
||||
});
|
||||
|
||||
if(localHistoryState.executingUIHistoryQuery && !enforce) {
|
||||
return;
|
||||
}
|
||||
|
||||
localHistoryState.executingUIHistoryQuery = true;
|
||||
conversation.queryHistory({ end: 1, begin: timestamp, limit: kMaxChatFrameMessageSize }).then(result => {
|
||||
localHistoryState.executingUIHistoryQuery = false;
|
||||
localHistoryState.historyErrorMessage = undefined;
|
||||
localHistoryState.historyRetryTimestamp = result.nextAllowedQuery;
|
||||
|
||||
switch (result.status) {
|
||||
case "success":
|
||||
this.uiEvents.fire_react("notify_conversation_history", {
|
||||
chatId: conversation.getChatId(),
|
||||
state: "success",
|
||||
|
||||
hasMoreMessages: result.moreEvents,
|
||||
retryTimestamp: localHistoryState.historyRetryTimestamp,
|
||||
|
||||
events: result.events
|
||||
});
|
||||
break;
|
||||
|
||||
case "private":
|
||||
this.uiEvents.fire_react("notify_conversation_history", {
|
||||
chatId: conversation.getChatId(),
|
||||
state: "error",
|
||||
errorMessage: localHistoryState.historyErrorMessage = tr("chat is private"),
|
||||
retryTimestamp: localHistoryState.historyRetryTimestamp
|
||||
});
|
||||
break;
|
||||
|
||||
case "no-permission":
|
||||
this.uiEvents.fire_react("notify_conversation_history", {
|
||||
chatId: conversation.getChatId(),
|
||||
state: "error",
|
||||
errorMessage: localHistoryState.historyErrorMessage = tra("failed on {}", result.failedPermission || tr("unknown permission")),
|
||||
retryTimestamp: localHistoryState.historyRetryTimestamp
|
||||
});
|
||||
break;
|
||||
|
||||
case "error":
|
||||
this.uiEvents.fire_react("notify_conversation_history", {
|
||||
chatId: conversation.getChatId(),
|
||||
state: "error",
|
||||
errorMessage: localHistoryState.historyErrorMessage = result.errorMessage,
|
||||
retryTimestamp: localHistoryState.historyRetryTimestamp
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected getCurrentConversation() : ConversationType | undefined {
|
||||
return this.currentSelectedConversation;
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("query_conversation_state")
|
||||
protected handleQueryConversationState(event: ConversationUIEvents["query_conversation_state"]) {
|
||||
const conversation = this.conversationManager.findConversationById(event.chatId);
|
||||
if(!conversation) {
|
||||
this.uiEvents.fire_react("notify_conversation_state", {
|
||||
state: "error",
|
||||
errorMessage: tr("Unknown conversation"),
|
||||
|
||||
chatId: event.chatId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(conversation.getCurrentMode() === "unloaded") {
|
||||
/* will switch the state to "loading" and already reports the state to the ui */
|
||||
conversation.queryCurrentMessages();
|
||||
} else {
|
||||
this.reportStateToUI(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("query_conversation_history")
|
||||
protected handleQueryHistory(event: ConversationUIEvents["query_conversation_history"]) {
|
||||
const conversation = this.conversationManager.findConversationById(event.chatId);
|
||||
if(!conversation) {
|
||||
this.uiEvents.fire_react("notify_conversation_history", {
|
||||
state: "error",
|
||||
errorMessage: tr("Unknown conversation"),
|
||||
retryTimestamp: Date.now() + 10 * 1000,
|
||||
|
||||
chatId: event.chatId
|
||||
});
|
||||
|
||||
log.error(LogCategory.CLIENT, tr("Tried to query history for an unknown conversation with id %s"), event.chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
conversation?.setUnreadTimestamp(Date.now());
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("action_send_message")
|
||||
protected handleSendMessage(event: ConversationUIEvents["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;
|
||||
}
|
||||
|
||||
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);
|
||||
if(!conversation) {
|
||||
log.error(LogCategory.CLIENT, tr("Tried to jump to present for an unknown conversation with id %s"), event.chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.reportStateToUI(conversation);
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("query_selected_chat")
|
||||
private handleQuerySelectedChat() {
|
||||
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);
|
||||
this.conversationManager.setSelectedConversation(conversation);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import * as React from "react";
|
||||
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
|
||||
import {ChatBox} from "tc-shared/ui/frames/side/ChatBox";
|
||||
import {Ref, useEffect, useRef, useState} from "react";
|
||||
import {AvatarRenderer} from "tc-shared/ui/react-elements/Avatar";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
|
@ -23,8 +22,9 @@ import {BBCodeRenderer} from "tc-shared/text/bbcode";
|
|||
import {getGlobalAvatarManagerFactory} from "tc-shared/file/Avatars";
|
||||
import {ColloquialFormat, date_format, format_date_general, formatDayTime} from "tc-shared/utils/DateUtils";
|
||||
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
|
||||
import {ChatBox} from "tc-shared/ui/react-elements/ChatBox";
|
||||
|
||||
const cssStyle = require("./ConversationUI.scss");
|
||||
const cssStyle = require("./AbstractConversationRenderer.scss");
|
||||
|
||||
const ChatMessageTextRenderer = React.memo((props: { text: string }) => {
|
||||
if(typeof props.text !== "string") { debugger; }
|
||||
|
@ -664,8 +664,9 @@ class ConversationMessages extends React.PureComponent<ConversationMessagesPrope
|
|||
|
||||
@EventHandler<ConversationUIEvents>("notify_selected_chat")
|
||||
private handleNotifySelectedChat(event: ConversationUIEvents["notify_selected_chat"]) {
|
||||
if(this.currentChatId === event.chatId)
|
||||
if(this.currentChatId === event.chatId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentChatId = event.chatId;
|
||||
this.chatEvents = [];
|
||||
|
@ -758,40 +759,33 @@ class ConversationMessages extends React.PureComponent<ConversationMessagesPrope
|
|||
|
||||
@EventHandler<ConversationUIEvents>("notify_chat_message_delete")
|
||||
private handleMessageDeleted(event: ConversationUIEvents["notify_chat_message_delete"]) {
|
||||
if(event.chatId !== this.currentChatId)
|
||||
if(event.chatId !== this.currentChatId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let limit = { current: event.criteria.limit };
|
||||
this.chatEvents = this.chatEvents.filter(mEvent => {
|
||||
if(mEvent.type !== "message")
|
||||
return;
|
||||
|
||||
const message = mEvent.message;
|
||||
if(message.sender_database_id !== event.criteria.cldbid)
|
||||
return true;
|
||||
|
||||
if(event.criteria.end != 0 && message.timestamp > event.criteria.end)
|
||||
return true;
|
||||
|
||||
if(event.criteria.begin != 0 && message.timestamp < event.criteria.begin)
|
||||
return true;
|
||||
|
||||
return --limit.current < 0;
|
||||
});
|
||||
this.chatEvents = this.chatEvents.filter(mEvent => event.messageIds.indexOf(mEvent.uniqueId) === -1);
|
||||
|
||||
this.buildView();
|
||||
this.forceUpdate(() => this.scrollToBottom());
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("action_clear_unread_flag")
|
||||
private handleMessageUnread(event: ConversationUIEvents["action_clear_unread_flag"]) {
|
||||
if (event.chatId !== this.currentChatId || this.unreadTimestamp === undefined)
|
||||
@EventHandler<ConversationUIEvents>("notify_unread_timestamp_changed")
|
||||
private handleUnreadTimestampChanged(event: ConversationUIEvents["notify_unread_timestamp_changed"]) {
|
||||
if (event.chatId !== this.currentChatId)
|
||||
return;
|
||||
|
||||
this.unreadTimestamp = undefined;
|
||||
this.buildView();
|
||||
this.forceUpdate();
|
||||
};
|
||||
const oldUnreadTimestamp = this.unreadTimestamp;
|
||||
if(this.chatEvents.last()?.timestamp > event.timestamp) {
|
||||
this.unreadTimestamp = event.timestamp;
|
||||
} else {
|
||||
this.unreadTimestamp = undefined;
|
||||
}
|
||||
|
||||
if(oldUnreadTimestamp !== this.unreadTimestamp) {
|
||||
this.buildView();
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("notify_panel_show")
|
||||
private handlePanelShow() {
|
|
@ -1,395 +0,0 @@
|
|||
import {
|
||||
ChatEvent,
|
||||
ChatEventMessage, ChatHistoryState, ChatMessage,
|
||||
ChatState, ConversationHistoryResponse,
|
||||
ConversationUIEvents
|
||||
} from "../../../ui/frames/side/ConversationDefinitions";
|
||||
import {ConnectionHandler} from "../../../ConnectionHandler";
|
||||
import {EventHandler, Registry} from "../../../events";
|
||||
import {preprocessChatMessageForSend} from "../../../text/chat";
|
||||
import {CommandResult} from "../../../connection/ServerConnectionDeclaration";
|
||||
import * as log from "../../../log";
|
||||
import {LogCategory} from "../../../log";
|
||||
import {tra, tr} from "../../../i18n/localize";
|
||||
import {ErrorCode} from "../../../connection/ErrorCode";
|
||||
|
||||
export const kMaxChatFrameMessageSize = 50; /* max 100 messages, since the server does not support more than 100 messages queried at once */
|
||||
|
||||
export abstract class AbstractChat<Events extends ConversationUIEvents> {
|
||||
protected readonly connection: ConnectionHandler;
|
||||
protected readonly chatId: string;
|
||||
protected readonly events: Registry<Events>;
|
||||
protected presentMessages: ChatEvent[] = [];
|
||||
protected presentEvents: Exclude<ChatEvent, ChatEventMessage>[] = []; /* everything excluding chat messages */
|
||||
|
||||
protected mode: ChatState = "unloaded";
|
||||
protected failedPermission: string;
|
||||
protected errorMessage: string;
|
||||
|
||||
protected conversationPrivate: boolean = false;
|
||||
protected crossChannelChatSupported: boolean = true;
|
||||
|
||||
protected unreadTimestamp: number | undefined = undefined;
|
||||
protected lastReadMessage: number = 0;
|
||||
|
||||
protected historyErrorMessage: string;
|
||||
protected historyRetryTimestamp: number = 0;
|
||||
protected executingUIHistoryQuery = false;
|
||||
|
||||
protected messageSendEnabled: boolean = true;
|
||||
|
||||
protected hasHistory = false;
|
||||
|
||||
protected constructor(connection: ConnectionHandler, chatId: string, events: Registry<Events>) {
|
||||
this.connection = connection;
|
||||
this.events = events;
|
||||
this.chatId = chatId;
|
||||
}
|
||||
|
||||
public currentMode() : ChatState { return this.mode; };
|
||||
|
||||
protected registerChatEvent(event: ChatEvent, triggerUnread: boolean) {
|
||||
if(event.type === "message") {
|
||||
let index = 0;
|
||||
while(index < this.presentMessages.length && this.presentMessages[index].timestamp <= event.timestamp)
|
||||
index++;
|
||||
|
||||
this.presentMessages.splice(index, 0, event);
|
||||
|
||||
const deleteMessageCount = Math.max(0, this.presentMessages.length - kMaxChatFrameMessageSize);
|
||||
this.presentMessages.splice(0, deleteMessageCount);
|
||||
if(deleteMessageCount > 0)
|
||||
this.hasHistory = true;
|
||||
index -= deleteMessageCount;
|
||||
|
||||
if(event.isOwnMessage) {
|
||||
this.setUnreadTimestamp(undefined);
|
||||
} else if(!this.unreadTimestamp) {
|
||||
this.setUnreadTimestamp(event.message.timestamp);
|
||||
}
|
||||
|
||||
/* let all other events run before */
|
||||
this.events.fire_react("notify_chat_event", {
|
||||
chatId: this.chatId,
|
||||
triggerUnread: triggerUnread,
|
||||
event: event
|
||||
});
|
||||
} else {
|
||||
this.presentEvents.push(event);
|
||||
this.presentEvents.sort((a, b) => a.timestamp - b.timestamp);
|
||||
/* TODO: Cutoff too old events! */
|
||||
|
||||
this.events.fire("notify_chat_event", {
|
||||
chatId: this.chatId,
|
||||
triggerUnread: triggerUnread,
|
||||
event: event
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected registerIncomingMessage(message: ChatMessage, isOwnMessage: boolean, uniqueId: string) {
|
||||
this.registerChatEvent({
|
||||
type: "message",
|
||||
isOwnMessage: isOwnMessage,
|
||||
uniqueId: uniqueId,
|
||||
timestamp: message.timestamp,
|
||||
message: message
|
||||
}, !isOwnMessage);
|
||||
}
|
||||
|
||||
public reportStateToUI() {
|
||||
let historyState: ChatHistoryState;
|
||||
if(Date.now() < this.historyRetryTimestamp && this.historyErrorMessage) {
|
||||
historyState = "error";
|
||||
} else if(this.executingUIHistoryQuery) {
|
||||
historyState = "loading";
|
||||
} else if(this.hasHistory) {
|
||||
historyState = "available";
|
||||
} else {
|
||||
historyState = "none";
|
||||
}
|
||||
|
||||
switch (this.mode) {
|
||||
case "normal":
|
||||
if(this.conversationPrivate && !this.canClientAccessChat()) {
|
||||
this.events.fire_react("notify_conversation_state", {
|
||||
chatId: this.chatId,
|
||||
state: "private",
|
||||
crossChannelChatSupported: this.crossChannelChatSupported
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.events.fire_react("notify_conversation_state", {
|
||||
chatId: this.chatId,
|
||||
state: "normal",
|
||||
|
||||
historyState: historyState,
|
||||
historyErrorMessage: this.historyErrorMessage,
|
||||
historyRetryTimestamp: this.historyRetryTimestamp,
|
||||
|
||||
chatFrameMaxMessageCount: kMaxChatFrameMessageSize,
|
||||
unreadTimestamp: this.unreadTimestamp,
|
||||
|
||||
showUserSwitchEvents: this.conversationPrivate || !this.crossChannelChatSupported,
|
||||
sendEnabled: this.messageSendEnabled,
|
||||
|
||||
events: [...this.presentEvents, ...this.presentMessages]
|
||||
});
|
||||
break;
|
||||
|
||||
case "loading":
|
||||
case "unloaded":
|
||||
this.events.fire_react("notify_conversation_state", {
|
||||
chatId: this.chatId,
|
||||
state: "loading"
|
||||
});
|
||||
break;
|
||||
|
||||
case "error":
|
||||
this.events.fire_react("notify_conversation_state", {
|
||||
chatId: this.chatId,
|
||||
state: "error",
|
||||
errorMessage: this.errorMessage
|
||||
});
|
||||
break;
|
||||
|
||||
case "no-permissions":
|
||||
this.events.fire_react("notify_conversation_state", {
|
||||
chatId: this.chatId,
|
||||
state: "no-permissions",
|
||||
failedPermission: this.failedPermission
|
||||
});
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected doSendMessage(message: string, targetMode: number, target: number) : Promise<boolean> {
|
||||
let msg = preprocessChatMessageForSend(message);
|
||||
return this.connection.serverConnection.send_command("sendtextmessage", {
|
||||
targetmode: targetMode,
|
||||
cid: target,
|
||||
target: target,
|
||||
msg: msg
|
||||
}, { process_result: false }).then(async () => true).catch(error => {
|
||||
if(error instanceof CommandResult) {
|
||||
if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
||||
this.registerChatEvent({
|
||||
type: "message-failed",
|
||||
uniqueId: "msf-" + this.chatId + "-" + Date.now(),
|
||||
timestamp: Date.now(),
|
||||
error: "permission",
|
||||
failedPermission: this.connection.permissions.resolveInfo(parseInt(error.json["failed_permid"]))?.name || tr("unknown")
|
||||
}, false);
|
||||
} else {
|
||||
this.registerChatEvent({
|
||||
type: "message-failed",
|
||||
uniqueId: "msf-" + this.chatId + "-" + Date.now(),
|
||||
timestamp: Date.now(),
|
||||
error: "error",
|
||||
errorMessage: error.formattedMessage()
|
||||
}, false);
|
||||
}
|
||||
} else if(typeof error === "string") {
|
||||
this.registerChatEvent({
|
||||
type: "message-failed",
|
||||
uniqueId: "msf-" + this.chatId + "-" + Date.now(),
|
||||
timestamp: Date.now(),
|
||||
error: "error",
|
||||
errorMessage: error
|
||||
}, false);
|
||||
} else {
|
||||
log.warn(LogCategory.CHAT, tr("Failed to send channel chat message to %s: %o"), this.chatId, error);
|
||||
this.registerChatEvent({
|
||||
type: "message-failed",
|
||||
uniqueId: "msf-" + this.chatId + "-" + Date.now(),
|
||||
timestamp: Date.now(),
|
||||
error: "error",
|
||||
errorMessage: tr("lookup the console")
|
||||
}, false);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public isUnread() {
|
||||
return this.unreadTimestamp !== undefined;
|
||||
}
|
||||
|
||||
public setUnreadTimestamp(timestamp: number | undefined) {
|
||||
if(timestamp === undefined)
|
||||
this.lastReadMessage = Date.now();
|
||||
|
||||
if(this.unreadTimestamp === timestamp)
|
||||
return;
|
||||
|
||||
this.unreadTimestamp = timestamp;
|
||||
this.events.fire_react("notify_unread_timestamp_changed", { chatId: this.chatId, timestamp: timestamp });
|
||||
}
|
||||
|
||||
public jumpToPresent() {
|
||||
this.reportStateToUI();
|
||||
}
|
||||
|
||||
public uiQueryHistory(timestamp: number, enforce?: boolean) {
|
||||
if(this.executingUIHistoryQuery && !enforce)
|
||||
return;
|
||||
|
||||
this.executingUIHistoryQuery = true;
|
||||
this.queryHistory({ end: 1, begin: timestamp, limit: kMaxChatFrameMessageSize }).then(result => {
|
||||
this.executingUIHistoryQuery = false;
|
||||
this.historyErrorMessage = undefined;
|
||||
this.historyRetryTimestamp = result.nextAllowedQuery;
|
||||
|
||||
switch (result.status) {
|
||||
case "success":
|
||||
this.events.fire_react("notify_conversation_history", {
|
||||
chatId: this.chatId,
|
||||
state: "success",
|
||||
|
||||
hasMoreMessages: result.moreEvents,
|
||||
retryTimestamp: this.historyRetryTimestamp,
|
||||
|
||||
events: result.events
|
||||
});
|
||||
break;
|
||||
|
||||
case "private":
|
||||
this.events.fire_react("notify_conversation_history", {
|
||||
chatId: this.chatId,
|
||||
state: "error",
|
||||
errorMessage: this.historyErrorMessage = tr("chat is private"),
|
||||
retryTimestamp: this.historyRetryTimestamp
|
||||
});
|
||||
break;
|
||||
|
||||
case "no-permission":
|
||||
this.events.fire_react("notify_conversation_history", {
|
||||
chatId: this.chatId,
|
||||
state: "error",
|
||||
errorMessage: this.historyErrorMessage = tra("failed on {}", result.failedPermission || tr("unknown permission")),
|
||||
retryTimestamp: this.historyRetryTimestamp
|
||||
});
|
||||
break;
|
||||
|
||||
case "error":
|
||||
this.events.fire_react("notify_conversation_history", {
|
||||
chatId: this.chatId,
|
||||
state: "error",
|
||||
errorMessage: this.historyErrorMessage = result.errorMessage,
|
||||
retryTimestamp: this.historyRetryTimestamp
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected lastEvent() : ChatEvent | undefined {
|
||||
if(this.presentMessages.length === 0)
|
||||
return this.presentEvents.last();
|
||||
else if(this.presentEvents.length === 0 || this.presentMessages.last().timestamp > this.presentEvents.last().timestamp)
|
||||
return this.presentMessages.last();
|
||||
else
|
||||
return this.presentEvents.last();
|
||||
}
|
||||
|
||||
protected sendMessageSendingEnabled(enabled: boolean) {
|
||||
if(this.messageSendEnabled === enabled)
|
||||
return;
|
||||
|
||||
this.messageSendEnabled = enabled;
|
||||
this.events.fire("notify_send_enabled", { chatId: this.chatId, enabled: enabled });
|
||||
}
|
||||
|
||||
protected abstract canClientAccessChat() : boolean;
|
||||
public abstract queryHistory(criteria: { begin?: number, end?: number, limit?: number }) : Promise<ConversationHistoryResponse>;
|
||||
public abstract queryCurrentMessages();
|
||||
public abstract sendMessage(text: string);
|
||||
}
|
||||
|
||||
export abstract class AbstractChatManager<Events extends ConversationUIEvents> {
|
||||
protected readonly uiEvents: Registry<Events>;
|
||||
|
||||
protected constructor() {
|
||||
this.uiEvents = new Registry<Events>();
|
||||
}
|
||||
|
||||
handlePanelShow() {
|
||||
this.uiEvents.fire("notify_panel_show");
|
||||
}
|
||||
|
||||
protected abstract findChat(id: string) : AbstractChat<Events>;
|
||||
|
||||
@EventHandler<ConversationUIEvents>("query_conversation_state")
|
||||
protected handleQueryConversationState(event: ConversationUIEvents["query_conversation_state"]) {
|
||||
const conversation = this.findChat(event.chatId);
|
||||
if(!conversation) {
|
||||
this.uiEvents.fire_react("notify_conversation_state", {
|
||||
state: "error",
|
||||
errorMessage: tr("Unknown conversation"),
|
||||
|
||||
chatId: event.chatId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(conversation.currentMode() === "unloaded") {
|
||||
conversation.queryCurrentMessages();
|
||||
} else {
|
||||
conversation.reportStateToUI();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("query_conversation_history")
|
||||
protected handleQueryHistory(event: ConversationUIEvents["query_conversation_history"]) {
|
||||
const conversation = this.findChat(event.chatId);
|
||||
if(!conversation) {
|
||||
this.uiEvents.fire_react("notify_conversation_history", {
|
||||
state: "error",
|
||||
errorMessage: tr("Unknown conversation"),
|
||||
retryTimestamp: Date.now() + 10 * 1000,
|
||||
|
||||
chatId: event.chatId
|
||||
});
|
||||
|
||||
log.error(LogCategory.CLIENT, tr("Tried to query history for an unknown conversation with id %s"), event.chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
conversation.uiQueryHistory(event.timestamp);
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("action_clear_unread_flag")
|
||||
protected handleClearUnreadFlag(event: ConversationUIEvents["action_clear_unread_flag"]) {
|
||||
this.findChat(event.chatId)?.setUnreadTimestamp(undefined);
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("action_self_typing")
|
||||
protected handleActionSelfTyping(event: ConversationUIEvents["action_self_typing"]) {
|
||||
if(this.findChat(event.chatId)?.isUnread())
|
||||
this.uiEvents.fire("action_clear_unread_flag", { chatId: event.chatId });
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("action_send_message")
|
||||
protected handleSendMessage(event: ConversationUIEvents["action_send_message"]) {
|
||||
const conversation = this.findChat(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;
|
||||
}
|
||||
|
||||
conversation.sendMessage(event.text);
|
||||
}
|
||||
|
||||
@EventHandler<ConversationUIEvents>("action_jump_to_present")
|
||||
protected handleJumpToPresent(event: ConversationUIEvents["action_jump_to_present"]) {
|
||||
const conversation = this.findChat(event.chatId);
|
||||
if(!conversation) {
|
||||
log.error(LogCategory.CLIENT, tr("Tried to jump to present for an unknown conversation with id %s"), event.chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
conversation.jumpToPresent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import * as React from "react";
|
||||
import {ConnectionHandler} from "../../../ConnectionHandler";
|
||||
import {EventHandler} from "../../../events";
|
||||
import * as log from "../../../log";
|
||||
import {LogCategory} from "../../../log";
|
||||
import {tr} from "../../../i18n/localize";
|
||||
import ReactDOM = require("react-dom");
|
||||
import {
|
||||
ConversationUIEvents
|
||||
} from "../../../ui/frames/side/ConversationDefinitions";
|
||||
import {ConversationPanel} from "./AbstractConversationRenderer";
|
||||
import {AbstractConversationController} from "./AbstractConversationController";
|
||||
import {
|
||||
ChannelConversation, ChannelConversationEvents,
|
||||
ChannelConversationManager,
|
||||
ChannelConversationManagerEvents
|
||||
} from "tc-shared/conversations/ChannelConversationManager";
|
||||
|
||||
export class ChannelConversationController extends AbstractConversationController<
|
||||
ConversationUIEvents,
|
||||
ChannelConversationManager,
|
||||
ChannelConversationManagerEvents,
|
||||
ChannelConversation,
|
||||
ChannelConversationEvents
|
||||
> {
|
||||
readonly connection: ConnectionHandler;
|
||||
readonly htmlTag: HTMLDivElement;
|
||||
|
||||
constructor(connection: ConnectionHandler) {
|
||||
super(connection.getChannelConversations() as any);
|
||||
this.connection = connection;
|
||||
|
||||
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,
|
||||
noFirstMessageOverlay: false,
|
||||
messagesDeletable: true
|
||||
}).open().then(() => {
|
||||
console.error("Opened");
|
||||
});
|
||||
*/
|
||||
|
||||
this.uiEvents.on("notify_destroy", connection.events().on("notify_visibility_changed", event => {
|
||||
if(!event.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handlePanelShow();
|
||||
}));
|
||||
|
||||
this.uiEvents.register_handler(this, true);
|
||||
}
|
||||
|
||||
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);
|
||||
if(!conversation) {
|
||||
log.error(LogCategory.CLIENT, tr("Tried to delete a chat message from an unknown conversation with id %s"), event.chatId);
|
||||
return;
|
||||
}
|
||||
|
||||
conversation.deleteMessage(event.uniqueId);
|
||||
}
|
||||
|
||||
protected registerConversationEvents(conversation: ChannelConversation) {
|
||||
super.registerConversationEvents(conversation);
|
||||
this.currentSelectedListener.push(conversation.events.on("notify_messages_deleted", event => {
|
||||
this.uiEvents.fire_react("notify_chat_message_delete", { messageIds: event.messages, chatId: conversation.getChatId() });
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -95,7 +95,7 @@ export class ClientInfoController {
|
|||
}));
|
||||
|
||||
this.listenerConnection.push(this.connection.events().on("notify_connection_state_changed", event => {
|
||||
if(event.new_state !== ConnectionState.CONNECTED && this.currentClientStatus) {
|
||||
if(event.newState !== ConnectionState.CONNECTED && this.currentClientStatus) {
|
||||
this.currentClient = undefined;
|
||||
this.currentClientStatus.leaveTimestamp = Date.now() / 1000;
|
||||
this.sendOnline();
|
||||
|
@ -196,7 +196,7 @@ export class ClientInfoController {
|
|||
microphoneMuted: client.properties.client_input_muted,
|
||||
microphoneDisabled: !client.properties.client_input_hardware,
|
||||
speakerMuted: client.properties.client_output_muted,
|
||||
speakerDisabled: client.properties.client_output_hardware
|
||||
speakerDisabled: !client.properties.client_output_hardware
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -250,6 +250,11 @@ export class ClientInfoController {
|
|||
}
|
||||
|
||||
destroy() {
|
||||
ReactDOM.unmountComponentAtNode(this.htmlContainer);
|
||||
|
||||
this.listenerClient.forEach(callback => callback());
|
||||
this.listenerClient = [];
|
||||
|
||||
this.listenerConnection.forEach(callback => callback());
|
||||
this.listenerConnection.splice(0, this.listenerConnection.length);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
|||
import {getIconManager} from "tc-shared/file/Icons";
|
||||
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||
|
||||
const cssStyle = require("./ClientInfo.scss");
|
||||
const cssStyle = require("./ClientInfoRenderer.scss");
|
||||
|
||||
const EventsContext = React.createContext<Registry<ClientInfoEvents>>(undefined);
|
||||
const ClientContext = React.createContext<OptionalClientInfoInfo>(undefined);
|
||||
|
|
|
@ -9,6 +9,7 @@ export interface ChatMessage {
|
|||
|
||||
/* ---------- Chat events ---------- */
|
||||
export type ChatEvent = { timestamp: number; uniqueId: string; } & (
|
||||
ChatEventUnreadTrigger |
|
||||
ChatEventMessage |
|
||||
ChatEventMessageSendFailed |
|
||||
ChatEventLocalUserSwitch |
|
||||
|
@ -18,6 +19,10 @@ export type ChatEvent = { timestamp: number; uniqueId: string; } & (
|
|||
ChatEventPartnerAction
|
||||
);
|
||||
|
||||
export interface ChatEventUnreadTrigger {
|
||||
type: "unread-trigger";
|
||||
}
|
||||
|
||||
export interface ChatEventMessageSendFailed {
|
||||
type: "message-failed";
|
||||
|
||||
|
@ -136,11 +141,11 @@ export interface ConversationUIEvents {
|
|||
},
|
||||
notify_chat_message_delete: {
|
||||
chatId: string,
|
||||
criteria: { begin: number, end: number, cldbid: number, limit: number }
|
||||
messageIds: string[]
|
||||
},
|
||||
notify_unread_timestamp_changed: {
|
||||
chatId: string,
|
||||
timestamp: number | undefined
|
||||
timestamp: number,
|
||||
}
|
||||
notify_private_state_changed: {
|
||||
chatId: string,
|
||||
|
|
|
@ -0,0 +1,269 @@
|
|||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import {SideHeaderRenderer} from "./HeaderRenderer";
|
||||
import * as React from "react";
|
||||
import {SideHeaderEvents, SideHeaderState} from "tc-shared/ui/frames/side/HeaderDefinitions";
|
||||
import * as _ from "lodash";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {ChannelEntry, ChannelProperties} from "tc-shared/tree/Channel";
|
||||
import {ClientEntry, LocalClientEntry} from "tc-shared/tree/Client";
|
||||
import {openMusicManage} from "tc-shared/ui/modal/ModalMusicManage";
|
||||
|
||||
const ChannelInfoUpdateProperties: (keyof ChannelProperties)[] = [
|
||||
"channel_name",
|
||||
"channel_icon_id",
|
||||
|
||||
"channel_flag_maxclients_unlimited",
|
||||
"channel_maxclients",
|
||||
|
||||
"channel_flag_maxfamilyclients_inherited",
|
||||
"channel_flag_maxfamilyclients_unlimited",
|
||||
"channel_maxfamilyclients"
|
||||
];
|
||||
|
||||
/* TODO: Remove the ping interval handler. It's currently still there since the clients are not emiting the event yet */
|
||||
export class SideHeader {
|
||||
private readonly htmlTag: HTMLDivElement;
|
||||
private readonly uiEvents: Registry<SideHeaderEvents>;
|
||||
|
||||
private connection: ConnectionHandler;
|
||||
|
||||
private listenerConnection: (() => void)[];
|
||||
private listenerVoiceChannel: (() => void)[];
|
||||
private listenerTextChannel: (() => void)[];
|
||||
|
||||
private currentState: SideHeaderState;
|
||||
private currentVoiceChannel: ChannelEntry;
|
||||
private currentTextChannel: ChannelEntry;
|
||||
|
||||
private pingUpdateInterval: number;
|
||||
|
||||
constructor() {
|
||||
this.uiEvents = new Registry<SideHeaderEvents>();
|
||||
this.listenerConnection = [];
|
||||
this.listenerVoiceChannel = [];
|
||||
this.listenerTextChannel = [];
|
||||
|
||||
this.htmlTag = document.createElement("div");
|
||||
this.htmlTag.style.display = "flex";
|
||||
this.htmlTag.style.flexDirection = "column";
|
||||
this.htmlTag.style.flexShrink = "0";
|
||||
this.htmlTag.style.flexGrow = "0";
|
||||
|
||||
ReactDOM.render(React.createElement(SideHeaderRenderer, { events: this.uiEvents }), this.htmlTag);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.uiEvents.on("action_open_conversation", () => {
|
||||
const selectedClient = this.connection.side_bar.getClientInfo().getClient()
|
||||
if(selectedClient) {
|
||||
const conversations = this.connection.getPrivateConversations();
|
||||
conversations.setSelectedConversation(conversations.findOrCreateConversation(selectedClient));
|
||||
}
|
||||
this.connection.side_bar.showPrivateConversations();
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_switch_channel_chat", () => {
|
||||
this.connection.side_bar.showChannelConversations();
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_bot_manage", () => {
|
||||
const bot = this.connection.side_bar.music_info().current_bot();
|
||||
if(!bot) return;
|
||||
|
||||
openMusicManage(this.connection, bot);
|
||||
});
|
||||
|
||||
this.uiEvents.on("action_bot_manage", () => this.connection.side_bar.music_info().events.fire("action_song_add"));
|
||||
|
||||
this.uiEvents.on("query_current_channel_state", event => this.sendChannelState(event.mode));
|
||||
this.uiEvents.on("query_private_conversations", () => this.sendPrivateConversationInfo());
|
||||
this.uiEvents.on("query_ping", () => this.sendPing());
|
||||
}
|
||||
|
||||
private initializeConnection() {
|
||||
this.listenerConnection.push(this.connection.channelTree.events.on("notify_client_moved", event => {
|
||||
if(event.client instanceof LocalClientEntry) {
|
||||
this.updateVoiceChannel();
|
||||
}
|
||||
}));
|
||||
this.listenerConnection.push(this.connection.channelTree.events.on("notify_client_enter_view", event => {
|
||||
if(event.client instanceof LocalClientEntry) {
|
||||
this.updateVoiceChannel();
|
||||
}
|
||||
}));
|
||||
this.listenerConnection.push(this.connection.events().on("notify_connection_state_changed", () => {
|
||||
this.updateVoiceChannel();
|
||||
this.updateTextChannel();
|
||||
this.sendPing();
|
||||
if(this.connection.connected) {
|
||||
if(!this.pingUpdateInterval) {
|
||||
this.pingUpdateInterval = setInterval(() => this.sendPing(), 2000);
|
||||
}
|
||||
} else if(this.pingUpdateInterval) {
|
||||
clearInterval(this.pingUpdateInterval);
|
||||
this.pingUpdateInterval = undefined;
|
||||
}
|
||||
}));
|
||||
this.listenerConnection.push(this.connection.getChannelConversations().events.on("notify_selected_changed", () => this.updateTextChannel()));
|
||||
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()));
|
||||
}
|
||||
|
||||
setConnectionHandler(connection: ConnectionHandler) {
|
||||
if(this.connection === connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listenerConnection.forEach(callback => callback());
|
||||
this.listenerConnection = [];
|
||||
|
||||
this.connection = connection;
|
||||
if(connection) {
|
||||
this.initializeConnection();
|
||||
/* TODO: Update state! */
|
||||
} else {
|
||||
this.setState({ state: "none" });
|
||||
}
|
||||
}
|
||||
|
||||
getConnectionHandler() : ConnectionHandler | undefined {
|
||||
return this.connection;
|
||||
}
|
||||
|
||||
getHtmlTag() : HTMLDivElement {
|
||||
return this.htmlTag;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
ReactDOM.unmountComponentAtNode(this.htmlTag);
|
||||
|
||||
this.listenerConnection.forEach(callback => callback());
|
||||
this.listenerConnection = [];
|
||||
|
||||
this.listenerTextChannel.forEach(callback => callback());
|
||||
this.listenerTextChannel = [];
|
||||
|
||||
this.listenerVoiceChannel.forEach(callback => callback());
|
||||
this.listenerVoiceChannel = [];
|
||||
|
||||
clearInterval(this.pingUpdateInterval);
|
||||
this.pingUpdateInterval = undefined;
|
||||
}
|
||||
|
||||
setState(state: SideHeaderState) {
|
||||
if(_.isEqual(this.currentState, state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentState = state;
|
||||
this.uiEvents.fire_react("notify_header_state", { state: state });
|
||||
}
|
||||
|
||||
private sendChannelState(mode: "voice" | "text") {
|
||||
const channel = mode === "voice" ? this.currentVoiceChannel : this.currentTextChannel;
|
||||
if(channel) {
|
||||
let maxClients = -1;
|
||||
if(!channel.properties.channel_flag_maxclients_unlimited) {
|
||||
maxClients = channel.properties.channel_maxclients;
|
||||
}
|
||||
|
||||
this.uiEvents.fire_react("notify_current_channel_state", {
|
||||
mode: mode,
|
||||
state: {
|
||||
state: "connected",
|
||||
channelName: channel.parsed_channel_name.text,
|
||||
channelIcon: {
|
||||
handlerId: this.connection.handlerId,
|
||||
serverUniqueId: this.connection.getCurrentServerUniqueId(),
|
||||
iconId: channel.properties.channel_icon_id
|
||||
},
|
||||
channelUserCount: channel.clients(false).length,
|
||||
channelMaxUser: maxClients
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.uiEvents.fire_react("notify_current_channel_state", { mode: mode, state: { state: "not-connected" }});
|
||||
}
|
||||
}
|
||||
|
||||
private updateVoiceChannel() {
|
||||
let targetChannel = this.connection?.connected ? this.connection.getClient().currentChannel() : undefined;
|
||||
if(this.currentVoiceChannel === targetChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listenerVoiceChannel.forEach(callback => callback());
|
||||
this.listenerVoiceChannel = [];
|
||||
|
||||
this.currentVoiceChannel = targetChannel;
|
||||
this.sendChannelState("voice");
|
||||
|
||||
if(targetChannel) {
|
||||
this.listenerTextChannel.push(targetChannel.events.on("notify_properties_updated", event => {
|
||||
for(const key of ChannelInfoUpdateProperties) {
|
||||
if(key in event.updated_properties) {
|
||||
this.sendChannelState("voice");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private updateTextChannel() {
|
||||
let targetChannel: ChannelEntry;
|
||||
let targetChannelId = this.connection?.connected ? parseInt(this.connection.getChannelConversations().getSelectedConversation()?.getChatId()) : -1;
|
||||
if(!isNaN(targetChannelId) && targetChannelId >= 0) {
|
||||
targetChannel = this.connection.channelTree.findChannel(targetChannelId);
|
||||
}
|
||||
|
||||
if(this.currentTextChannel === targetChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listenerTextChannel.forEach(callback => callback());
|
||||
this.listenerTextChannel = [];
|
||||
|
||||
this.currentTextChannel = targetChannel;
|
||||
this.sendChannelState("text");
|
||||
|
||||
|
||||
if(targetChannel) {
|
||||
this.listenerTextChannel.push(targetChannel.events.on("notify_properties_updated", event => {
|
||||
for(const key of ChannelInfoUpdateProperties) {
|
||||
if(key in event.updated_properties) {
|
||||
this.sendChannelState("text");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private sendPing() {
|
||||
if(this.connection?.connected) {
|
||||
const ping = this.connection.getServerConnection().ping();
|
||||
this.uiEvents.fire_react("notify_ping", {
|
||||
ping: {
|
||||
native: typeof ping.native !== "number" ? -1 : ping.native,
|
||||
javaScript: ping.javascript
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.uiEvents.fire_react("notify_ping", { ping: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
private sendPrivateConversationInfo() {
|
||||
const conversations = this.connection.getPrivateConversations();
|
||||
this.uiEvents.fire_react("notify_private_conversations", {
|
||||
info: {
|
||||
open: conversations.getConversations().length,
|
||||
unread: conversations.getUnreadCount()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import {RemoteIconInfo} from "tc-shared/file/Icons";
|
||||
|
||||
export type SideHeaderState = SideHeaderStateNone | SideHeaderStateConversation | SideHeaderStateClient | SideHeaderStateMusicBot;
|
||||
export type SideHeaderStateNone = {
|
||||
state: "none"
|
||||
};
|
||||
|
||||
export type SideHeaderStateConversation = {
|
||||
state: "conversation",
|
||||
mode: "channel" | "private"
|
||||
};
|
||||
|
||||
export type SideHeaderStateClient = {
|
||||
state: "client",
|
||||
ownClient: boolean
|
||||
}
|
||||
|
||||
export type SideHeaderStateMusicBot = {
|
||||
state: "music-bot"
|
||||
}
|
||||
|
||||
export type SideHeaderChannelState = {
|
||||
state: "not-connected"
|
||||
} | {
|
||||
state: "connected",
|
||||
channelName: string,
|
||||
channelIcon: RemoteIconInfo,
|
||||
channelUserCount: number,
|
||||
channelMaxUser: number | -1
|
||||
};
|
||||
|
||||
export type SideHeaderPingInfo = {
|
||||
native: number,
|
||||
javaScript: number | undefined
|
||||
};
|
||||
|
||||
export type PrivateConversationInfo = {
|
||||
unread: number,
|
||||
open: number
|
||||
};
|
||||
|
||||
export interface SideHeaderEvents {
|
||||
action_bot_manage: {},
|
||||
action_bot_add_song: {},
|
||||
action_switch_channel_chat: {},
|
||||
action_open_conversation: {},
|
||||
|
||||
query_current_channel_state: { mode: "voice" | "text" },
|
||||
query_ping: {},
|
||||
query_private_conversations: {},
|
||||
|
||||
notify_header_state: {
|
||||
state: SideHeaderState
|
||||
},
|
||||
notify_current_channel_state: {
|
||||
mode: "voice" | "text",
|
||||
state: SideHeaderChannelState
|
||||
},
|
||||
notify_ping: {
|
||||
ping: SideHeaderPingInfo | undefined
|
||||
},
|
||||
notify_private_conversations: {
|
||||
info: PrivateConversationInfo
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
|
||||
@import "./ClientInfoRenderer";
|
||||
|
||||
.container {
|
||||
user-select: none;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
height: 9em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
|
||||
background-color: var(--side-info-background);
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
|
||||
-moz-box-shadow: inset 0 0 5px var(--side-info-shadow);
|
||||
-webkit-box-shadow: inset 0 0 5px var(--side-info-shadow);
|
||||
box-shadow: inset 0 0 5px var(--side-info-shadow);
|
||||
|
||||
.lane {
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
height: 3.25em;
|
||||
max-width: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.block, .button {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.block {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 0;
|
||||
|
||||
&.right {
|
||||
text-align: right;
|
||||
|
||||
&.mode-client_info {
|
||||
max-width: calc(50% - #{$client_info_avatar_size / 2});
|
||||
margin-left: calc(#{$client_info_avatar_size / 2});
|
||||
}
|
||||
}
|
||||
|
||||
&.left {
|
||||
margin-right: .5em;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
|
||||
&.mode-client_info {
|
||||
max-width: calc(50% - #{$client_info_avatar_size / 2});
|
||||
margin-right: calc(#{$client_info_avatar_size} / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.title, .value, .smallValue {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
color: var(--side-info-title);
|
||||
|
||||
.containerIndicator {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
background: var(--side-info-indicator-background);
|
||||
border: 1px solid var(--side-info-indicator-border);
|
||||
border-radius: 4px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
vertical-align: text-top;
|
||||
|
||||
color: var(--side-info-indicator);
|
||||
|
||||
font-size: .66em;
|
||||
height: 1.3em;
|
||||
min-width: .9em;
|
||||
|
||||
padding-right: 2px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--side-info-value);
|
||||
background-color: var(--side-info-value-background);
|
||||
|
||||
display: inline-block;
|
||||
|
||||
border-radius: .18em;
|
||||
padding-right: .31em;
|
||||
padding-left: .31em;
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.icon {
|
||||
vertical-align: text-top;
|
||||
margin-right: .25em;
|
||||
}
|
||||
|
||||
&.ping {
|
||||
&.veryGood {
|
||||
color: var(--side-info-ping-very-good);
|
||||
}
|
||||
&.good {
|
||||
color: var(--side-info-ping-good);
|
||||
}
|
||||
&.medium {
|
||||
color: var(--side-info-ping-medium);
|
||||
}
|
||||
&.poor {
|
||||
color: var(--side-info-ping-poor);
|
||||
}
|
||||
&.veryPoor {
|
||||
color: var(--side-info-ping-very-poor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.smallValue {
|
||||
display: inline-block;
|
||||
color: var(--side-info-value);
|
||||
font-size: .66em;
|
||||
vertical-align: top;
|
||||
margin-top: -.2em;
|
||||
margin-left: .25em;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: var(--side-info-value);
|
||||
background-color: var(--side-info-value-background);
|
||||
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
&.botAddSong {
|
||||
color: var(--side-info-bot-add-song);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #4e4e4e; /* TODO: Evaluate color */
|
||||
}
|
||||
|
||||
@include transition(background-color $button_hover_animation_time ease-in-out);
|
||||
}
|
||||
}
|
||||
|
||||
&.musicBotInfo {
|
||||
.right {
|
||||
margin-left: 8.5em;
|
||||
}
|
||||
|
||||
.left {
|
||||
margin-right: 8.5em;
|
||||
}
|
||||
|
||||
width: 60em; /* same width so flex-shrik applies equaly */
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,281 @@
|
|||
import * as React from "react";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {
|
||||
SideHeaderEvents,
|
||||
SideHeaderState,
|
||||
SideHeaderChannelState,
|
||||
SideHeaderPingInfo, PrivateConversationInfo
|
||||
} from "tc-shared/ui/frames/side/HeaderDefinitions";
|
||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {useContext, useState} from "react";
|
||||
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||
import {getIconManager} from "tc-shared/file/Icons";
|
||||
|
||||
const StateContext = React.createContext<SideHeaderState>(undefined);
|
||||
const EventsContext = React.createContext<Registry<SideHeaderEvents>>(undefined);
|
||||
|
||||
|
||||
const cssStyle = require("./HeaderRenderer.scss");
|
||||
|
||||
const Block = (props: { children: [React.ReactElement, React.ReactElement], target: "left" | "right" }) => (
|
||||
<div className={cssStyle.block + " " + cssStyle[props.target]}>
|
||||
<div className={cssStyle.title}>{props.children[0]}</div>
|
||||
{props.children[1]}
|
||||
</div>
|
||||
)
|
||||
|
||||
const ChannelStateRenderer = (props: { info: SideHeaderChannelState }) => {
|
||||
if(props.info.state === "not-connected") {
|
||||
return <div className={cssStyle.value} key={"not-connected"}><Translatable>Not connected</Translatable></div>;
|
||||
} else {
|
||||
let limit;
|
||||
if(props.info.channelMaxUser === -1) {
|
||||
limit = <Translatable key={"unlimited"}>Unlimited</Translatable>
|
||||
} else {
|
||||
limit = props.info.channelMaxUser;
|
||||
}
|
||||
|
||||
let icon;
|
||||
if(props.info.channelIcon.iconId !== 0) {
|
||||
const remoteIcon = getIconManager().resolveIcon(props.info.channelIcon.iconId, props.info.channelIcon.serverUniqueId, props.info.channelIcon.handlerId);
|
||||
icon = <RemoteIconRenderer icon={remoteIcon} className={cssStyle.icon} key={"icon-" + props.info.channelIcon.iconId} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={"connected"}>
|
||||
<div className={cssStyle.value}>{icon}{props.info.channelName}</div>
|
||||
<div className={cssStyle.smallValue}>{props.info.channelUserCount} / {limit}</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const BlockChannelState = (props: { mode: "voice" | "text" }) => {
|
||||
const events = useContext(EventsContext);
|
||||
const [ info, setInfo ] = useState<SideHeaderChannelState>(() => {
|
||||
events.fire("query_current_channel_state", { mode: props.mode });
|
||||
return { state: "not-connected" };
|
||||
});
|
||||
|
||||
events.reactUse("notify_current_channel_state", event => event.mode === props.mode && setInfo(event.state));
|
||||
|
||||
let title;
|
||||
if(props.mode === "voice") {
|
||||
title = <Translatable key={"voice"}>You're talking in Channel</Translatable>;
|
||||
} else {
|
||||
title = <Translatable key={"text"}>You're chatting in Channel</Translatable>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Block target={"left"}>
|
||||
{title}
|
||||
<ChannelStateRenderer info={info} />
|
||||
</Block>
|
||||
);
|
||||
}
|
||||
|
||||
const BlockPing = () => {
|
||||
const events = useContext(EventsContext);
|
||||
const [ pingInfo, setPingInfo ] = useState<SideHeaderPingInfo>(() => {
|
||||
events.fire("query_ping");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
events.reactUse("notify_ping", event => setPingInfo(event.ping));
|
||||
|
||||
let value, title;
|
||||
if(!pingInfo) {
|
||||
value = (
|
||||
<div className={cssStyle.value} key={"not-connected"} title={tr("You're not connected to any server")}>
|
||||
<Translatable>Not connected</Translatable>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
let pingClass;
|
||||
if(pingInfo.native <= 30) {
|
||||
pingClass = cssStyle.veryGood;
|
||||
} else if(pingInfo.native <= 50) {
|
||||
pingClass = cssStyle.good;
|
||||
} else if(pingInfo.native <= 90) {
|
||||
pingClass = cssStyle.medium;
|
||||
} else if(pingInfo.native <= 200) {
|
||||
pingClass = cssStyle.poor;
|
||||
} else {
|
||||
pingClass = cssStyle.veryPoor;
|
||||
}
|
||||
|
||||
if(pingInfo.javaScript === undefined) {
|
||||
title = tr("Ping: " + pingInfo.native.toFixed(3) + "ms");
|
||||
} else {
|
||||
title = tr("Native: " + pingInfo.native.toFixed(3) + "ms\nJavascript: " + pingInfo.javaScript.toFixed(3) + "ms");
|
||||
}
|
||||
value = <div className={cssStyle.value + " " + cssStyle.ping + " " + pingClass} key={"ping"} title={title}>{pingInfo.native.toFixed(0)}ms</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Block target={"right"}>
|
||||
<Translatable>Your Ping</Translatable>
|
||||
{value}
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockPrivateChats = (props: { asButton: boolean }) => {
|
||||
const events = useContext(EventsContext);
|
||||
const [ info, setInfo ] = useState<PrivateConversationInfo>(() => {
|
||||
events.fire("query_private_conversations");
|
||||
return { unread: 0, open: 0 };
|
||||
});
|
||||
|
||||
events.reactUse("notify_private_conversations", event => setInfo(event.info));
|
||||
|
||||
let body;
|
||||
if(info.open === 0) {
|
||||
body = <Translatable key={"no-conversations"}>No conversations</Translatable>;
|
||||
} else if(info.open === 1) {
|
||||
body = <Translatable key={"1-conversations"}>One conversation</Translatable>;
|
||||
} else {
|
||||
body = <VariadicTranslatable text={"{} conversations"} key={"n-conversations"}>{info.open}</VariadicTranslatable>;
|
||||
}
|
||||
|
||||
let title;
|
||||
if(info.unread === 0) {
|
||||
title = <Translatable key={"zero"}>Private Chats</Translatable>;
|
||||
} else {
|
||||
title = (
|
||||
<React.Fragment key={"unread"}>
|
||||
<Translatable>Private Chats</Translatable>
|
||||
<div className={cssStyle.containerIndicator}>
|
||||
{info.unread}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Block target={"right"}>
|
||||
{title}
|
||||
<div className={cssStyle.value + " " + (props.asButton ? cssStyle.button : "")} onClick={() => props.asButton && events.fire("action_open_conversation")}>
|
||||
{body}
|
||||
</div>
|
||||
</Block>
|
||||
);
|
||||
}
|
||||
|
||||
const BlockButtonSwitchToChannelChat = () => {
|
||||
const events = useContext(EventsContext);
|
||||
return (
|
||||
<Block target={"left"}>
|
||||
<> </>
|
||||
<div className={cssStyle.value + " " + cssStyle.button} onClick={() => events.fire("action_switch_channel_chat")}>
|
||||
<Translatable>Switch to channel chat</Translatable>
|
||||
</div>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
const BlockButtonOpenConversation = () => {
|
||||
const events = useContext(EventsContext);
|
||||
return (
|
||||
<Block target={"right"}>
|
||||
<> </>
|
||||
<div className={cssStyle.value + " " + cssStyle.button} onClick={() => events.fire("action_open_conversation")}>
|
||||
<Translatable>Open conversation</Translatable>
|
||||
</div>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
const BlockButtonBotManage = () => {
|
||||
const events = useContext(EventsContext);
|
||||
return (
|
||||
<Block target={"left"}>
|
||||
<> </>
|
||||
<div className={cssStyle.value + " " + cssStyle.button} onClick={() => events.fire("action_bot_manage")}>
|
||||
<Translatable>Manage bot</Translatable>
|
||||
</div>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
const BlockButtonBotSongAdd = () => {
|
||||
const events = useContext(EventsContext);
|
||||
return (
|
||||
<Block target={"right"}>
|
||||
<> </>
|
||||
<div className={cssStyle.value + " " + cssStyle.button + " " + cssStyle.botAddSong} onClick={() => events.fire("action_bot_add_song")}>
|
||||
<Translatable>Add song</Translatable>
|
||||
</div>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const BlockTopLeft = () => <BlockChannelState mode={"voice"} />;
|
||||
const BlockTopRight = () => <BlockPing />;
|
||||
|
||||
const BlockBottomLeft = () => {
|
||||
const state = useContext(StateContext);
|
||||
|
||||
switch (state.state) {
|
||||
case "conversation":
|
||||
if(state.mode === "private") {
|
||||
return <BlockButtonSwitchToChannelChat key={"switch-channel-chat"} />;
|
||||
} else {
|
||||
return <BlockChannelState mode={"text"} key={"text-state"} />;
|
||||
}
|
||||
|
||||
case "music-bot":
|
||||
return <BlockButtonBotManage key={"button-manage-bot"} />;
|
||||
|
||||
case "none":
|
||||
case "client":
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const BlockBottomRight = () => {
|
||||
const state = useContext(StateContext);
|
||||
|
||||
switch (state.state) {
|
||||
case "client":
|
||||
if(state.ownClient) {
|
||||
return null;
|
||||
} else {
|
||||
return <BlockButtonOpenConversation key={"button-open-conversation"} />;
|
||||
}
|
||||
|
||||
case "conversation":
|
||||
return <BlockPrivateChats key={"conversation"} asButton={state.mode !== "private"} />;
|
||||
|
||||
case "music-bot":
|
||||
return <BlockButtonBotSongAdd key={"button-add-song"} />;
|
||||
|
||||
case "none":
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const SideHeaderRenderer = (props: { events: Registry<SideHeaderEvents> }) => {
|
||||
const [ state, setState ] = useState<SideHeaderState>({ state: "none" });
|
||||
props.events.reactUse("notify_header_state", event => setState(event.state));
|
||||
|
||||
return (
|
||||
<EventsContext.Provider value={props.events}>
|
||||
<StateContext.Provider value={state}>
|
||||
<div className={cssStyle.container}>
|
||||
<div className={cssStyle.lane}>
|
||||
<BlockTopLeft />
|
||||
<BlockTopRight />
|
||||
</div>
|
||||
<div className={cssStyle.lane + " " + (state.state === "music-bot" ? cssStyle.musicBotInfo : "")}>
|
||||
<BlockBottomLeft />
|
||||
<BlockBottomRight />
|
||||
</div>
|
||||
</div>
|
||||
</StateContext.Provider>
|
||||
</EventsContext.Provider>
|
||||
);
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import {Registry, RegistryMap} from "tc-shared/events";
|
||||
import {ConversationUIEvents} from "tc-shared/ui/frames/side/ConversationDefinitions";
|
||||
import {ConversationPanel} from "tc-shared/ui/frames/side/ConversationUI";
|
||||
import {ConversationPanel} from "./AbstractConversationRenderer";
|
||||
import * as React from "react";
|
||||
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
|
||||
|
||||
class PopoutConversationUI extends AbstractModal {
|
||||
class PopoutConversationRenderer extends AbstractModal {
|
||||
private readonly events: Registry<ConversationUIEvents>;
|
||||
private readonly userData: any;
|
||||
|
||||
|
@ -28,4 +28,4 @@ class PopoutConversationUI extends AbstractModal {
|
|||
}
|
||||
}
|
||||
|
||||
export = PopoutConversationUI;
|
||||
export = PopoutConversationRenderer;
|
|
@ -0,0 +1,162 @@
|
|||
import {ConnectionHandler} from "../../../ConnectionHandler";
|
||||
import {EventHandler} from "../../../events";
|
||||
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";
|
||||
import * as log from "../../../log";
|
||||
import {LogCategory} from "../../../log";
|
||||
import {AbstractConversationController} from "./AbstractConversationController";
|
||||
import { tr } from "tc-shared/i18n/localize";
|
||||
import {
|
||||
PrivateConversation,
|
||||
PrivateConversationEvents,
|
||||
PrivateConversationManager,
|
||||
PrivateConversationManagerEvents
|
||||
} from "tc-shared/conversations/PrivateConversationManager";
|
||||
|
||||
export type OutOfViewClient = {
|
||||
nickname: string,
|
||||
clientId: number,
|
||||
uniqueId: string
|
||||
}
|
||||
|
||||
function generateConversationUiInfo(conversation: PrivateConversation) : PrivateConversationInfo {
|
||||
const lastMessage = conversation.getPresentMessages().last();
|
||||
const lastClientInfo = conversation.getLastClientInfo();
|
||||
return {
|
||||
nickname: lastClientInfo.nickname,
|
||||
uniqueId: lastClientInfo.uniqueId,
|
||||
clientId: lastClientInfo.clientId,
|
||||
chatId: conversation.clientUniqueId,
|
||||
|
||||
lastMessage: lastMessage ? lastMessage.timestamp : 0,
|
||||
unreadMessages: conversation.isUnread()
|
||||
};
|
||||
}
|
||||
|
||||
export class PrivateConversationController extends AbstractConversationController<
|
||||
PrivateConversationUIEvents,
|
||||
PrivateConversationManager,
|
||||
PrivateConversationManagerEvents,
|
||||
PrivateConversation,
|
||||
PrivateConversationEvents
|
||||
> {
|
||||
public readonly htmlTag: HTMLDivElement;
|
||||
public readonly connection: ConnectionHandler;
|
||||
|
||||
private listenerConversation: {[key: string]:(() => void)[]};
|
||||
|
||||
constructor(connection: ConnectionHandler) {
|
||||
super(connection.getPrivateConversations());
|
||||
this.connection = connection;
|
||||
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);
|
||||
|
||||
this.uiEvents.on("notify_destroy", connection.events().on("notify_visibility_changed", event => {
|
||||
if(!event.visible)
|
||||
return;
|
||||
|
||||
this.handlePanelShow();
|
||||
}));
|
||||
|
||||
this.listenerManager.push(this.conversationManager.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 => {
|
||||
this.uiEvents.fire_react("notify_partner_changed", { chatId: conversation.getChatId(), clientId: event.clientId, name: event.name });
|
||||
}));
|
||||
events.push(conversation.events.on("notify_partner_name_changed", event => {
|
||||
this.uiEvents.fire_react("notify_partner_name_changed", { chatId: conversation.getChatId(), name: event.name });
|
||||
}));
|
||||
events.push(conversation.events.on("notify_partner_typing", () => {
|
||||
this.uiEvents.fire_react("notify_partner_typing", { chatId: conversation.getChatId() });
|
||||
}));
|
||||
events.push(conversation.events.on("notify_unread_state_changed", event => {
|
||||
this.uiEvents.fire_react("notify_unread_state_changed", { chatId: conversation.getChatId(), unread: event.unread });
|
||||
}));
|
||||
|
||||
this.reportConversationList();
|
||||
}));
|
||||
this.listenerManager.push(this.conversationManager.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();
|
||||
}
|
||||
|
||||
focusInput() {
|
||||
this.uiEvents.fire_react("action_focus_chat");
|
||||
}
|
||||
|
||||
private reportConversationList() {
|
||||
this.uiEvents.fire_react("notify_private_conversations", {
|
||||
conversations: this.conversationManager.getConversations().map(generateConversationUiInfo),
|
||||
selected: this.conversationManager.getSelectedConversation()?.clientUniqueId || "unselected"
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler<PrivateConversationUIEvents>("query_private_conversations")
|
||||
private handleQueryPrivateConversations() {
|
||||
this.reportConversationList();
|
||||
}
|
||||
|
||||
@EventHandler<PrivateConversationUIEvents>("action_close_chat")
|
||||
private handleConversationClose(event: PrivateConversationUIEvents["action_close_chat"]) {
|
||||
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;
|
||||
}
|
||||
|
||||
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"]) {
|
||||
const conversation = this.getCurrentConversation();
|
||||
if(!conversation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clientId = conversation.currentClientId();
|
||||
if(!clientId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.connection.serverConnection.send_command("clientchatcomposing", { clid: clientId }).catch(error => {
|
||||
log.warn(LogCategory.CHAT, tr("Failed to send chat composing to server for chat %d: %o"), clientId, error);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -15,11 +15,11 @@ export interface PrivateConversationUIEvents extends ConversationUIEvents {
|
|||
action_close_chat: { chatId: string },
|
||||
|
||||
query_private_conversations: {},
|
||||
|
||||
notify_private_conversations: {
|
||||
conversations: PrivateConversationInfo[],
|
||||
selected: string
|
||||
}
|
||||
|
||||
},
|
||||
notify_partner_changed: {
|
||||
chatId: string,
|
||||
clientId: number,
|
||||
|
@ -28,5 +28,9 @@ export interface PrivateConversationUIEvents extends ConversationUIEvents {
|
|||
notify_partner_name_changed: {
|
||||
chatId: string,
|
||||
name: string
|
||||
},
|
||||
notify_unread_state_changed: {
|
||||
chatId: string,
|
||||
unread: boolean
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import {
|
|||
} from "tc-shared/ui/frames/side/PrivateConversationDefinitions";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {ContextDivider} from "tc-shared/ui/react-elements/ContextDivider";
|
||||
import {ConversationPanel} from "tc-shared/ui/frames/side/ConversationUI";
|
||||
import {ConversationPanel} from "./AbstractConversationRenderer";
|
||||
import {useContext, useEffect, useState} from "react";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import {AvatarRenderer} from "tc-shared/ui/react-elements/Avatar";
|
||||
|
@ -14,7 +14,7 @@ import {TimestampRenderer} from "tc-shared/ui/react-elements/TimestampRenderer";
|
|||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {getGlobalAvatarManagerFactory} from "tc-shared/file/Avatars";
|
||||
|
||||
const cssStyle = require("./PrivateConversationUI.scss");
|
||||
const cssStyle = require("./PrivateConversationRenderer.scss");
|
||||
const kTypingTimeout = 5000;
|
||||
|
||||
const HandlerIdContext = React.createContext<string>(undefined);
|
||||
|
@ -119,22 +119,17 @@ const ConversationEntryUnreadMarker = React.memo((props: { chatId: string, initi
|
|||
const events = useContext(EventContext);
|
||||
const [ unread, setUnread ] = useState(props.initialUnread);
|
||||
|
||||
events.reactUse("notify_unread_timestamp_changed", event => {
|
||||
if(event.chatId !== props.chatId)
|
||||
events.reactUse("notify_unread_state_changed", event => {
|
||||
if(event.chatId !== props.chatId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUnread(event.timestamp !== undefined);
|
||||
setUnread(event.unread);
|
||||
});
|
||||
|
||||
events.reactUse("notify_chat_event", event => {
|
||||
if(event.chatId !== props.chatId || !event.triggerUnread)
|
||||
return;
|
||||
|
||||
setUnread(true);
|
||||
});
|
||||
|
||||
if(!unread)
|
||||
if(!unread) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div key={"unread-marker"} className={cssStyle.unread} />;
|
||||
});
|
|
@ -141,8 +141,9 @@ export class MusicInfo {
|
|||
this._container_playlist = this._html_tag.find(".container-playlist");
|
||||
|
||||
this._html_tag.find(".button-close").on('click', () => {
|
||||
if(this.previous_frame_content === FrameContent.CLIENT_INFO)
|
||||
if(this.previous_frame_content === FrameContent.CLIENT_INFO) {
|
||||
this.previous_frame_content = FrameContent.NONE;
|
||||
}
|
||||
|
||||
this.handle.set_content(this.previous_frame_content);
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
@import "../../../css/static/mixin.scss";
|
||||
@import "../../../css/static/properties.scss";
|
||||
|
||||
html:root {
|
||||
--chatbox-emoji-hover-background: #454545;
|
|
@ -23,7 +23,7 @@ registerHandler({
|
|||
|
||||
registerHandler({
|
||||
name: "conversation",
|
||||
loadClass: async () => await import("tc-shared/ui/frames/side/PopoutConversationUI")
|
||||
loadClass: async () => await import("../../frames/side/PopoutConversationRenderer")
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react";
|
||||
import {parseMessageWithArguments} from "tc-shared/ui/frames/chat";
|
||||
import {cloneElement, ReactNode} from "react";
|
||||
import {cloneElement} from "react";
|
||||
|
||||
let instances = [];
|
||||
export class Translatable extends React.Component<{
|
||||
|
@ -51,7 +51,7 @@ export class Translatable extends React.Component<{
|
|||
}
|
||||
|
||||
let renderBrElementIndex = 0;
|
||||
export type VariadicTranslatableChild = React.ReactElement | string;
|
||||
export type VariadicTranslatableChild = React.ReactElement | string | number;
|
||||
export const VariadicTranslatable = (props: { text: string, __cacheKey?: string, children?: VariadicTranslatableChild[] | VariadicTranslatableChild }) => {
|
||||
const args = Array.isArray(props.children) ? props.children : [props.children];
|
||||
const argsUseCount = [...new Array(args.length)].map(() => 0);
|
||||
|
@ -64,7 +64,7 @@ export const VariadicTranslatable = (props: { text: string, __cacheKey?: string,
|
|||
if(typeof e === "string") {
|
||||
return e.split("\n").reduce((result, element) => {
|
||||
if(result.length > 0) {
|
||||
result.push(<br key={++this.renderBrElementIndex}/>);
|
||||
result.push(<br key={++renderBrElementIndex}/>);
|
||||
}
|
||||
result.push(element);
|
||||
return result;
|
||||
|
@ -73,7 +73,7 @@ export const VariadicTranslatable = (props: { text: string, __cacheKey?: string,
|
|||
|
||||
let element = args[e];
|
||||
if(argsUseCount[e]) {
|
||||
if(typeof element === "string") {
|
||||
if(typeof element === "string" || typeof element === "number") {
|
||||
/* do nothing */
|
||||
} else {
|
||||
element = cloneElement(element);
|
||||
|
|
|
@ -134,7 +134,7 @@ class ChannelTreeController {
|
|||
}
|
||||
|
||||
private handleConnectionStateChanged(event: ConnectionEvents["notify_connection_state_changed"]) {
|
||||
if(event.new_state !== ConnectionState.CONNECTED) {
|
||||
if(event.newState !== ConnectionState.CONNECTED) {
|
||||
this.channelTreeInitialized = false;
|
||||
this.sendChannelTreeEntries();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
AbstractServerConnection,
|
||||
CommandOptionDefaults,
|
||||
CommandOptions,
|
||||
CommandOptions, ConnectionPing,
|
||||
ConnectionStateListener,
|
||||
ConnectionStatistics,
|
||||
} from "tc-shared/connection/ConnectionBase";
|
||||
|
@ -378,6 +378,7 @@ export class ServerConnection extends AbstractServerConnection {
|
|||
this.pingStatistics.lastResponseTimestamp = 'now' in performance ? performance.now() : Date.now();
|
||||
this.pingStatistics.currentJsValue = this.pingStatistics.lastResponseTimestamp - this.pingStatistics.lastRequestTimestamp;
|
||||
this.pingStatistics.currentNativeValue = parseInt(json["ping_native"]) / 1000; /* we're getting it in microseconds and not milliseconds */
|
||||
this.events.fire("notify_ping_updated", { newPing: this.ping() });
|
||||
//log.debug(LogCategory.NETWORKING, tr("Received new pong. Updating ping to: JS: %o Native: %o"), this._ping.value.toFixed(3), this._ping.value_native.toFixed(3));
|
||||
}
|
||||
} else if(json["type"] === "WebRTC") {
|
||||
|
@ -526,10 +527,11 @@ export class ServerConnection extends AbstractServerConnection {
|
|||
}
|
||||
}
|
||||
|
||||
ping(): { native: number; javascript?: number } {
|
||||
ping(): ConnectionPing {
|
||||
return {
|
||||
javascript: this.pingStatistics.currentJsValue,
|
||||
native: this.pingStatistics.currentNativeValue
|
||||
/* if the native value is zero that means we don't have any */
|
||||
native: this.pingStatistics.currentNativeValue === 0 ? this.pingStatistics.currentJsValue : this.pingStatistics.currentNativeValue
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue