From 34fd945f835cc03f2bbd5b045cf907fb70187c44 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Fri, 10 Dec 2021 14:51:54 -0800 Subject: [PATCH] No Backbone in data layer; server/client interfaces are now similar --- ts/ConversationController.ts | 29 +- ts/background.ts | 3 +- ts/groups/joinViaLink.ts | 4 +- ts/messageModifiers/AttachmentDownloads.ts | 4 +- ts/messageModifiers/Deletes.ts | 10 +- ts/messageModifiers/MessageReceipts.ts | 24 +- ts/messageModifiers/Reactions.ts | 10 +- ts/messageModifiers/ReadSyncs.ts | 11 +- ts/messageModifiers/ViewOnceOpenSyncs.ts | 9 +- ts/messageModifiers/ViewSyncs.ts | 11 +- ts/messages/getMessageById.ts | 13 +- ts/messages/helpers.ts | 103 ++++++ ts/models/conversations.ts | 113 +++---- ts/models/messages.ts | 154 ++------- ts/reactions/enqueueReactionForSend.ts | 3 +- ts/sql/Client.ts | 235 ++++---------- ts/sql/Interface.ts | 296 +++++++----------- ts/sql/Server.ts | 41 ++- ts/state/selectors/message.ts | 2 +- ts/test-electron/models/messages_test.ts | 5 +- ts/test-electron/sql/allMedia_test.ts | 56 +--- ts/test-electron/sql/fullTextSearch_test.ts | 42 +-- ts/test-electron/sql/markRead_test.ts | 72 +---- ts/test-electron/sql/sendLog_test.ts | 2 +- ts/test-electron/sql/stories_test.ts | 28 +- ts/test-electron/sql/timelineFetches_test.ts | 181 +++-------- .../textsecure/KeyChangeListener_test.ts | 12 +- ts/util/MessageController.ts | 12 +- ts/util/cleanup.ts | 36 +++ ts/util/handleRetry.ts | 6 +- ts/views/conversation_view.ts | 67 ++-- 31 files changed, 573 insertions(+), 1021 deletions(-) create mode 100644 ts/messages/helpers.ts create mode 100644 ts/util/cleanup.ts diff --git a/ts/ConversationController.ts b/ts/ConversationController.ts index cf842c23c577..a68496f3f120 100644 --- a/ts/ConversationController.ts +++ b/ts/ConversationController.ts @@ -11,6 +11,7 @@ import type { ConversationAttributesTypeType, } from './model-types.d'; import type { ConversationModel } from './models/conversations'; +import { getContactId } from './messages/helpers'; import { maybeDeriveGroupV2Id } from './groups'; import { assert } from './util/assert'; import { map, reduce } from './util/iterables'; @@ -676,9 +677,7 @@ export class ConversationController { log.warn( 'combineConversations: Delete the obsolete conversation from the database' ); - await removeConversation(obsoleteId, { - Conversation: window.Whisper.Conversation, - }); + await removeConversation(obsoleteId); log.warn('combineConversations: Update messages table'); await migrateConversationMessages(obsoleteId, currentId); @@ -714,13 +713,11 @@ export class ConversationController { targetFromId: string, targetTimestamp: number ): Promise { - const messages = await getMessagesBySentAt(targetTimestamp, { - MessageCollection: window.Whisper.MessageCollection, - }); - const targetMessage = messages.find(m => m.getContactId() === targetFromId); + const messages = await getMessagesBySentAt(targetTimestamp); + const targetMessage = messages.find(m => getContactId(m) === targetFromId); if (targetMessage) { - return targetMessage.getConversation(); + return this.get(targetMessage.conversationId); } return null; @@ -729,9 +726,7 @@ export class ConversationController { async getAllGroupsInvolvingUuid( uuid: UUID ): Promise> { - const groups = await getAllGroupsInvolvingUuid(uuid.toString(), { - ConversationCollection: window.Whisper.ConversationCollection, - }); + const groups = await getAllGroupsInvolvingUuid(uuid.toString()); return groups.map(group => { const existing = this.get(group.id); if (existing) { @@ -767,13 +762,11 @@ export class ConversationController { } try { - const collection = await getAllConversations({ - ConversationCollection: window.Whisper.ConversationCollection, - }); + const collection = await getAllConversations(); // Get rid of temporary conversations const temporaryConversations = collection.filter(conversation => - Boolean(conversation.get('isTemporary')) + Boolean(conversation.isTemporary) ); if (temporaryConversations.length) { @@ -788,16 +781,14 @@ export class ConversationController { }); queue.addAll( temporaryConversations.map(item => async () => { - await removeConversation(item.id, { - Conversation: window.Whisper.Conversation, - }); + await removeConversation(item.id); }) ); await queue.onIdle(); // Hydrate the final set of conversations this._conversations.add( - collection.filter(conversation => !conversation.get('isTemporary')) + collection.filter(conversation => !conversation.isTemporary) ); this._initialFetchComplete = true; diff --git a/ts/background.ts b/ts/background.ts index b47a04275cdb..5ff1af8653d3 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -44,6 +44,7 @@ import { routineProfileRefresh } from './routineProfileRefresh'; import { isMoreRecentThan, isOlderThan, toDayMillis } from './util/timestamp'; import { isValidReactionEmoji } from './reactions/isValidReactionEmoji'; import type { ConversationModel } from './models/conversations'; +import { getContact } from './messages/helpers'; import { getMessageById } from './messages/getMessageById'; import { createBatcher } from './util/batcher'; import { updateConversationsWithUuidLookup } from './updateConversationsWithUuidLookup'; @@ -2841,7 +2842,7 @@ export async function startApp(): Promise { isIncoming(message.attributes) && !message.get('unidentifiedDeliveryReceived') ) { - const sender = message.getContact(); + const sender = getContact(message.attributes); if (!sender) { throw new Error('MessageModel has no sender.'); diff --git a/ts/groups/joinViaLink.ts b/ts/groups/joinViaLink.ts index 158e52eff220..fdd5312c11b9 100644 --- a/ts/groups/joinViaLink.ts +++ b/ts/groups/joinViaLink.ts @@ -318,9 +318,7 @@ export async function joinViaLink(hash: string): Promise { window.ConversationController.dangerouslyRemoveById( tempConversation.id ); - await window.Signal.Data.removeConversation(tempConversation.id, { - Conversation: window.Whisper.Conversation, - }); + await window.Signal.Data.removeConversation(tempConversation.id); } throw error; diff --git a/ts/messageModifiers/AttachmentDownloads.ts b/ts/messageModifiers/AttachmentDownloads.ts index f9204414530e..018cc1f36d2c 100644 --- a/ts/messageModifiers/AttachmentDownloads.ts +++ b/ts/messageModifiers/AttachmentDownloads.ts @@ -196,9 +196,7 @@ async function _runJob(job?: AttachmentDownloadJobType): Promise { const found = window.MessageController.getById(messageId) || - (await getMessageById(messageId, { - Message: window.Whisper.Message, - })); + (await getMessageById(messageId)); if (!found) { logger.error('_runJob: Source message not found, deleting job'); await _finishJob(null, id); diff --git a/ts/messageModifiers/Deletes.ts b/ts/messageModifiers/Deletes.ts index 278c03088605..d2541690eee4 100644 --- a/ts/messageModifiers/Deletes.ts +++ b/ts/messageModifiers/Deletes.ts @@ -5,6 +5,7 @@ import { Collection, Model } from 'backbone'; import type { MessageModel } from '../models/messages'; +import { getContactId } from '../messages/helpers'; import * as log from '../logging/log'; type DeleteAttributesType = { @@ -30,7 +31,7 @@ export class Deletes extends Collection { const matchingDeletes = this.filter(item => { return ( item.get('targetSentTimestamp') === message.get('sent_at') && - item.get('fromId') === message.getContactId() + item.get('fromId') === getContactId(message.attributes) ); }); @@ -68,14 +69,11 @@ export class Deletes extends Collection { log.info('Handling DOE for', del.get('targetSentTimestamp')); const messages = await window.Signal.Data.getMessagesBySentAt( - del.get('targetSentTimestamp'), - { - MessageCollection: window.Whisper.MessageCollection, - } + del.get('targetSentTimestamp') ); const targetMessage = messages.find( - m => del.get('fromId') === m.getContactId() + m => del.get('fromId') === getContactId(m) ); if (!targetMessage) { diff --git a/ts/messageModifiers/MessageReceipts.ts b/ts/messageModifiers/MessageReceipts.ts index 9feb6cf8d970..2a72dab25a87 100644 --- a/ts/messageModifiers/MessageReceipts.ts +++ b/ts/messageModifiers/MessageReceipts.ts @@ -8,7 +8,7 @@ import { Collection, Model } from 'backbone'; import type { ConversationModel } from '../models/conversations'; import type { MessageModel } from '../models/messages'; -import type { MessageModelCollectionType } from '../model-types.d'; +import type { MessageAttributesType } from '../model-types.d'; import { isOutgoing } from '../state/selectors/message'; import { isDirectConversation } from '../util/whatTypeOfConversation'; import { getOwn } from '../util/getOwn'; @@ -60,32 +60,25 @@ const deleteSentProtoBatcher = createWaitBatcher({ async function getTargetMessage( sourceId: string, sourceUuid: UUIDStringType, - messages: MessageModelCollectionType + messages: ReadonlyArray ): Promise { if (messages.length === 0) { return null; } const message = messages.find( - item => - isOutgoing(item.attributes) && sourceId === item.get('conversationId') + item => isOutgoing(item) && sourceId === item.conversationId ); if (message) { return window.MessageController.register(message.id, message); } - const groups = await window.Signal.Data.getAllGroupsInvolvingUuid( - sourceUuid, - { - ConversationCollection: window.Whisper.ConversationCollection, - } - ); + const groups = await window.Signal.Data.getAllGroupsInvolvingUuid(sourceUuid); - const ids = groups.pluck('id'); + const ids = groups.map(item => item.id); ids.push(sourceId); const target = messages.find( - item => - isOutgoing(item.attributes) && ids.includes(item.get('conversationId')) + item => isOutgoing(item) && ids.includes(item.conversationId) ); if (!target) { return null; @@ -147,10 +140,7 @@ export class MessageReceipts extends Collection { try { const messages = await window.Signal.Data.getMessagesBySentAt( - messageSentAt, - { - MessageCollection: window.Whisper.MessageCollection, - } + messageSentAt ); const message = await getTargetMessage( diff --git a/ts/messageModifiers/Reactions.ts b/ts/messageModifiers/Reactions.ts index ee7e88ece162..b9f416c4e10e 100644 --- a/ts/messageModifiers/Reactions.ts +++ b/ts/messageModifiers/Reactions.ts @@ -5,6 +5,7 @@ import { Collection, Model } from 'backbone'; import type { MessageModel } from '../models/messages'; +import { getContactId, getContact } from '../messages/helpers'; import { isOutgoing } from '../state/selectors/message'; import type { ReactionAttributesType } from '../model-types.d'; import * as log from '../logging/log'; @@ -35,7 +36,7 @@ export class Reactions extends Collection { } } - const senderId = message.getContactId(); + const senderId = getContactId(message.attributes); const sentAt = message.get('sent_at'); const reactionsBySource = this.filter(re => { const targetSenderId = window.ConversationController.ensureContactIds({ @@ -87,15 +88,12 @@ export class Reactions extends Collection { log.info('Handling reaction for', reaction.get('targetTimestamp')); const messages = await window.Signal.Data.getMessagesBySentAt( - reaction.get('targetTimestamp'), - { - MessageCollection: window.Whisper.MessageCollection, - } + reaction.get('targetTimestamp') ); // Message is fetched inside the conversation queue so we have the // most recent data const targetMessage = messages.find(m => { - const contact = m.getContact(); + const contact = getContact(m); if (!contact) { return false; diff --git a/ts/messageModifiers/ReadSyncs.ts b/ts/messageModifiers/ReadSyncs.ts index d29eb8f52d8e..4f58a3b0f28b 100644 --- a/ts/messageModifiers/ReadSyncs.ts +++ b/ts/messageModifiers/ReadSyncs.ts @@ -80,19 +80,16 @@ export class ReadSyncs extends Collection { async onSync(sync: ReadSyncModel): Promise { try { const messages = await window.Signal.Data.getMessagesBySentAt( - sync.get('timestamp'), - { - MessageCollection: window.Whisper.MessageCollection, - } + sync.get('timestamp') ); const found = messages.find(item => { const senderId = window.ConversationController.ensureContactIds({ - e164: item.get('source'), - uuid: item.get('sourceUuid'), + e164: item.source, + uuid: item.sourceUuid, }); - return isIncoming(item.attributes) && senderId === sync.get('senderId'); + return isIncoming(item) && senderId === sync.get('senderId'); }); if (!found) { diff --git a/ts/messageModifiers/ViewOnceOpenSyncs.ts b/ts/messageModifiers/ViewOnceOpenSyncs.ts index fc832b0fc1ba..c269e0c8a2b9 100644 --- a/ts/messageModifiers/ViewOnceOpenSyncs.ts +++ b/ts/messageModifiers/ViewOnceOpenSyncs.ts @@ -57,16 +57,13 @@ export class ViewOnceOpenSyncs extends Collection { async onSync(sync: ViewOnceOpenSyncModel): Promise { try { const messages = await window.Signal.Data.getMessagesBySentAt( - sync.get('timestamp'), - { - MessageCollection: window.Whisper.MessageCollection, - } + sync.get('timestamp') ); const found = messages.find(item => { - const itemSourceUuid = item.get('sourceUuid'); + const itemSourceUuid = item.sourceUuid; const syncSourceUuid = sync.get('sourceUuid'); - const itemSource = item.get('source'); + const itemSource = item.source; const syncSource = sync.get('source'); return Boolean( diff --git a/ts/messageModifiers/ViewSyncs.ts b/ts/messageModifiers/ViewSyncs.ts index e36a582fd125..6c2369305731 100644 --- a/ts/messageModifiers/ViewSyncs.ts +++ b/ts/messageModifiers/ViewSyncs.ts @@ -58,19 +58,16 @@ export class ViewSyncs extends Collection { async onSync(sync: ViewSyncModel): Promise { try { const messages = await window.Signal.Data.getMessagesBySentAt( - sync.get('timestamp'), - { - MessageCollection: window.Whisper.MessageCollection, - } + sync.get('timestamp') ); const found = messages.find(item => { const senderId = window.ConversationController.ensureContactIds({ - e164: item.get('source'), - uuid: item.get('sourceUuid'), + e164: item.source, + uuid: item.sourceUuid, }); - return isIncoming(item.attributes) && senderId === sync.get('senderId'); + return isIncoming(item) && senderId === sync.get('senderId'); }); if (!found) { diff --git a/ts/messages/getMessageById.ts b/ts/messages/getMessageById.ts index c1d327fafb88..3421641a50f3 100644 --- a/ts/messages/getMessageById.ts +++ b/ts/messages/getMessageById.ts @@ -2,21 +2,21 @@ // SPDX-License-Identifier: AGPL-3.0-only import * as log from '../logging/log'; +import type { MessageAttributesType } from '../model-types.d'; import type { MessageModel } from '../models/messages'; import * as Errors from '../types/errors'; export async function getMessageById( messageId: string ): Promise { - let message = window.MessageController.getById(messageId); + const message = window.MessageController.getById(messageId); if (message) { return message; } + let found: MessageAttributesType | undefined; try { - message = await window.Signal.Data.getMessageById(messageId, { - Message: window.Whisper.Message, - }); + found = await window.Signal.Data.getMessageById(messageId); } catch (err: unknown) { log.error( `failed to load message with id ${messageId} ` + @@ -24,10 +24,9 @@ export async function getMessageById( ); } - if (!message) { + if (!found) { return undefined; } - message = window.MessageController.register(message.id, message); - return message; + return window.MessageController.register(found.id, found); } diff --git a/ts/messages/helpers.ts b/ts/messages/helpers.ts new file mode 100644 index 000000000000..037283badd31 --- /dev/null +++ b/ts/messages/helpers.ts @@ -0,0 +1,103 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import * as log from '../logging/log'; +import type { ConversationModel } from '../models/conversations'; +import type { + CustomError, + MessageAttributesType, + QuotedMessageType, +} from '../model-types.d'; +import type { UUIDStringType } from '../types/UUID'; +import { isIncoming, isOutgoing } from '../state/selectors/message'; + +export function isQuoteAMatch( + message: MessageAttributesType | null | undefined, + conversationId: string, + quote: QuotedMessageType +): message is MessageAttributesType { + if (!message) { + return false; + } + + const { authorUuid, id } = quote; + const authorConversationId = window.ConversationController.ensureContactIds({ + e164: 'author' in quote ? quote.author : undefined, + uuid: authorUuid, + }); + + return ( + message.sent_at === id && + message.conversationId === conversationId && + getContactId(message) === authorConversationId + ); +} + +export function getContactId( + message: MessageAttributesType +): string | undefined { + const source = getSource(message); + const sourceUuid = getSourceUuid(message); + + if (!source && !sourceUuid) { + return window.ConversationController.getOurConversationId(); + } + + return window.ConversationController.ensureContactIds({ + e164: source, + uuid: sourceUuid, + }); +} + +export function getContact( + message: MessageAttributesType +): ConversationModel | undefined { + const id = getContactId(message); + return window.ConversationController.get(id); +} + +export function getSource(message: MessageAttributesType): string | undefined { + if (isIncoming(message)) { + return message.source; + } + if (!isOutgoing(message)) { + log.warn('Message.getSource: Called for non-incoming/non-outgoing message'); + } + + return window.textsecure.storage.user.getNumber(); +} + +export function getSourceDevice( + message: MessageAttributesType +): string | number | undefined { + const { sourceDevice } = message; + + if (isIncoming(message)) { + return sourceDevice; + } + if (!isOutgoing(message)) { + log.warn( + 'Message.getSourceDevice: Called for non-incoming/non-outgoing message' + ); + } + + return sourceDevice || window.textsecure.storage.user.getDeviceId(); +} + +export function getSourceUuid( + message: MessageAttributesType +): UUIDStringType | undefined { + if (isIncoming(message)) { + return message.sourceUuid; + } + if (!isOutgoing(message)) { + log.warn( + 'Message.getSourceUuid: Called for non-incoming/non-outgoing message' + ); + } + + return window.textsecure.storage.user.getUuid()?.toString(); +} + +export const isCustomError = (e: unknown): e is CustomError => + e instanceof Error; diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 5d4ce9a6de66..243f9016c91e 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -9,7 +9,6 @@ import type { ConversationModelCollectionType, LastMessageStatus, MessageAttributesType, - MessageModelCollectionType, QuotedMessageType, SenderKeyInfoType, VerificationOptions, @@ -38,6 +37,7 @@ import type { CustomColorType, } from '../types/Colors'; import type { MessageModel } from './messages'; +import { getContact } from '../messages/helpers'; import { strictAssert } from '../util/assert'; import { isMuted } from '../util/isMuted'; import { isConversationSMSOnly } from '../util/isConversationSMSOnly'; @@ -1305,12 +1305,6 @@ export class ConversationModel extends window.Backbone }); } - async cleanup(): Promise { - await Conversation.deleteExternalFiles(this.attributes, { - deleteAttachmentData, - }); - } - async onNewMessage(message: MessageModel): Promise { const uuid = message.get('sourceUuid'); const e164 = message.get('source'); @@ -1407,13 +1401,11 @@ export class ConversationModel extends window.Backbone let scrollToLatestUnread = true; if (newestMessageId) { - const newestInMemoryMessage = await getMessageById(newestMessageId, { - Message: window.Whisper.Message, - }); + const newestInMemoryMessage = await getMessageById(newestMessageId); if (newestInMemoryMessage) { // If newest in-memory message is unread, scrolling down would mean going to // the very bottom, not the oldest unread. - if (isMessageUnread(newestInMemoryMessage.attributes)) { + if (isMessageUnread(newestInMemoryMessage)) { scrollToLatestUnread = false; } } else { @@ -1443,7 +1435,6 @@ export class ConversationModel extends window.Backbone const messages = await getOlderMessagesByConversation(conversationId, { limit: MESSAGE_LOAD_CHUNK_SIZE, - MessageCollection: window.Whisper.MessageCollection, }); const cleaned: Array = await this.cleanModels(messages); @@ -1481,23 +1472,20 @@ export class ConversationModel extends window.Backbone const finish = this.setInProgressFetch(); try { - const message = await getMessageById(oldestMessageId, { - Message: window.Whisper.Message, - }); + const message = await getMessageById(oldestMessageId); if (!message) { throw new Error( `loadOlderMessages: failed to load message ${oldestMessageId}` ); } - const receivedAt = message.get('received_at'); - const sentAt = message.get('sent_at'); + const receivedAt = message.received_at; + const sentAt = message.sent_at; const models = await getOlderMessagesByConversation(conversationId, { receivedAt, sentAt, messageId: oldestMessageId, limit: MESSAGE_LOAD_CHUNK_SIZE, - MessageCollection: window.Whisper.MessageCollection, }); if (models.length < 1) { @@ -1533,22 +1521,19 @@ export class ConversationModel extends window.Backbone const finish = this.setInProgressFetch(); try { - const message = await getMessageById(newestMessageId, { - Message: window.Whisper.Message, - }); + const message = await getMessageById(newestMessageId); if (!message) { throw new Error( `loadNewerMessages: failed to load message ${newestMessageId}` ); } - const receivedAt = message.get('received_at'); - const sentAt = message.get('sent_at'); + const receivedAt = message.received_at; + const sentAt = message.sent_at; const models = await getNewerMessagesByConversation(conversationId, { receivedAt, sentAt, limit: MESSAGE_LOAD_CHUNK_SIZE, - MessageCollection: window.Whisper.MessageCollection, }); if (models.length < 1) { @@ -1587,33 +1572,29 @@ export class ConversationModel extends window.Backbone const finish = this.setInProgressFetch(); try { - const message = await getMessageById(messageId, { - Message: window.Whisper.Message, - }); + const message = await getMessageById(messageId); if (!message) { throw new Error( `loadMoreAndScroll: failed to load message ${messageId}` ); } - const receivedAt = message.get('received_at'); - const sentAt = message.get('sent_at'); + const receivedAt = message.received_at; + const sentAt = message.sent_at; const older = await getOlderMessagesByConversation(conversationId, { limit: MESSAGE_LOAD_CHUNK_SIZE, receivedAt, sentAt, messageId, - MessageCollection: window.Whisper.MessageCollection, }); const newer = await getNewerMessagesByConversation(conversationId, { limit: MESSAGE_LOAD_CHUNK_SIZE, receivedAt, sentAt, - MessageCollection: window.Whisper.MessageCollection, }); const metrics = await getMessageMetricsForConversation(conversationId); - const all = [...older.models, message, ...newer.models]; + const all = [...older, message, ...newer]; const cleaned: Array = await this.cleanModels(all); const scrollToMessageId = @@ -1636,19 +1617,18 @@ export class ConversationModel extends window.Backbone } async cleanModels( - collection: MessageModelCollectionType | Array + messages: ReadonlyArray ): Promise> { - const result = collection - .filter((message: MessageModel) => Boolean(message.id)) - .map((message: MessageModel) => - window.MessageController.register(message.id, message) - ); + const result = messages + .filter(message => Boolean(message.id)) + .map(message => window.MessageController.register(message.id, message)); - const eliminated = collection.length - result.length; + const eliminated = messages.length - result.length; if (eliminated > 0) { log.warn(`cleanModels: Eliminated ${eliminated} messages without an id`); } + let upgraded = 0; for (let max = result.length, i = 0; i < max; i += 1) { const message = result[i]; const { attributes } = message; @@ -1661,8 +1641,12 @@ export class ConversationModel extends window.Backbone message.set(upgradedMessage); // eslint-disable-next-line no-await-in-loop await window.Signal.Data.saveMessage(upgradedMessage); + upgraded += 1; } } + if (upgraded > 0) { + log.warn(`cleanModels: Upgraded schema of ${upgraded} messages`); + } return result; } @@ -1972,18 +1956,17 @@ export class ConversationModel extends window.Backbone ): Promise { const { isLocalAction } = options; - let messages: MessageModelCollectionType | undefined; + let messages: Array | undefined; do { - const first = messages ? messages.first() : undefined; + const first = messages ? messages[0] : undefined; // eslint-disable-next-line no-await-in-loop messages = await window.Signal.Data.getOlderMessagesByConversation( this.get('id'), { - MessageCollection: window.Whisper.MessageCollection, limit: 100, - receivedAt: first ? first.get('received_at') : undefined, - sentAt: first ? first.get('sent_at') : undefined, + receivedAt: first ? first.received_at : undefined, + sentAt: first ? first.sent_at : undefined, messageId: first ? first.id : undefined, } ); @@ -1992,9 +1975,7 @@ export class ConversationModel extends window.Backbone return; } - const readMessages = messages.filter( - m => !hasErrors(m.attributes) && isIncoming(m.attributes) - ); + const readMessages = messages.filter(m => !hasErrors(m) && isIncoming(m)); if (isLocalAction) { // eslint-disable-next-line no-await-in-loop @@ -2002,9 +1983,9 @@ export class ConversationModel extends window.Backbone window.storage, readMessages.map(m => ({ messageId: m.id, - senderE164: m.get('source'), - senderUuid: m.get('sourceUuid'), - timestamp: m.get('sent_at'), + senderE164: m.source, + senderUuid: m.sourceUuid, + timestamp: m.sent_at, })) ); } @@ -3209,10 +3190,7 @@ export class ConversationModel extends window.Backbone const message = window.MessageController.getById(notificationId); if (message) { - message.cleanup(); - window.Signal.Data.removeMessage(message.id, { - Message: window.Whisper.Message, - }); + window.Signal.Data.removeMessage(message.id); } if (this.get('expireTimer') || forceRemove) { @@ -3628,7 +3606,7 @@ export class ConversationModel extends window.Backbone async makeQuote(quotedMessage: MessageModel): Promise { const { getName } = EmbeddedContact; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const contact = quotedMessage.getContact()!; + const contact = getContact(quotedMessage.attributes)!; const attachments = quotedMessage.get('attachments'); const preview = quotedMessage.get('preview'); const sticker = quotedMessage.get('sticker'); @@ -4082,7 +4060,6 @@ export class ConversationModel extends window.Backbone const lastMessages = await window.Signal.Data.getLastConversationMessages({ conversationId, ourUuid, - Message: window.Whisper.Message, }); // This runs as a job to avoid race conditions @@ -4090,22 +4067,21 @@ export class ConversationModel extends window.Backbone this.maybeSetPendingUniversalTimer(lastMessages.hasUserInitiatedMessages) ); - let { preview: previewMessage, activity: activityMessage } = lastMessages; + const { preview, activity } = lastMessages; + let previewMessage: MessageModel | undefined; + let activityMessage: MessageModel | undefined; // Register the message with MessageController so that if it already exists // in memory we use that data instead of the data from the db which may // be out of date. - if (previewMessage) { - previewMessage = window.MessageController.register( - previewMessage.id, - previewMessage - ); + if (preview) { + previewMessage = window.MessageController.register(preview.id, preview); } - if (activityMessage) { + if (activity) { activityMessage = window.MessageController.register( - activityMessage.id, - activityMessage + activity.id, + activity ); } @@ -4748,9 +4724,7 @@ export class ConversationModel extends window.Backbone this.deriveProfileKeyVersionIfNeeded(), ]); - window.Signal.Data.updateConversation(this.attributes, { - Conversation: window.Whisper.Conversation, - }); + window.Signal.Data.updateConversation(this.attributes); } } @@ -4814,7 +4788,6 @@ export class ConversationModel extends window.Backbone await window.Signal.Data.removeAllMessagesInConversation(this.id, { logId: this.idForLogging(), - MessageCollection: window.Whisper.MessageCollection, }); } @@ -5090,7 +5063,7 @@ export class ConversationModel extends window.Backbone const sender = reaction ? window.ConversationController.get(reaction.get('fromId')) - : message.getContact(); + : getContact(message.attributes); const senderName = sender ? sender.getTitle() : window.i18n('unknownContact'); diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 835304e5eeb5..cc65dfa61bf9 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -41,11 +41,9 @@ import * as expirationTimer from '../util/expirationTimer'; import type { ReactionType } from '../types/Reactions'; import { UUID, UUIDKind } from '../types/UUID'; -import type { UUIDStringType } from '../types/UUID'; import * as reactionUtil from '../reactions/util'; import { copyStickerToAttachments, - deletePackReference, savePackMetadata, getStickerPackStatus, } from '../types/Stickers'; @@ -134,6 +132,16 @@ import type { LinkPreviewType } from '../types/message/LinkPreviews'; import * as log from '../logging/log'; import * as Bytes from '../Bytes'; import { computeHash } from '../Crypto'; +import { cleanupMessage, deleteMessageData } from '../util/cleanup'; +import { + getContact, + getContactId, + getSource, + getSourceDevice, + getSourceUuid, + isCustomError, + isQuoteAMatch, +} from '../messages/helpers'; /* eslint-disable camelcase */ /* eslint-disable more/no-then */ @@ -148,36 +156,10 @@ declare const _: typeof window._; window.Whisper = window.Whisper || {}; const { Message: TypedMessage } = window.Signal.Types; -const { deleteExternalMessageFiles, upgradeMessageSchema } = - window.Signal.Migrations; +const { upgradeMessageSchema } = window.Signal.Migrations; const { getTextWithMentions, GoogleChrome } = window.Signal.Util; - const { addStickerPackReference, getMessageBySender } = window.Signal.Data; -export function isQuoteAMatch( - message: MessageModel | null | undefined, - conversationId: string, - quote: QuotedMessageType -): message is MessageModel { - if (!message) { - return false; - } - - const { authorUuid, id } = quote; - const authorConversationId = window.ConversationController.ensureContactIds({ - e164: 'author' in quote ? quote.author : undefined, - uuid: authorUuid, - }); - - return ( - message.get('sent_at') === id && - message.get('conversationId') === conversationId && - message.getContactId() === authorConversationId - ); -} - -const isCustomError = (e: unknown): e is CustomError => e instanceof Error; - export class MessageModel extends window.Backbone.Model { static getLongMessageAttachment: ( attachment: typeof window.WhatIsThis @@ -307,7 +289,7 @@ export class MessageModel extends window.Backbone.Model { let conversationIds: Array; /* eslint-disable @typescript-eslint/no-non-null-assertion */ if (isIncoming(this.attributes)) { - conversationIds = [this.getContactId()!]; + conversationIds = [getContactId(this.attributes)!]; } else if (!isEmpty(sendStateByConversationId)) { if (isMessageJustForMe(sendStateByConversationId, ourConversationId)) { conversationIds = [ourConversationId]; @@ -516,7 +498,7 @@ export class MessageModel extends window.Backbone.Model { if (isGroupUpdate(attributes)) { const groupUpdate = this.get('group_update'); - const fromContact = this.getContact(); + const fromContact = getContact(this.attributes); const messages = []; if (!groupUpdate) { throw new Error('getNotificationData: Missing group_update'); @@ -749,8 +731,9 @@ export class MessageModel extends window.Backbone.Model { // General idForLogging(): string { - const account = this.getSourceUuid() || this.getSource(); - const device = this.getSourceDevice(); + const account = + getSourceUuid(this.attributes) || getSource(this.attributes); + const device = getSourceDevice(this.attributes); const timestamp = this.get('sent_at'); return `${account}.${device} ${timestamp}`; @@ -785,29 +768,11 @@ export class MessageModel extends window.Backbone.Model { } async cleanup(): Promise { - window.reduxActions?.conversations?.messageDeleted( - this.id, - this.get('conversationId') - ); - - this.getConversation()?.debouncedUpdateLastMessage?.(); - - window.MessageController.unregister(this.id); - await this.deleteData(); + await cleanupMessage(this.attributes); } async deleteData(): Promise { - await deleteExternalMessageFiles(this.attributes); - - const sticker = this.get('sticker'); - if (!sticker) { - return; - } - - const { packId } = sticker; - if (packId) { - await deletePackReference(this.id, packId); - } + await deleteMessageData(this.attributes); } isValidTapToView(): boolean { @@ -875,8 +840,8 @@ export class MessageModel extends window.Backbone.Model { await this.eraseContents(); if (!fromSync) { - const sender = this.getSource(); - const senderUuid = this.getSourceUuid(); + const sender = getSource(this.attributes); + const senderUuid = getSourceUuid(this.attributes); if (senderUuid === undefined) { throw new Error('senderUuid is undefined'); @@ -929,7 +894,7 @@ export class MessageModel extends window.Backbone.Model { Number(sentAt) ); const matchingMessage = find(inMemoryMessages, message => - isQuoteAMatch(message, this.get('conversationId'), quote) + isQuoteAMatch(message.attributes, this.get('conversationId'), quote) ); if (!matchingMessage) { log.info( @@ -1078,66 +1043,6 @@ export class MessageModel extends window.Backbone.Model { return unidentifiedDeliveriesSet.has(contactId); } - getSource(): string | undefined { - if (isIncoming(this.attributes)) { - return this.get('source'); - } - if (!isOutgoing(this.attributes)) { - log.warn( - 'Message.getSource: Called for non-incoming/non-outoing message' - ); - } - - return window.textsecure.storage.user.getNumber(); - } - - getSourceDevice(): string | number | undefined { - const sourceDevice = this.get('sourceDevice'); - - if (isIncoming(this.attributes)) { - return sourceDevice; - } - if (!isOutgoing(this.attributes)) { - log.warn( - 'Message.getSourceDevice: Called for non-incoming/non-outoing message' - ); - } - - return sourceDevice || window.textsecure.storage.user.getDeviceId(); - } - - getSourceUuid(): UUIDStringType | undefined { - if (isIncoming(this.attributes)) { - return this.get('sourceUuid'); - } - if (!isOutgoing(this.attributes)) { - log.warn( - 'Message.getSourceUuid: Called for non-incoming/non-outoing message' - ); - } - - return window.textsecure.storage.user.getUuid()?.toString(); - } - - getContactId(): string | undefined { - const source = this.getSource(); - const sourceUuid = this.getSourceUuid(); - - if (!source && !sourceUuid) { - return window.ConversationController.getOurConversationId(); - } - - return window.ConversationController.ensureContactIds({ - e164: source, - uuid: sourceUuid, - }); - } - - getContact(): ConversationModel | undefined { - const id = this.getContactId(); - return window.ConversationController.get(id); - } - async saveErrors( providedErrors: Error | Array, options: { skipSave?: boolean } = {} @@ -2106,7 +2011,7 @@ export class MessageModel extends window.Backbone.Model { const inMemoryMessages = window.MessageController.filterBySentAt(id); const matchingMessage = find(inMemoryMessages, item => - isQuoteAMatch(item, conversationId, result) + isQuoteAMatch(item.attributes, conversationId, result) ); let queryMessage: undefined | MessageModel; @@ -2115,10 +2020,8 @@ export class MessageModel extends window.Backbone.Model { queryMessage = matchingMessage; } else { log.info('copyFromQuotedMessage: db lookup needed', id); - const collection = await window.Signal.Data.getMessagesBySentAt(id, { - MessageCollection: window.Whisper.MessageCollection, - }); - const found = collection.find(item => + const messages = await window.Signal.Data.getMessagesBySentAt(id); + const found = messages.find(item => isQuoteAMatch(item, conversationId, result) ); @@ -2256,7 +2159,7 @@ export class MessageModel extends window.Backbone.Model { const conversationId = message.get('conversationId'); const GROUP_TYPES = Proto.GroupContext.Type; - const fromContact = this.getContact(); + const fromContact = getContact(this.attributes); if (fromContact) { fromContact.setRegistered(); } @@ -2281,10 +2184,7 @@ export class MessageModel extends window.Backbone.Model { ); } const existingMessage = - inMemoryMessage || - (await getMessageBySender(this.attributes, { - Message: window.Whisper.Message, - })); + inMemoryMessage || (await getMessageBySender(this.attributes)); const isUpdate = Boolean(data && data.isRecipientUpdate); if (existingMessage && type === 'incoming') { @@ -2702,7 +2602,7 @@ export class MessageModel extends window.Backbone.Model { if (conversation.get('left')) { log.warn('re-added to a left group'); attributes.left = false; - conversation.set({ addedBy: message.getContactId() }); + conversation.set({ addedBy: getContactId(message.attributes) }); } } else if (dataMessage.group.type === GROUP_TYPES.QUIT) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/ts/reactions/enqueueReactionForSend.ts b/ts/reactions/enqueueReactionForSend.ts index ce993e938f38..14c3b0c697e0 100644 --- a/ts/reactions/enqueueReactionForSend.ts +++ b/ts/reactions/enqueueReactionForSend.ts @@ -4,6 +4,7 @@ import { ReactionModel } from '../messageModifiers/Reactions'; import { ReactionSource } from './ReactionSource'; import { getMessageById } from '../messages/getMessageById'; +import { getSourceUuid } from '../messages/helpers'; import { strictAssert } from '../util/assert'; export async function enqueueReactionForSend({ @@ -18,7 +19,7 @@ export async function enqueueReactionForSend({ const message = await getMessageById(messageId); strictAssert(message, 'enqueueReactionForSend: no message found'); - const targetAuthorUuid = message.getSourceUuid(); + const targetAuthorUuid = getSourceUuid(message.attributes); strictAssert( targetAuthorUuid, `enqueueReactionForSend: message ${message.idForLogging()} had no source UUID` diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts index 3073afb7cbbf..c2ed7e9ca17d 100644 --- a/ts/sql/Client.ts +++ b/ts/sql/Client.ts @@ -26,6 +26,7 @@ import { uniq, } from 'lodash'; +import { deleteExternalFiles } from '../types/Conversation'; import * as Bytes from '../Bytes'; import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message'; import { createBatcher } from '../util/batcher'; @@ -40,12 +41,9 @@ import type { RemoveAllConfiguration } from '../types/RemoveAllConfiguration'; import createTaskWithTimeout from '../textsecure/TaskWithTimeout'; import * as log from '../logging/log'; -import type { - ConversationModelCollectionType, - MessageModelCollectionType, -} from '../model-types.d'; import type { StoredJob } from '../jobs/types'; import { formatJobForInsert } from '../jobs/formatJobForInsert'; +import { cleanupMessage } from '../util/cleanup'; import type { AttachmentDownloadJobType, @@ -54,18 +52,17 @@ import type { ClientSearchResultMessageType, ConversationType, DeleteSentProtoRecipientOptionsType, - IdentityKeyType, IdentityKeyIdType, + IdentityKeyType, ItemKeyType, ItemType, LastConversationMessagesType, MessageType, MessageTypeUnhydrated, - PreKeyType, PreKeyIdType, - SearchResultMessageType, - SenderKeyType, + PreKeyType, SenderKeyIdType, + SenderKeyType, SentMessageDBType, SentMessagesType, SentProtoType, @@ -73,15 +70,16 @@ import type { SentRecipientsDBType, SentRecipientsType, ServerInterface, - SessionType, + ServerSearchResultMessageType, SessionIdType, - SignedPreKeyType, + SessionType, SignedPreKeyIdType, + SignedPreKeyType, StickerPackStatusType, StickerPackType, StickerType, - StoryDistributionType, StoryDistributionMemberType, + StoryDistributionType, StoryDistributionWithMembersType, StoryReadType, UnprocessedType, @@ -89,8 +87,6 @@ import type { } from './Interface'; import Server from './Server'; import { isCorruptionError } from './errors'; -import type { MessageModel } from '../models/messages'; -import type { ConversationModel } from '../models/conversations'; // We listen to a lot of events on ipc, often on the same channel. This prevents // any warnings that might be sent to the console in that case. @@ -160,16 +156,16 @@ const dataInterface: ClientInterface = { createOrUpdateSignedPreKey, getSignedPreKeyById, - getAllSignedPreKeys, bulkAddSignedPreKeys, removeSignedPreKeyById, removeAllSignedPreKeys, + getAllSignedPreKeys, createOrUpdateItem, getItemById, - getAllItems, removeItemById, removeAllItems, + getAllItems, createOrUpdateSenderKey, getSenderKeyById, @@ -197,6 +193,7 @@ const dataInterface: ClientInterface = { removeAllSessions, getAllSessions, + eraseStorageServiceStateFromConversations, getConversationCount, saveConversation, saveConversations, @@ -206,7 +203,6 @@ const dataInterface: ClientInterface = { removeConversation, updateAllConversationColors, - eraseStorageServiceStateFromConversations, getAllConversations, getAllConversationIds, getAllPrivateConversations, @@ -229,10 +225,11 @@ const dataInterface: ClientInterface = { addReaction, _getAllReactions, _removeAllReactions, - getMessageBySender, getMessageById, getMessagesById, + _getAllMessages, + _removeAllMessages, getAllMessageIds, getMessagesBySentAt, getExpiredMessages, @@ -243,8 +240,8 @@ const dataInterface: ClientInterface = { getOlderMessagesByConversation, getOlderStories, getNewerMessagesByConversation, - getLastConversationMessages, getMessageMetricsForConversation, + getLastConversationMessages, hasGroupCallHistoryMessage, migrateConversationMessages, @@ -263,13 +260,13 @@ const dataInterface: ClientInterface = { removeAttachmentDownloadJob, removeAllAttachmentDownloadJobs, - getStickerCount, createOrUpdateStickerPack, updateStickerPackStatus, createOrUpdateSticker, updateStickerLastUsed, addStickerPackReference, deleteStickerPackReference, + getStickerCount, deleteStickerPack, getAllStickerPacks, getAllStickers, @@ -317,11 +314,6 @@ const dataInterface: ClientInterface = { getStatisticsForLogging, - // Test-only - - _getAllMessages, - _removeAllMessages, - // Client-side only shutdown, @@ -335,7 +327,6 @@ const dataInterface: ClientInterface = { startInRendererProcess, goBackToMainProcess, - _removeConversations, _jobs, }; @@ -971,17 +962,8 @@ async function saveConversations(array: Array) { await channels.saveConversations(array); } -async function getConversationById( - id: string, - { Conversation }: { Conversation: typeof ConversationModel } -) { - const data = await channels.getConversationById(id); - - if (!data) { - return undefined; - } - - return new Conversation(data); +async function getConversationById(id: string) { + return channels.getConversationById(id); } const updateConversationBatcher = createBatcher({ @@ -1015,40 +997,25 @@ async function updateConversations(array: Array) { await channels.updateConversations(cleaned); } -async function removeConversation( - id: string, - { Conversation }: { Conversation: typeof ConversationModel } -) { - const existing = await getConversationById(id, { Conversation }); +async function removeConversation(id: string) { + const existing = await getConversationById(id); // Note: It's important to have a fully database-hydrated model to delete here because // it needs to delete all associated on-disk files along with the database delete. if (existing) { await channels.removeConversation(id); - await existing.cleanup(); + await deleteExternalFiles(existing, { + deleteAttachmentData: window.Signal.Migrations.deleteAttachmentData, + }); } } -// Note: this method will not clean up external files, just delete from SQL -async function _removeConversations(ids: Array) { - await channels.removeConversation(ids); -} - async function eraseStorageServiceStateFromConversations() { await channels.eraseStorageServiceStateFromConversations(); } -async function getAllConversations({ - ConversationCollection, -}: { - ConversationCollection: typeof ConversationModelCollectionType; -}): Promise { - const conversations = await channels.getAllConversations(); - - const collection = new ConversationCollection(); - collection.add(conversations); - - return collection; +async function getAllConversations() { + return channels.getAllConversations(); } async function getAllConversationIds() { @@ -1057,33 +1024,12 @@ async function getAllConversationIds() { return ids; } -async function getAllPrivateConversations({ - ConversationCollection, -}: { - ConversationCollection: typeof ConversationModelCollectionType; -}) { - const conversations = await channels.getAllPrivateConversations(); - - const collection = new ConversationCollection(); - collection.add(conversations); - - return collection; +async function getAllPrivateConversations() { + return channels.getAllPrivateConversations(); } -async function getAllGroupsInvolvingUuid( - uuid: UUIDStringType, - { - ConversationCollection, - }: { - ConversationCollection: typeof ConversationModelCollectionType; - } -) { - const conversations = await channels.getAllGroupsInvolvingUuid(uuid); - - const collection = new ConversationCollection(); - collection.add(conversations); - - return collection; +async function getAllGroupsInvolvingUuid(uuid: UUIDStringType) { + return channels.getAllGroupsInvolvingUuid(uuid); } async function searchConversations(query: string) { @@ -1093,7 +1039,7 @@ async function searchConversations(query: string) { } function handleSearchMessageJSON( - messages: Array + messages: Array ): Array { return messages.map(message => ({ json: message.json, @@ -1163,17 +1109,14 @@ async function saveMessages( window.Whisper.TapToViewMessagesListener.update(); } -async function removeMessage( - id: string, - { Message }: { Message: typeof MessageModel } -) { - const message = await getMessageById(id, { Message }); +async function removeMessage(id: string) { + const message = await getMessageById(id); // Note: It's important to have a fully database-hydrated model to delete here because // it needs to delete all associated on-disk files along with the database delete. if (message) { await channels.removeMessage(id); - await message.cleanup(); + await cleanupMessage(message); } } @@ -1182,16 +1125,8 @@ async function removeMessages(ids: Array) { await channels.removeMessages(ids); } -async function getMessageById( - id: string, - { Message }: { Message: typeof MessageModel } -) { - const message = await channels.getMessageById(id); - if (!message) { - return undefined; - } - - return new Message(message); +async function getMessageById(id: string) { + return channels.getMessageById(id); } async function getMessagesById(messageIds: Array) { @@ -1202,14 +1137,8 @@ async function getMessagesById(messageIds: Array) { } // For testing only -async function _getAllMessages({ - MessageCollection, -}: { - MessageCollection: typeof MessageModelCollectionType; -}) { - const messages = await channels._getAllMessages(); - - return new MessageCollection(messages); +async function _getAllMessages() { + return channels._getAllMessages(); } async function _removeAllMessages() { await channels._removeAllMessages(); @@ -1221,31 +1150,23 @@ async function getAllMessageIds() { return ids; } -async function getMessageBySender( - { - source, - sourceUuid, - sourceDevice, - sent_at, - }: { - source: string; - sourceUuid: string; - sourceDevice: number; - sent_at: number; - }, - { Message }: { Message: typeof MessageModel } -) { - const messages = await channels.getMessageBySender({ +async function getMessageBySender({ + source, + sourceUuid, + sourceDevice, + sent_at, +}: { + source: string; + sourceUuid: string; + sourceDevice: number; + sent_at: number; +}) { + return channels.getMessageBySender({ source, sourceUuid, sourceDevice, sent_at, }); - if (!messages || !messages.length) { - return null; - } - - return new Message(messages[0]); } async function getTotalUnreadForConversation( @@ -1299,7 +1220,9 @@ async function _removeAllReactions() { await channels._removeAllReactions(); } -function handleMessageJSON(messages: Array) { +function handleMessageJSON( + messages: Array +): Array { return messages.map(message => JSON.parse(message.json)); } @@ -1307,14 +1230,12 @@ async function getOlderMessagesByConversation( conversationId: string, { limit = 100, - MessageCollection, messageId, receivedAt = Number.MAX_VALUE, sentAt = Number.MAX_VALUE, storyId, }: { limit?: number; - MessageCollection: typeof MessageModelCollectionType; messageId?: string; receivedAt?: number; sentAt?: number; @@ -1332,7 +1253,7 @@ async function getOlderMessagesByConversation( } ); - return new MessageCollection(handleMessageJSON(messages)); + return handleMessageJSON(messages); } async function getOlderStories(options: { conversationId?: string; @@ -1348,13 +1269,11 @@ async function getNewerMessagesByConversation( conversationId: string, { limit = 100, - MessageCollection, receivedAt = 0, sentAt = 0, storyId, }: { limit?: number; - MessageCollection: typeof MessageModelCollectionType; receivedAt?: number; sentAt?: number; storyId?: UUIDStringType; @@ -1370,16 +1289,14 @@ async function getNewerMessagesByConversation( } ); - return new MessageCollection(handleMessageJSON(messages)); + return handleMessageJSON(messages); } async function getLastConversationMessages({ conversationId, ourUuid, - Message, }: { conversationId: string; ourUuid: UUIDStringType; - Message: typeof MessageModel; }): Promise { const { preview, activity, hasUserInitiatedMessages } = await channels.getLastConversationMessages({ @@ -1388,8 +1305,8 @@ async function getLastConversationMessages({ }); return { - preview: preview ? new Message(preview) : undefined, - activity: activity ? new Message(activity) : undefined, + preview, + activity, hasUserInitiatedMessages, }; } @@ -1421,10 +1338,8 @@ async function removeAllMessagesInConversation( conversationId: string, { logId, - MessageCollection, }: { logId: string; - MessageCollection: typeof MessageModelCollectionType; } ) { let messages; @@ -1437,21 +1352,22 @@ async function removeAllMessagesInConversation( // time so we don't use too much memory. messages = await getOlderMessagesByConversation(conversationId, { limit: chunkSize, - MessageCollection, }); if (!messages.length) { return; } - const ids = messages.map((message: MessageModel) => message.id); + const ids = messages.map(message => message.id); log.info(`removeAllMessagesInConversation/${logId}: Cleanup...`); // Note: It's very important that these models are fully hydrated because // we need to delete all associated on-disk files along with the database delete. const queue = new window.PQueue({ concurrency: 3, timeout: 1000 * 60 * 2 }); queue.addAll( - messages.map((message: MessageModel) => async () => message.cleanup()) + messages.map( + (message: MessageType) => async () => cleanupMessage(message) + ) ); await queue.onIdle(); @@ -1460,25 +1376,12 @@ async function removeAllMessagesInConversation( } while (messages.length > 0); } -async function getMessagesBySentAt( - sentAt: number, - { - MessageCollection, - }: { MessageCollection: typeof MessageModelCollectionType } -) { - const messages = await channels.getMessagesBySentAt(sentAt); - - return new MessageCollection(messages); +async function getMessagesBySentAt(sentAt: number) { + return channels.getMessagesBySentAt(sentAt); } -async function getExpiredMessages({ - MessageCollection, -}: { - MessageCollection: typeof MessageModelCollectionType; -}) { - const messages = await channels.getExpiredMessages(); - - return new MessageCollection(messages); +async function getExpiredMessages() { + return channels.getExpiredMessages(); } function getMessagesUnexpectedlyMissingExpirationStartTimestamp() { @@ -1492,14 +1395,8 @@ function getSoonestMessageExpiry() { async function getNextTapToViewMessageTimestampToAgeOut() { return channels.getNextTapToViewMessageTimestampToAgeOut(); } -async function getTapToViewMessagesNeedingErase({ - MessageCollection, -}: { - MessageCollection: typeof MessageModelCollectionType; -}) { - const messages = await channels.getTapToViewMessagesNeedingErase(); - - return new MessageCollection(messages); +async function getTapToViewMessagesNeedingErase() { + return channels.getTapToViewMessagesNeedingErase(); } // Unprocessed diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 74845fec3aa5..893e49b08509 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -6,13 +6,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { ConversationAttributesType, - ConversationModelCollectionType, MessageAttributesType, - MessageModelCollectionType, SenderKeyInfoType, } from '../model-types.d'; -import type { MessageModel } from '../models/messages'; -import type { ConversationModel } from '../models/conversations'; import type { StoredJob } from '../jobs/types'; import type { ReactionType } from '../types/Reactions'; import type { ConversationColorType, CustomColorType } from '../types/Colors'; @@ -91,7 +87,7 @@ export type PreKeyType = { publicKey: Uint8Array; }; export type PreKeyIdType = PreKeyType['id']; -export type SearchResultMessageType = { +export type ServerSearchResultMessageType = { json: string; snippet: string; }; @@ -222,18 +218,12 @@ export type UnprocessedUpdateType = { decrypted?: string; }; -export type LastConversationMessagesServerType = { +export type LastConversationMessagesType = { activity?: MessageType; preview?: MessageType; hasUserInitiatedMessages: boolean; }; -export type LastConversationMessagesType = { - activity?: MessageModel; - preview?: MessageModel; - hasUserInitiatedMessages: boolean; -}; - export type DeleteSentProtoRecipientOptionsType = Readonly<{ timestamp: number; recipientUuid: string; @@ -353,15 +343,33 @@ export type DataInterface = { getConversationCount: () => Promise; saveConversation: (data: ConversationType) => Promise; saveConversations: (array: Array) => Promise; + getConversationById: (id: string) => Promise; + // updateConversation is a normal data method on Server, a sync batch-add on Client updateConversations: (array: Array) => Promise; + // removeConversation handles either one id or an array on Server, and one id on Client + updateAllConversationColors: ( + conversationColor?: ConversationColorType, + customColorData?: { + id: string; + value: CustomColorType; + } + ) => Promise; + + getAllConversations: () => Promise>; getAllConversationIds: () => Promise>; + getAllPrivateConversations: () => Promise>; + getAllGroupsInvolvingUuid: ( + id: UUIDStringType + ) => Promise>; searchConversations: ( query: string, options?: { limit?: number } ) => Promise>; + // searchMessages is JSON on server, full message on Client + // searchMessagesInConversation is JSON on server, full message on Client - getMessagesById: (messageIds: Array) => Promise>; + getMessageCount: (conversationId?: string) => Promise; saveMessage: ( data: MessageType, options?: { @@ -373,30 +381,8 @@ export type DataInterface = { arrayOfMessages: Array, options?: { forceSave?: boolean } ) => Promise; - getMessageCount: (conversationId?: string) => Promise; - getAllMessageIds: () => Promise>; - getOlderStories: (options: { - conversationId?: string; - limit?: number; - receivedAt?: number; - sentAt?: number; - sourceUuid?: string; - }) => Promise>; - getMessageMetricsForConversation: ( - conversationId: string, - storyId?: UUIDStringType - ) => Promise; - hasGroupCallHistoryMessage: ( - conversationId: string, - eraId: string - ) => Promise; - migrateConversationMessages: ( - obsoleteId: string, - currentId: string - ) => Promise; - getNextTapToViewMessageTimestampToAgeOut: () => Promise; - _removeAllMessages: () => Promise; - + removeMessage: (id: string) => Promise; + removeMessages: (ids: Array) => Promise; getTotalUnreadForConversation: ( conversationId: string, storyId?: UUIDStringType @@ -433,6 +419,50 @@ export type DataInterface = { addReaction: (reactionObj: ReactionType) => Promise; _getAllReactions: () => Promise>; _removeAllReactions: () => Promise; + getMessageBySender: (options: { + source: string; + sourceUuid: string; + sourceDevice: number; + sent_at: number; + }) => Promise; + getMessageById: (id: string) => Promise; + getMessagesById: (messageIds: Array) => Promise>; + _getAllMessages: () => Promise>; + _removeAllMessages: () => Promise; + getAllMessageIds: () => Promise>; + getMessagesBySentAt: (sentAt: number) => Promise>; + getExpiredMessages: () => Promise>; + getMessagesUnexpectedlyMissingExpirationStartTimestamp: () => Promise< + Array + >; + getSoonestMessageExpiry: () => Promise; + getNextTapToViewMessageTimestampToAgeOut: () => Promise; + getTapToViewMessagesNeedingErase: () => Promise>; + // getOlderMessagesByConversation is JSON on server, full message on Client + getOlderStories: (options: { + conversationId?: string; + limit?: number; + receivedAt?: number; + sentAt?: number; + sourceUuid?: string; + }) => Promise>; + // getNewerMessagesByConversation is JSON on server, full message on Client + getMessageMetricsForConversation: ( + conversationId: string, + storyId?: UUIDStringType + ) => Promise; + getLastConversationMessages: (options: { + conversationId: string; + ourUuid: UUIDStringType; + }) => Promise; + hasGroupCallHistoryMessage: ( + conversationId: string, + eraId: string + ) => Promise; + migrateConversationMessages: ( + obsoleteId: string, + currentId: string + ) => Promise; getUnprocessedCount: () => Promise; getAllUnprocessed: () => Promise>; @@ -452,11 +482,11 @@ export type DataInterface = { options?: { timestamp?: number } ) => Promise>; saveAttachmentDownloadJob: (job: AttachmentDownloadJobType) => Promise; + resetAttachmentDownloadPending: () => Promise; setAttachmentDownloadJobPending: ( id: string, pending: boolean ) => Promise; - resetAttachmentDownloadPending: () => Promise; removeAttachmentDownloadJob: (id: string) => Promise; removeAllAttachmentDownloadJobs: () => Promise; @@ -541,10 +571,6 @@ export type DataInterface = { getMessageServerGuidsForSpam: ( conversationId: string ) => Promise>; - getMessagesUnexpectedlyMissingExpirationStartTimestamp: () => Promise< - Array - >; - getSoonestMessageExpiry: () => Promise; getJobsInQueue(queueType: string): Promise>; insertJob(job: Readonly): Promise; @@ -556,42 +582,27 @@ export type DataInterface = { processGroupCallRingCancelation(ringId: bigint): Promise; cleanExpiredGroupCallRings(): Promise; - updateAllConversationColors: ( - conversationColor?: ConversationColorType, - customColorData?: { - id: string; - value: CustomColorType; - } - ) => Promise; - getMaxMessageCounter(): Promise; + getStatisticsForLogging(): Promise>; }; -// The reason for client/server divergence is the need to inject Backbone models and -// collections into data calls so those are the objects returned. This was necessary in -// July 2018 when creating the Data API as a drop-in replacement for previous database -// requests via ORM. - -// Note: It is extremely important that items are duplicated between these two. Client.js -// loops over all of its local functions to generate the server-side IPC-based API. - export type ServerInterface = DataInterface & { - getAllConversations: () => Promise>; - getAllGroupsInvolvingUuid: ( - id: UUIDStringType - ) => Promise>; - getAllPrivateConversations: () => Promise>; - getConversationById: (id: string) => Promise; - getExpiredMessages: () => Promise>; - getMessageById: (id: string) => Promise; - getMessageBySender: (options: { - source: string; - sourceUuid: string; - sourceDevice: number; - sent_at: number; - }) => Promise>; - getMessagesBySentAt: (sentAt: number) => Promise>; + // Differing signature on client/server + + updateConversation: (data: ConversationType) => Promise; + removeConversation: (id: Array | string) => Promise; + + searchMessages: ( + query: string, + options?: { limit?: number } + ) => Promise>; + searchMessagesInConversation: ( + query: string, + conversationId: string, + options?: { limit?: number } + ) => Promise>; + getOlderMessagesByConversation: ( conversationId: string, options?: { @@ -611,38 +622,15 @@ export type ServerInterface = DataInterface & { storyId?: UUIDStringType; } ) => Promise>; - getLastConversationMessages: (options: { - conversationId: string; - ourUuid: UUIDStringType; - }) => Promise; - getTapToViewMessagesNeedingErase: () => Promise>; - removeConversation: (id: Array | string) => Promise; - removeMessage: (id: string) => Promise; - removeMessages: (ids: Array) => Promise; - searchMessages: ( - query: string, - options?: { limit?: number } - ) => Promise>; - searchMessagesInConversation: ( - query: string, - conversationId: string, - options?: { limit?: number } - ) => Promise>; - updateConversation: (data: ConversationType) => Promise; - - // For testing only - _getAllMessages: () => Promise>; // Server-only getCorruptionLog: () => string; - initialize: (options: { configDir: string; key: string; logger: LoggerType; }) => Promise; - initializeRenderer: (options: { configDir: string; key: string; @@ -659,83 +647,11 @@ export type ServerInterface = DataInterface & { }; export type ClientInterface = DataInterface & { - getAllConversations: (options: { - ConversationCollection: typeof ConversationModelCollectionType; - }) => Promise; - getAllGroupsInvolvingUuid: ( - id: UUIDStringType, - options: { - ConversationCollection: typeof ConversationModelCollectionType; - } - ) => Promise; - getAllPrivateConversations: (options: { - ConversationCollection: typeof ConversationModelCollectionType; - }) => Promise; - getConversationById: ( - id: string, - options: { Conversation: typeof ConversationModel } - ) => Promise; - getExpiredMessages: (options: { - MessageCollection: typeof MessageModelCollectionType; - }) => Promise; - getMessageById: ( - id: string, - options: { Message: typeof MessageModel } - ) => Promise; - getMessageBySender: ( - data: { - source: string; - sourceUuid: string; - sourceDevice: number; - sent_at: number; - }, - options: { Message: typeof MessageModel } - ) => Promise; - getMessagesBySentAt: ( - sentAt: number, - options: { MessageCollection: typeof MessageModelCollectionType } - ) => Promise; - getOlderMessagesByConversation: ( - conversationId: string, - options: { - limit?: number; - MessageCollection: typeof MessageModelCollectionType; - messageId?: string; - receivedAt?: number; - sentAt?: number; - storyId?: UUIDStringType; - } - ) => Promise; - getNewerMessagesByConversation: ( - conversationId: string, - options: { - limit?: number; - MessageCollection: typeof MessageModelCollectionType; - receivedAt?: number; - sentAt?: number; - storyId?: UUIDStringType; - } - ) => Promise; - getLastConversationMessages: (options: { - conversationId: string; - ourUuid: UUIDStringType; - Message: typeof MessageModel; - }) => Promise; - getTapToViewMessagesNeedingErase: (options: { - MessageCollection: typeof MessageModelCollectionType; - }) => Promise; - removeConversation: ( - id: string, - options: { Conversation: typeof ConversationModel } - ) => Promise; - removeMessage: ( - id: string, - options: { Message: typeof MessageModel } - ) => Promise; - removeMessages: ( - ids: Array, - options: { Message: typeof MessageModel } - ) => Promise; + // Differing signature on client/server + + updateConversation: (data: ConversationType) => void; + removeConversation: (id: string) => Promise; + searchMessages: ( query: string, options?: { limit?: number } @@ -745,13 +661,26 @@ export type ClientInterface = DataInterface & { conversationId: string, options?: { limit?: number } ) => Promise>; - updateConversation: (data: ConversationType, extra?: unknown) => void; - // Test-only - - _getAllMessages: (options: { - MessageCollection: typeof MessageModelCollectionType; - }) => Promise; + getOlderMessagesByConversation: ( + conversationId: string, + options: { + limit?: number; + messageId?: string; + receivedAt?: number; + sentAt?: number; + storyId?: UUIDStringType; + } + ) => Promise>; + getNewerMessagesByConversation: ( + conversationId: string, + options: { + limit?: number; + receivedAt?: number; + sentAt?: number; + storyId?: UUIDStringType; + } + ) => Promise>; // Client-side only @@ -760,21 +689,16 @@ export type ClientInterface = DataInterface & { conversationId: string, options: { logId: string; - MessageCollection: typeof MessageModelCollectionType; } ) => Promise; removeOtherData: () => Promise; cleanupOrphanedAttachments: () => Promise; ensureFilePermissions: () => Promise; - // Client-side only, and test-only - - _removeConversations: (ids: Array) => Promise; _jobs: { [id: string]: ClientJobType }; - // These are defined on the server-only and used in the client to determine - // whether we should use IPC to use the database in the main process or - // use the db already running in the renderer. + // To decide whether to use IPC to use the database in the main process or + // use the db already running in the renderer. goBackToMainProcess: () => Promise; startInRendererProcess: (isTesting?: boolean) => Promise; }; diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 505456ceaa83..f025ce813ddf 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -80,13 +80,13 @@ import type { IdentityKeyType, ItemKeyType, ItemType, - LastConversationMessagesServerType, + LastConversationMessagesType, MessageMetricsType, MessageType, MessageTypeUnhydrated, PreKeyIdType, PreKeyType, - SearchResultMessageType, + ServerSearchResultMessageType, SenderKeyIdType, SenderKeyType, SentMessageDBType, @@ -152,16 +152,16 @@ const dataInterface: ServerInterface = { createOrUpdateSignedPreKey, getSignedPreKeyById, - getAllSignedPreKeys, bulkAddSignedPreKeys, removeSignedPreKeyById, removeAllSignedPreKeys, + getAllSignedPreKeys, createOrUpdateItem, getItemById, - getAllItems, removeItemById, removeAllItems, + getAllItems, createOrUpdateSenderKey, getSenderKeyById, @@ -189,6 +189,7 @@ const dataInterface: ServerInterface = { removeAllSessions, getAllSessions, + eraseStorageServiceStateFromConversations, getConversationCount, saveConversation, saveConversations, @@ -196,12 +197,12 @@ const dataInterface: ServerInterface = { updateConversation, updateConversations, removeConversation, - eraseStorageServiceStateFromConversations, + updateAllConversationColors, + getAllConversations, getAllConversationIds, getAllPrivateConversations, getAllGroupsInvolvingUuid, - updateAllConversationColors, searchConversations, searchMessages, @@ -250,8 +251,8 @@ const dataInterface: ServerInterface = { getNextAttachmentDownloadJobs, saveAttachmentDownloadJob, - setAttachmentDownloadJobPending, resetAttachmentDownloadPending, + setAttachmentDownloadJobPending, removeAttachmentDownloadJob, removeAllAttachmentDownloadJobs, @@ -1557,7 +1558,7 @@ async function searchConversations( async function searchMessages( query: string, params: { limit?: number; conversationId?: string } = {} -): Promise> { +): Promise> { const { limit = 500, conversationId } = params; const db = getInstance(); @@ -1663,7 +1664,7 @@ async function searchMessagesInConversation( query: string, conversationId: string, { limit = 100 }: { limit?: number } = {} -): Promise> { +): Promise> { return searchMessages(query, { conversationId, limit }); } @@ -2024,7 +2025,7 @@ async function getMessageBySender({ sourceUuid: string; sourceDevice: number; sent_at: number; -}): Promise> { +}): Promise { const db = getInstance(); const rows: JSONRows = prepare( db, @@ -2032,7 +2033,8 @@ async function getMessageBySender({ SELECT json FROM messages WHERE (source = $source OR sourceUuid = $sourceUuid) AND sourceDevice = $sourceDevice AND - sent_at = $sent_at; + sent_at = $sent_at + LIMIT 2; ` ).all({ source, @@ -2041,7 +2043,20 @@ async function getMessageBySender({ sent_at, }); - return rows.map(row => jsonToObject(row.json)); + if (rows.length > 1) { + log.warn('getMessageBySender: More than one message found for', { + sent_at, + source, + sourceUuid, + sourceDevice, + }); + } + + if (rows.length < 1) { + return undefined; + } + + return jsonToObject(rows[0].json); } async function getUnreadByConversationAndMarkRead({ @@ -2605,7 +2620,7 @@ async function getLastConversationMessages({ }: { conversationId: string; ourUuid: UUIDStringType; -}): Promise { +}): Promise { const db = getInstance(); return db.transaction(() => { diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index ac0779fb6124..30d86fec4be9 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -187,7 +187,7 @@ export type GetContactOptions = Pick< 'conversationSelector' | 'ourConversationId' | 'ourNumber' | 'ourUuid' >; -function getContactId( +export function getContactId( message: MessageWithUIFieldsType, { conversationSelector, diff --git a/ts/test-electron/models/messages_test.ts b/ts/test-electron/models/messages_test.ts index edd03f29647d..f0b3db8ad82a 100644 --- a/ts/test-electron/models/messages_test.ts +++ b/ts/test-electron/models/messages_test.ts @@ -12,6 +12,7 @@ import type { CallbackResultType } from '../../textsecure/Types.d'; import type { StorageAccessType } from '../../types/Storage.d'; import { UUID } from '../../types/UUID'; import { SignalService as Proto } from '../../protobuf'; +import { getContact } from '../../messages/helpers'; describe('Message', () => { const STORAGE_KEYS_TO_RESTORE: Array = [ @@ -204,7 +205,7 @@ describe('Message', () => { it('gets outgoing contact', () => { const messages = new window.Whisper.MessageCollection(); const message = messages.add(attributes); - message.getContact(); + assert.exists(getContact(message.attributes)); }); it('gets incoming contact', () => { @@ -213,7 +214,7 @@ describe('Message', () => { type: 'incoming', source, }); - message.getContact(); + assert.exists(getContact(message.attributes)); }); }); diff --git a/ts/test-electron/sql/allMedia_test.ts b/ts/test-electron/sql/allMedia_test.ts index 369cf463440b..f3fafcffa639 100644 --- a/ts/test-electron/sql/allMedia_test.ts +++ b/ts/test-electron/sql/allMedia_test.ts @@ -28,12 +28,7 @@ describe('sql/allMedia', () => { describe('getMessagesWithVisualMediaAttachments', () => { it('returns messages matching with visual attachments', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const now = Date.now(); const conversationId = getUuid(); @@ -69,12 +64,7 @@ describe('sql/allMedia', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const searchResults = await getMessagesWithVisualMediaAttachments( conversationId, @@ -85,12 +75,7 @@ describe('sql/allMedia', () => { }); it('excludes stories and story replies', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const now = Date.now(); const conversationId = getUuid(); @@ -129,12 +114,7 @@ describe('sql/allMedia', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const searchResults = await getMessagesWithVisualMediaAttachments( conversationId, @@ -147,12 +127,7 @@ describe('sql/allMedia', () => { describe('getMessagesWithFileAttachments', () => { it('returns messages matching with visual attachments', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const now = Date.now(); const conversationId = getUuid(); @@ -188,12 +163,7 @@ describe('sql/allMedia', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const searchResults = await getMessagesWithFileAttachments( conversationId, @@ -204,12 +174,7 @@ describe('sql/allMedia', () => { }); it('excludes stories and story replies', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const now = Date.now(); const conversationId = getUuid(); @@ -248,12 +213,7 @@ describe('sql/allMedia', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const searchResults = await getMessagesWithFileAttachments( conversationId, diff --git a/ts/test-electron/sql/fullTextSearch_test.ts b/ts/test-electron/sql/fullTextSearch_test.ts index 0f1f88dd74bf..acfdf72d82ff 100644 --- a/ts/test-electron/sql/fullTextSearch_test.ts +++ b/ts/test-electron/sql/fullTextSearch_test.ts @@ -27,12 +27,7 @@ describe('sql/fullTextSearch', () => { }); it('returns messages matching query', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const now = Date.now(); const conversationId = getUuid(); @@ -66,12 +61,7 @@ describe('sql/fullTextSearch', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const searchResults = await searchMessages('unique'); assert.lengthOf(searchResults, 1); @@ -87,12 +77,7 @@ describe('sql/fullTextSearch', () => { }); it('excludes messages with isViewOnce = true', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const now = Date.now(); const conversationId = getUuid(); @@ -128,12 +113,7 @@ describe('sql/fullTextSearch', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const searchResults = await searchMessages('unique'); assert.lengthOf(searchResults, 1); @@ -148,12 +128,7 @@ describe('sql/fullTextSearch', () => { }); it('excludes messages with storyId !== null', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const now = Date.now(); const conversationId = getUuid(); @@ -189,12 +164,7 @@ describe('sql/fullTextSearch', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const searchResults = await searchMessages('unique'); assert.lengthOf(searchResults, 1); diff --git a/ts/test-electron/sql/markRead_test.ts b/ts/test-electron/sql/markRead_test.ts index 79d70e057a6c..7c7c9e02ed8c 100644 --- a/ts/test-electron/sql/markRead_test.ts +++ b/ts/test-electron/sql/markRead_test.ts @@ -34,12 +34,7 @@ describe('sql/markRead', () => { }); it('properly finds and reads unread messages in current conversation', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const start = Date.now(); const readAt = start + 20; @@ -125,12 +120,7 @@ describe('sql/markRead', () => { } ); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 7 - ); + assert.lengthOf(await _getAllMessages(), 7); assert.strictEqual( await getTotalUnreadForConversation(conversationId), 3, @@ -179,12 +169,7 @@ describe('sql/markRead', () => { }); it('properly finds and reads unread messages in story', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const start = Date.now(); const readAt = start + 20; @@ -276,12 +261,7 @@ describe('sql/markRead', () => { } ); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 7 - ); + assert.lengthOf(await _getAllMessages(), 7); const markedRead = await getUnreadByConversationAndMarkRead({ conversationId, @@ -311,12 +291,7 @@ describe('sql/markRead', () => { }); it('properly starts disappearing message timer, even if message is already read', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const start = Date.now(); const readAt = start + 20; @@ -388,12 +363,7 @@ describe('sql/markRead', () => { 2, 'unread count' ); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 5 - ); + assert.lengthOf(await _getAllMessages(), 5); const markedRead = await getUnreadByConversationAndMarkRead({ conversationId, @@ -413,21 +383,21 @@ describe('sql/markRead', () => { 'unread count' ); - const allMessages = await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }); + const allMessages = await _getAllMessages(); + const sorted = allMessages.sort( + (left, right) => left.timestamp - right.timestamp + ); - // Ascending order, since it's sorted by MessageCollection - assert.strictEqual(allMessages.at(1).id, message2.id); + assert.strictEqual(sorted[1].id, message2.id, 'checking message 2'); assert.isAtMost( - allMessages.at(1).get('expirationStartTimestamp') ?? Infinity, + sorted[1].expirationStartTimestamp ?? Infinity, Date.now(), 'checking message 2 expirationStartTimestamp' ); - assert.strictEqual(allMessages.at(3).id, message4.id, 'checking message 4'); + assert.strictEqual(sorted[3].id, message4.id, 'checking message 4'); assert.isAtMost( - allMessages.at(3).get('expirationStartTimestamp') ?? Infinity, + sorted[3].expirationStartTimestamp ?? Infinity, Date.now(), 'checking message 4 expirationStartTimestamp' ); @@ -490,12 +460,7 @@ describe('sql/markRead', () => { await saveMessages([message1, message2, message3, message4, message5], { forceSave: true, }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 5 - ); + assert.lengthOf(await _getAllMessages(), 5); const reaction1: ReactionType = { conversationId, @@ -642,12 +607,7 @@ describe('sql/markRead', () => { await saveMessages([message1, message2, message3, message4, message5], { forceSave: true, }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 5 - ); + assert.lengthOf(await _getAllMessages(), 5); const reaction1: ReactionType = { conversationId, diff --git a/ts/test-electron/sql/sendLog_test.ts b/ts/test-electron/sql/sendLog_test.ts index ca952311096b..1f1f22e789de 100644 --- a/ts/test-electron/sql/sendLog_test.ts +++ b/ts/test-electron/sql/sendLog_test.ts @@ -124,7 +124,7 @@ describe('sql/sendLog', () => { assert.strictEqual(actual.timestamp, proto.timestamp); - await removeMessage(id, { Message: window.Whisper.Message }); + await removeMessage(id); assert.lengthOf(await getAllSentProtos(), 0); }); diff --git a/ts/test-electron/sql/stories_test.ts b/ts/test-electron/sql/stories_test.ts index d36bd06b75d0..1c924be79c30 100644 --- a/ts/test-electron/sql/stories_test.ts +++ b/ts/test-electron/sql/stories_test.ts @@ -23,12 +23,7 @@ describe('sql/stories', () => { describe('getOlderStories', () => { it('returns N most recent stories overall, or in converation, or by author', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const now = Date.now(); const conversationId = getUuid(); @@ -89,12 +84,7 @@ describe('sql/stories', () => { forceSave: true, }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 5 - ); + assert.lengthOf(await _getAllMessages(), 5); const stories = await getOlderStories({ limit: 5, @@ -155,12 +145,7 @@ describe('sql/stories', () => { }); it('returns N stories older than provided receivedAt/sentAt', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const start = Date.now(); const conversationId = getUuid(); @@ -214,12 +199,7 @@ describe('sql/stories', () => { forceSave: true, }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 5 - ); + assert.lengthOf(await _getAllMessages(), 5); const stories = await getOlderStories({ receivedAt: story4.received_at, diff --git a/ts/test-electron/sql/timelineFetches_test.ts b/ts/test-electron/sql/timelineFetches_test.ts index 9c3c8f9de2c3..767ee099b897 100644 --- a/ts/test-electron/sql/timelineFetches_test.ts +++ b/ts/test-electron/sql/timelineFetches_test.ts @@ -30,12 +30,7 @@ describe('sql/timelineFetches', () => { describe('getOlderMessagesByConversation', () => { it('returns N most recent messages', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const now = Date.now(); const conversationId = getUuid(); @@ -92,30 +87,20 @@ describe('sql/timelineFetches', () => { forceSave: true, }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 5 - ); + assert.lengthOf(await _getAllMessages(), 5); const messages = await getOlderMessagesByConversation(conversationId, { - MessageCollection: window.Whisper.MessageCollection, limit: 5, }); assert.lengthOf(messages, 2); - // They are not in DESC order because MessageCollection is sorting them - assert.strictEqual(messages.at(0).attributes.id, message1.id); - assert.strictEqual(messages.at(1).attributes.id, message2.id); + + // Fetched with DESC query, but with reverse() call afterwards + assert.strictEqual(messages[0].id, message1.id); + assert.strictEqual(messages[1].id, message2.id); }); it('returns N most recent messages for a given story', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const now = Date.now(); const conversationId = getUuid(); @@ -152,29 +137,18 @@ describe('sql/timelineFetches', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const messages = await getOlderMessagesByConversation(conversationId, { - MessageCollection: window.Whisper.MessageCollection, limit: 5, storyId, }); assert.lengthOf(messages, 1); - assert.strictEqual(messages.at(0).attributes.id, message2.id); + assert.strictEqual(messages[0].id, message2.id); }); it('returns N messages older than provided received_at', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const target = Date.now(); const conversationId = getUuid(); @@ -208,30 +182,19 @@ describe('sql/timelineFetches', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const messages = await getOlderMessagesByConversation(conversationId, { - MessageCollection: window.Whisper.MessageCollection, limit: 5, receivedAt: target, sentAt: target, }); assert.lengthOf(messages, 1); - assert.strictEqual(messages.at(0).attributes.id, message1.id); + assert.strictEqual(messages[0].id, message1.id); }); it('returns N older messages with received_at, lesser sent_at', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const target = Date.now(); const conversationId = getUuid(); @@ -265,33 +228,23 @@ describe('sql/timelineFetches', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const messages = await getOlderMessagesByConversation(conversationId, { - MessageCollection: window.Whisper.MessageCollection, limit: 5, receivedAt: target, sentAt: target, }); assert.lengthOf(messages, 2); - // They are not in DESC order because MessageCollection is sorting them - assert.strictEqual(messages.at(0).attributes.id, message1.id); - assert.strictEqual(messages.at(1).attributes.id, message2.id); + + // Fetched with DESC query, but with reverse() call afterwards + assert.strictEqual(messages[0].id, message1.id, 'checking message 1'); + assert.strictEqual(messages[1].id, message2.id, 'checking message 2'); }); it('returns N older messages, same received_at/sent_at but excludes messageId', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const target = Date.now(); const conversationId = getUuid(); @@ -325,15 +278,9 @@ describe('sql/timelineFetches', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const messages = await getOlderMessagesByConversation(conversationId, { - MessageCollection: window.Whisper.MessageCollection, limit: 5, receivedAt: target, sentAt: target, @@ -341,18 +288,13 @@ describe('sql/timelineFetches', () => { }); assert.lengthOf(messages, 1); - assert.strictEqual(messages.at(0).attributes.id, message1.id); + assert.strictEqual(messages[0].id, message1.id); }); }); describe('getNewerMessagesByConversation', () => { it('returns N oldest messages with no parameters', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const now = Date.now(); const conversationId = getUuid(); @@ -409,30 +351,19 @@ describe('sql/timelineFetches', () => { forceSave: true, }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 5 - ); + assert.lengthOf(await _getAllMessages(), 5); const messages = await getNewerMessagesByConversation(conversationId, { - MessageCollection: window.Whisper.MessageCollection, limit: 5, }); assert.lengthOf(messages, 2); - assert.strictEqual(messages.at(0).attributes.id, message4.id); - assert.strictEqual(messages.at(1).attributes.id, message5.id); + assert.strictEqual(messages[0].id, message4.id, 'checking message 4'); + assert.strictEqual(messages[1].id, message5.id, 'checking message 5'); }); it('returns N oldest messages for a given story with no parameters', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const now = Date.now(); const conversationId = getUuid(); @@ -469,30 +400,19 @@ describe('sql/timelineFetches', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const messages = await getNewerMessagesByConversation(conversationId, { - MessageCollection: window.Whisper.MessageCollection, limit: 5, storyId, }); assert.lengthOf(messages, 1); - assert.strictEqual(messages.at(0).attributes.id, message2.id); + assert.strictEqual(messages[0].id, message2.id); }); it('returns N messages newer than provided received_at', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const target = Date.now(); const conversationId = getUuid(); @@ -526,30 +446,19 @@ describe('sql/timelineFetches', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const messages = await getNewerMessagesByConversation(conversationId, { - MessageCollection: window.Whisper.MessageCollection, limit: 5, receivedAt: target, sentAt: target, }); assert.lengthOf(messages, 1); - assert.strictEqual(messages.at(0).attributes.id, message3.id); + assert.strictEqual(messages[0].id, message3.id); }); it('returns N newer messages with same received_at, greater sent_at', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const target = Date.now(); const conversationId = getUuid(); @@ -583,15 +492,9 @@ describe('sql/timelineFetches', () => { await saveMessages([message1, message2, message3], { forceSave: true }); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 3 - ); + assert.lengthOf(await _getAllMessages(), 3); const messages = await getNewerMessagesByConversation(conversationId, { - MessageCollection: window.Whisper.MessageCollection, limit: 5, receivedAt: target, sentAt: target, @@ -599,19 +502,14 @@ describe('sql/timelineFetches', () => { assert.lengthOf(messages, 2); // They are not in DESC order because MessageCollection is sorting them - assert.strictEqual(messages.at(0).attributes.id, message2.id); - assert.strictEqual(messages.at(1).attributes.id, message3.id); + assert.strictEqual(messages[0].id, message2.id); + assert.strictEqual(messages[1].id, message3.id); }); }); describe('getMessageMetricsForConversation', () => { it('returns metrics properly for story and non-story timelines', async () => { - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 0 - ); + assert.lengthOf(await _getAllMessages(), 0); const target = Date.now(); const conversationId = getUuid(); @@ -710,12 +608,7 @@ describe('sql/timelineFetches', () => { { forceSave: true } ); - assert.lengthOf( - await _getAllMessages({ - MessageCollection: window.Whisper.MessageCollection, - }), - 8 - ); + assert.lengthOf(await _getAllMessages(), 8); const metricsInTimeline = await getMessageMetricsForConversation( conversationId diff --git a/ts/test-electron/textsecure/KeyChangeListener_test.ts b/ts/test-electron/textsecure/KeyChangeListener_test.ts index f043f79018e7..aa08aab61579 100644 --- a/ts/test-electron/textsecure/KeyChangeListener_test.ts +++ b/ts/test-electron/textsecure/KeyChangeListener_test.ts @@ -11,8 +11,6 @@ import { SignalProtocolStore } from '../../SignalProtocolStore'; import type { ConversationModel } from '../../models/conversations'; import * as KeyChangeListener from '../../textsecure/KeyChangeListener'; -const { Whisper } = window; - describe('KeyChangeListener', () => { let oldNumberId: string | undefined; let oldUuidId: string | undefined; @@ -71,11 +69,8 @@ describe('KeyChangeListener', () => { afterEach(async () => { await window.Signal.Data.removeAllMessagesInConversation(convo.id, { logId: uuidWithKeyChange, - MessageCollection: Whisper.MessageCollection, - }); - await window.Signal.Data.removeConversation(convo.id, { - Conversation: Whisper.Conversation, }); + await window.Signal.Data.removeConversation(convo.id); await store.removeIdentityKey(new UUID(uuidWithKeyChange)); }); @@ -107,11 +102,8 @@ describe('KeyChangeListener', () => { afterEach(async () => { await window.Signal.Data.removeAllMessagesInConversation(groupConvo.id, { logId: uuidWithKeyChange, - MessageCollection: Whisper.MessageCollection, - }); - await window.Signal.Data.removeConversation(groupConvo.id, { - Conversation: Whisper.Conversation, }); + await window.Signal.Data.removeConversation(groupConvo.id); }); it('generates a key change notice in the group conversation with this contact', done => { diff --git a/ts/util/MessageController.ts b/ts/util/MessageController.ts index b19ca11c1f59..8d66502cedce 100644 --- a/ts/util/MessageController.ts +++ b/ts/util/MessageController.ts @@ -5,6 +5,7 @@ import type { MessageModel } from '../models/messages'; import * as durations from './durations'; import { map, filter } from './iterables'; import { isNotNil } from './isNotNil'; +import type { MessageAttributesType } from '../model-types.d'; const FIVE_MINUTES = 5 * durations.MINUTE; @@ -29,9 +30,12 @@ export class MessageController { return instance; } - register(id: string, message: MessageModel): MessageModel { - if (!id || !message) { - return message; + register( + id: string, + data: MessageModel | MessageAttributesType + ): MessageModel { + if (!id || !data) { + throw new Error('MessageController.register: Got falsey id or message'); } const existing = this.messageLookup[id]; @@ -43,6 +47,8 @@ export class MessageController { return existing.message; } + const message = + 'attributes' in data ? data : new window.Whisper.Message(data); this.messageLookup[id] = { message, timestamp: Date.now(), diff --git a/ts/util/cleanup.ts b/ts/util/cleanup.ts new file mode 100644 index 000000000000..f35dde361b7b --- /dev/null +++ b/ts/util/cleanup.ts @@ -0,0 +1,36 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { MessageAttributesType } from '../model-types.d'; +import { deletePackReference } from '../types/Stickers'; + +export async function cleanupMessage( + message: MessageAttributesType +): Promise { + const { id, conversationId } = message; + + window.reduxActions?.conversations.messageDeleted(id, conversationId); + + const parentConversation = window.ConversationController.get(conversationId); + parentConversation?.debouncedUpdateLastMessage?.(); + + window.MessageController.unregister(id); + + await deleteMessageData(message); +} + +export async function deleteMessageData( + message: MessageAttributesType +): Promise { + await window.Signal.Migrations.deleteExternalMessageFiles(message); + + const { sticker } = message; + if (!sticker) { + return; + } + + const { packId } = sticker; + if (packId) { + await deletePackReference(message.id, packId); + } +} diff --git a/ts/util/handleRetry.ts b/ts/util/handleRetry.ts index 52883a8079cb..0847b5cc1e5f 100644 --- a/ts/util/handleRetry.ts +++ b/ts/util/handleRetry.ts @@ -347,9 +347,7 @@ async function getRetryConversation({ } const [messageId] = messageIds; - const message = await window.Signal.Data.getMessageById(messageId, { - Message: window.Whisper.Message, - }); + const message = await window.Signal.Data.getMessageById(messageId); if (!message) { log.warn( `getRetryConversation/${logId}: Unable to find message ${messageId}` @@ -358,7 +356,7 @@ async function getRetryConversation({ return window.ConversationController.get(requestGroupId); } - const conversationId = message.get('conversationId'); + const { conversationId } = message; return window.ConversationController.get(conversationId); } diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index 06bfc2805a7f..e01f5dfb339f 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -29,6 +29,7 @@ import type { MessageAttributesType as MediaItemMessageType, } from '../types/MediaItem'; import type { MessageModel } from '../models/messages'; +import { getContactId } from '../messages/helpers'; import { strictAssert } from '../util/assert'; import { maybeParseUrl } from '../util/url'; import { enqueueReactionForSend } from '../reactions/enqueueReactionForSend'; @@ -447,14 +448,12 @@ export class ConversationView extends window.Backbone.View { const { authorId, sentAt } = options; const conversationId = this.model.id; - const messages = await getMessagesBySentAt(sentAt, { - MessageCollection: Whisper.MessageCollection, - }); + const messages = await getMessagesBySentAt(sentAt); const message = messages.find(item => Boolean( - item.get('conversationId') === conversationId && + item.conversationId === conversationId && authorId && - item.getContactId() === authorId + getContactId(item) === authorId ) ); @@ -471,14 +470,12 @@ export class ConversationView extends window.Backbone.View { return; } - const message = await getMessageById(messageId, { - Message: Whisper.Message, - }); + const message = await getMessageById(messageId); if (!message) { throw new Error(`markMessageRead: failed to load message ${messageId}`); } - await this.model.markRead(message.get('received_at')); + await this.model.markRead(message.received_at); }; const createMessageRequestResponseHandler = @@ -883,9 +880,7 @@ export class ConversationView extends window.Backbone.View { } async scrollToMessage(messageId: string): Promise { - const message = await getMessageById(messageId, { - Message: Whisper.Message, - }); + const message = await getMessageById(messageId); if (!message) { throw new Error(`scrollToMessage: failed to load message ${messageId}`); } @@ -1201,9 +1196,7 @@ export class ConversationView extends window.Backbone.View { async onOpened(messageId: string): Promise { if (messageId) { - const message = await getMessageById(messageId, { - Message: Whisper.Message, - }); + const message = await getMessageById(messageId); if (message) { this.model.loadAndScroll(messageId); @@ -1254,16 +1247,14 @@ export class ConversationView extends window.Backbone.View { if (!messageFromCache) { log.info('showForwardMessageModal: Fetching message from database'); } - const message = - messageFromCache || - (await window.Signal.Data.getMessageById(messageId, { - Message: window.Whisper.Message, - })); + const found = + messageFromCache || (await window.Signal.Data.getMessageById(messageId)); - if (!message) { + if (!found) { throw new Error(`showForwardMessageModal: Message ${messageId} missing!`); } + const message = window.MessageController.register(found.id, found); const attachments = getAttachmentsForMessage(message.attributes); this.forwardMessageModal = new Whisper.ReactWrapperView({ @@ -1911,10 +1902,7 @@ export class ConversationView extends window.Backbone.View { message: window.i18n('deleteWarning'), okText: window.i18n('delete'), resolve: () => { - window.Signal.Data.removeMessage(message.id, { - Message: Whisper.Message, - }); - message.cleanup(); + window.Signal.Data.removeMessage(message.id); if (isOutgoing(message.attributes)) { this.model.decrementSentMessageCount(); } else { @@ -2676,18 +2664,16 @@ export class ConversationView extends window.Backbone.View { } async setQuoteMessage(messageId: null | string): Promise { - const { model }: { model: ConversationModel } = this; - - const message: MessageModel | undefined = messageId - ? await getMessageById(messageId, { - Message: Whisper.Message, - }) + const { model } = this; + const found = messageId ? await getMessageById(messageId) : undefined; + const message = found + ? window.MessageController.register(found.id, found) : undefined; if ( - message && + found && !canReply( - message.attributes, + found, window.ConversationController.getOurConversationIdOrThrow(), findAndFormatContact ) @@ -2724,18 +2710,11 @@ export class ConversationView extends window.Backbone.View { } if (message) { - const quotedMessage = window.MessageController.register( - message.id, - message - ); - this.quotedMessage = quotedMessage; + this.quotedMessage = message; + this.quote = await model.makeQuote(this.quotedMessage); - if (quotedMessage) { - this.quote = await model.makeQuote(this.quotedMessage); - - this.enableMessageField(); - this.focusMessageField(); - } + this.enableMessageField(); + this.focusMessageField(); } this.renderQuotedMessage();