src/chat/ChatManager.js
import {List, Map} from 'immutable';
import Logs from '../Logs';
import {bindMethods} from '../utils/bindMethods';
import {EventEmitter} from '../eventemitter';
import {Chat} from './Chat';
import {ChatType} from './ChatType';
import {ChatParticipantRole} from './ChatParticipantRole';
import {uuid} from 'stanza/Utils';
import {CHAT_INVITE_REASONS} from '../wac-proxy/wac-stack/ChatService';
const log = Logs.instance.getLogger('SippoJS/chat/ChatManager');
function sortByTimestamp(chatA, chatB) {
const lastA = chatA.getLastMessageTimestamp();
const lastB = chatB.getLastMessageTimestamp();
if (lastA < lastB) {
return 1;
}
if (lastA > lastB) {
return -1;
}
return 0;
}
/**
* Provides access to methods for using chat.
* A Chat manager object is obtained by calling the {@link Session#getChatManager} method.
*
* # Events
* - `chats` - Emitted every time the chat list changes.
* - `conferenceChats` - Emitted every time the conference chats list changes.
* - `file` - Emitted every time a new chat message containing a file is received.
* - `message` - Emitted every time a new chat message is received.
* - `messageDisplayed` - Emitted every time messages are marked as displayed by this user.
* - `supportChat` - Emitted every time a support chat is received.
*
* @example <caption>Getting chats</caption>
* const chatManager = await session.getChatManager();
* const chats = chatManager.getChats();
*
* @example <caption>Obtaining an individual chat</caption>
* const chat = chatManager.getIndividualChat('alice@quobis');
*/
export class ChatManager {
/**
* @protected
* @return {Promise<ChatManager>}
*/
static async newInstance(chatService, fileManager, user, sessionToken) {
const chatManager = new ChatManager(chatService, fileManager, user, sessionToken);
return chatManager.init();
}
/** @private */
constructor(chatService, fileManager, user, sessionToken) {
/** @private */
this.ownAddress = user.getAddress().toLowerCase();
/** @private */
this.chatService = chatService;
/** @private */
this.fileManager = fileManager;
/** @private */
this.user = user;
/** @private */
this.sessionToken = sessionToken;
/** @private */
this.individualChats = new Map();
/** @private */
this.groupChats = new Map();
/** @private */
this.chats = new List();
/** @private */
this.conferenceChats = new List();
this.supportChats = new List();
/** @type {EventEmitter} */
this.emitter = new EventEmitter();
bindMethods(this, [
'onIndividualChatAdded',
'onIndividualChatDeleted',
'onMucInvite',
'onMucEnd',
'onChat',
'onDisplayed',
'onMessage',
'onMessageSent',
'onFile',
]);
}
/** @private */
async init() {
this.bindEventHandlers();
await this.chatService.start();
return this;
}
/** @private */
async fetchIndividualChats() {
const jids = await this.chatService.getIndividualChats();
this.individualChats = this.deleteOutdatedChats(this.individualChats, jids);
for (const jid of jids) {
if (this.individualChats.has(jid)) {
continue;
}
this.onIndividualChatAdded(jid);
}
}
/** @private */
async fetchGroupChats() {
const bookmarks = await this.chatService.getBookmarks();
this.groupChats = this.deleteOutdatedChats(this.groupChats, bookmarks);
await Promise.all(bookmarks.map(async (jid) => {
try {
await this.chatService.joinRoom(jid, this.ownAddress);
if (this.groupChats.has(jid)) {
return;
}
const chat = await this.newGroupChat(jid);
this.groupChats = this.groupChats.set(jid, chat);
this.chats = this.chats.unshift(chat);
} catch (error) {
log.warn(`Unable to join MUC ${jid}`);
log.warn(error);
this.onMucEnd({chatId: jid});
}
}));
}
/** @private */
deleteOutdatedChats(immutableMap, validIds) {
const remotelyDeletedIds = [...immutableMap.keys()].filter(id => !validIds.includes(id));
this.chats = this.chats.filter(chat => !remotelyDeletedIds.includes(chat.getId()));
return immutableMap.withMutations((map) => {
for (const id of remotelyDeletedIds) {
const chat = map.get(id);
chat.unbindEventListeners();
map.delete(id);
}
});
}
/** @protected */
uninit() {
this.unbindEventHandlers();
}
/** @private */
bindEventHandlers() {
this.chatService.emitter.on('individualChatAdded', this.onIndividualChatAdded);
this.chatService.emitter.on('individualChatDeleted', this.onIndividualChatDeleted);
this.chatService.emitter.on('mucInvite', this.onMucInvite);
this.chatService.emitter.on('mucEnd', this.onMucEnd);
this.chatService.emitter.on('chat', this.onChat);
this.chatService.emitter.on(['displayed', '*'], this.onDisplayed);
this.chatService.emitter.on(['message:sent', '*'], this.onMessageSent);
this.chatService.emitter.on(['message', '*'], this.onMessage);
this.chatService.emitter.on(['file', '*'], this.onFile);
}
/** @private */
unbindEventHandlers() {
this.chatService.emitter.off('individualChatAdded', this.onIndividualChatAdded);
this.chatService.emitter.off('individualChatDeleted', this.onIndividualChatDeleted);
this.chatService.emitter.off('mucInvite', this.onMucInvite);
this.chatService.emitter.off('mucEnd', this.onMucEnd);
this.chatService.emitter.off('chat', this.onChat);
this.chatService.emitter.off(['displayed', '*'], this.onDisplayed);
this.chatService.emitter.off(['message:sent', '*'], this.onMessageSent);
this.chatService.emitter.off(['message', '*'], this.onMessage);
this.chatService.emitter.off(['file', '*'], this.onFile);
}
/** @private */
newIndividualChat(id) {
const type = ChatType.INDIVIDUAL;
const participants = [{
jid: this.ownAddress,
affiliation: ChatParticipantRole.MEMBER,
}, {
jid: id,
affiliation: ChatParticipantRole.MEMBER,
}];
const chat = Chat.newInstance(this.chatService, this.fileManager, this.user, {id, type, participants});
chat.bindEventListeners();
return chat;
}
/** @private */
async newGroupChat(id) {
const participants = await this.chatService.getRoomMembers(id);
const info = await this.chatService.getRoomInfo(id);
const name = info.name;
const subject = info.subject;
const type = ChatType.GROUP;
const chat = Chat.newInstance(this.chatService, this.fileManager, this.user, {id, type, name, subject, participants});
chat.bindEventListeners();
return chat;
}
/** @private */
promoteChat({jid, type}) {
const chat = type === 'chat' ? this.individualChats.get(jid) : this.groupChats.get(jid);
const index = this.chats.indexOf(chat);
if (index > 0) {
this.chats = this.chats.delete(index).unshift(chat);
this.emitter.emit('chats');
}
}
/** @private */
onIndividualChatAdded(jid) {
let chat = this.individualChats.get(jid);
if (!chat) {
chat = this.newIndividualChat(jid);
this.individualChats = this.individualChats.set(jid, chat);
this.chats = this.chats.unshift(chat);
this.emitter.emit('chats');
}
}
/** @private */
onIndividualChatDeleted(jid) {
const chat = this.individualChats.get(jid);
if (chat) {
chat.unbindEventListeners();
this.individualChats = this.individualChats.remove(jid);
this.chats = this.chats.filter(c => c !== chat);
this.emitter.emit('chats');
}
}
/** @private */
onDisplayed(event) {
if (event.from === this.ownAddress) {
this.emitter.emit('messageDisplayed');
}
}
/** @private */
onMessage(event) {
this.emitter.emit('message', {
chat: event.chat, // jid we use for indexing
from: event.from,
to: event.to,
timestamp: event.timestamp,
body: event.body,
type: event.type,
});
this.promoteChat({jid: event.chat, type: event.type});
}
/** @private */
onMessageSent(event) {
if (event.displayed) {
this.emitter.emit('messageDisplayed');
} else if (event.links || event.body) {
this.promoteChat({jid: event.chat, type: event.type});
}
}
/** @private */
onFile(event) {
this.emitter.emit('file', {
chat: event.chat,
from: event.from,
to: event.to,
timestamp: event.timestamp,
desc: event.desc,
url: event.url,
type: event.type,
});
this.promoteChat({jid: event.chat, type: event.type});
}
/** @private */
onChat(event) {
if (event.type === 'chat') {
this.getIndividualChat(event.id);
}
}
/** @private */
async onMucInvite(event) {
const id = event.chatId;
let chat = this.groupChats.get(id);
if (!chat) {
await this.chatService.joinRoom(id, this.ownAddress);
chat = await this.newGroupChat(id);
this.groupChats = this.groupChats.set(id, chat);
await chat.sync();
if (event.conferenceChat) {
this.conferenceChats = this.conferenceChats.unshift(chat);
this.emitter.emit('conferenceChats', chat);
} else if (event.reason === CHAT_INVITE_REASONS.SUPPORT) {
this.supportChats = this.supportChats.unshift(chat);
this.emitter.emit('supportChat', chat);
} else {
await this.chatService.addBookmark(id);
this.chats = this.chats.unshift(chat);
this.emitter.emit('chats', chat);
}
}
}
/** @private */
async onMucEnd(event) {
const chat = this.groupChats.get(event.chatId);
if (!chat) {
return;
}
chat.unbindEventListeners();
this.groupChats = this.groupChats.remove(chat.getId());
if (this.conferenceChats.includes(chat)) {
this.conferenceChats = this.conferenceChats.filter(c => c !== chat);
this.emitter.emit('conferenceChats', chat);
} else {
this.chats = this.chats.filter(c => c !== chat);
await this.chatService.removeBookmark(event.chatId);
this.emitter.emit('chats', chat);
}
}
/**
* Sets the Push Notifications service type and token.
* @param {Object} config
* @param {string} config.jid The bare JID of the Push Server (e.g.: push.sippo).
* @param {"fcm"|"apns"} config.type The type of the Push Server. Supported values are fcm and apns.
* @param {string} config.token The token obtained from the Push Service.
*/
setPushConfig({jid, type, token}) {
const node = this.sessionToken;
/** @private */
this.pushConfig = {jid, type, token, node};
}
/**
* Enables push notifications.
* @return {Promise}
*/
enablePushNotifications() {
return this.chatService.enableNotifications(this.pushConfig);
}
/**
* Disables push notifications.
* @return {Promise}
*/
disablePushNotifications() {
return this.chatService.disableNotifications(this.pushConfig);
}
/**
* Creates a new group chat.
* @param {string} [name=''] The name of the new group chat.
* @param {string} [subject=''] The subject of the new group chat.
* @returns {Promise<Chat>}
*/
async createChat(name = '', subject = '') {
const id = await this.chatService.createRoom(this.ownAddress, name, subject);
await this.chatService.addBookmark(id);
const chat = await this.newGroupChat(id);
this.groupChats = this.groupChats.set(id, chat);
this.chats = this.chats.unshift(chat);
this.emitter.emit('chats', chat);
return chat;
}
/**
* Obtains an individual chat with the specified user.
* @param {string} participant Address of the remote participant of the chat.
* @return {Chat}
*/
getIndividualChat(participant) {
participant = participant.toLowerCase();
let chat = this.individualChats.get(participant);
if (!chat) {
chat = this.newIndividualChat(participant);
this.individualChats = this.individualChats.set(participant, chat);
this.chatService.addIndividualChat(participant);
this.chats = this.chats.unshift(chat);
this.emitter.emit('chats');
}
return chat;
}
/**
* Obtains the specified group chat.
* @param {String} id Address of the group chat.
* @return {Chat}
*/
getGroupChat(id) {
let chat = this.groupChats.get(id);
if (!chat) {
throw Error(`There is no group with id: ${id}`);
}
return chat;
}
/**
* Returns a list with all the chats except the associated with conference rooms.
* @return {ImmutableList<Chat>}
*/
getChats() {
return this.chats;
}
/**
* Returns a list with chats associated to conference rooms.
* @return {ImmutableList<Chat>}
*/
getConferenceChats() {
return this.conferenceChats;
}
/**
* Synchronizes latest chat messages from the sever and reorders the chat list.
* This method needs to be called after initialization and every time session is
* resumed.
*/
async sync() {
await Promise.all([
this.fetchIndividualChats(),
this.fetchGroupChats(),
]);
await this.chatService.syncMucInvitations(this.ownAddress);
await Promise.all(this.chats.map(chat => chat.sync()));
this.chats = this.chats.sort(sortByTimestamp);
this.emitter.emit('chats');
}
/**
* Creates a support chat, which is a group chat where a agent is added in the
* moment of the creation.
* @param {object} context Context of supportChat
* @return {Promise<Chat>}
*/
async createSupportChat(context = {}) {
return new Promise((resolve, reject) => {
const id = uuid();
const onSupportChat = (chat) => {
resolve(chat);
this.emitter.off('supportChat', onSupportChat);
};
this.emitter.on('supportChat', onSupportChat);
this.emitter.on('suppe', reject);
this.chatService.createSupportChat(id, context).catch(reject);
});
}
}