TeaWeb/shared/js/ui/frames/side/AbstractConversion.ts

393 lines
No EOL
15 KiB
TypeScript

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} 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_async("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_async("notify_conversation_state", {
chatId: this.chatId,
state: "private",
crossChannelChatSupported: this.crossChannelChatSupported
});
return;
}
this.events.fire_async("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_async("notify_conversation_state", {
chatId: this.chatId,
state: "loading"
});
break;
case "error":
this.events.fire_async("notify_conversation_state", {
chatId: this.chatId,
state: "error",
errorMessage: this.errorMessage
});
break;
case "no-permissions":
this.events.fire_async("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_async("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_async("notify_conversation_history", {
chatId: this.chatId,
state: "success",
hasMoreMessages: result.moreEvents,
retryTimestamp: this.historyRetryTimestamp,
events: result.events
});
break;
case "private":
this.events.fire_async("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_async("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_async("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_async("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_async("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();
}
}