TeaWeb/shared/js/conversations/PrivateConversationManager.ts

389 lines
14 KiB
TypeScript

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<PrivateConversationEvents> {
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<ConversationHistoryResponse> {
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<PrivateConversation> { }
export class PrivateConversationManager extends AbstractChatManager<PrivateConversationManagerEvents, PrivateConversation, PrivateConversationEvents> {
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();
}
}
}