@salad-labs/loopz-typescript
Version:
The Official Loopz TypeScript SDK
884 lines • 242 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { Engine, User, Conversation, QIError, Message, ConversationReport, MessageReport, ConversationTradingPool, MessageImportant, ConversationPin, Crypto, } from "./core/chat";
import { addBlockedUser, addImportantToMessage, addMembersToConversation, addPinToConversation, addPinToMessage, addReactionToMessage, addReportToConversation, addReportToMessage, archiveConversation, archiveConversations, createConversationGroup, createConversationOneToOne, deleteBatchConversationMessages, deleteConversationMessage, deleteRequestTrade, editMessage, ejectMember, eraseConversationByAdmin, joinConversation, leaveConversation, muteConversation, removeBlockedUser, removeImportantFromMessage, removePinFromConversation, removePinFromMessage, removeReactionFromMessage, requestTrade, sendMessage, unarchiveConversation, unarchiveConversations, unmuteConversation, updateConversationGroup, updateRequestTrade, updateUserInfo, } from "./constants/chat/mutations";
import { findUsersByUsername, getConversationById, getCurrentUser, listAllActiveUserConversationIds, listConversationsByIds, listConversationsPinnedByCurrentUser, listMessagesByConversationId, listMessagesImportantByUserConversationId, listConversationMemberByUserId, listUsersByIds, listTradesByConversationId, getConversationTradingPoolById, listMessagesByRangeOrder, listMessagesUpdated, getMembersFromConversationById, } from "./constants/chat/queries";
import { ConversationMember } from "./core/chat/conversationmember";
import { onDeleteMessage, onEditMessage, onSendMessage, onRemoveReaction, onAddReaction, onAddPinMessage, onRemovePinMessage, onUpdateConversationGroup, onEjectMember, onLeaveConversation, onAddPinConversation, onRemovePinConversation, onMuteConversation, onUnmuteConversation, onUpdateUser, onRequestTrade, onDeleteRequestTrade, onUpdateRequestTrade, onAddMembersToConversation, onBatchDeleteMessages, onChatMemberEvents, onChatMessageEvents, onChatJoinEvents, } from "./constants/chat/subscriptions";
import { ActiveUserConversationType, ConversationTradingPoolStatus, MessageType, } from "./enums";
import { Converter, findAddedAndRemovedConversation, Serpens } from "./core";
import { Reaction } from "./core/chat/reaction";
import { v4 as uuidv4 } from "uuid";
import { Auth } from ".";
import { DetectiveMessage } from "./core/chat/detectivemessage";
export class Chat extends Engine {
constructor() {
if (!Chat._config)
throw new Error("Chat must be configured before getting the instance");
super(Chat._config);
this._isSyncing = false;
this._syncingCounter = 0;
this._eventsCallbacks = [];
this._unsubscribeSyncSet = [];
this._conversationsMap = [];
this._canChat = false;
this._hookMessageCreated = false;
this._hookMessageUpdated = false;
this._hookMessageDeleted = false;
this._hookConversationCreated = false;
this._hookConversationUpdated = false;
this._syncRunning = false;
this._hookMessageCreatingFn = null;
this._hookMessageUpdatingFn = null;
this._hookConversationCreatingFn = null;
this._hookConversationUpdatingFn = null;
this._syncTimeout = null;
this.SYNCING_TIME_MS = 60000;
this._currentPublicConversation = null;
//let's build the instance of DetectiveMessage, starting the scan immediately
if (!Chat._detectiveMessage) {
DetectiveMessage.config({ storage: Chat._config.storage });
Chat._detectiveMessage = DetectiveMessage.getInstance();
}
this._defineHookFnLocalDB();
this._syncPublicConversationState();
this._setupStorageEventListener();
Chat._instance = this;
}
/** static methods */
static config(config) {
if (Chat._config)
throw new Error("Chat already configured");
Chat._config = config;
}
static getInstance() {
var _a;
return (_a = Chat._instance) !== null && _a !== void 0 ? _a : new Chat();
}
static getDetectiveMessageInstance() {
return Chat._detectiveMessage;
}
static silentRestoreSubscriptionSync() {
if (!Chat._instance)
return;
//let's clear the timeout
if (Chat._instance._syncTimeout)
clearTimeout(Chat._instance._syncTimeout);
//let's unsubscribe everything from the previous results
Chat._instance._removeSubscriptionsSync();
//let's clear the _unsubscribeSyncSet from the previous results
Chat._instance._unsubscribeSyncSet = [];
//add member to conversation. This event is global, basically the user is always listening if
//someone wants to add him into a conversation.
const onAddMembersToConversation = Chat._instance.onAddMembersToConversation((response, source, uuid) => {
Chat._instance._onAddMembersToConversationSync(response, source, uuid);
}, true);
if (!(onAddMembersToConversation instanceof QIError)) {
const { unsubscribe, uuid } = onAddMembersToConversation;
Chat._instance._unsubscribeSyncSet.push({
type: "onAddMembersToConversation",
unsubscribe,
uuid,
});
}
else {
const error = Chat._instance._handleUnauthorizedQIError(onAddMembersToConversation);
if (error) {
;
(() => __awaiter(this, void 0, void 0, function* () {
if (!Chat._instance)
return;
yield Auth.fetchAuthToken();
Chat._instance.silentReset();
}))();
return;
}
Chat._instance._emit("syncError", {
error: onAddMembersToConversation,
});
Chat._instance.unsync();
return;
}
//now that we have a _conversationsMap array filled, we can add subscription for every conversation that is currently active
Chat._instance._addSubscriptionsSync();
(() => __awaiter(this, void 0, void 0, function* () {
if (!Chat._instance)
return;
yield Chat._instance._sync(Chat._instance._syncingCounter);
}))();
}
static unsyncBrutal() {
return new Promise((resolve, reject) => {
if (!Chat._instance) {
reject("instance not setup correctly.");
return;
}
Chat._instance.unsync().then(resolve).catch(reject);
});
}
/** message content handling */
_getMessageContent(content, type) {
if ((type === MessageType.Textual ||
type === MessageType.Attachment ||
type === MessageType.Rent ||
type === MessageType.TradeProposal) &&
typeof content !== "string")
throw new Error("The content of a textual message can not be different from a string.");
switch (type) {
case MessageType.Textual:
case MessageType.Attachment:
case MessageType.Rent:
case MessageType.TradeProposal:
return content;
break;
case MessageType.Nft:
return JSON.stringify(content);
break;
}
}
/** private instance methods */
/**
* Internal method to set the current public conversation
* @param {string} conversationId
*/
_setCurrentPublicConversation(conversationId) {
this._currentPublicConversation = {
conversationId,
};
}
/**
* Synchronize the public conversation state across windows
*/
_syncPublicConversationState() {
const storedConversation = localStorage.getItem(Chat.PUBLIC_CONVERSATION_KEY);
if (storedConversation) {
const parsedConversation = JSON.parse(storedConversation);
this._currentPublicConversation = parsedConversation;
}
else {
this._currentPublicConversation = null;
}
}
/**
* Updates the public conversation state in localStorage
*/
_updatePublicConversationStorage(conversationId) {
if (conversationId) {
// Save the new conversation
localStorage.setItem(Chat.PUBLIC_CONVERSATION_KEY, JSON.stringify({
conversationId,
}));
}
else {
// Remove the current conversation
localStorage.removeItem(Chat.PUBLIC_CONVERSATION_KEY);
}
}
/**
* Adds a listener for cross-window storage events
*/
_setupStorageEventListener() {
window.addEventListener("storage", (event) => {
if (event.key === Chat.PUBLIC_CONVERSATION_KEY) {
// Another window has changed the conversation state
this._syncPublicConversationState();
this._emit("publicConversationChange", {
oldConversation: event.oldValue,
newConversation: event.newValue,
});
}
});
}
_defineHookFnLocalDB() {
this._hookMessageCreatingFn = (primaryKey, record) => {
const _message = Object.assign(Object.assign({}, record), { content: Crypto.decryptAESorFail(record.content, this.findKeyPairById(record.conversationId)), reactions: record.reactions
? record.reactions.map((reaction) => {
return Object.assign(Object.assign({}, reaction), { content: Crypto.decryptAESorFail(reaction.content, this.findKeyPairById(record.conversationId)) });
})
: null, createdAt: new Date(record.createdAt), updatedAt: record.updatedAt ? new Date(record.updatedAt) : null, deletedAt: record.deletedAt ? new Date(record.deletedAt) : null });
_message.messageRoot = record.messageRoot
? Object.assign(Object.assign({}, record.messageRoot), { content: Crypto.decryptAESorFail(record.messageRoot.content, this.findKeyPairById(record.conversationId)), reactions: record.messageRoot.reactions
? record.messageRoot.reactions.map((reaction) => {
return Object.assign(Object.assign({}, reaction), { content: Crypto.decryptAESorFail(reaction.content, this.findKeyPairById(record.conversationId)) });
})
: null, createdAt: new Date(record.messageRoot.createdAt), updatedAt: record.messageRoot.updatedAt
? new Date(record.messageRoot.updatedAt)
: null, deletedAt: record.messageRoot.deletedAt
? new Date(record.messageRoot.deletedAt)
: null }) : null;
this._emit("messageCreatedLDB", _message);
};
this._hookMessageUpdatingFn = (modifications, primaryKey, record) => {
const _message = Object.assign(Object.assign({}, record), { content: Crypto.decryptAESorFail(record.content, this.findKeyPairById(record.conversationId)), reactions: record.reactions
? record.reactions.map((reaction) => {
return Object.assign(Object.assign({}, reaction), { content: Crypto.decryptAESorFail(reaction.content, this.findKeyPairById(record.conversationId)) });
})
: null, createdAt: new Date(record.createdAt), updatedAt: record.updatedAt ? new Date(record.updatedAt) : null, deletedAt: record.deletedAt ? new Date(record.deletedAt) : null });
_message.messageRoot = record.messageRoot
? Object.assign(Object.assign({}, record.messageRoot), { content: Crypto.decryptAESorFail(record.messageRoot.content, this.findKeyPairById(record.conversationId)), reactions: record.messageRoot.reactions
? record.messageRoot.reactions.map((reaction) => {
return Object.assign(Object.assign({}, reaction), { content: Crypto.decryptAESorFail(reaction.content, this.findKeyPairById(record.conversationId)) });
})
: null, createdAt: new Date(record.messageRoot.createdAt), updatedAt: record.messageRoot.updatedAt
? new Date(record.messageRoot.updatedAt)
: null, deletedAt: record.messageRoot.deletedAt
? new Date(record.messageRoot.deletedAt)
: null }) : null;
this._emit("messageUpdatedLDB", _message);
};
this._hookConversationCreatingFn = (primaryKey, record) => {
const _conversation = Object.assign(Object.assign({}, record), { name: Crypto.decryptAESorFail(record.name, this.findKeyPairById(record.id)), description: Crypto.decryptAESorFail(record.description, this.findKeyPairById(record.id)), imageURL: Crypto.decryptAESorFail(record.imageURL, this.findKeyPairById(record.id)), bannerImageURL: Crypto.decryptAESorFail(record.bannerImageURL, this.findKeyPairById(record.id)) });
this._emit("conversationCreatedLDB", _conversation);
};
this._hookConversationUpdatingFn = (modifications, primaryKey, record) => {
const _conversation = Object.assign(Object.assign({}, record), { name: modifications.name
? Crypto.decryptAESorFail(modifications.name, this.findKeyPairById(record.id))
: Crypto.decryptAESorFail(record.name, this.findKeyPairById(record.id)), description: modifications.description
? Crypto.decryptAESorFail(modifications.description, this.findKeyPairById(record.id))
: Crypto.decryptAESorFail(record.description, this.findKeyPairById(record.id)), imageURL: modifications.imageURL
? Crypto.decryptAESorFail(modifications.imageURL, this.findKeyPairById(record.id))
: Crypto.decryptAESorFail(record.imageURL, this.findKeyPairById(record.id)), bannerImageURL: modifications.bannerImageURL
? Crypto.decryptAESorFail(modifications.bannerImageURL, this.findKeyPairById(record.id))
: Crypto.decryptAESorFail(record.bannerImageURL, this.findKeyPairById(record.id)), lastMessageText: modifications.lastMessageText
? Crypto.decryptAESorFail(modifications.lastMessageText, this.findKeyPairById(record.id))
: record.lastMessageText
? Crypto.decryptAESorFail(record.lastMessageText, this.findKeyPairById(record.id))
: null, lastMessageSentAt: modifications.lastMessageSentAt
? modifications.lastMessageSentAt
: record.lastMessageSentAt
? record.lastMessageSentAt
: null, lastMessageAuthor: modifications.lastMessageAuthor
? modifications.lastMessageAuthor
: record.lastMessageAuthor
? record.lastMessageAuthor
: null, lastMessageAuthorId: modifications.lastMessageAuthorId
? modifications.lastMessageAuthorId
: record.lastMessageAuthorId
? record.lastMessageAuthorId
: null, lastMessageSentId: modifications.lastMessageSentId
? modifications.lastMessageSentId
: record.lastMessageSentId
? record.lastMessageSentId
: null, lastMessageSentOrder: modifications.lastMessageSentOrder
? modifications.lastMessageSentOrder
: record.lastMessageSentOrder
? record.lastMessageSentOrder
: null, lastMessageReadId: modifications.lastMessageReadId
? modifications.lastMessageReadId
: record.lastMessageReadId
? record.lastMessageReadId
: null, lastMessageReadOrder: modifications.lastMessageReadOrder
? modifications.lastMessageReadOrder
: record.lastMessageReadOrder
? record.lastMessageReadOrder
: null, messagesToRead: modifications.messagesToRead
? modifications.messagesToRead
: record.messagesToRead, hasLastMessageSentAt: modifications.lastMessageSentAt
? true
: record.hasLastMessageSentAt });
this._emit("conversationUpdatedLDB", _conversation);
};
}
_emit(event, args) {
const index = this._eventsCallbacks.findIndex((item) => {
return item.event === event;
});
if (index > -1)
this,
this._eventsCallbacks[index].callbacks.forEach((callback) => {
callback(args);
});
}
/** syncing data with backend*/
_recoverUserConversations(type) {
return __awaiter(this, void 0, void 0, function* () {
try {
let AUCfirstSet = yield this.listAllActiveUserConversationIds({
type,
}, true);
if (AUCfirstSet instanceof QIError) {
const error = this._handleUnauthorizedQIError(AUCfirstSet);
if (error)
throw "_401_";
throw new Error(JSON.stringify(AUCfirstSet));
}
let { nextToken, items } = AUCfirstSet;
let activeIds = [...items];
while (nextToken) {
const set = yield this.listAllActiveUserConversationIds({
type,
nextToken,
}, true);
if (set instanceof QIError) {
const error = this._handleUnauthorizedQIError(set);
if (error)
throw "_401_";
break;
}
const { nextToken: token, items } = set;
activeIds = [...activeIds, ...items];
if (token)
nextToken = token;
else
break;
}
let conversationfirstSet = yield this.listConversationsByIds(activeIds, true);
if (conversationfirstSet instanceof QIError) {
const error = this._handleUnauthorizedQIError(conversationfirstSet);
if (error)
throw "_401_";
throw new Error(JSON.stringify(conversationfirstSet));
}
let { unprocessedKeys, items: conversations } = conversationfirstSet;
let conversationsItems = [...conversations];
while (unprocessedKeys) {
const set = yield this.listConversationsByIds(unprocessedKeys, true);
if (set instanceof QIError) {
const error = this._handleUnauthorizedQIError(set);
if (error)
throw "_401_";
break;
}
const { unprocessedKeys: ids, items } = set;
conversationsItems = [...conversationsItems, ...items];
if (ids)
unprocessedKeys = ids;
else
break;
}
const currentUser = yield this.getCurrentUser(true);
if (currentUser instanceof QIError) {
const error = this._handleUnauthorizedQIError(currentUser);
if (error)
throw "_401_";
throw new Error(JSON.stringify(currentUser));
}
//stores/update the conversations into the local db
if (conversationsItems.length > 0) {
const conversationsStored = yield new Promise((resolve, reject) => {
Serpens.addAction(() => {
this._storage.conversation
.where("indexDid")
.equals(Auth.account.did)
.toArray()
.then(resolve)
.catch(reject);
});
});
yield this._storage.insertBulkSafe("conversation", conversationsItems.map((conversation) => {
let conversationStored = conversationsStored.find((item) => {
return item.id === conversation.id;
});
let isConversationArchived = false;
if (currentUser.archivedConversations) {
const index = currentUser.archivedConversations.findIndex((id) => {
return id === conversation.id;
});
if (index > -1)
isConversationArchived = true;
}
return Converter.fromConversationToLocalDBConversation(conversation, Auth.account.did, Auth.account.organizationId, conversation.ownerId, isConversationArchived, conversationStored ? conversationStored.lastMessageAuthor : null, conversationStored
? conversationStored.lastMessageAuthorId
: null, conversationStored ? conversationStored.lastMessageText : null, conversationStored ? conversationStored.lastMessageSentId : null, conversationStored
? conversationStored.lastMessageSentOrder
: null, conversationStored ? conversationStored.messagesToRead : 0, conversationStored
? conversationStored.lastMessageReadOrder
: null, conversationStored ? conversationStored.lastMessageReadId : null);
}));
}
return conversationsItems;
}
catch (error) {
if (typeof error === "string" && error === "_401_") {
yield Auth.fetchAuthToken();
this.silentReset();
return "_401_";
}
console.log("[ERROR]: recoverUserConversations() -> ", error);
}
return null;
});
}
_recoverKeysFromConversations() {
return __awaiter(this, void 0, void 0, function* () {
try {
let firstConversationMemberSet = yield this.listConversationMemberByUserId(undefined, true);
if (firstConversationMemberSet instanceof QIError) {
const error = this._handleUnauthorizedQIError(firstConversationMemberSet);
if (error)
throw "_401_";
throw new Error(JSON.stringify(firstConversationMemberSet));
}
let { nextToken, items } = firstConversationMemberSet;
let conversationMemberItems = [...items];
while (nextToken) {
const set = yield this.listConversationMemberByUserId(nextToken, true);
if (set instanceof QIError) {
const error = this._handleUnauthorizedQIError(set);
if (error)
throw "_401_";
break;
}
const { nextToken: token, items } = set;
conversationMemberItems = [...conversationMemberItems, ...items];
if (token)
nextToken = token;
else
break;
}
Chat._config &&
Chat._config.devMode &&
console.log("user key pair ", this.getUserKeyPair());
//now, from the private key of the user, we will decrypt all the information about the conversation member.
//we will store these decrypted pairs public keys/private keys into the _keyPairsMap array.
const _keyPairsMap = [];
let isError = false;
for (const conversationMember of conversationMemberItems) {
const { encryptedConversationIVKey, encryptedConversationAESKey } = conversationMember;
const iv = Crypto.decryptStringOrFail(this.getUserKeyPair().privateKey, encryptedConversationIVKey);
const AES = Crypto.decryptStringOrFail(this.getUserKeyPair().privateKey, encryptedConversationAESKey);
_keyPairsMap.push({
id: conversationMember.conversationId,
AES,
iv,
});
}
if (isError)
throw new Error("Failed to convert a public/private key pair.");
this.setKeyPairMap(_keyPairsMap);
Chat._config &&
Chat._config.devMode &&
console.log("user key pair ", this.getUserKeyPair());
Chat._config &&
Chat._config.devMode &&
console.log("key pair map is ", _keyPairsMap);
return true;
}
catch (error) {
console.log("[ERROR]: recoverKeysFromConversations() -> ", error);
if (typeof error === "string" && error === "_401_") {
yield Auth.fetchAuthToken();
this.silentReset();
return "_401_";
}
}
return false;
});
}
_recoverMessagesFromConversations(conversations) {
return __awaiter(this, void 0, void 0, function* () {
try {
for (const conversation of conversations) {
const { id, lastMessageSentAt } = conversation;
//if the conversation hasn't any message it's useless to download the messages.
if (!lastMessageSentAt)
continue;
//let's see if the last message sent into the conversation is more recent than the last message stored in the database
//messages important handling
const messagesImportantFirstSet = yield this.listMessagesImportantByUserConversationId({
conversationId: id,
}, true);
if (messagesImportantFirstSet instanceof QIError) {
const error = this._handleUnauthorizedQIError(messagesImportantFirstSet);
if (error)
throw "_401_";
throw new Error(JSON.stringify(messagesImportantFirstSet));
}
let { nextToken, items } = messagesImportantFirstSet;
let messagesImportant = [...items];
while (nextToken) {
const set = yield this.listMessagesImportantByUserConversationId({
conversationId: id,
nextToken,
}, true);
if (set instanceof QIError) {
const error = this._handleUnauthorizedQIError(set);
if (error)
throw "_401_";
break;
}
const { nextToken: token, items } = set;
messagesImportant = [...messagesImportant, ...items];
if (token)
nextToken = token;
else
break;
}
//messages handling
let lastMessageStored = yield new Promise((resolve, reject) => {
Serpens.addAction(() => {
this._storage.message
.where("conversationId")
.equals(id)
.filter((element) => element.origin === "USER" &&
element.userDid === Auth.account.did)
.toArray()
.then((array) => {
resolve(array.length > 0
? array.sort((a, b) => {
return (new Date(b.createdAt).getTime() -
new Date(a.createdAt).getTime());
})[0]
: undefined);
})
.catch(reject);
});
});
const canDownloadMessages = !lastMessageStored ||
(lastMessageStored &&
new Date(lastMessageStored.createdAt).getTime() <
new Date(lastMessageSentAt).getTime());
//the check of history message is already done on backend side
if (canDownloadMessages) {
const messagesFirstSet = yield this.listMessagesByConversationId({
id,
}, true);
if (messagesFirstSet instanceof QIError) {
const error = this._handleUnauthorizedQIError(messagesFirstSet);
if (error)
throw "_401_";
throw new Error(JSON.stringify(messagesFirstSet));
}
let { nextToken, items } = messagesFirstSet;
let messages = [...items];
while (nextToken) {
const set = yield this.listMessagesByConversationId({
id,
nextToken,
}, true);
if (set instanceof QIError) {
const error = this._handleUnauthorizedQIError(set);
if (error)
throw "_401_";
break;
}
const { nextToken: token, items } = set;
messages = [...messages, ...items];
if (token)
nextToken = token;
else
break;
}
//let's store the messages without create duplicates
if (messages.length > 0) {
//it's possible this array is empty when the chat history settings has value 'false'
this._storage.insertBulkSafe("message", messages.map((message) => {
const isMessageImportant = messagesImportant.findIndex((important) => {
return important.messageId === message.id;
}) > -1;
return Converter.fromMessageToLocalDBMessage(message, Auth.account.did, Auth.account.organizationId, isMessageImportant, "USER");
}));
yield new Promise((resolve, reject) => {
Serpens.addAction(() => this._storage.conversation
.where("[id+userDid]")
.equals([messages[0].conversationId, Auth.account.did])
.modify((conversation) => {
const lastMessage = messages[0]; //messages are ordered from the most recent to the less recent message, so the first item is the last message sent
conversation.lastMessageAuthor = lastMessage.user.username;
conversation.lastMessageAuthorId = lastMessage.user.id;
conversation.lastMessageText = lastMessage.content;
conversation.lastMessageSentAt = lastMessage.createdAt;
conversation.lastMessageSentId = lastMessage.id;
conversation.lastMessageSentOrder = lastMessage.order;
conversation.messagesToRead =
conversation.messagesToRead +
messages.filter((message) => {
return message.user.id !== Auth.account.dynamoDBUserID;
}).length;
})
.then(resolve)
.catch(reject));
});
//let's collect these messages for our detective message instance (only if this sync is not the first)
/*if (this._syncingCounter > 0)
messages.forEach((message) => {
Chat._detectiveMessage.collectClue(
message,
Auth.account!.did,
Auth.account!.organizationId
)
})*/
}
}
//now we check if we have some messages that were edited (adding/removing a reaction or editing the text)
//first, we get the last sync date from the current user
let user = (yield this._storage.get("user", "[did+organizationId]", [
Auth.account.did,
Auth.account.organizationId,
]));
//we do a check only if lastSyncAt is !== null
if (user.lastSyncAt) {
const { lastSyncAt } = user;
const messagesUpdatedFirstSet = yield this.listMessagesUpdated({
conversationId: id,
greaterThanDate: lastSyncAt,
}, true);
if (messagesUpdatedFirstSet instanceof QIError) {
const error = this._handleUnauthorizedQIError(messagesUpdatedFirstSet);
if (error)
throw "_401_";
throw new Error(JSON.stringify(messagesUpdatedFirstSet));
}
let { nextToken, items } = messagesUpdatedFirstSet;
let messagesUpdated = [...items];
while (nextToken) {
const set = yield this.listMessagesUpdated({
conversationId: id,
greaterThanDate: lastSyncAt,
nextToken,
}, true);
if (set instanceof QIError) {
const error = this._handleUnauthorizedQIError(set);
if (error)
throw "_401_";
break;
}
const { nextToken: token, items } = set;
messagesUpdated = [...messagesUpdated, ...items];
if (token)
nextToken = token;
else
break;
}
//let's update the messages in the local table
if (messagesUpdated.length > 0) {
//it's possible this array is empty when the chat history settings has value 'false'
this._storage.insertBulkSafe("message", messagesUpdated.map((message) => {
const isMessageImportant = messagesImportant.findIndex((important) => {
return important.messageId === message.id;
}) > -1;
return Converter.fromMessageToLocalDBMessage(message, Auth.account.did, Auth.account.organizationId, isMessageImportant, "USER");
}));
}
}
}
return true;
}
catch (error) {
console.log("[ERROR]: recoverMessagesFromConversations() -> ", error);
if (typeof error === "string" && error === "_401_") {
yield Auth.fetchAuthToken();
this.silentReset();
return "_401_";
}
}
return false;
});
}
_sync(syncingCounter) {
return __awaiter(this, void 0, void 0, function* () {
this._isSyncing = true;
this._emit("syncing", this._syncingCounter);
//first operation. Recover the list of the conversations in which the user is a member.
//unactive conversations are the convos in which the user left the group or has been ejected
const activeConversations = yield this._recoverUserConversations(ActiveUserConversationType.Active);
const unactiveConversations = yield this._recoverUserConversations(ActiveUserConversationType.Canceled);
Chat._config &&
Chat._config.devMode &&
console.log("activeConversations", activeConversations);
Chat._config &&
Chat._config.devMode &&
console.log("unactiveConversations", unactiveConversations);
if (!activeConversations || !unactiveConversations) {
this._emit("syncError", { error: `error during conversation syncing.` });
this.unsync();
return;
}
//_sync(..) is called internally by silentReset()
if (activeConversations === "_401_" || unactiveConversations === "_401_")
return;
Chat._config &&
Chat._config.devMode &&
console.log("after check if (!activeConversations || !unactiveConversations)");
//second operation. Recover the list of conversation member objects, in order to retrieve the public & private keys of all conversations.
const keysRecovered = yield this._recoverKeysFromConversations();
Chat._config &&
Chat._config.devMode &&
console.log("keysRecovered", keysRecovered);
if (typeof keysRecovered === "boolean" && !keysRecovered) {
this._emit("syncError", {
error: `error during recovering of the keys from conversations.`,
});
this.unsync();
return;
}
//_sync(..) is called internally by silentReset()
if (keysRecovered === "_401_")
return;
Chat._config &&
Chat._config.devMode &&
console.log("after check if (!keysRecovered)");
//third operation. For each conversation, we need to download the messages if the lastMessageSentAt of the conversation is != null
//and the date of the last message stored in the local db is less recent than the lastMessageSentAt date.
const messagesRecovered = yield this._recoverMessagesFromConversations([
...activeConversations,
...unactiveConversations,
]);
Chat._config &&
Chat._config.devMode &&
console.log("messagesRecovered", messagesRecovered);
if (!messagesRecovered) {
this._emit("syncError", {
error: `error during recovering of the messages from conversations.`,
});
this.unsync();
return;
}
//_sync(..) is called internally by silentReset()
if (messagesRecovered === "_401_")
return;
Chat._config &&
Chat._config.devMode &&
console.log("after check if (!messagesRecovered)");
//let's setup an array of the conversations in the first sync cycle.
//This will allow to map the conversations in every single cycle that comes after the first one.
if (syncingCounter === 0) {
Chat._config &&
Chat._config.devMode &&
console.log("inside check if (syncingCounter === 0)");
for (const activeConversation of activeConversations)
this._conversationsMap.push({
type: "ACTIVE",
conversationId: activeConversation.id,
conversation: activeConversation,
});
for (const unactiveConversation of unactiveConversations)
this._conversationsMap.push({
type: "CANCELED",
conversationId: unactiveConversation.id,
conversation: unactiveConversation,
});
Chat._config &&
Chat._config.devMode &&
console.log("inside check if (syncingCounter === 0) this._conversationsMap is ", this._conversationsMap);
}
else {
//this situation happens when a subscription between onAddMembersToConversation, onEjectMember, onLeaveConversation doesn't fire properly.
//here we can check if there are differences between the previous sync and the current one
//theoretically since we have subscriptions, we should be in a situation in which we don't have any difference
//since the subscription role is to keep the array _conversationsMap synchronized.
//But it can be also the opposite. So inside this block we will check if there are conversations that need
//subscriptions to be added or the opposite (so subscriptions that need to be removed)
Chat._config &&
Chat._config.devMode &&
console.log("inside else syncingCounter is > 0, now its value is ", this._syncingCounter);
const conversations = [...activeConversations, ...unactiveConversations];
const flatConversationMap = this._conversationsMap.map((item) => item.conversation);
const { added, removed } = findAddedAndRemovedConversation(flatConversationMap, conversations);
Chat._config &&
Chat._config.devMode &&
console.log("added and removed are ", added, removed);
if (added.length > 0)
for (const conversation of added) {
const conversationAdded = this._conversationsMap.find((item) => {
return item.conversationId === conversation.id;
});
if (conversationAdded) {
conversationAdded.type = "ACTIVE";
}
else {
this._conversationsMap.push({
type: "ACTIVE",
conversationId: conversation.id,
conversation,
});
}
this._emit("conversationNewMembers", {
conversation,
conversationId: conversation.id,
});
}
if (removed.length > 0) {
for (const conversation of removed) {
const conversationRemoved = this._conversationsMap.find((item) => {
return item.conversationId === conversation.id;
});
if (conversationRemoved)
conversationRemoved.type = "CANCELED";
}
}
}
//we add the internal events for the local database
if (!this._hookMessageCreated)
this._onMessageCreatedLDB();
if (!this._hookMessageUpdated)
this._onMessageUpdatedLDB();
if (!this._hookMessageDeleted)
this._onMessageDeletedLDB();
if (!this._hookConversationCreated)
this._onConversationCreatedLDB();
if (!this._hookConversationUpdated)
this._onConversationUpdatedLDB();
this._isSyncing = false;
Chat._config &&
Chat._config.devMode &&
console.log("ready to emit sync or syncUpdate, this._syncingCounter is ", this._syncingCounter);
syncingCounter === 0
? this._emit("sync")
: this._emit("syncUpdate", this._syncingCounter);
this._syncingCounter++;
Chat._config &&
Chat._config.devMode &&
console.log("let's update the last sync date...");
let user = (yield this._storage.get("user", "[did+organizationId]", [
Auth.account.did,
Auth.account.organizationId,
]));
yield new Promise((resolve, reject) => {
Serpens.addAction(() => {
this._storage.user
.update(user, {
lastSyncAt: new Date(),
})
.then(resolve)
.catch(reject);
});
});
Chat._config &&
Chat._config.devMode &&
console.log("calling another _sync()");
if (this._syncTimeout)
clearTimeout(this._syncTimeout);
this._syncTimeout = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
yield this._sync(this._syncingCounter);
}), this.SYNCING_TIME_MS);
});
}
_onAddMembersToConversationSync(response, source, uuid) {
return __awaiter(this, void 0, void 0, function* () {
let operation = null;
if (response instanceof QIError) {
this._emit("addMembersToConversationSubscriptionError", response);
return;
}
try {
const keypairMap = this.getKeyPairMap();
const alreadyMember = keypairMap.find((item) => {
return item.id === response.conversationId;
});
operation = alreadyMember ? "add_new_members" : "create_conversation";
if (!alreadyMember) {
//we need to update the _keyPairsMap with the new keys of the new conversation
const { conversationId, items } = response;
const item = items.find((item) => { var _a; return item.userId === ((_a = Auth.account) === null || _a === void 0 ? void 0 : _a.dynamoDBUserID); });
const { encryptedConversationIVKey, encryptedConversationAESKey } = item;
//these pair is encrypted with the public key of the current user, so we need to decrypt them
const conversationIVKey = Crypto.decryptStringOrFail(this._userKeyPair.privateKey, encryptedConversationIVKey);
const conversationAESKey = Crypto.decryptStringOrFail(this._userKeyPair.privateKey, encryptedConversationAESKey);
//this add a key pair only if it doesn't exist. if it does, then internally skip this operation
this.addKeyPairItem({
id: conversationId,
AES: conversationAESKey,
iv: conversationIVKey,
});
//we update also the _unsubscribeSyncSet array using the uuid emitted by the subscription
//in order to map the unsubscribe function with the conversation
const index = this._unsubscribeSyncSet.findIndex((item) => {
return item.u