No Backbone in data layer; server/client interfaces are now similar

This commit is contained in:
Scott Nonnenberg 2021-12-10 14:51:54 -08:00 committed by GitHub
parent 064bbfe97a
commit 34fd945f83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 573 additions and 1021 deletions

View file

@ -11,6 +11,7 @@ import type {
ConversationAttributesTypeType, ConversationAttributesTypeType,
} from './model-types.d'; } from './model-types.d';
import type { ConversationModel } from './models/conversations'; import type { ConversationModel } from './models/conversations';
import { getContactId } from './messages/helpers';
import { maybeDeriveGroupV2Id } from './groups'; import { maybeDeriveGroupV2Id } from './groups';
import { assert } from './util/assert'; import { assert } from './util/assert';
import { map, reduce } from './util/iterables'; import { map, reduce } from './util/iterables';
@ -676,9 +677,7 @@ export class ConversationController {
log.warn( log.warn(
'combineConversations: Delete the obsolete conversation from the database' 'combineConversations: Delete the obsolete conversation from the database'
); );
await removeConversation(obsoleteId, { await removeConversation(obsoleteId);
Conversation: window.Whisper.Conversation,
});
log.warn('combineConversations: Update messages table'); log.warn('combineConversations: Update messages table');
await migrateConversationMessages(obsoleteId, currentId); await migrateConversationMessages(obsoleteId, currentId);
@ -714,13 +713,11 @@ export class ConversationController {
targetFromId: string, targetFromId: string,
targetTimestamp: number targetTimestamp: number
): Promise<ConversationModel | null | undefined> { ): Promise<ConversationModel | null | undefined> {
const messages = await getMessagesBySentAt(targetTimestamp, { const messages = await getMessagesBySentAt(targetTimestamp);
MessageCollection: window.Whisper.MessageCollection, const targetMessage = messages.find(m => getContactId(m) === targetFromId);
});
const targetMessage = messages.find(m => m.getContactId() === targetFromId);
if (targetMessage) { if (targetMessage) {
return targetMessage.getConversation(); return this.get(targetMessage.conversationId);
} }
return null; return null;
@ -729,9 +726,7 @@ export class ConversationController {
async getAllGroupsInvolvingUuid( async getAllGroupsInvolvingUuid(
uuid: UUID uuid: UUID
): Promise<Array<ConversationModel>> { ): Promise<Array<ConversationModel>> {
const groups = await getAllGroupsInvolvingUuid(uuid.toString(), { const groups = await getAllGroupsInvolvingUuid(uuid.toString());
ConversationCollection: window.Whisper.ConversationCollection,
});
return groups.map(group => { return groups.map(group => {
const existing = this.get(group.id); const existing = this.get(group.id);
if (existing) { if (existing) {
@ -767,13 +762,11 @@ export class ConversationController {
} }
try { try {
const collection = await getAllConversations({ const collection = await getAllConversations();
ConversationCollection: window.Whisper.ConversationCollection,
});
// Get rid of temporary conversations // Get rid of temporary conversations
const temporaryConversations = collection.filter(conversation => const temporaryConversations = collection.filter(conversation =>
Boolean(conversation.get('isTemporary')) Boolean(conversation.isTemporary)
); );
if (temporaryConversations.length) { if (temporaryConversations.length) {
@ -788,16 +781,14 @@ export class ConversationController {
}); });
queue.addAll( queue.addAll(
temporaryConversations.map(item => async () => { temporaryConversations.map(item => async () => {
await removeConversation(item.id, { await removeConversation(item.id);
Conversation: window.Whisper.Conversation,
});
}) })
); );
await queue.onIdle(); await queue.onIdle();
// Hydrate the final set of conversations // Hydrate the final set of conversations
this._conversations.add( this._conversations.add(
collection.filter(conversation => !conversation.get('isTemporary')) collection.filter(conversation => !conversation.isTemporary)
); );
this._initialFetchComplete = true; this._initialFetchComplete = true;

View file

@ -44,6 +44,7 @@ import { routineProfileRefresh } from './routineProfileRefresh';
import { isMoreRecentThan, isOlderThan, toDayMillis } from './util/timestamp'; import { isMoreRecentThan, isOlderThan, toDayMillis } from './util/timestamp';
import { isValidReactionEmoji } from './reactions/isValidReactionEmoji'; import { isValidReactionEmoji } from './reactions/isValidReactionEmoji';
import type { ConversationModel } from './models/conversations'; import type { ConversationModel } from './models/conversations';
import { getContact } from './messages/helpers';
import { getMessageById } from './messages/getMessageById'; import { getMessageById } from './messages/getMessageById';
import { createBatcher } from './util/batcher'; import { createBatcher } from './util/batcher';
import { updateConversationsWithUuidLookup } from './updateConversationsWithUuidLookup'; import { updateConversationsWithUuidLookup } from './updateConversationsWithUuidLookup';
@ -2841,7 +2842,7 @@ export async function startApp(): Promise<void> {
isIncoming(message.attributes) && isIncoming(message.attributes) &&
!message.get('unidentifiedDeliveryReceived') !message.get('unidentifiedDeliveryReceived')
) { ) {
const sender = message.getContact(); const sender = getContact(message.attributes);
if (!sender) { if (!sender) {
throw new Error('MessageModel has no sender.'); throw new Error('MessageModel has no sender.');

View file

@ -318,9 +318,7 @@ export async function joinViaLink(hash: string): Promise<void> {
window.ConversationController.dangerouslyRemoveById( window.ConversationController.dangerouslyRemoveById(
tempConversation.id tempConversation.id
); );
await window.Signal.Data.removeConversation(tempConversation.id, { await window.Signal.Data.removeConversation(tempConversation.id);
Conversation: window.Whisper.Conversation,
});
} }
throw error; throw error;

View file

@ -196,9 +196,7 @@ async function _runJob(job?: AttachmentDownloadJobType): Promise<void> {
const found = const found =
window.MessageController.getById(messageId) || window.MessageController.getById(messageId) ||
(await getMessageById(messageId, { (await getMessageById(messageId));
Message: window.Whisper.Message,
}));
if (!found) { if (!found) {
logger.error('_runJob: Source message not found, deleting job'); logger.error('_runJob: Source message not found, deleting job');
await _finishJob(null, id); await _finishJob(null, id);

View file

@ -5,6 +5,7 @@
import { Collection, Model } from 'backbone'; import { Collection, Model } from 'backbone';
import type { MessageModel } from '../models/messages'; import type { MessageModel } from '../models/messages';
import { getContactId } from '../messages/helpers';
import * as log from '../logging/log'; import * as log from '../logging/log';
type DeleteAttributesType = { type DeleteAttributesType = {
@ -30,7 +31,7 @@ export class Deletes extends Collection<DeleteModel> {
const matchingDeletes = this.filter(item => { const matchingDeletes = this.filter(item => {
return ( return (
item.get('targetSentTimestamp') === message.get('sent_at') && 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<DeleteModel> {
log.info('Handling DOE for', del.get('targetSentTimestamp')); log.info('Handling DOE for', del.get('targetSentTimestamp'));
const messages = await window.Signal.Data.getMessagesBySentAt( const messages = await window.Signal.Data.getMessagesBySentAt(
del.get('targetSentTimestamp'), del.get('targetSentTimestamp')
{
MessageCollection: window.Whisper.MessageCollection,
}
); );
const targetMessage = messages.find( const targetMessage = messages.find(
m => del.get('fromId') === m.getContactId() m => del.get('fromId') === getContactId(m)
); );
if (!targetMessage) { if (!targetMessage) {

View file

@ -8,7 +8,7 @@ import { Collection, Model } from 'backbone';
import type { ConversationModel } from '../models/conversations'; import type { ConversationModel } from '../models/conversations';
import type { MessageModel } from '../models/messages'; 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 { isOutgoing } from '../state/selectors/message';
import { isDirectConversation } from '../util/whatTypeOfConversation'; import { isDirectConversation } from '../util/whatTypeOfConversation';
import { getOwn } from '../util/getOwn'; import { getOwn } from '../util/getOwn';
@ -60,32 +60,25 @@ const deleteSentProtoBatcher = createWaitBatcher({
async function getTargetMessage( async function getTargetMessage(
sourceId: string, sourceId: string,
sourceUuid: UUIDStringType, sourceUuid: UUIDStringType,
messages: MessageModelCollectionType messages: ReadonlyArray<MessageAttributesType>
): Promise<MessageModel | null> { ): Promise<MessageModel | null> {
if (messages.length === 0) { if (messages.length === 0) {
return null; return null;
} }
const message = messages.find( const message = messages.find(
item => item => isOutgoing(item) && sourceId === item.conversationId
isOutgoing(item.attributes) && sourceId === item.get('conversationId')
); );
if (message) { if (message) {
return window.MessageController.register(message.id, message); return window.MessageController.register(message.id, message);
} }
const groups = await window.Signal.Data.getAllGroupsInvolvingUuid( const groups = await window.Signal.Data.getAllGroupsInvolvingUuid(sourceUuid);
sourceUuid,
{
ConversationCollection: window.Whisper.ConversationCollection,
}
);
const ids = groups.pluck('id'); const ids = groups.map(item => item.id);
ids.push(sourceId); ids.push(sourceId);
const target = messages.find( const target = messages.find(
item => item => isOutgoing(item) && ids.includes(item.conversationId)
isOutgoing(item.attributes) && ids.includes(item.get('conversationId'))
); );
if (!target) { if (!target) {
return null; return null;
@ -147,10 +140,7 @@ export class MessageReceipts extends Collection<MessageReceiptModel> {
try { try {
const messages = await window.Signal.Data.getMessagesBySentAt( const messages = await window.Signal.Data.getMessagesBySentAt(
messageSentAt, messageSentAt
{
MessageCollection: window.Whisper.MessageCollection,
}
); );
const message = await getTargetMessage( const message = await getTargetMessage(

View file

@ -5,6 +5,7 @@
import { Collection, Model } from 'backbone'; import { Collection, Model } from 'backbone';
import type { MessageModel } from '../models/messages'; import type { MessageModel } from '../models/messages';
import { getContactId, getContact } from '../messages/helpers';
import { isOutgoing } from '../state/selectors/message'; import { isOutgoing } from '../state/selectors/message';
import type { ReactionAttributesType } from '../model-types.d'; import type { ReactionAttributesType } from '../model-types.d';
import * as log from '../logging/log'; import * as log from '../logging/log';
@ -35,7 +36,7 @@ export class Reactions extends Collection<ReactionModel> {
} }
} }
const senderId = message.getContactId(); const senderId = getContactId(message.attributes);
const sentAt = message.get('sent_at'); const sentAt = message.get('sent_at');
const reactionsBySource = this.filter(re => { const reactionsBySource = this.filter(re => {
const targetSenderId = window.ConversationController.ensureContactIds({ const targetSenderId = window.ConversationController.ensureContactIds({
@ -87,15 +88,12 @@ export class Reactions extends Collection<ReactionModel> {
log.info('Handling reaction for', reaction.get('targetTimestamp')); log.info('Handling reaction for', reaction.get('targetTimestamp'));
const messages = await window.Signal.Data.getMessagesBySentAt( const messages = await window.Signal.Data.getMessagesBySentAt(
reaction.get('targetTimestamp'), reaction.get('targetTimestamp')
{
MessageCollection: window.Whisper.MessageCollection,
}
); );
// Message is fetched inside the conversation queue so we have the // Message is fetched inside the conversation queue so we have the
// most recent data // most recent data
const targetMessage = messages.find(m => { const targetMessage = messages.find(m => {
const contact = m.getContact(); const contact = getContact(m);
if (!contact) { if (!contact) {
return false; return false;

View file

@ -80,19 +80,16 @@ export class ReadSyncs extends Collection {
async onSync(sync: ReadSyncModel): Promise<void> { async onSync(sync: ReadSyncModel): Promise<void> {
try { try {
const messages = await window.Signal.Data.getMessagesBySentAt( const messages = await window.Signal.Data.getMessagesBySentAt(
sync.get('timestamp'), sync.get('timestamp')
{
MessageCollection: window.Whisper.MessageCollection,
}
); );
const found = messages.find(item => { const found = messages.find(item => {
const senderId = window.ConversationController.ensureContactIds({ const senderId = window.ConversationController.ensureContactIds({
e164: item.get('source'), e164: item.source,
uuid: item.get('sourceUuid'), uuid: item.sourceUuid,
}); });
return isIncoming(item.attributes) && senderId === sync.get('senderId'); return isIncoming(item) && senderId === sync.get('senderId');
}); });
if (!found) { if (!found) {

View file

@ -57,16 +57,13 @@ export class ViewOnceOpenSyncs extends Collection<ViewOnceOpenSyncModel> {
async onSync(sync: ViewOnceOpenSyncModel): Promise<void> { async onSync(sync: ViewOnceOpenSyncModel): Promise<void> {
try { try {
const messages = await window.Signal.Data.getMessagesBySentAt( const messages = await window.Signal.Data.getMessagesBySentAt(
sync.get('timestamp'), sync.get('timestamp')
{
MessageCollection: window.Whisper.MessageCollection,
}
); );
const found = messages.find(item => { const found = messages.find(item => {
const itemSourceUuid = item.get('sourceUuid'); const itemSourceUuid = item.sourceUuid;
const syncSourceUuid = sync.get('sourceUuid'); const syncSourceUuid = sync.get('sourceUuid');
const itemSource = item.get('source'); const itemSource = item.source;
const syncSource = sync.get('source'); const syncSource = sync.get('source');
return Boolean( return Boolean(

View file

@ -58,19 +58,16 @@ export class ViewSyncs extends Collection {
async onSync(sync: ViewSyncModel): Promise<void> { async onSync(sync: ViewSyncModel): Promise<void> {
try { try {
const messages = await window.Signal.Data.getMessagesBySentAt( const messages = await window.Signal.Data.getMessagesBySentAt(
sync.get('timestamp'), sync.get('timestamp')
{
MessageCollection: window.Whisper.MessageCollection,
}
); );
const found = messages.find(item => { const found = messages.find(item => {
const senderId = window.ConversationController.ensureContactIds({ const senderId = window.ConversationController.ensureContactIds({
e164: item.get('source'), e164: item.source,
uuid: item.get('sourceUuid'), uuid: item.sourceUuid,
}); });
return isIncoming(item.attributes) && senderId === sync.get('senderId'); return isIncoming(item) && senderId === sync.get('senderId');
}); });
if (!found) { if (!found) {

View file

@ -2,21 +2,21 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as log from '../logging/log'; import * as log from '../logging/log';
import type { MessageAttributesType } from '../model-types.d';
import type { MessageModel } from '../models/messages'; import type { MessageModel } from '../models/messages';
import * as Errors from '../types/errors'; import * as Errors from '../types/errors';
export async function getMessageById( export async function getMessageById(
messageId: string messageId: string
): Promise<MessageModel | undefined> { ): Promise<MessageModel | undefined> {
let message = window.MessageController.getById(messageId); const message = window.MessageController.getById(messageId);
if (message) { if (message) {
return message; return message;
} }
let found: MessageAttributesType | undefined;
try { try {
message = await window.Signal.Data.getMessageById(messageId, { found = await window.Signal.Data.getMessageById(messageId);
Message: window.Whisper.Message,
});
} catch (err: unknown) { } catch (err: unknown) {
log.error( log.error(
`failed to load message with id ${messageId} ` + `failed to load message with id ${messageId} ` +
@ -24,10 +24,9 @@ export async function getMessageById(
); );
} }
if (!message) { if (!found) {
return undefined; return undefined;
} }
message = window.MessageController.register(message.id, message); return window.MessageController.register(found.id, found);
return message;
} }

103
ts/messages/helpers.ts Normal file
View file

@ -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;

View file

@ -9,7 +9,6 @@ import type {
ConversationModelCollectionType, ConversationModelCollectionType,
LastMessageStatus, LastMessageStatus,
MessageAttributesType, MessageAttributesType,
MessageModelCollectionType,
QuotedMessageType, QuotedMessageType,
SenderKeyInfoType, SenderKeyInfoType,
VerificationOptions, VerificationOptions,
@ -38,6 +37,7 @@ import type {
CustomColorType, CustomColorType,
} from '../types/Colors'; } from '../types/Colors';
import type { MessageModel } from './messages'; import type { MessageModel } from './messages';
import { getContact } from '../messages/helpers';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { isMuted } from '../util/isMuted'; import { isMuted } from '../util/isMuted';
import { isConversationSMSOnly } from '../util/isConversationSMSOnly'; import { isConversationSMSOnly } from '../util/isConversationSMSOnly';
@ -1305,12 +1305,6 @@ export class ConversationModel extends window.Backbone
}); });
} }
async cleanup(): Promise<void> {
await Conversation.deleteExternalFiles(this.attributes, {
deleteAttachmentData,
});
}
async onNewMessage(message: MessageModel): Promise<void> { async onNewMessage(message: MessageModel): Promise<void> {
const uuid = message.get('sourceUuid'); const uuid = message.get('sourceUuid');
const e164 = message.get('source'); const e164 = message.get('source');
@ -1407,13 +1401,11 @@ export class ConversationModel extends window.Backbone
let scrollToLatestUnread = true; let scrollToLatestUnread = true;
if (newestMessageId) { if (newestMessageId) {
const newestInMemoryMessage = await getMessageById(newestMessageId, { const newestInMemoryMessage = await getMessageById(newestMessageId);
Message: window.Whisper.Message,
});
if (newestInMemoryMessage) { if (newestInMemoryMessage) {
// If newest in-memory message is unread, scrolling down would mean going to // If newest in-memory message is unread, scrolling down would mean going to
// the very bottom, not the oldest unread. // the very bottom, not the oldest unread.
if (isMessageUnread(newestInMemoryMessage.attributes)) { if (isMessageUnread(newestInMemoryMessage)) {
scrollToLatestUnread = false; scrollToLatestUnread = false;
} }
} else { } else {
@ -1443,7 +1435,6 @@ export class ConversationModel extends window.Backbone
const messages = await getOlderMessagesByConversation(conversationId, { const messages = await getOlderMessagesByConversation(conversationId, {
limit: MESSAGE_LOAD_CHUNK_SIZE, limit: MESSAGE_LOAD_CHUNK_SIZE,
MessageCollection: window.Whisper.MessageCollection,
}); });
const cleaned: Array<MessageModel> = await this.cleanModels(messages); const cleaned: Array<MessageModel> = await this.cleanModels(messages);
@ -1481,23 +1472,20 @@ export class ConversationModel extends window.Backbone
const finish = this.setInProgressFetch(); const finish = this.setInProgressFetch();
try { try {
const message = await getMessageById(oldestMessageId, { const message = await getMessageById(oldestMessageId);
Message: window.Whisper.Message,
});
if (!message) { if (!message) {
throw new Error( throw new Error(
`loadOlderMessages: failed to load message ${oldestMessageId}` `loadOlderMessages: failed to load message ${oldestMessageId}`
); );
} }
const receivedAt = message.get('received_at'); const receivedAt = message.received_at;
const sentAt = message.get('sent_at'); const sentAt = message.sent_at;
const models = await getOlderMessagesByConversation(conversationId, { const models = await getOlderMessagesByConversation(conversationId, {
receivedAt, receivedAt,
sentAt, sentAt,
messageId: oldestMessageId, messageId: oldestMessageId,
limit: MESSAGE_LOAD_CHUNK_SIZE, limit: MESSAGE_LOAD_CHUNK_SIZE,
MessageCollection: window.Whisper.MessageCollection,
}); });
if (models.length < 1) { if (models.length < 1) {
@ -1533,22 +1521,19 @@ export class ConversationModel extends window.Backbone
const finish = this.setInProgressFetch(); const finish = this.setInProgressFetch();
try { try {
const message = await getMessageById(newestMessageId, { const message = await getMessageById(newestMessageId);
Message: window.Whisper.Message,
});
if (!message) { if (!message) {
throw new Error( throw new Error(
`loadNewerMessages: failed to load message ${newestMessageId}` `loadNewerMessages: failed to load message ${newestMessageId}`
); );
} }
const receivedAt = message.get('received_at'); const receivedAt = message.received_at;
const sentAt = message.get('sent_at'); const sentAt = message.sent_at;
const models = await getNewerMessagesByConversation(conversationId, { const models = await getNewerMessagesByConversation(conversationId, {
receivedAt, receivedAt,
sentAt, sentAt,
limit: MESSAGE_LOAD_CHUNK_SIZE, limit: MESSAGE_LOAD_CHUNK_SIZE,
MessageCollection: window.Whisper.MessageCollection,
}); });
if (models.length < 1) { if (models.length < 1) {
@ -1587,33 +1572,29 @@ export class ConversationModel extends window.Backbone
const finish = this.setInProgressFetch(); const finish = this.setInProgressFetch();
try { try {
const message = await getMessageById(messageId, { const message = await getMessageById(messageId);
Message: window.Whisper.Message,
});
if (!message) { if (!message) {
throw new Error( throw new Error(
`loadMoreAndScroll: failed to load message ${messageId}` `loadMoreAndScroll: failed to load message ${messageId}`
); );
} }
const receivedAt = message.get('received_at'); const receivedAt = message.received_at;
const sentAt = message.get('sent_at'); const sentAt = message.sent_at;
const older = await getOlderMessagesByConversation(conversationId, { const older = await getOlderMessagesByConversation(conversationId, {
limit: MESSAGE_LOAD_CHUNK_SIZE, limit: MESSAGE_LOAD_CHUNK_SIZE,
receivedAt, receivedAt,
sentAt, sentAt,
messageId, messageId,
MessageCollection: window.Whisper.MessageCollection,
}); });
const newer = await getNewerMessagesByConversation(conversationId, { const newer = await getNewerMessagesByConversation(conversationId, {
limit: MESSAGE_LOAD_CHUNK_SIZE, limit: MESSAGE_LOAD_CHUNK_SIZE,
receivedAt, receivedAt,
sentAt, sentAt,
MessageCollection: window.Whisper.MessageCollection,
}); });
const metrics = await getMessageMetricsForConversation(conversationId); const metrics = await getMessageMetricsForConversation(conversationId);
const all = [...older.models, message, ...newer.models]; const all = [...older, message, ...newer];
const cleaned: Array<MessageModel> = await this.cleanModels(all); const cleaned: Array<MessageModel> = await this.cleanModels(all);
const scrollToMessageId = const scrollToMessageId =
@ -1636,19 +1617,18 @@ export class ConversationModel extends window.Backbone
} }
async cleanModels( async cleanModels(
collection: MessageModelCollectionType | Array<MessageModel> messages: ReadonlyArray<MessageAttributesType>
): Promise<Array<MessageModel>> { ): Promise<Array<MessageModel>> {
const result = collection const result = messages
.filter((message: MessageModel) => Boolean(message.id)) .filter(message => Boolean(message.id))
.map((message: MessageModel) => .map(message => window.MessageController.register(message.id, message));
window.MessageController.register(message.id, message)
);
const eliminated = collection.length - result.length; const eliminated = messages.length - result.length;
if (eliminated > 0) { if (eliminated > 0) {
log.warn(`cleanModels: Eliminated ${eliminated} messages without an id`); log.warn(`cleanModels: Eliminated ${eliminated} messages without an id`);
} }
let upgraded = 0;
for (let max = result.length, i = 0; i < max; i += 1) { for (let max = result.length, i = 0; i < max; i += 1) {
const message = result[i]; const message = result[i];
const { attributes } = message; const { attributes } = message;
@ -1661,8 +1641,12 @@ export class ConversationModel extends window.Backbone
message.set(upgradedMessage); message.set(upgradedMessage);
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await window.Signal.Data.saveMessage(upgradedMessage); await window.Signal.Data.saveMessage(upgradedMessage);
upgraded += 1;
} }
} }
if (upgraded > 0) {
log.warn(`cleanModels: Upgraded schema of ${upgraded} messages`);
}
return result; return result;
} }
@ -1972,18 +1956,17 @@ export class ConversationModel extends window.Backbone
): Promise<void> { ): Promise<void> {
const { isLocalAction } = options; const { isLocalAction } = options;
let messages: MessageModelCollectionType | undefined; let messages: Array<MessageAttributesType> | undefined;
do { do {
const first = messages ? messages.first() : undefined; const first = messages ? messages[0] : undefined;
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
messages = await window.Signal.Data.getOlderMessagesByConversation( messages = await window.Signal.Data.getOlderMessagesByConversation(
this.get('id'), this.get('id'),
{ {
MessageCollection: window.Whisper.MessageCollection,
limit: 100, limit: 100,
receivedAt: first ? first.get('received_at') : undefined, receivedAt: first ? first.received_at : undefined,
sentAt: first ? first.get('sent_at') : undefined, sentAt: first ? first.sent_at : undefined,
messageId: first ? first.id : undefined, messageId: first ? first.id : undefined,
} }
); );
@ -1992,9 +1975,7 @@ export class ConversationModel extends window.Backbone
return; return;
} }
const readMessages = messages.filter( const readMessages = messages.filter(m => !hasErrors(m) && isIncoming(m));
m => !hasErrors(m.attributes) && isIncoming(m.attributes)
);
if (isLocalAction) { if (isLocalAction) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
@ -2002,9 +1983,9 @@ export class ConversationModel extends window.Backbone
window.storage, window.storage,
readMessages.map(m => ({ readMessages.map(m => ({
messageId: m.id, messageId: m.id,
senderE164: m.get('source'), senderE164: m.source,
senderUuid: m.get('sourceUuid'), senderUuid: m.sourceUuid,
timestamp: m.get('sent_at'), timestamp: m.sent_at,
})) }))
); );
} }
@ -3209,10 +3190,7 @@ export class ConversationModel extends window.Backbone
const message = window.MessageController.getById(notificationId); const message = window.MessageController.getById(notificationId);
if (message) { if (message) {
message.cleanup(); window.Signal.Data.removeMessage(message.id);
window.Signal.Data.removeMessage(message.id, {
Message: window.Whisper.Message,
});
} }
if (this.get('expireTimer') || forceRemove) { if (this.get('expireTimer') || forceRemove) {
@ -3628,7 +3606,7 @@ export class ConversationModel extends window.Backbone
async makeQuote(quotedMessage: MessageModel): Promise<QuotedMessageType> { async makeQuote(quotedMessage: MessageModel): Promise<QuotedMessageType> {
const { getName } = EmbeddedContact; const { getName } = EmbeddedContact;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // 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 attachments = quotedMessage.get('attachments');
const preview = quotedMessage.get('preview'); const preview = quotedMessage.get('preview');
const sticker = quotedMessage.get('sticker'); const sticker = quotedMessage.get('sticker');
@ -4082,7 +4060,6 @@ export class ConversationModel extends window.Backbone
const lastMessages = await window.Signal.Data.getLastConversationMessages({ const lastMessages = await window.Signal.Data.getLastConversationMessages({
conversationId, conversationId,
ourUuid, ourUuid,
Message: window.Whisper.Message,
}); });
// This runs as a job to avoid race conditions // This runs as a job to avoid race conditions
@ -4090,22 +4067,21 @@ export class ConversationModel extends window.Backbone
this.maybeSetPendingUniversalTimer(lastMessages.hasUserInitiatedMessages) 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 // 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 // in memory we use that data instead of the data from the db which may
// be out of date. // be out of date.
if (previewMessage) { if (preview) {
previewMessage = window.MessageController.register( previewMessage = window.MessageController.register(preview.id, preview);
previewMessage.id,
previewMessage
);
} }
if (activityMessage) { if (activity) {
activityMessage = window.MessageController.register( activityMessage = window.MessageController.register(
activityMessage.id, activity.id,
activityMessage activity
); );
} }
@ -4748,9 +4724,7 @@ export class ConversationModel extends window.Backbone
this.deriveProfileKeyVersionIfNeeded(), this.deriveProfileKeyVersionIfNeeded(),
]); ]);
window.Signal.Data.updateConversation(this.attributes, { window.Signal.Data.updateConversation(this.attributes);
Conversation: window.Whisper.Conversation,
});
} }
} }
@ -4814,7 +4788,6 @@ export class ConversationModel extends window.Backbone
await window.Signal.Data.removeAllMessagesInConversation(this.id, { await window.Signal.Data.removeAllMessagesInConversation(this.id, {
logId: this.idForLogging(), logId: this.idForLogging(),
MessageCollection: window.Whisper.MessageCollection,
}); });
} }
@ -5090,7 +5063,7 @@ export class ConversationModel extends window.Backbone
const sender = reaction const sender = reaction
? window.ConversationController.get(reaction.get('fromId')) ? window.ConversationController.get(reaction.get('fromId'))
: message.getContact(); : getContact(message.attributes);
const senderName = sender const senderName = sender
? sender.getTitle() ? sender.getTitle()
: window.i18n('unknownContact'); : window.i18n('unknownContact');

View file

@ -41,11 +41,9 @@ import * as expirationTimer from '../util/expirationTimer';
import type { ReactionType } from '../types/Reactions'; import type { ReactionType } from '../types/Reactions';
import { UUID, UUIDKind } from '../types/UUID'; import { UUID, UUIDKind } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
import * as reactionUtil from '../reactions/util'; import * as reactionUtil from '../reactions/util';
import { import {
copyStickerToAttachments, copyStickerToAttachments,
deletePackReference,
savePackMetadata, savePackMetadata,
getStickerPackStatus, getStickerPackStatus,
} from '../types/Stickers'; } from '../types/Stickers';
@ -134,6 +132,16 @@ import type { LinkPreviewType } from '../types/message/LinkPreviews';
import * as log from '../logging/log'; import * as log from '../logging/log';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { computeHash } from '../Crypto'; 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 camelcase */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
@ -148,36 +156,10 @@ declare const _: typeof window._;
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
const { Message: TypedMessage } = window.Signal.Types; const { Message: TypedMessage } = window.Signal.Types;
const { deleteExternalMessageFiles, upgradeMessageSchema } = const { upgradeMessageSchema } = window.Signal.Migrations;
window.Signal.Migrations;
const { getTextWithMentions, GoogleChrome } = window.Signal.Util; const { getTextWithMentions, GoogleChrome } = window.Signal.Util;
const { addStickerPackReference, getMessageBySender } = window.Signal.Data; 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<MessageAttributesType> { export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
static getLongMessageAttachment: ( static getLongMessageAttachment: (
attachment: typeof window.WhatIsThis attachment: typeof window.WhatIsThis
@ -307,7 +289,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
let conversationIds: Array<string>; let conversationIds: Array<string>;
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
if (isIncoming(this.attributes)) { if (isIncoming(this.attributes)) {
conversationIds = [this.getContactId()!]; conversationIds = [getContactId(this.attributes)!];
} else if (!isEmpty(sendStateByConversationId)) { } else if (!isEmpty(sendStateByConversationId)) {
if (isMessageJustForMe(sendStateByConversationId, ourConversationId)) { if (isMessageJustForMe(sendStateByConversationId, ourConversationId)) {
conversationIds = [ourConversationId]; conversationIds = [ourConversationId];
@ -516,7 +498,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
if (isGroupUpdate(attributes)) { if (isGroupUpdate(attributes)) {
const groupUpdate = this.get('group_update'); const groupUpdate = this.get('group_update');
const fromContact = this.getContact(); const fromContact = getContact(this.attributes);
const messages = []; const messages = [];
if (!groupUpdate) { if (!groupUpdate) {
throw new Error('getNotificationData: Missing group_update'); throw new Error('getNotificationData: Missing group_update');
@ -749,8 +731,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// General // General
idForLogging(): string { idForLogging(): string {
const account = this.getSourceUuid() || this.getSource(); const account =
const device = this.getSourceDevice(); getSourceUuid(this.attributes) || getSource(this.attributes);
const device = getSourceDevice(this.attributes);
const timestamp = this.get('sent_at'); const timestamp = this.get('sent_at');
return `${account}.${device} ${timestamp}`; return `${account}.${device} ${timestamp}`;
@ -785,29 +768,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
} }
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
window.reduxActions?.conversations?.messageDeleted( await cleanupMessage(this.attributes);
this.id,
this.get('conversationId')
);
this.getConversation()?.debouncedUpdateLastMessage?.();
window.MessageController.unregister(this.id);
await this.deleteData();
} }
async deleteData(): Promise<void> { async deleteData(): Promise<void> {
await deleteExternalMessageFiles(this.attributes); await deleteMessageData(this.attributes);
const sticker = this.get('sticker');
if (!sticker) {
return;
}
const { packId } = sticker;
if (packId) {
await deletePackReference(this.id, packId);
}
} }
isValidTapToView(): boolean { isValidTapToView(): boolean {
@ -875,8 +840,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
await this.eraseContents(); await this.eraseContents();
if (!fromSync) { if (!fromSync) {
const sender = this.getSource(); const sender = getSource(this.attributes);
const senderUuid = this.getSourceUuid(); const senderUuid = getSourceUuid(this.attributes);
if (senderUuid === undefined) { if (senderUuid === undefined) {
throw new Error('senderUuid is undefined'); throw new Error('senderUuid is undefined');
@ -929,7 +894,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
Number(sentAt) Number(sentAt)
); );
const matchingMessage = find(inMemoryMessages, message => const matchingMessage = find(inMemoryMessages, message =>
isQuoteAMatch(message, this.get('conversationId'), quote) isQuoteAMatch(message.attributes, this.get('conversationId'), quote)
); );
if (!matchingMessage) { if (!matchingMessage) {
log.info( log.info(
@ -1078,66 +1043,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return unidentifiedDeliveriesSet.has(contactId); 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( async saveErrors(
providedErrors: Error | Array<Error>, providedErrors: Error | Array<Error>,
options: { skipSave?: boolean } = {} options: { skipSave?: boolean } = {}
@ -2106,7 +2011,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const inMemoryMessages = window.MessageController.filterBySentAt(id); const inMemoryMessages = window.MessageController.filterBySentAt(id);
const matchingMessage = find(inMemoryMessages, item => const matchingMessage = find(inMemoryMessages, item =>
isQuoteAMatch(item, conversationId, result) isQuoteAMatch(item.attributes, conversationId, result)
); );
let queryMessage: undefined | MessageModel; let queryMessage: undefined | MessageModel;
@ -2115,10 +2020,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
queryMessage = matchingMessage; queryMessage = matchingMessage;
} else { } else {
log.info('copyFromQuotedMessage: db lookup needed', id); log.info('copyFromQuotedMessage: db lookup needed', id);
const collection = await window.Signal.Data.getMessagesBySentAt(id, { const messages = await window.Signal.Data.getMessagesBySentAt(id);
MessageCollection: window.Whisper.MessageCollection, const found = messages.find(item =>
});
const found = collection.find(item =>
isQuoteAMatch(item, conversationId, result) isQuoteAMatch(item, conversationId, result)
); );
@ -2256,7 +2159,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const conversationId = message.get('conversationId'); const conversationId = message.get('conversationId');
const GROUP_TYPES = Proto.GroupContext.Type; const GROUP_TYPES = Proto.GroupContext.Type;
const fromContact = this.getContact(); const fromContact = getContact(this.attributes);
if (fromContact) { if (fromContact) {
fromContact.setRegistered(); fromContact.setRegistered();
} }
@ -2281,10 +2184,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
); );
} }
const existingMessage = const existingMessage =
inMemoryMessage || inMemoryMessage || (await getMessageBySender(this.attributes));
(await getMessageBySender(this.attributes, {
Message: window.Whisper.Message,
}));
const isUpdate = Boolean(data && data.isRecipientUpdate); const isUpdate = Boolean(data && data.isRecipientUpdate);
if (existingMessage && type === 'incoming') { if (existingMessage && type === 'incoming') {
@ -2702,7 +2602,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
if (conversation.get('left')) { if (conversation.get('left')) {
log.warn('re-added to a left group'); log.warn('re-added to a left group');
attributes.left = false; attributes.left = false;
conversation.set({ addedBy: message.getContactId() }); conversation.set({ addedBy: getContactId(message.attributes) });
} }
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) { } else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion

View file

@ -4,6 +4,7 @@
import { ReactionModel } from '../messageModifiers/Reactions'; import { ReactionModel } from '../messageModifiers/Reactions';
import { ReactionSource } from './ReactionSource'; import { ReactionSource } from './ReactionSource';
import { getMessageById } from '../messages/getMessageById'; import { getMessageById } from '../messages/getMessageById';
import { getSourceUuid } from '../messages/helpers';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
export async function enqueueReactionForSend({ export async function enqueueReactionForSend({
@ -18,7 +19,7 @@ export async function enqueueReactionForSend({
const message = await getMessageById(messageId); const message = await getMessageById(messageId);
strictAssert(message, 'enqueueReactionForSend: no message found'); strictAssert(message, 'enqueueReactionForSend: no message found');
const targetAuthorUuid = message.getSourceUuid(); const targetAuthorUuid = getSourceUuid(message.attributes);
strictAssert( strictAssert(
targetAuthorUuid, targetAuthorUuid,
`enqueueReactionForSend: message ${message.idForLogging()} had no source UUID` `enqueueReactionForSend: message ${message.idForLogging()} had no source UUID`

View file

@ -26,6 +26,7 @@ import {
uniq, uniq,
} from 'lodash'; } from 'lodash';
import { deleteExternalFiles } from '../types/Conversation';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message'; import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message';
import { createBatcher } from '../util/batcher'; import { createBatcher } from '../util/batcher';
@ -40,12 +41,9 @@ import type { RemoveAllConfiguration } from '../types/RemoveAllConfiguration';
import createTaskWithTimeout from '../textsecure/TaskWithTimeout'; import createTaskWithTimeout from '../textsecure/TaskWithTimeout';
import * as log from '../logging/log'; import * as log from '../logging/log';
import type {
ConversationModelCollectionType,
MessageModelCollectionType,
} from '../model-types.d';
import type { StoredJob } from '../jobs/types'; import type { StoredJob } from '../jobs/types';
import { formatJobForInsert } from '../jobs/formatJobForInsert'; import { formatJobForInsert } from '../jobs/formatJobForInsert';
import { cleanupMessage } from '../util/cleanup';
import type { import type {
AttachmentDownloadJobType, AttachmentDownloadJobType,
@ -54,18 +52,17 @@ import type {
ClientSearchResultMessageType, ClientSearchResultMessageType,
ConversationType, ConversationType,
DeleteSentProtoRecipientOptionsType, DeleteSentProtoRecipientOptionsType,
IdentityKeyType,
IdentityKeyIdType, IdentityKeyIdType,
IdentityKeyType,
ItemKeyType, ItemKeyType,
ItemType, ItemType,
LastConversationMessagesType, LastConversationMessagesType,
MessageType, MessageType,
MessageTypeUnhydrated, MessageTypeUnhydrated,
PreKeyType,
PreKeyIdType, PreKeyIdType,
SearchResultMessageType, PreKeyType,
SenderKeyType,
SenderKeyIdType, SenderKeyIdType,
SenderKeyType,
SentMessageDBType, SentMessageDBType,
SentMessagesType, SentMessagesType,
SentProtoType, SentProtoType,
@ -73,15 +70,16 @@ import type {
SentRecipientsDBType, SentRecipientsDBType,
SentRecipientsType, SentRecipientsType,
ServerInterface, ServerInterface,
SessionType, ServerSearchResultMessageType,
SessionIdType, SessionIdType,
SignedPreKeyType, SessionType,
SignedPreKeyIdType, SignedPreKeyIdType,
SignedPreKeyType,
StickerPackStatusType, StickerPackStatusType,
StickerPackType, StickerPackType,
StickerType, StickerType,
StoryDistributionType,
StoryDistributionMemberType, StoryDistributionMemberType,
StoryDistributionType,
StoryDistributionWithMembersType, StoryDistributionWithMembersType,
StoryReadType, StoryReadType,
UnprocessedType, UnprocessedType,
@ -89,8 +87,6 @@ import type {
} from './Interface'; } from './Interface';
import Server from './Server'; import Server from './Server';
import { isCorruptionError } from './errors'; 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 // 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. // any warnings that might be sent to the console in that case.
@ -160,16 +156,16 @@ const dataInterface: ClientInterface = {
createOrUpdateSignedPreKey, createOrUpdateSignedPreKey,
getSignedPreKeyById, getSignedPreKeyById,
getAllSignedPreKeys,
bulkAddSignedPreKeys, bulkAddSignedPreKeys,
removeSignedPreKeyById, removeSignedPreKeyById,
removeAllSignedPreKeys, removeAllSignedPreKeys,
getAllSignedPreKeys,
createOrUpdateItem, createOrUpdateItem,
getItemById, getItemById,
getAllItems,
removeItemById, removeItemById,
removeAllItems, removeAllItems,
getAllItems,
createOrUpdateSenderKey, createOrUpdateSenderKey,
getSenderKeyById, getSenderKeyById,
@ -197,6 +193,7 @@ const dataInterface: ClientInterface = {
removeAllSessions, removeAllSessions,
getAllSessions, getAllSessions,
eraseStorageServiceStateFromConversations,
getConversationCount, getConversationCount,
saveConversation, saveConversation,
saveConversations, saveConversations,
@ -206,7 +203,6 @@ const dataInterface: ClientInterface = {
removeConversation, removeConversation,
updateAllConversationColors, updateAllConversationColors,
eraseStorageServiceStateFromConversations,
getAllConversations, getAllConversations,
getAllConversationIds, getAllConversationIds,
getAllPrivateConversations, getAllPrivateConversations,
@ -229,10 +225,11 @@ const dataInterface: ClientInterface = {
addReaction, addReaction,
_getAllReactions, _getAllReactions,
_removeAllReactions, _removeAllReactions,
getMessageBySender, getMessageBySender,
getMessageById, getMessageById,
getMessagesById, getMessagesById,
_getAllMessages,
_removeAllMessages,
getAllMessageIds, getAllMessageIds,
getMessagesBySentAt, getMessagesBySentAt,
getExpiredMessages, getExpiredMessages,
@ -243,8 +240,8 @@ const dataInterface: ClientInterface = {
getOlderMessagesByConversation, getOlderMessagesByConversation,
getOlderStories, getOlderStories,
getNewerMessagesByConversation, getNewerMessagesByConversation,
getLastConversationMessages,
getMessageMetricsForConversation, getMessageMetricsForConversation,
getLastConversationMessages,
hasGroupCallHistoryMessage, hasGroupCallHistoryMessage,
migrateConversationMessages, migrateConversationMessages,
@ -263,13 +260,13 @@ const dataInterface: ClientInterface = {
removeAttachmentDownloadJob, removeAttachmentDownloadJob,
removeAllAttachmentDownloadJobs, removeAllAttachmentDownloadJobs,
getStickerCount,
createOrUpdateStickerPack, createOrUpdateStickerPack,
updateStickerPackStatus, updateStickerPackStatus,
createOrUpdateSticker, createOrUpdateSticker,
updateStickerLastUsed, updateStickerLastUsed,
addStickerPackReference, addStickerPackReference,
deleteStickerPackReference, deleteStickerPackReference,
getStickerCount,
deleteStickerPack, deleteStickerPack,
getAllStickerPacks, getAllStickerPacks,
getAllStickers, getAllStickers,
@ -317,11 +314,6 @@ const dataInterface: ClientInterface = {
getStatisticsForLogging, getStatisticsForLogging,
// Test-only
_getAllMessages,
_removeAllMessages,
// Client-side only // Client-side only
shutdown, shutdown,
@ -335,7 +327,6 @@ const dataInterface: ClientInterface = {
startInRendererProcess, startInRendererProcess,
goBackToMainProcess, goBackToMainProcess,
_removeConversations,
_jobs, _jobs,
}; };
@ -971,17 +962,8 @@ async function saveConversations(array: Array<ConversationType>) {
await channels.saveConversations(array); await channels.saveConversations(array);
} }
async function getConversationById( async function getConversationById(id: string) {
id: string, return channels.getConversationById(id);
{ Conversation }: { Conversation: typeof ConversationModel }
) {
const data = await channels.getConversationById(id);
if (!data) {
return undefined;
}
return new Conversation(data);
} }
const updateConversationBatcher = createBatcher<ConversationType>({ const updateConversationBatcher = createBatcher<ConversationType>({
@ -1015,40 +997,25 @@ async function updateConversations(array: Array<ConversationType>) {
await channels.updateConversations(cleaned); await channels.updateConversations(cleaned);
} }
async function removeConversation( async function removeConversation(id: string) {
id: string, const existing = await getConversationById(id);
{ Conversation }: { Conversation: typeof ConversationModel }
) {
const existing = await getConversationById(id, { Conversation });
// Note: It's important to have a fully database-hydrated model to delete here because // 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. // it needs to delete all associated on-disk files along with the database delete.
if (existing) { if (existing) {
await channels.removeConversation(id); 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<string>) {
await channels.removeConversation(ids);
}
async function eraseStorageServiceStateFromConversations() { async function eraseStorageServiceStateFromConversations() {
await channels.eraseStorageServiceStateFromConversations(); await channels.eraseStorageServiceStateFromConversations();
} }
async function getAllConversations({ async function getAllConversations() {
ConversationCollection, return channels.getAllConversations();
}: {
ConversationCollection: typeof ConversationModelCollectionType;
}): Promise<ConversationModelCollectionType> {
const conversations = await channels.getAllConversations();
const collection = new ConversationCollection();
collection.add(conversations);
return collection;
} }
async function getAllConversationIds() { async function getAllConversationIds() {
@ -1057,33 +1024,12 @@ async function getAllConversationIds() {
return ids; return ids;
} }
async function getAllPrivateConversations({ async function getAllPrivateConversations() {
ConversationCollection, return channels.getAllPrivateConversations();
}: {
ConversationCollection: typeof ConversationModelCollectionType;
}) {
const conversations = await channels.getAllPrivateConversations();
const collection = new ConversationCollection();
collection.add(conversations);
return collection;
} }
async function getAllGroupsInvolvingUuid( async function getAllGroupsInvolvingUuid(uuid: UUIDStringType) {
uuid: UUIDStringType, return channels.getAllGroupsInvolvingUuid(uuid);
{
ConversationCollection,
}: {
ConversationCollection: typeof ConversationModelCollectionType;
}
) {
const conversations = await channels.getAllGroupsInvolvingUuid(uuid);
const collection = new ConversationCollection();
collection.add(conversations);
return collection;
} }
async function searchConversations(query: string) { async function searchConversations(query: string) {
@ -1093,7 +1039,7 @@ async function searchConversations(query: string) {
} }
function handleSearchMessageJSON( function handleSearchMessageJSON(
messages: Array<SearchResultMessageType> messages: Array<ServerSearchResultMessageType>
): Array<ClientSearchResultMessageType> { ): Array<ClientSearchResultMessageType> {
return messages.map(message => ({ return messages.map(message => ({
json: message.json, json: message.json,
@ -1163,17 +1109,14 @@ async function saveMessages(
window.Whisper.TapToViewMessagesListener.update(); window.Whisper.TapToViewMessagesListener.update();
} }
async function removeMessage( async function removeMessage(id: string) {
id: string, const message = await getMessageById(id);
{ Message }: { Message: typeof MessageModel }
) {
const message = await getMessageById(id, { Message });
// Note: It's important to have a fully database-hydrated model to delete here because // 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. // it needs to delete all associated on-disk files along with the database delete.
if (message) { if (message) {
await channels.removeMessage(id); await channels.removeMessage(id);
await message.cleanup(); await cleanupMessage(message);
} }
} }
@ -1182,16 +1125,8 @@ async function removeMessages(ids: Array<string>) {
await channels.removeMessages(ids); await channels.removeMessages(ids);
} }
async function getMessageById( async function getMessageById(id: string) {
id: string, return channels.getMessageById(id);
{ Message }: { Message: typeof MessageModel }
) {
const message = await channels.getMessageById(id);
if (!message) {
return undefined;
}
return new Message(message);
} }
async function getMessagesById(messageIds: Array<string>) { async function getMessagesById(messageIds: Array<string>) {
@ -1202,14 +1137,8 @@ async function getMessagesById(messageIds: Array<string>) {
} }
// For testing only // For testing only
async function _getAllMessages({ async function _getAllMessages() {
MessageCollection, return channels._getAllMessages();
}: {
MessageCollection: typeof MessageModelCollectionType;
}) {
const messages = await channels._getAllMessages();
return new MessageCollection(messages);
} }
async function _removeAllMessages() { async function _removeAllMessages() {
await channels._removeAllMessages(); await channels._removeAllMessages();
@ -1221,8 +1150,7 @@ async function getAllMessageIds() {
return ids; return ids;
} }
async function getMessageBySender( async function getMessageBySender({
{
source, source,
sourceUuid, sourceUuid,
sourceDevice, sourceDevice,
@ -1232,20 +1160,13 @@ async function getMessageBySender(
sourceUuid: string; sourceUuid: string;
sourceDevice: number; sourceDevice: number;
sent_at: number; sent_at: number;
}, }) {
{ Message }: { Message: typeof MessageModel } return channels.getMessageBySender({
) {
const messages = await channels.getMessageBySender({
source, source,
sourceUuid, sourceUuid,
sourceDevice, sourceDevice,
sent_at, sent_at,
}); });
if (!messages || !messages.length) {
return null;
}
return new Message(messages[0]);
} }
async function getTotalUnreadForConversation( async function getTotalUnreadForConversation(
@ -1299,7 +1220,9 @@ async function _removeAllReactions() {
await channels._removeAllReactions(); await channels._removeAllReactions();
} }
function handleMessageJSON(messages: Array<MessageTypeUnhydrated>) { function handleMessageJSON(
messages: Array<MessageTypeUnhydrated>
): Array<MessageType> {
return messages.map(message => JSON.parse(message.json)); return messages.map(message => JSON.parse(message.json));
} }
@ -1307,14 +1230,12 @@ async function getOlderMessagesByConversation(
conversationId: string, conversationId: string,
{ {
limit = 100, limit = 100,
MessageCollection,
messageId, messageId,
receivedAt = Number.MAX_VALUE, receivedAt = Number.MAX_VALUE,
sentAt = Number.MAX_VALUE, sentAt = Number.MAX_VALUE,
storyId, storyId,
}: { }: {
limit?: number; limit?: number;
MessageCollection: typeof MessageModelCollectionType;
messageId?: string; messageId?: string;
receivedAt?: number; receivedAt?: number;
sentAt?: number; sentAt?: number;
@ -1332,7 +1253,7 @@ async function getOlderMessagesByConversation(
} }
); );
return new MessageCollection(handleMessageJSON(messages)); return handleMessageJSON(messages);
} }
async function getOlderStories(options: { async function getOlderStories(options: {
conversationId?: string; conversationId?: string;
@ -1348,13 +1269,11 @@ async function getNewerMessagesByConversation(
conversationId: string, conversationId: string,
{ {
limit = 100, limit = 100,
MessageCollection,
receivedAt = 0, receivedAt = 0,
sentAt = 0, sentAt = 0,
storyId, storyId,
}: { }: {
limit?: number; limit?: number;
MessageCollection: typeof MessageModelCollectionType;
receivedAt?: number; receivedAt?: number;
sentAt?: number; sentAt?: number;
storyId?: UUIDStringType; storyId?: UUIDStringType;
@ -1370,16 +1289,14 @@ async function getNewerMessagesByConversation(
} }
); );
return new MessageCollection(handleMessageJSON(messages)); return handleMessageJSON(messages);
} }
async function getLastConversationMessages({ async function getLastConversationMessages({
conversationId, conversationId,
ourUuid, ourUuid,
Message,
}: { }: {
conversationId: string; conversationId: string;
ourUuid: UUIDStringType; ourUuid: UUIDStringType;
Message: typeof MessageModel;
}): Promise<LastConversationMessagesType> { }): Promise<LastConversationMessagesType> {
const { preview, activity, hasUserInitiatedMessages } = const { preview, activity, hasUserInitiatedMessages } =
await channels.getLastConversationMessages({ await channels.getLastConversationMessages({
@ -1388,8 +1305,8 @@ async function getLastConversationMessages({
}); });
return { return {
preview: preview ? new Message(preview) : undefined, preview,
activity: activity ? new Message(activity) : undefined, activity,
hasUserInitiatedMessages, hasUserInitiatedMessages,
}; };
} }
@ -1421,10 +1338,8 @@ async function removeAllMessagesInConversation(
conversationId: string, conversationId: string,
{ {
logId, logId,
MessageCollection,
}: { }: {
logId: string; logId: string;
MessageCollection: typeof MessageModelCollectionType;
} }
) { ) {
let messages; let messages;
@ -1437,21 +1352,22 @@ async function removeAllMessagesInConversation(
// time so we don't use too much memory. // time so we don't use too much memory.
messages = await getOlderMessagesByConversation(conversationId, { messages = await getOlderMessagesByConversation(conversationId, {
limit: chunkSize, limit: chunkSize,
MessageCollection,
}); });
if (!messages.length) { if (!messages.length) {
return; return;
} }
const ids = messages.map((message: MessageModel) => message.id); const ids = messages.map(message => message.id);
log.info(`removeAllMessagesInConversation/${logId}: Cleanup...`); log.info(`removeAllMessagesInConversation/${logId}: Cleanup...`);
// Note: It's very important that these models are fully hydrated because // 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. // 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 }); const queue = new window.PQueue({ concurrency: 3, timeout: 1000 * 60 * 2 });
queue.addAll( queue.addAll(
messages.map((message: MessageModel) => async () => message.cleanup()) messages.map(
(message: MessageType) => async () => cleanupMessage(message)
)
); );
await queue.onIdle(); await queue.onIdle();
@ -1460,25 +1376,12 @@ async function removeAllMessagesInConversation(
} while (messages.length > 0); } while (messages.length > 0);
} }
async function getMessagesBySentAt( async function getMessagesBySentAt(sentAt: number) {
sentAt: number, return channels.getMessagesBySentAt(sentAt);
{
MessageCollection,
}: { MessageCollection: typeof MessageModelCollectionType }
) {
const messages = await channels.getMessagesBySentAt(sentAt);
return new MessageCollection(messages);
} }
async function getExpiredMessages({ async function getExpiredMessages() {
MessageCollection, return channels.getExpiredMessages();
}: {
MessageCollection: typeof MessageModelCollectionType;
}) {
const messages = await channels.getExpiredMessages();
return new MessageCollection(messages);
} }
function getMessagesUnexpectedlyMissingExpirationStartTimestamp() { function getMessagesUnexpectedlyMissingExpirationStartTimestamp() {
@ -1492,14 +1395,8 @@ function getSoonestMessageExpiry() {
async function getNextTapToViewMessageTimestampToAgeOut() { async function getNextTapToViewMessageTimestampToAgeOut() {
return channels.getNextTapToViewMessageTimestampToAgeOut(); return channels.getNextTapToViewMessageTimestampToAgeOut();
} }
async function getTapToViewMessagesNeedingErase({ async function getTapToViewMessagesNeedingErase() {
MessageCollection, return channels.getTapToViewMessagesNeedingErase();
}: {
MessageCollection: typeof MessageModelCollectionType;
}) {
const messages = await channels.getTapToViewMessagesNeedingErase();
return new MessageCollection(messages);
} }
// Unprocessed // Unprocessed

View file

@ -6,13 +6,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import type { import type {
ConversationAttributesType, ConversationAttributesType,
ConversationModelCollectionType,
MessageAttributesType, MessageAttributesType,
MessageModelCollectionType,
SenderKeyInfoType, SenderKeyInfoType,
} from '../model-types.d'; } from '../model-types.d';
import type { MessageModel } from '../models/messages';
import type { ConversationModel } from '../models/conversations';
import type { StoredJob } from '../jobs/types'; import type { StoredJob } from '../jobs/types';
import type { ReactionType } from '../types/Reactions'; import type { ReactionType } from '../types/Reactions';
import type { ConversationColorType, CustomColorType } from '../types/Colors'; import type { ConversationColorType, CustomColorType } from '../types/Colors';
@ -91,7 +87,7 @@ export type PreKeyType = {
publicKey: Uint8Array; publicKey: Uint8Array;
}; };
export type PreKeyIdType = PreKeyType['id']; export type PreKeyIdType = PreKeyType['id'];
export type SearchResultMessageType = { export type ServerSearchResultMessageType = {
json: string; json: string;
snippet: string; snippet: string;
}; };
@ -222,18 +218,12 @@ export type UnprocessedUpdateType = {
decrypted?: string; decrypted?: string;
}; };
export type LastConversationMessagesServerType = { export type LastConversationMessagesType = {
activity?: MessageType; activity?: MessageType;
preview?: MessageType; preview?: MessageType;
hasUserInitiatedMessages: boolean; hasUserInitiatedMessages: boolean;
}; };
export type LastConversationMessagesType = {
activity?: MessageModel;
preview?: MessageModel;
hasUserInitiatedMessages: boolean;
};
export type DeleteSentProtoRecipientOptionsType = Readonly<{ export type DeleteSentProtoRecipientOptionsType = Readonly<{
timestamp: number; timestamp: number;
recipientUuid: string; recipientUuid: string;
@ -353,15 +343,33 @@ export type DataInterface = {
getConversationCount: () => Promise<number>; getConversationCount: () => Promise<number>;
saveConversation: (data: ConversationType) => Promise<void>; saveConversation: (data: ConversationType) => Promise<void>;
saveConversations: (array: Array<ConversationType>) => Promise<void>; saveConversations: (array: Array<ConversationType>) => Promise<void>;
getConversationById: (id: string) => Promise<ConversationType | undefined>;
// updateConversation is a normal data method on Server, a sync batch-add on Client
updateConversations: (array: Array<ConversationType>) => Promise<void>; updateConversations: (array: Array<ConversationType>) => Promise<void>;
// removeConversation handles either one id or an array on Server, and one id on Client
updateAllConversationColors: (
conversationColor?: ConversationColorType,
customColorData?: {
id: string;
value: CustomColorType;
}
) => Promise<void>;
getAllConversations: () => Promise<Array<ConversationType>>;
getAllConversationIds: () => Promise<Array<string>>; getAllConversationIds: () => Promise<Array<string>>;
getAllPrivateConversations: () => Promise<Array<ConversationType>>;
getAllGroupsInvolvingUuid: (
id: UUIDStringType
) => Promise<Array<ConversationType>>;
searchConversations: ( searchConversations: (
query: string, query: string,
options?: { limit?: number } options?: { limit?: number }
) => Promise<Array<ConversationType>>; ) => Promise<Array<ConversationType>>;
// searchMessages is JSON on server, full message on Client
// searchMessagesInConversation is JSON on server, full message on Client
getMessagesById: (messageIds: Array<string>) => Promise<Array<MessageType>>; getMessageCount: (conversationId?: string) => Promise<number>;
saveMessage: ( saveMessage: (
data: MessageType, data: MessageType,
options?: { options?: {
@ -373,30 +381,8 @@ export type DataInterface = {
arrayOfMessages: Array<MessageType>, arrayOfMessages: Array<MessageType>,
options?: { forceSave?: boolean } options?: { forceSave?: boolean }
) => Promise<void>; ) => Promise<void>;
getMessageCount: (conversationId?: string) => Promise<number>; removeMessage: (id: string) => Promise<void>;
getAllMessageIds: () => Promise<Array<string>>; removeMessages: (ids: Array<string>) => Promise<void>;
getOlderStories: (options: {
conversationId?: string;
limit?: number;
receivedAt?: number;
sentAt?: number;
sourceUuid?: string;
}) => Promise<Array<MessageType>>;
getMessageMetricsForConversation: (
conversationId: string,
storyId?: UUIDStringType
) => Promise<ConversationMetricsType>;
hasGroupCallHistoryMessage: (
conversationId: string,
eraId: string
) => Promise<boolean>;
migrateConversationMessages: (
obsoleteId: string,
currentId: string
) => Promise<void>;
getNextTapToViewMessageTimestampToAgeOut: () => Promise<undefined | number>;
_removeAllMessages: () => Promise<void>;
getTotalUnreadForConversation: ( getTotalUnreadForConversation: (
conversationId: string, conversationId: string,
storyId?: UUIDStringType storyId?: UUIDStringType
@ -433,6 +419,50 @@ export type DataInterface = {
addReaction: (reactionObj: ReactionType) => Promise<void>; addReaction: (reactionObj: ReactionType) => Promise<void>;
_getAllReactions: () => Promise<Array<ReactionType>>; _getAllReactions: () => Promise<Array<ReactionType>>;
_removeAllReactions: () => Promise<void>; _removeAllReactions: () => Promise<void>;
getMessageBySender: (options: {
source: string;
sourceUuid: string;
sourceDevice: number;
sent_at: number;
}) => Promise<MessageType | undefined>;
getMessageById: (id: string) => Promise<MessageType | undefined>;
getMessagesById: (messageIds: Array<string>) => Promise<Array<MessageType>>;
_getAllMessages: () => Promise<Array<MessageType>>;
_removeAllMessages: () => Promise<void>;
getAllMessageIds: () => Promise<Array<string>>;
getMessagesBySentAt: (sentAt: number) => Promise<Array<MessageType>>;
getExpiredMessages: () => Promise<Array<MessageType>>;
getMessagesUnexpectedlyMissingExpirationStartTimestamp: () => Promise<
Array<MessageType>
>;
getSoonestMessageExpiry: () => Promise<undefined | number>;
getNextTapToViewMessageTimestampToAgeOut: () => Promise<undefined | number>;
getTapToViewMessagesNeedingErase: () => Promise<Array<MessageType>>;
// getOlderMessagesByConversation is JSON on server, full message on Client
getOlderStories: (options: {
conversationId?: string;
limit?: number;
receivedAt?: number;
sentAt?: number;
sourceUuid?: string;
}) => Promise<Array<MessageType>>;
// getNewerMessagesByConversation is JSON on server, full message on Client
getMessageMetricsForConversation: (
conversationId: string,
storyId?: UUIDStringType
) => Promise<ConversationMetricsType>;
getLastConversationMessages: (options: {
conversationId: string;
ourUuid: UUIDStringType;
}) => Promise<LastConversationMessagesType>;
hasGroupCallHistoryMessage: (
conversationId: string,
eraId: string
) => Promise<boolean>;
migrateConversationMessages: (
obsoleteId: string,
currentId: string
) => Promise<void>;
getUnprocessedCount: () => Promise<number>; getUnprocessedCount: () => Promise<number>;
getAllUnprocessed: () => Promise<Array<UnprocessedType>>; getAllUnprocessed: () => Promise<Array<UnprocessedType>>;
@ -452,11 +482,11 @@ export type DataInterface = {
options?: { timestamp?: number } options?: { timestamp?: number }
) => Promise<Array<AttachmentDownloadJobType>>; ) => Promise<Array<AttachmentDownloadJobType>>;
saveAttachmentDownloadJob: (job: AttachmentDownloadJobType) => Promise<void>; saveAttachmentDownloadJob: (job: AttachmentDownloadJobType) => Promise<void>;
resetAttachmentDownloadPending: () => Promise<void>;
setAttachmentDownloadJobPending: ( setAttachmentDownloadJobPending: (
id: string, id: string,
pending: boolean pending: boolean
) => Promise<void>; ) => Promise<void>;
resetAttachmentDownloadPending: () => Promise<void>;
removeAttachmentDownloadJob: (id: string) => Promise<void>; removeAttachmentDownloadJob: (id: string) => Promise<void>;
removeAllAttachmentDownloadJobs: () => Promise<void>; removeAllAttachmentDownloadJobs: () => Promise<void>;
@ -541,10 +571,6 @@ export type DataInterface = {
getMessageServerGuidsForSpam: ( getMessageServerGuidsForSpam: (
conversationId: string conversationId: string
) => Promise<Array<string>>; ) => Promise<Array<string>>;
getMessagesUnexpectedlyMissingExpirationStartTimestamp: () => Promise<
Array<MessageType>
>;
getSoonestMessageExpiry: () => Promise<undefined | number>;
getJobsInQueue(queueType: string): Promise<Array<StoredJob>>; getJobsInQueue(queueType: string): Promise<Array<StoredJob>>;
insertJob(job: Readonly<StoredJob>): Promise<void>; insertJob(job: Readonly<StoredJob>): Promise<void>;
@ -556,42 +582,27 @@ export type DataInterface = {
processGroupCallRingCancelation(ringId: bigint): Promise<void>; processGroupCallRingCancelation(ringId: bigint): Promise<void>;
cleanExpiredGroupCallRings(): Promise<void>; cleanExpiredGroupCallRings(): Promise<void>;
updateAllConversationColors: (
conversationColor?: ConversationColorType,
customColorData?: {
id: string;
value: CustomColorType;
}
) => Promise<void>;
getMaxMessageCounter(): Promise<number | undefined>; getMaxMessageCounter(): Promise<number | undefined>;
getStatisticsForLogging(): Promise<Record<string, string>>; getStatisticsForLogging(): Promise<Record<string, string>>;
}; };
// 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 & { export type ServerInterface = DataInterface & {
getAllConversations: () => Promise<Array<ConversationType>>; // Differing signature on client/server
getAllGroupsInvolvingUuid: (
id: UUIDStringType updateConversation: (data: ConversationType) => Promise<void>;
) => Promise<Array<ConversationType>>; removeConversation: (id: Array<string> | string) => Promise<void>;
getAllPrivateConversations: () => Promise<Array<ConversationType>>;
getConversationById: (id: string) => Promise<ConversationType | undefined>; searchMessages: (
getExpiredMessages: () => Promise<Array<MessageType>>; query: string,
getMessageById: (id: string) => Promise<MessageType | undefined>; options?: { limit?: number }
getMessageBySender: (options: { ) => Promise<Array<ServerSearchResultMessageType>>;
source: string; searchMessagesInConversation: (
sourceUuid: string; query: string,
sourceDevice: number; conversationId: string,
sent_at: number; options?: { limit?: number }
}) => Promise<Array<MessageType>>; ) => Promise<Array<ServerSearchResultMessageType>>;
getMessagesBySentAt: (sentAt: number) => Promise<Array<MessageType>>;
getOlderMessagesByConversation: ( getOlderMessagesByConversation: (
conversationId: string, conversationId: string,
options?: { options?: {
@ -611,38 +622,15 @@ export type ServerInterface = DataInterface & {
storyId?: UUIDStringType; storyId?: UUIDStringType;
} }
) => Promise<Array<MessageTypeUnhydrated>>; ) => Promise<Array<MessageTypeUnhydrated>>;
getLastConversationMessages: (options: {
conversationId: string;
ourUuid: UUIDStringType;
}) => Promise<LastConversationMessagesServerType>;
getTapToViewMessagesNeedingErase: () => Promise<Array<MessageType>>;
removeConversation: (id: Array<string> | string) => Promise<void>;
removeMessage: (id: string) => Promise<void>;
removeMessages: (ids: Array<string>) => Promise<void>;
searchMessages: (
query: string,
options?: { limit?: number }
) => Promise<Array<SearchResultMessageType>>;
searchMessagesInConversation: (
query: string,
conversationId: string,
options?: { limit?: number }
) => Promise<Array<SearchResultMessageType>>;
updateConversation: (data: ConversationType) => Promise<void>;
// For testing only
_getAllMessages: () => Promise<Array<MessageType>>;
// Server-only // Server-only
getCorruptionLog: () => string; getCorruptionLog: () => string;
initialize: (options: { initialize: (options: {
configDir: string; configDir: string;
key: string; key: string;
logger: LoggerType; logger: LoggerType;
}) => Promise<void>; }) => Promise<void>;
initializeRenderer: (options: { initializeRenderer: (options: {
configDir: string; configDir: string;
key: string; key: string;
@ -659,83 +647,11 @@ export type ServerInterface = DataInterface & {
}; };
export type ClientInterface = DataInterface & { export type ClientInterface = DataInterface & {
getAllConversations: (options: { // Differing signature on client/server
ConversationCollection: typeof ConversationModelCollectionType;
}) => Promise<ConversationModelCollectionType>; updateConversation: (data: ConversationType) => void;
getAllGroupsInvolvingUuid: ( removeConversation: (id: string) => Promise<void>;
id: UUIDStringType,
options: {
ConversationCollection: typeof ConversationModelCollectionType;
}
) => Promise<ConversationModelCollectionType>;
getAllPrivateConversations: (options: {
ConversationCollection: typeof ConversationModelCollectionType;
}) => Promise<ConversationModelCollectionType>;
getConversationById: (
id: string,
options: { Conversation: typeof ConversationModel }
) => Promise<ConversationModel | undefined>;
getExpiredMessages: (options: {
MessageCollection: typeof MessageModelCollectionType;
}) => Promise<MessageModelCollectionType>;
getMessageById: (
id: string,
options: { Message: typeof MessageModel }
) => Promise<MessageModel | undefined>;
getMessageBySender: (
data: {
source: string;
sourceUuid: string;
sourceDevice: number;
sent_at: number;
},
options: { Message: typeof MessageModel }
) => Promise<MessageModel | null>;
getMessagesBySentAt: (
sentAt: number,
options: { MessageCollection: typeof MessageModelCollectionType }
) => Promise<MessageModelCollectionType>;
getOlderMessagesByConversation: (
conversationId: string,
options: {
limit?: number;
MessageCollection: typeof MessageModelCollectionType;
messageId?: string;
receivedAt?: number;
sentAt?: number;
storyId?: UUIDStringType;
}
) => Promise<MessageModelCollectionType>;
getNewerMessagesByConversation: (
conversationId: string,
options: {
limit?: number;
MessageCollection: typeof MessageModelCollectionType;
receivedAt?: number;
sentAt?: number;
storyId?: UUIDStringType;
}
) => Promise<MessageModelCollectionType>;
getLastConversationMessages: (options: {
conversationId: string;
ourUuid: UUIDStringType;
Message: typeof MessageModel;
}) => Promise<LastConversationMessagesType>;
getTapToViewMessagesNeedingErase: (options: {
MessageCollection: typeof MessageModelCollectionType;
}) => Promise<MessageModelCollectionType>;
removeConversation: (
id: string,
options: { Conversation: typeof ConversationModel }
) => Promise<void>;
removeMessage: (
id: string,
options: { Message: typeof MessageModel }
) => Promise<void>;
removeMessages: (
ids: Array<string>,
options: { Message: typeof MessageModel }
) => Promise<void>;
searchMessages: ( searchMessages: (
query: string, query: string,
options?: { limit?: number } options?: { limit?: number }
@ -745,13 +661,26 @@ export type ClientInterface = DataInterface & {
conversationId: string, conversationId: string,
options?: { limit?: number } options?: { limit?: number }
) => Promise<Array<ClientSearchResultMessageType>>; ) => Promise<Array<ClientSearchResultMessageType>>;
updateConversation: (data: ConversationType, extra?: unknown) => void;
// Test-only getOlderMessagesByConversation: (
conversationId: string,
_getAllMessages: (options: { options: {
MessageCollection: typeof MessageModelCollectionType; limit?: number;
}) => Promise<MessageModelCollectionType>; messageId?: string;
receivedAt?: number;
sentAt?: number;
storyId?: UUIDStringType;
}
) => Promise<Array<MessageAttributesType>>;
getNewerMessagesByConversation: (
conversationId: string,
options: {
limit?: number;
receivedAt?: number;
sentAt?: number;
storyId?: UUIDStringType;
}
) => Promise<Array<MessageAttributesType>>;
// Client-side only // Client-side only
@ -760,20 +689,15 @@ export type ClientInterface = DataInterface & {
conversationId: string, conversationId: string,
options: { options: {
logId: string; logId: string;
MessageCollection: typeof MessageModelCollectionType;
} }
) => Promise<void>; ) => Promise<void>;
removeOtherData: () => Promise<void>; removeOtherData: () => Promise<void>;
cleanupOrphanedAttachments: () => Promise<void>; cleanupOrphanedAttachments: () => Promise<void>;
ensureFilePermissions: () => Promise<void>; ensureFilePermissions: () => Promise<void>;
// Client-side only, and test-only
_removeConversations: (ids: Array<string>) => Promise<void>;
_jobs: { [id: string]: ClientJobType }; _jobs: { [id: string]: ClientJobType };
// These are defined on the server-only and used in the client to determine // To decide whether to use IPC to use the database in the main process or
// whether we should use IPC to use the database in the main process or
// use the db already running in the renderer. // use the db already running in the renderer.
goBackToMainProcess: () => Promise<void>; goBackToMainProcess: () => Promise<void>;
startInRendererProcess: (isTesting?: boolean) => Promise<void>; startInRendererProcess: (isTesting?: boolean) => Promise<void>;

View file

@ -80,13 +80,13 @@ import type {
IdentityKeyType, IdentityKeyType,
ItemKeyType, ItemKeyType,
ItemType, ItemType,
LastConversationMessagesServerType, LastConversationMessagesType,
MessageMetricsType, MessageMetricsType,
MessageType, MessageType,
MessageTypeUnhydrated, MessageTypeUnhydrated,
PreKeyIdType, PreKeyIdType,
PreKeyType, PreKeyType,
SearchResultMessageType, ServerSearchResultMessageType,
SenderKeyIdType, SenderKeyIdType,
SenderKeyType, SenderKeyType,
SentMessageDBType, SentMessageDBType,
@ -152,16 +152,16 @@ const dataInterface: ServerInterface = {
createOrUpdateSignedPreKey, createOrUpdateSignedPreKey,
getSignedPreKeyById, getSignedPreKeyById,
getAllSignedPreKeys,
bulkAddSignedPreKeys, bulkAddSignedPreKeys,
removeSignedPreKeyById, removeSignedPreKeyById,
removeAllSignedPreKeys, removeAllSignedPreKeys,
getAllSignedPreKeys,
createOrUpdateItem, createOrUpdateItem,
getItemById, getItemById,
getAllItems,
removeItemById, removeItemById,
removeAllItems, removeAllItems,
getAllItems,
createOrUpdateSenderKey, createOrUpdateSenderKey,
getSenderKeyById, getSenderKeyById,
@ -189,6 +189,7 @@ const dataInterface: ServerInterface = {
removeAllSessions, removeAllSessions,
getAllSessions, getAllSessions,
eraseStorageServiceStateFromConversations,
getConversationCount, getConversationCount,
saveConversation, saveConversation,
saveConversations, saveConversations,
@ -196,12 +197,12 @@ const dataInterface: ServerInterface = {
updateConversation, updateConversation,
updateConversations, updateConversations,
removeConversation, removeConversation,
eraseStorageServiceStateFromConversations, updateAllConversationColors,
getAllConversations, getAllConversations,
getAllConversationIds, getAllConversationIds,
getAllPrivateConversations, getAllPrivateConversations,
getAllGroupsInvolvingUuid, getAllGroupsInvolvingUuid,
updateAllConversationColors,
searchConversations, searchConversations,
searchMessages, searchMessages,
@ -250,8 +251,8 @@ const dataInterface: ServerInterface = {
getNextAttachmentDownloadJobs, getNextAttachmentDownloadJobs,
saveAttachmentDownloadJob, saveAttachmentDownloadJob,
setAttachmentDownloadJobPending,
resetAttachmentDownloadPending, resetAttachmentDownloadPending,
setAttachmentDownloadJobPending,
removeAttachmentDownloadJob, removeAttachmentDownloadJob,
removeAllAttachmentDownloadJobs, removeAllAttachmentDownloadJobs,
@ -1557,7 +1558,7 @@ async function searchConversations(
async function searchMessages( async function searchMessages(
query: string, query: string,
params: { limit?: number; conversationId?: string } = {} params: { limit?: number; conversationId?: string } = {}
): Promise<Array<SearchResultMessageType>> { ): Promise<Array<ServerSearchResultMessageType>> {
const { limit = 500, conversationId } = params; const { limit = 500, conversationId } = params;
const db = getInstance(); const db = getInstance();
@ -1663,7 +1664,7 @@ async function searchMessagesInConversation(
query: string, query: string,
conversationId: string, conversationId: string,
{ limit = 100 }: { limit?: number } = {} { limit = 100 }: { limit?: number } = {}
): Promise<Array<SearchResultMessageType>> { ): Promise<Array<ServerSearchResultMessageType>> {
return searchMessages(query, { conversationId, limit }); return searchMessages(query, { conversationId, limit });
} }
@ -2024,7 +2025,7 @@ async function getMessageBySender({
sourceUuid: string; sourceUuid: string;
sourceDevice: number; sourceDevice: number;
sent_at: number; sent_at: number;
}): Promise<Array<MessageType>> { }): Promise<MessageType | undefined> {
const db = getInstance(); const db = getInstance();
const rows: JSONRows = prepare( const rows: JSONRows = prepare(
db, db,
@ -2032,7 +2033,8 @@ async function getMessageBySender({
SELECT json FROM messages WHERE SELECT json FROM messages WHERE
(source = $source OR sourceUuid = $sourceUuid) AND (source = $source OR sourceUuid = $sourceUuid) AND
sourceDevice = $sourceDevice AND sourceDevice = $sourceDevice AND
sent_at = $sent_at; sent_at = $sent_at
LIMIT 2;
` `
).all({ ).all({
source, source,
@ -2041,7 +2043,20 @@ async function getMessageBySender({
sent_at, 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({ async function getUnreadByConversationAndMarkRead({
@ -2605,7 +2620,7 @@ async function getLastConversationMessages({
}: { }: {
conversationId: string; conversationId: string;
ourUuid: UUIDStringType; ourUuid: UUIDStringType;
}): Promise<LastConversationMessagesServerType> { }): Promise<LastConversationMessagesType> {
const db = getInstance(); const db = getInstance();
return db.transaction(() => { return db.transaction(() => {

View file

@ -187,7 +187,7 @@ export type GetContactOptions = Pick<
'conversationSelector' | 'ourConversationId' | 'ourNumber' | 'ourUuid' 'conversationSelector' | 'ourConversationId' | 'ourNumber' | 'ourUuid'
>; >;
function getContactId( export function getContactId(
message: MessageWithUIFieldsType, message: MessageWithUIFieldsType,
{ {
conversationSelector, conversationSelector,

View file

@ -12,6 +12,7 @@ import type { CallbackResultType } from '../../textsecure/Types.d';
import type { StorageAccessType } from '../../types/Storage.d'; import type { StorageAccessType } from '../../types/Storage.d';
import { UUID } from '../../types/UUID'; import { UUID } from '../../types/UUID';
import { SignalService as Proto } from '../../protobuf'; import { SignalService as Proto } from '../../protobuf';
import { getContact } from '../../messages/helpers';
describe('Message', () => { describe('Message', () => {
const STORAGE_KEYS_TO_RESTORE: Array<keyof StorageAccessType> = [ const STORAGE_KEYS_TO_RESTORE: Array<keyof StorageAccessType> = [
@ -204,7 +205,7 @@ describe('Message', () => {
it('gets outgoing contact', () => { it('gets outgoing contact', () => {
const messages = new window.Whisper.MessageCollection(); const messages = new window.Whisper.MessageCollection();
const message = messages.add(attributes); const message = messages.add(attributes);
message.getContact(); assert.exists(getContact(message.attributes));
}); });
it('gets incoming contact', () => { it('gets incoming contact', () => {
@ -213,7 +214,7 @@ describe('Message', () => {
type: 'incoming', type: 'incoming',
source, source,
}); });
message.getContact(); assert.exists(getContact(message.attributes));
}); });
}); });

View file

@ -28,12 +28,7 @@ describe('sql/allMedia', () => {
describe('getMessagesWithVisualMediaAttachments', () => { describe('getMessagesWithVisualMediaAttachments', () => {
it('returns messages matching with visual attachments', async () => { it('returns messages matching with visual attachments', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now(); const now = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -69,12 +64,7 @@ describe('sql/allMedia', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await getMessagesWithVisualMediaAttachments( const searchResults = await getMessagesWithVisualMediaAttachments(
conversationId, conversationId,
@ -85,12 +75,7 @@ describe('sql/allMedia', () => {
}); });
it('excludes stories and story replies', async () => { it('excludes stories and story replies', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now(); const now = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -129,12 +114,7 @@ describe('sql/allMedia', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await getMessagesWithVisualMediaAttachments( const searchResults = await getMessagesWithVisualMediaAttachments(
conversationId, conversationId,
@ -147,12 +127,7 @@ describe('sql/allMedia', () => {
describe('getMessagesWithFileAttachments', () => { describe('getMessagesWithFileAttachments', () => {
it('returns messages matching with visual attachments', async () => { it('returns messages matching with visual attachments', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now(); const now = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -188,12 +163,7 @@ describe('sql/allMedia', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await getMessagesWithFileAttachments( const searchResults = await getMessagesWithFileAttachments(
conversationId, conversationId,
@ -204,12 +174,7 @@ describe('sql/allMedia', () => {
}); });
it('excludes stories and story replies', async () => { it('excludes stories and story replies', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now(); const now = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -248,12 +213,7 @@ describe('sql/allMedia', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await getMessagesWithFileAttachments( const searchResults = await getMessagesWithFileAttachments(
conversationId, conversationId,

View file

@ -27,12 +27,7 @@ describe('sql/fullTextSearch', () => {
}); });
it('returns messages matching query', async () => { it('returns messages matching query', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now(); const now = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -66,12 +61,7 @@ describe('sql/fullTextSearch', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await searchMessages('unique'); const searchResults = await searchMessages('unique');
assert.lengthOf(searchResults, 1); assert.lengthOf(searchResults, 1);
@ -87,12 +77,7 @@ describe('sql/fullTextSearch', () => {
}); });
it('excludes messages with isViewOnce = true', async () => { it('excludes messages with isViewOnce = true', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now(); const now = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -128,12 +113,7 @@ describe('sql/fullTextSearch', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await searchMessages('unique'); const searchResults = await searchMessages('unique');
assert.lengthOf(searchResults, 1); assert.lengthOf(searchResults, 1);
@ -148,12 +128,7 @@ describe('sql/fullTextSearch', () => {
}); });
it('excludes messages with storyId !== null', async () => { it('excludes messages with storyId !== null', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now(); const now = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -189,12 +164,7 @@ describe('sql/fullTextSearch', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const searchResults = await searchMessages('unique'); const searchResults = await searchMessages('unique');
assert.lengthOf(searchResults, 1); assert.lengthOf(searchResults, 1);

View file

@ -34,12 +34,7 @@ describe('sql/markRead', () => {
}); });
it('properly finds and reads unread messages in current conversation', async () => { it('properly finds and reads unread messages in current conversation', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const start = Date.now(); const start = Date.now();
const readAt = start + 20; const readAt = start + 20;
@ -125,12 +120,7 @@ describe('sql/markRead', () => {
} }
); );
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 7);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
7
);
assert.strictEqual( assert.strictEqual(
await getTotalUnreadForConversation(conversationId), await getTotalUnreadForConversation(conversationId),
3, 3,
@ -179,12 +169,7 @@ describe('sql/markRead', () => {
}); });
it('properly finds and reads unread messages in story', async () => { it('properly finds and reads unread messages in story', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const start = Date.now(); const start = Date.now();
const readAt = start + 20; const readAt = start + 20;
@ -276,12 +261,7 @@ describe('sql/markRead', () => {
} }
); );
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 7);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
7
);
const markedRead = await getUnreadByConversationAndMarkRead({ const markedRead = await getUnreadByConversationAndMarkRead({
conversationId, conversationId,
@ -311,12 +291,7 @@ describe('sql/markRead', () => {
}); });
it('properly starts disappearing message timer, even if message is already read', async () => { it('properly starts disappearing message timer, even if message is already read', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const start = Date.now(); const start = Date.now();
const readAt = start + 20; const readAt = start + 20;
@ -388,12 +363,7 @@ describe('sql/markRead', () => {
2, 2,
'unread count' 'unread count'
); );
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 5);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const markedRead = await getUnreadByConversationAndMarkRead({ const markedRead = await getUnreadByConversationAndMarkRead({
conversationId, conversationId,
@ -413,21 +383,21 @@ describe('sql/markRead', () => {
'unread count' 'unread count'
); );
const allMessages = await _getAllMessages({ const allMessages = await _getAllMessages();
MessageCollection: window.Whisper.MessageCollection, const sorted = allMessages.sort(
}); (left, right) => left.timestamp - right.timestamp
);
// Ascending order, since it's sorted by MessageCollection assert.strictEqual(sorted[1].id, message2.id, 'checking message 2');
assert.strictEqual(allMessages.at(1).id, message2.id);
assert.isAtMost( assert.isAtMost(
allMessages.at(1).get('expirationStartTimestamp') ?? Infinity, sorted[1].expirationStartTimestamp ?? Infinity,
Date.now(), Date.now(),
'checking message 2 expirationStartTimestamp' '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( assert.isAtMost(
allMessages.at(3).get('expirationStartTimestamp') ?? Infinity, sorted[3].expirationStartTimestamp ?? Infinity,
Date.now(), Date.now(),
'checking message 4 expirationStartTimestamp' 'checking message 4 expirationStartTimestamp'
); );
@ -490,12 +460,7 @@ describe('sql/markRead', () => {
await saveMessages([message1, message2, message3, message4, message5], { await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true, forceSave: true,
}); });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 5);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const reaction1: ReactionType = { const reaction1: ReactionType = {
conversationId, conversationId,
@ -642,12 +607,7 @@ describe('sql/markRead', () => {
await saveMessages([message1, message2, message3, message4, message5], { await saveMessages([message1, message2, message3, message4, message5], {
forceSave: true, forceSave: true,
}); });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 5);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const reaction1: ReactionType = { const reaction1: ReactionType = {
conversationId, conversationId,

View file

@ -124,7 +124,7 @@ describe('sql/sendLog', () => {
assert.strictEqual(actual.timestamp, proto.timestamp); assert.strictEqual(actual.timestamp, proto.timestamp);
await removeMessage(id, { Message: window.Whisper.Message }); await removeMessage(id);
assert.lengthOf(await getAllSentProtos(), 0); assert.lengthOf(await getAllSentProtos(), 0);
}); });

View file

@ -23,12 +23,7 @@ describe('sql/stories', () => {
describe('getOlderStories', () => { describe('getOlderStories', () => {
it('returns N most recent stories overall, or in converation, or by author', async () => { it('returns N most recent stories overall, or in converation, or by author', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now(); const now = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -89,12 +84,7 @@ describe('sql/stories', () => {
forceSave: true, forceSave: true,
}); });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 5);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const stories = await getOlderStories({ const stories = await getOlderStories({
limit: 5, limit: 5,
@ -155,12 +145,7 @@ describe('sql/stories', () => {
}); });
it('returns N stories older than provided receivedAt/sentAt', async () => { it('returns N stories older than provided receivedAt/sentAt', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const start = Date.now(); const start = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -214,12 +199,7 @@ describe('sql/stories', () => {
forceSave: true, forceSave: true,
}); });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 5);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const stories = await getOlderStories({ const stories = await getOlderStories({
receivedAt: story4.received_at, receivedAt: story4.received_at,

View file

@ -30,12 +30,7 @@ describe('sql/timelineFetches', () => {
describe('getOlderMessagesByConversation', () => { describe('getOlderMessagesByConversation', () => {
it('returns N most recent messages', async () => { it('returns N most recent messages', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now(); const now = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -92,30 +87,20 @@ describe('sql/timelineFetches', () => {
forceSave: true, forceSave: true,
}); });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 5);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const messages = await getOlderMessagesByConversation(conversationId, { const messages = await getOlderMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5, limit: 5,
}); });
assert.lengthOf(messages, 2); assert.lengthOf(messages, 2);
// They are not in DESC order because MessageCollection is sorting them
assert.strictEqual(messages.at(0).attributes.id, message1.id); // Fetched with DESC query, but with reverse() call afterwards
assert.strictEqual(messages.at(1).attributes.id, message2.id); 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 () => { it('returns N most recent messages for a given story', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now(); const now = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -152,29 +137,18 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getOlderMessagesByConversation(conversationId, { const messages = await getOlderMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5, limit: 5,
storyId, storyId,
}); });
assert.lengthOf(messages, 1); 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 () => { it('returns N messages older than provided received_at', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const target = Date.now(); const target = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -208,30 +182,19 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getOlderMessagesByConversation(conversationId, { const messages = await getOlderMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5, limit: 5,
receivedAt: target, receivedAt: target,
sentAt: target, sentAt: target,
}); });
assert.lengthOf(messages, 1); 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 () => { it('returns N older messages with received_at, lesser sent_at', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const target = Date.now(); const target = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -265,33 +228,23 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getOlderMessagesByConversation(conversationId, { const messages = await getOlderMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5, limit: 5,
receivedAt: target, receivedAt: target,
sentAt: target, sentAt: target,
}); });
assert.lengthOf(messages, 2); assert.lengthOf(messages, 2);
// They are not in DESC order because MessageCollection is sorting them
assert.strictEqual(messages.at(0).attributes.id, message1.id); // Fetched with DESC query, but with reverse() call afterwards
assert.strictEqual(messages.at(1).attributes.id, message2.id); 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 () => { it('returns N older messages, same received_at/sent_at but excludes messageId', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const target = Date.now(); const target = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -325,15 +278,9 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getOlderMessagesByConversation(conversationId, { const messages = await getOlderMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5, limit: 5,
receivedAt: target, receivedAt: target,
sentAt: target, sentAt: target,
@ -341,18 +288,13 @@ describe('sql/timelineFetches', () => {
}); });
assert.lengthOf(messages, 1); assert.lengthOf(messages, 1);
assert.strictEqual(messages.at(0).attributes.id, message1.id); assert.strictEqual(messages[0].id, message1.id);
}); });
}); });
describe('getNewerMessagesByConversation', () => { describe('getNewerMessagesByConversation', () => {
it('returns N oldest messages with no parameters', async () => { it('returns N oldest messages with no parameters', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now(); const now = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -409,30 +351,19 @@ describe('sql/timelineFetches', () => {
forceSave: true, forceSave: true,
}); });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 5);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
5
);
const messages = await getNewerMessagesByConversation(conversationId, { const messages = await getNewerMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5, limit: 5,
}); });
assert.lengthOf(messages, 2); assert.lengthOf(messages, 2);
assert.strictEqual(messages.at(0).attributes.id, message4.id); assert.strictEqual(messages[0].id, message4.id, 'checking message 4');
assert.strictEqual(messages.at(1).attributes.id, message5.id); assert.strictEqual(messages[1].id, message5.id, 'checking message 5');
}); });
it('returns N oldest messages for a given story with no parameters', async () => { it('returns N oldest messages for a given story with no parameters', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const now = Date.now(); const now = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -469,30 +400,19 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getNewerMessagesByConversation(conversationId, { const messages = await getNewerMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5, limit: 5,
storyId, storyId,
}); });
assert.lengthOf(messages, 1); 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 () => { it('returns N messages newer than provided received_at', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const target = Date.now(); const target = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -526,30 +446,19 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getNewerMessagesByConversation(conversationId, { const messages = await getNewerMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5, limit: 5,
receivedAt: target, receivedAt: target,
sentAt: target, sentAt: target,
}); });
assert.lengthOf(messages, 1); 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 () => { it('returns N newer messages with same received_at, greater sent_at', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const target = Date.now(); const target = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -583,15 +492,9 @@ describe('sql/timelineFetches', () => {
await saveMessages([message1, message2, message3], { forceSave: true }); await saveMessages([message1, message2, message3], { forceSave: true });
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 3);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
3
);
const messages = await getNewerMessagesByConversation(conversationId, { const messages = await getNewerMessagesByConversation(conversationId, {
MessageCollection: window.Whisper.MessageCollection,
limit: 5, limit: 5,
receivedAt: target, receivedAt: target,
sentAt: target, sentAt: target,
@ -599,19 +502,14 @@ describe('sql/timelineFetches', () => {
assert.lengthOf(messages, 2); assert.lengthOf(messages, 2);
// They are not in DESC order because MessageCollection is sorting them // They are not in DESC order because MessageCollection is sorting them
assert.strictEqual(messages.at(0).attributes.id, message2.id); assert.strictEqual(messages[0].id, message2.id);
assert.strictEqual(messages.at(1).attributes.id, message3.id); assert.strictEqual(messages[1].id, message3.id);
}); });
}); });
describe('getMessageMetricsForConversation', () => { describe('getMessageMetricsForConversation', () => {
it('returns metrics properly for story and non-story timelines', async () => { it('returns metrics properly for story and non-story timelines', async () => {
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 0);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
0
);
const target = Date.now(); const target = Date.now();
const conversationId = getUuid(); const conversationId = getUuid();
@ -710,12 +608,7 @@ describe('sql/timelineFetches', () => {
{ forceSave: true } { forceSave: true }
); );
assert.lengthOf( assert.lengthOf(await _getAllMessages(), 8);
await _getAllMessages({
MessageCollection: window.Whisper.MessageCollection,
}),
8
);
const metricsInTimeline = await getMessageMetricsForConversation( const metricsInTimeline = await getMessageMetricsForConversation(
conversationId conversationId

View file

@ -11,8 +11,6 @@ import { SignalProtocolStore } from '../../SignalProtocolStore';
import type { ConversationModel } from '../../models/conversations'; import type { ConversationModel } from '../../models/conversations';
import * as KeyChangeListener from '../../textsecure/KeyChangeListener'; import * as KeyChangeListener from '../../textsecure/KeyChangeListener';
const { Whisper } = window;
describe('KeyChangeListener', () => { describe('KeyChangeListener', () => {
let oldNumberId: string | undefined; let oldNumberId: string | undefined;
let oldUuidId: string | undefined; let oldUuidId: string | undefined;
@ -71,11 +69,8 @@ describe('KeyChangeListener', () => {
afterEach(async () => { afterEach(async () => {
await window.Signal.Data.removeAllMessagesInConversation(convo.id, { await window.Signal.Data.removeAllMessagesInConversation(convo.id, {
logId: uuidWithKeyChange, 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)); await store.removeIdentityKey(new UUID(uuidWithKeyChange));
}); });
@ -107,11 +102,8 @@ describe('KeyChangeListener', () => {
afterEach(async () => { afterEach(async () => {
await window.Signal.Data.removeAllMessagesInConversation(groupConvo.id, { await window.Signal.Data.removeAllMessagesInConversation(groupConvo.id, {
logId: uuidWithKeyChange, 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 => { it('generates a key change notice in the group conversation with this contact', done => {

View file

@ -5,6 +5,7 @@ import type { MessageModel } from '../models/messages';
import * as durations from './durations'; import * as durations from './durations';
import { map, filter } from './iterables'; import { map, filter } from './iterables';
import { isNotNil } from './isNotNil'; import { isNotNil } from './isNotNil';
import type { MessageAttributesType } from '../model-types.d';
const FIVE_MINUTES = 5 * durations.MINUTE; const FIVE_MINUTES = 5 * durations.MINUTE;
@ -29,9 +30,12 @@ export class MessageController {
return instance; return instance;
} }
register(id: string, message: MessageModel): MessageModel { register(
if (!id || !message) { id: string,
return message; data: MessageModel | MessageAttributesType
): MessageModel {
if (!id || !data) {
throw new Error('MessageController.register: Got falsey id or message');
} }
const existing = this.messageLookup[id]; const existing = this.messageLookup[id];
@ -43,6 +47,8 @@ export class MessageController {
return existing.message; return existing.message;
} }
const message =
'attributes' in data ? data : new window.Whisper.Message(data);
this.messageLookup[id] = { this.messageLookup[id] = {
message, message,
timestamp: Date.now(), timestamp: Date.now(),

36
ts/util/cleanup.ts Normal file
View file

@ -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<void> {
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<void> {
await window.Signal.Migrations.deleteExternalMessageFiles(message);
const { sticker } = message;
if (!sticker) {
return;
}
const { packId } = sticker;
if (packId) {
await deletePackReference(message.id, packId);
}
}

View file

@ -347,9 +347,7 @@ async function getRetryConversation({
} }
const [messageId] = messageIds; const [messageId] = messageIds;
const message = await window.Signal.Data.getMessageById(messageId, { const message = await window.Signal.Data.getMessageById(messageId);
Message: window.Whisper.Message,
});
if (!message) { if (!message) {
log.warn( log.warn(
`getRetryConversation/${logId}: Unable to find message ${messageId}` `getRetryConversation/${logId}: Unable to find message ${messageId}`
@ -358,7 +356,7 @@ async function getRetryConversation({
return window.ConversationController.get(requestGroupId); return window.ConversationController.get(requestGroupId);
} }
const conversationId = message.get('conversationId'); const { conversationId } = message;
return window.ConversationController.get(conversationId); return window.ConversationController.get(conversationId);
} }

View file

@ -29,6 +29,7 @@ import type {
MessageAttributesType as MediaItemMessageType, MessageAttributesType as MediaItemMessageType,
} from '../types/MediaItem'; } from '../types/MediaItem';
import type { MessageModel } from '../models/messages'; import type { MessageModel } from '../models/messages';
import { getContactId } from '../messages/helpers';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { maybeParseUrl } from '../util/url'; import { maybeParseUrl } from '../util/url';
import { enqueueReactionForSend } from '../reactions/enqueueReactionForSend'; import { enqueueReactionForSend } from '../reactions/enqueueReactionForSend';
@ -447,14 +448,12 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
const { authorId, sentAt } = options; const { authorId, sentAt } = options;
const conversationId = this.model.id; const conversationId = this.model.id;
const messages = await getMessagesBySentAt(sentAt, { const messages = await getMessagesBySentAt(sentAt);
MessageCollection: Whisper.MessageCollection,
});
const message = messages.find(item => const message = messages.find(item =>
Boolean( Boolean(
item.get('conversationId') === conversationId && item.conversationId === conversationId &&
authorId && authorId &&
item.getContactId() === authorId getContactId(item) === authorId
) )
); );
@ -471,14 +470,12 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
return; return;
} }
const message = await getMessageById(messageId, { const message = await getMessageById(messageId);
Message: Whisper.Message,
});
if (!message) { if (!message) {
throw new Error(`markMessageRead: failed to load message ${messageId}`); 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 = const createMessageRequestResponseHandler =
@ -883,9 +880,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
} }
async scrollToMessage(messageId: string): Promise<void> { async scrollToMessage(messageId: string): Promise<void> {
const message = await getMessageById(messageId, { const message = await getMessageById(messageId);
Message: Whisper.Message,
});
if (!message) { if (!message) {
throw new Error(`scrollToMessage: failed to load message ${messageId}`); throw new Error(`scrollToMessage: failed to load message ${messageId}`);
} }
@ -1201,9 +1196,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
async onOpened(messageId: string): Promise<void> { async onOpened(messageId: string): Promise<void> {
if (messageId) { if (messageId) {
const message = await getMessageById(messageId, { const message = await getMessageById(messageId);
Message: Whisper.Message,
});
if (message) { if (message) {
this.model.loadAndScroll(messageId); this.model.loadAndScroll(messageId);
@ -1254,16 +1247,14 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
if (!messageFromCache) { if (!messageFromCache) {
log.info('showForwardMessageModal: Fetching message from database'); log.info('showForwardMessageModal: Fetching message from database');
} }
const message = const found =
messageFromCache || messageFromCache || (await window.Signal.Data.getMessageById(messageId));
(await window.Signal.Data.getMessageById(messageId, {
Message: window.Whisper.Message,
}));
if (!message) { if (!found) {
throw new Error(`showForwardMessageModal: Message ${messageId} missing!`); throw new Error(`showForwardMessageModal: Message ${messageId} missing!`);
} }
const message = window.MessageController.register(found.id, found);
const attachments = getAttachmentsForMessage(message.attributes); const attachments = getAttachmentsForMessage(message.attributes);
this.forwardMessageModal = new Whisper.ReactWrapperView({ this.forwardMessageModal = new Whisper.ReactWrapperView({
@ -1911,10 +1902,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
message: window.i18n('deleteWarning'), message: window.i18n('deleteWarning'),
okText: window.i18n('delete'), okText: window.i18n('delete'),
resolve: () => { resolve: () => {
window.Signal.Data.removeMessage(message.id, { window.Signal.Data.removeMessage(message.id);
Message: Whisper.Message,
});
message.cleanup();
if (isOutgoing(message.attributes)) { if (isOutgoing(message.attributes)) {
this.model.decrementSentMessageCount(); this.model.decrementSentMessageCount();
} else { } else {
@ -2676,18 +2664,16 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
} }
async setQuoteMessage(messageId: null | string): Promise<void> { async setQuoteMessage(messageId: null | string): Promise<void> {
const { model }: { model: ConversationModel } = this; const { model } = this;
const found = messageId ? await getMessageById(messageId) : undefined;
const message: MessageModel | undefined = messageId const message = found
? await getMessageById(messageId, { ? window.MessageController.register(found.id, found)
Message: Whisper.Message,
})
: undefined; : undefined;
if ( if (
message && found &&
!canReply( !canReply(
message.attributes, found,
window.ConversationController.getOurConversationIdOrThrow(), window.ConversationController.getOurConversationIdOrThrow(),
findAndFormatContact findAndFormatContact
) )
@ -2724,19 +2710,12 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
} }
if (message) { if (message) {
const quotedMessage = window.MessageController.register( this.quotedMessage = message;
message.id,
message
);
this.quotedMessage = quotedMessage;
if (quotedMessage) {
this.quote = await model.makeQuote(this.quotedMessage); this.quote = await model.makeQuote(this.quotedMessage);
this.enableMessageField(); this.enableMessageField();
this.focusMessageField(); this.focusMessageField();
} }
}
this.renderQuotedMessage(); this.renderQuotedMessage();
} }