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 { protected readonly connection: ConnectionHandler; protected readonly chatId: string; protected readonly events: Registry; protected presentMessages: ChatEvent[] = []; protected presentEvents: Exclude[] = []; /* 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) { 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 { 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; public abstract queryCurrentMessages(); public abstract sendMessage(text: string); } export abstract class AbstractChatManager { protected readonly uiEvents: Registry; protected constructor() { this.uiEvents = new Registry(); } handlePanelShow() { this.uiEvents.fire("notify_panel_show"); } protected abstract findChat(id: string) : AbstractChat; @EventHandler("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("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("action_clear_unread_flag") protected handleClearUnreadFlag(event: ConversationUIEvents["action_clear_unread_flag"]) { this.findChat(event.chatId)?.setUnreadTimestamp(undefined); } @EventHandler("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("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("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(); } }