diff --git a/app/definitions/IMessage.ts b/app/definitions/IMessage.ts index 423883b32..4b7183d10 100644 --- a/app/definitions/IMessage.ts +++ b/app/definitions/IMessage.ts @@ -54,7 +54,7 @@ export interface ILastMessage { } export interface IMessage { - _id?: string; + _id: string; msg?: string; t?: SubscriptionType; ts: Date; diff --git a/app/definitions/IThread.ts b/app/definitions/IThread.ts index 3edefbcad..cc9271c8c 100644 --- a/app/definitions/IThread.ts +++ b/app/definitions/IThread.ts @@ -73,6 +73,7 @@ export interface IThread { autoTranslate?: boolean; translations?: any; e2e?: string; + subscription: { id: string }; } export type TThreadModel = IThread & Model; diff --git a/app/definitions/IThreadMessage.ts b/app/definitions/IThreadMessage.ts index c773e4dc5..d4ab3fca0 100644 --- a/app/definitions/IThreadMessage.ts +++ b/app/definitions/IThreadMessage.ts @@ -38,7 +38,7 @@ export interface IThreadMessage { autoTranslate?: boolean; translations?: ITranslations[]; e2e?: string; - subscription?: { id: string }; + subscription: { id: string }; } export type TThreadMessageModel = IThreadMessage & Model; diff --git a/app/lib/database/services/Subscription.js b/app/lib/database/services/Subscription.ts similarity index 59% rename from app/lib/database/services/Subscription.js rename to app/lib/database/services/Subscription.ts index 19be6d58f..4f68f4320 100644 --- a/app/lib/database/services/Subscription.js +++ b/app/lib/database/services/Subscription.ts @@ -1,9 +1,10 @@ import database from '..'; +import { TAppDatabase } from '../interfaces'; import { SUBSCRIPTIONS_TABLE } from '../model/Subscription'; -const getCollection = db => db.get(SUBSCRIPTIONS_TABLE); +const getCollection = (db: TAppDatabase) => db.get(SUBSCRIPTIONS_TABLE); -export const getSubscriptionByRoomId = async rid => { +export const getSubscriptionByRoomId = async (rid: string) => { const db = database.active; const subCollection = getCollection(db); try { diff --git a/app/lib/methods/updateMessages.js b/app/lib/methods/updateMessages.js deleted file mode 100644 index 4b7dfdeb7..000000000 --- a/app/lib/methods/updateMessages.js +++ /dev/null @@ -1,174 +0,0 @@ -import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; -import { Q } from '@nozbe/watermelondb'; - -import log from '../../utils/log'; -import database from '../database'; -import { Encryption } from '../encryption'; -import { MESSAGE_TYPE_ANY_LOAD } from '../../constants/messageTypeLoad'; -import { generateLoadMoreId } from '../utils'; -import protectedFunction from './helpers/protectedFunction'; -import buildMessage from './helpers/buildMessage'; - -export default function updateMessages({ rid, update = [], remove = [], loaderItem }) { - try { - if (!((update && update.length) || (remove && remove.length))) { - return; - } - const db = database.active; - return db.action(async () => { - // Decrypt these messages - update = await Encryption.decryptMessages(update); - const subCollection = db.get('subscriptions'); - let sub; - try { - sub = await subCollection.find(rid); - } catch (error) { - sub = { id: rid }; - log(new Error('updateMessages: subscription not found')); - } - - const messagesIds = [...update.map(m => m._id), ...remove.map(m => m._id)]; - const msgCollection = db.get('messages'); - const threadCollection = db.get('threads'); - const threadMessagesCollection = db.get('thread_messages'); - const allMessagesRecords = await msgCollection - .query(Q.where('rid', rid), Q.or(Q.where('id', Q.oneOf(messagesIds)), Q.where('t', Q.oneOf(MESSAGE_TYPE_ANY_LOAD)))) - .fetch(); - const allThreadsRecords = await threadCollection.query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds))).fetch(); - const allThreadMessagesRecords = await threadMessagesCollection - .query(Q.where('subscription_id', rid), Q.where('id', Q.oneOf(messagesIds))) - .fetch(); - - update = update.map(m => buildMessage(m)); - - // filter messages - let msgsToCreate = update.filter(i1 => !allMessagesRecords.find(i2 => i1._id === i2.id)); - let msgsToUpdate = allMessagesRecords.filter(i1 => update.find(i2 => i1.id === i2._id)); - - // filter threads - const allThreads = update.filter(m => m.tlm); - let threadsToCreate = allThreads.filter(i1 => !allThreadsRecords.find(i2 => i1._id === i2.id)); - let threadsToUpdate = allThreadsRecords.filter(i1 => allThreads.find(i2 => i1.id === i2._id)); - - // filter thread messages - const allThreadMessages = update.filter(m => m.tmid); - let threadMessagesToCreate = allThreadMessages.filter(i1 => !allThreadMessagesRecords.find(i2 => i1._id === i2.id)); - let threadMessagesToUpdate = allThreadMessagesRecords.filter(i1 => allThreadMessages.find(i2 => i1.id === i2._id)); - - // filter loaders to delete - let loadersToDelete = allMessagesRecords.filter(i1 => update.find(i2 => i1.id === generateLoadMoreId(i2._id))); - - // Delete - let msgsToDelete = []; - let threadsToDelete = []; - let threadMessagesToDelete = []; - if (remove && remove.length) { - msgsToDelete = allMessagesRecords.filter(i1 => remove.find(i2 => i1.id === i2._id)); - msgsToDelete = msgsToDelete.map(m => m.prepareDestroyPermanently()); - threadsToDelete = allThreadsRecords.filter(i1 => remove.find(i2 => i1.id === i2._id)); - threadsToDelete = threadsToDelete.map(t => t.prepareDestroyPermanently()); - threadMessagesToDelete = allThreadMessagesRecords.filter(i1 => remove.find(i2 => i1.id === i2._id)); - threadMessagesToDelete = threadMessagesToDelete.map(tm => tm.prepareDestroyPermanently()); - } - - // Delete loaders - loadersToDelete = loadersToDelete.map(m => m.prepareDestroyPermanently()); - if (loaderItem) { - loadersToDelete.push(loaderItem.prepareDestroyPermanently()); - } - - // Create - msgsToCreate = msgsToCreate.map(message => - msgCollection.prepareCreate( - protectedFunction(m => { - m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema); - m.subscription.id = sub.id; - Object.assign(m, message); - }) - ) - ); - threadsToCreate = threadsToCreate.map(thread => - threadCollection.prepareCreate( - protectedFunction(t => { - t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema); - t.subscription.id = sub.id; - Object.assign(t, thread); - }) - ) - ); - threadMessagesToCreate = threadMessagesToCreate.map(threadMessage => - threadMessagesCollection.prepareCreate( - protectedFunction(tm => { - tm._raw = sanitizedRaw({ id: threadMessage._id }, threadMessagesCollection.schema); - Object.assign(tm, threadMessage); - tm.subscription.id = sub.id; - tm.rid = threadMessage.tmid; - delete threadMessage.tmid; - }) - ) - ); - - // Update - msgsToUpdate = msgsToUpdate.map(message => { - const newMessage = update.find(m => m._id === message.id); - try { - return message.prepareUpdate( - protectedFunction(m => { - Object.assign(m, newMessage); - }) - ); - } catch { - return null; - } - }); - threadsToUpdate = threadsToUpdate.map(thread => { - const newThread = allThreads.find(t => t._id === thread.id); - try { - return thread.prepareUpdate( - protectedFunction(t => { - Object.assign(t, newThread); - }) - ); - } catch { - return null; - } - }); - threadMessagesToUpdate = threadMessagesToUpdate.map(threadMessage => { - const newThreadMessage = allThreadMessages.find(t => t._id === threadMessage.id); - try { - return threadMessage.prepareUpdate( - protectedFunction(tm => { - Object.assign(tm, newThreadMessage); - tm.rid = threadMessage.tmid; - delete threadMessage.tmid; - }) - ); - } catch { - return null; - } - }); - - const allRecords = [ - ...msgsToCreate, - ...msgsToUpdate, - ...msgsToDelete, - ...threadsToCreate, - ...threadsToUpdate, - ...threadsToDelete, - ...threadMessagesToCreate, - ...threadMessagesToUpdate, - ...threadMessagesToDelete, - ...loadersToDelete - ]; - - try { - await db.batch(...allRecords); - } catch (e) { - log(e); - } - return allRecords.length; - }); - } catch (e) { - log(e); - } -} diff --git a/app/lib/methods/updateMessages.ts b/app/lib/methods/updateMessages.ts new file mode 100644 index 000000000..f273aaead --- /dev/null +++ b/app/lib/methods/updateMessages.ts @@ -0,0 +1,183 @@ +import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; +import { Q } from '@nozbe/watermelondb'; + +import database from '../database'; +import { Encryption } from '../encryption'; +import { MESSAGE_TYPE_ANY_LOAD } from '../../constants/messageTypeLoad'; +import { generateLoadMoreId } from '../utils'; +import protectedFunction from './helpers/protectedFunction'; +import buildMessage from './helpers/buildMessage'; +import { IMessage, TMessageModel, TThreadMessageModel, TThreadModel } from '../../definitions'; +import { getSubscriptionByRoomId } from '../database/services/Subscription'; + +interface IUpdateMessages { + rid: string; + update: IMessage[]; + remove: IMessage[]; + loaderItem?: TMessageModel; +} + +export default async function updateMessages({ + rid, + update = [], + remove = [], + loaderItem +}: IUpdateMessages): Promise { + if (!((update && update.length) || (remove && remove.length))) { + return Promise.resolve(0); + } + + const sub = await getSubscriptionByRoomId(rid); + if (!sub) { + throw new Error('updateMessages: subscription not found'); + } + + const db = database.active; + return db.write(async () => { + // Decrypt these messages + update = await Encryption.decryptMessages(update); + + const messagesIds: string[] = [...update.map(m => m._id), ...remove.map(m => m._id)]; + const msgCollection = db.get('messages'); + const threadCollection = db.get('threads'); + const threadMessagesCollection = db.get('thread_messages'); + const allMessagesRecords = await msgCollection + .query(Q.where('rid', rid), Q.or(Q.where('id', Q.oneOf(messagesIds)), Q.where('t', Q.oneOf(MESSAGE_TYPE_ANY_LOAD)))) + .fetch(); + const allThreadsRecords = await threadCollection.query(Q.where('rid', rid), Q.where('id', Q.oneOf(messagesIds))).fetch(); + const allThreadMessagesRecords = await threadMessagesCollection + .query(Q.where('subscription_id', rid), Q.where('id', Q.oneOf(messagesIds))) + .fetch(); + + update = update.map(m => buildMessage(m)); + + // filter loaders to delete + let loadersToDelete: TMessageModel[] = allMessagesRecords.filter(i1 => + update.find(i2 => i1.id === generateLoadMoreId(i2._id)) + ); + + // Delete + let msgsToDelete: TMessageModel[] = []; + let threadsToDelete: TThreadModel[] = []; + let threadMessagesToDelete: TThreadMessageModel[] = []; + if (remove && remove.length) { + msgsToDelete = allMessagesRecords.filter(i1 => remove.find(i2 => i1.id === i2._id)); + msgsToDelete = msgsToDelete.map(m => m.prepareDestroyPermanently()); + threadsToDelete = allThreadsRecords.filter(i1 => remove.find(i2 => i1.id === i2._id)); + threadsToDelete = threadsToDelete.map(t => t.prepareDestroyPermanently()); + threadMessagesToDelete = allThreadMessagesRecords.filter(i1 => remove.find(i2 => i1.id === i2._id)); + threadMessagesToDelete = threadMessagesToDelete.map(tm => tm.prepareDestroyPermanently()); + } + + // Delete loaders + loadersToDelete = loadersToDelete.map(m => m.prepareDestroyPermanently()); + if (loaderItem) { + loadersToDelete.push(loaderItem.prepareDestroyPermanently()); + } + + // filter messages + const filteredMsgsToCreate = update.filter(i1 => !allMessagesRecords.find(i2 => i1._id === i2.id)); + const filteredMsgsToUpdate = allMessagesRecords.filter(i1 => update.find(i2 => i1.id === i2._id)); + + // filter threads + const allThreads = update.filter(m => m.tlm); + const filteredThreadsToCreate = allThreads.filter(i1 => !allThreadsRecords.find(i2 => i1._id === i2.id)); + const filteredThreadsToUpdate = allThreadsRecords.filter(i1 => allThreads.find(i2 => i1.id === i2._id)); + + // filter thread messages + const allThreadMessages = update.filter(m => m.tmid); + const filteredThreadMessagesToCreate = allThreadMessages.filter(i1 => !allThreadMessagesRecords.find(i2 => i1._id === i2.id)); + const filteredThreadMessagesToUpdate = allThreadMessagesRecords.filter(i1 => allThreadMessages.find(i2 => i1.id === i2._id)); + + // Create + const msgsToCreate = filteredMsgsToCreate.map(message => + msgCollection.prepareCreate( + protectedFunction((m: TMessageModel) => { + m._raw = sanitizedRaw({ id: message._id }, msgCollection.schema); + m.subscription.id = sub.id; + Object.assign(m, message); + }) + ) + ); + const threadsToCreate = filteredThreadsToCreate.map(thread => + threadCollection.prepareCreate( + protectedFunction((t: TThreadModel) => { + t._raw = sanitizedRaw({ id: thread._id }, threadCollection.schema); + t.subscription.id = sub.id; + Object.assign(t, thread); + }) + ) + ); + const threadMessagesToCreate = filteredThreadMessagesToCreate.map(threadMessage => + threadMessagesCollection.prepareCreate( + protectedFunction((tm: TThreadMessageModel) => { + tm._raw = sanitizedRaw({ id: threadMessage._id }, threadMessagesCollection.schema); + Object.assign(tm, threadMessage); + tm.subscription.id = sub.id; + if (threadMessage.tmid) { + tm.rid = threadMessage.tmid; + } + delete threadMessage.tmid; + }) + ) + ); + + // Update + const msgsToUpdate = filteredMsgsToUpdate.map(message => { + const newMessage = update.find(m => m._id === message.id); + try { + return message.prepareUpdate( + protectedFunction((m: TMessageModel) => { + Object.assign(m, newMessage); + }) + ); + } catch { + return null; + } + }); + const threadsToUpdate = filteredThreadsToUpdate.map(thread => { + const newThread = allThreads.find(t => t._id === thread.id); + try { + return thread.prepareUpdate( + protectedFunction((t: TThreadModel) => { + Object.assign(t, newThread); + }) + ); + } catch { + return null; + } + }); + const threadMessagesToUpdate = filteredThreadMessagesToUpdate.map(threadMessage => { + const newThreadMessage = allThreadMessages.find(t => t._id === threadMessage.id); + try { + return threadMessage.prepareUpdate( + protectedFunction((tm: TThreadMessageModel) => { + Object.assign(tm, newThreadMessage); + if (threadMessage.tmid) { + tm.rid = threadMessage.tmid; + } + delete threadMessage.tmid; + }) + ); + } catch { + return null; + } + }); + + const allRecords = [ + ...msgsToDelete, + ...threadsToDelete, + ...threadMessagesToDelete, + ...loadersToDelete, + ...msgsToCreate, + ...msgsToUpdate, + ...threadsToCreate, + ...threadsToUpdate, + ...threadMessagesToCreate, + ...threadMessagesToUpdate + ]; + + await db.batch(...allRecords); + return allRecords.length; + }); +} diff --git a/e2e/helpers/app.js b/e2e/helpers/app.js index df696cff6..b929b4dc6 100644 --- a/e2e/helpers/app.js +++ b/e2e/helpers/app.js @@ -191,6 +191,9 @@ const checkServer = async server => { await waitFor(element(by.label(label))) .toBeVisible() .withTimeout(10000); + await waitFor(element(by.id('sidebar-close-drawer'))) + .toBeVisible() + .withTimeout(10000); await element(by.id('sidebar-close-drawer')).tap(); }; diff --git a/e2e/tests/assorted/11-deeplinking.spec.js b/e2e/tests/assorted/11-deeplinking.spec.js index 4917b279e..3f8e7864c 100644 --- a/e2e/tests/assorted/11-deeplinking.spec.js +++ b/e2e/tests/assorted/11-deeplinking.spec.js @@ -41,7 +41,7 @@ describe('Deep linking', () => { }); await waitFor(element(by[textMatcher]("You've been logged out by the server. Please log in again."))) .toExist() - .withTimeout(10000); // TODO: we need to improve this message + .withTimeout(30000); // TODO: we need to improve this message }); const authAndNavigate = async () => { @@ -97,7 +97,7 @@ describe('Deep linking', () => { }); await waitFor(element(by.id(`room-view-title-${data.groups.private.name}`))) .toExist() - .withTimeout(10000); + .withTimeout(30000); }); it('should navigate to the thread using path', async () => { @@ -108,7 +108,7 @@ describe('Deep linking', () => { }); await waitFor(element(by.id(`room-view-title-${threadMessage}`))) .toExist() - .withTimeout(10000); + .withTimeout(30000); }); it('should navigate to the room using rid', async () => { @@ -120,7 +120,7 @@ describe('Deep linking', () => { }); await waitFor(element(by.id(`room-view-title-${data.groups.private.name}`))) .toExist() - .withTimeout(15000); + .withTimeout(30000); await tapBack(); }); }); @@ -144,7 +144,7 @@ describe('Deep linking', () => { }); await waitFor(element(by.id(`room-view-title-${data.groups.private.name}`))) .toExist() - .withTimeout(10000); + .withTimeout(30000); }); it('should add a not existing server and fallback to the previous one', async () => { @@ -155,7 +155,7 @@ describe('Deep linking', () => { }); await waitFor(element(by.id('rooms-list-view'))) .toBeVisible() - .withTimeout(10000); + .withTimeout(30000); await checkServer(data.server); }); });