import { AbstractChat, AbstractConversationEvents, AbstractChatManager, AbstractChatManagerEvents } from "tc-shared/conversations/AbstractConversion"; import {ClientEntry} from "tc-shared/tree/Client"; import {ChatEvent, ChatMessage, ConversationHistoryResponse} from "../ui/frames/side/AbstractConversationDefinitions"; import {ChannelTreeEvents} from "tc-shared/tree/ChannelTree"; import {queryConversationEvents, registerConversationEvent} from "tc-shared/conversations/PrivateConversationHistory"; import {LogCategory, logWarn} from "tc-shared/log"; import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler"; export type OutOfViewClient = { nickname: string, clientId: number, uniqueId: string } let receivingEventUniqueIdIndex = 0; export interface PrivateConversationEvents extends AbstractConversationEvents { 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 { public readonly clientUniqueId: string; private activeClientListener: (() => void)[] | undefined = undefined; private activeClient: ClientEntry | OutOfViewClient | undefined = undefined; private lastClientInfo: OutOfViewClient; private conversationOpen: boolean = false; constructor(manager: PrivateConversationManager, client: ClientEntry | OutOfViewClient) { super(manager.connection, client instanceof ClientEntry ? client.clientUid() : client.uniqueId); this.activeClient = client; if(client instanceof ClientEntry) { this.registerClientEvents(client); this.clientUniqueId = client.clientUid(); } else { this.clientUniqueId = client.uniqueId; } this.updateClientInfo(); } destroy() { super.destroy(); this.unregisterClientEvents(); } getActiveClient(): ClientEntry | OutOfViewClient | undefined { return this.activeClient; } currentClientId() { 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) { return; } if(this.activeClient instanceof ClientEntry) { this.activeClient.setUnread(false); /* clear the unread flag */ if(client instanceof ClientEntry) { this.registerChatEvent({ type: "partner-instance-changed", oldClient: this.activeClient.clientNickName(), newClient: client.clientNickName(), timestamp: Date.now(), uniqueId: "pic-" + this.chatId + "-" + Date.now() + "-" + (++receivingEventUniqueIdIndex) }, false); } } this.unregisterClientEvents(); this.activeClient = client; if(this.activeClient instanceof ClientEntry) { this.registerClientEvents(this.activeClient); } this.updateClientInfo(); } hasUnreadMessages() : boolean { return this.unreadTimestamp !== undefined; } handleIncomingMessage(client: ClientEntry | OutOfViewClient, isOwnMessage: boolean, message: ChatMessage) { if(!isOwnMessage) { this.setActiveClientEntry(client); } 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) { return; } this.registerChatEvent({ type: "partner-action", action: "close", timestamp: Date.now(), uniqueId: "pa-" + this.chatId + "-" + Date.now() + "-" + (++receivingEventUniqueIdIndex) }, true); } handleClientEnteredView(client: ClientEntry, mode: "server-join" | "local-reconnect" | "appear") { if(mode === "local-reconnect") { this.registerChatEvent({ type: "local-action", action: "reconnect", timestamp: Date.now(), uniqueId: "la-" + this.chatId + "-" + Date.now() + "-" + (++receivingEventUniqueIdIndex) }, false); } else if(this.lastClientInfo.clientId === 0 || mode === "server-join") { this.registerChatEvent({ type: "partner-action", action: "reconnect", timestamp: Date.now(), uniqueId: "pa-" + this.chatId + "-" + Date.now() + "-" + (++receivingEventUniqueIdIndex) }, true); } this.setActiveClientEntry(client); } handleRemoteComposing(_clientId: number) { this.events.fire("notify_partner_typing", { }); } sendMessage(text: string) { 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) { this.doSendMessage(text, 1, this.activeClient.clientId).then(succeeded => succeeded && (this.conversationOpen = true)); } else { this.registerChatEvent({ type: "message-failed", uniqueId: "msf-" + this.chatId + "-" + Date.now(), timestamp: Date.now(), error: "error", errorMessage: tr("target client is offline/invisible") }, false); } } sendChatClose() { if(!this.conversationOpen) { return; } this.conversationOpen = false; if(this.lastClientInfo.clientId > 0 && this.connection.connected) { this.connection.serverConnection.send_command("clientchatclosed", { clid: this.lastClientInfo.clientId }, { process_result: false }).catch(() => { /* nothing really to do here */ }); } } handleEventLeftView(event: ChannelTreeEvents["notify_client_leave_view"]) { if(event.client !== this.activeClient) { return; } if(event.isServerLeave) { this.setActiveClientEntry(undefined); this.registerChatEvent({ type: "partner-action", action: "disconnect", timestamp: Date.now(), uniqueId: "pa-" + this.chatId + "-" + Date.now() + "-" + (++receivingEventUniqueIdIndex) }, true); } else { this.setActiveClientEntry({ uniqueId: event.client.clientUid(), nickname: event.client.clientNickName(), clientId: event.client.clientId() } as OutOfViewClient) } } private registerClientEvents(client: ClientEntry) { this.activeClientListener = []; this.activeClientListener.push(client.events.on("notify_properties_updated", event => { if('client_nickname' in event.updated_properties) { this.updateClientInfo(); } })); } private unregisterClientEvents() { if(this.activeClientListener === undefined) { return; } this.activeClientListener.forEach(e => e()); this.activeClientListener = undefined; } private updateClientInfo() { let newInfo: OutOfViewClient; if(this.activeClient instanceof ClientEntry) { newInfo = { clientId: this.activeClient.clientId(), nickname: this.activeClient.clientNickName(), uniqueId: this.activeClient.clientUid() }; } else { newInfo = Object.assign({}, this.activeClient); if(!newInfo.nickname) newInfo.nickname = this.lastClientInfo.nickname; if(!newInfo.uniqueId) newInfo.uniqueId = this.clientUniqueId; if(!newInfo.clientId || this.activeClient === undefined) newInfo.clientId = 0; } if(this.lastClientInfo) { if(newInfo.clientId !== this.lastClientInfo.clientId) { this.events.fire("notify_partner_changed", { chatId: this.clientUniqueId, clientId: newInfo.clientId, name: newInfo.nickname }); } else if(newInfo.nickname !== this.lastClientInfo.nickname) { this.events.fire("notify_partner_name_changed", { chatId: this.clientUniqueId, name: newInfo.nickname }); } } this.lastClientInfo = newInfo; this.sendMessageSendingEnabled(this.lastClientInfo.clientId !== 0); } setUnreadTimestamp(timestamp: number) { super.setUnreadTimestamp(timestamp); /* TODO: Move this somehow to the client itself? */ if(this.activeClient instanceof ClientEntry) { this.activeClient.setUnread(this.isUnread()); } } public canClientAccessChat(): boolean { return true; } handleLocalClientDisconnect(explicitDisconnect: boolean) { this.setActiveClientEntry(undefined); if(explicitDisconnect) { this.registerChatEvent({ type: "local-action", uniqueId: "la-" + this.chatId + "-" + Date.now(), timestamp: Date.now(), action: "disconnect" }, false); } } queryCurrentMessages() { 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.setHistory(!!result.hasMore); this.setCurrentMode("normal"); }).catch(error => { console.error("Error open!"); this.presentEvents = []; this.presentMessages = []; this.setHistory(false); this.registerChatEvent({ type: "query-failed", timestamp: Date.now(), uniqueId: "la-" + this.chatId + "-" + Date.now(), message: tr("Failed to query chat history:\n") + error }, false); this.setCurrentMode("normal"); }); } public registerChatEvent(event: ChatEvent, triggerUnread: boolean) { super.registerChatEvent(event, triggerUnread); registerConversationEvent(this.clientUniqueId, event).catch(error => { logWarn(LogCategory.CHAT, tr("Failed to register private conversation chat event for %s: %o"), this.clientUniqueId, error); }); } async queryHistory(criteria: { begin?: number; end?: number; limit?: number }): Promise { const result = await queryConversationEvents(this.clientUniqueId, { limit: criteria.limit, direction: "backwards", begin: criteria.begin, end: criteria.end }); return { status: "success", events: result.events, moreEvents: result.hasMore, nextAllowedQuery: 0 } } } export interface PrivateConversationManagerEvents extends AbstractChatManagerEvents { } export class PrivateConversationManager extends AbstractChatManager { public readonly connection: ConnectionHandler; private channelTreeInitialized = false; constructor(connection: ConnectionHandler) { super(connection); this.connection = connection; 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.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.listenerConnection.push(connection.channelTree.events.on("notify_channel_list_received", _event => { this.channelTreeInitialized = true; })); } destroy() { super.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.getConversations().find(e => e.clientUniqueId === uniqueId); } findOrCreateConversation(client: ClientEntry | OutOfViewClient) { let conversation = this.findConversation(client instanceof ClientEntry ? client : client.uniqueId); if(!conversation) { conversation = new PrivateConversation(this, client); this.registerConversation(conversation); } return conversation; } closeConversation(...conversations: PrivateConversation[]) { for(const conversation of conversations) { conversation.sendChatClose(); this.unregisterConversation(conversation); conversation.destroy(); } } }