389 lines
14 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
} |