Remove messageCollection from Conversation model

This commit is contained in:
Scott Nonnenberg 2021-06-15 17:44:14 -07:00 committed by GitHub
parent 61ad1231df
commit 1520c80013
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 332 additions and 431 deletions

View file

@ -107,8 +107,11 @@
const conversation = ConversationController.get( const conversation = ConversationController.get(
message.get('conversationId') message.get('conversationId')
); );
if (conversation) { const updateLeftPane = conversation
conversation.trigger('delivered', message); ? conversation.debouncedUpdateLastMessage
: undefined;
if (updateLeftPane) {
updateLeftPane();
} }
this.remove(receipt); this.remove(receipt);

View file

@ -41,7 +41,9 @@
const conversation = message.getConversation(); const conversation = message.getConversation();
if (conversation) { if (conversation) {
conversation.trigger('expired', message); // An expired message only counts as decrementing the message count, not
// the sent message count
conversation.decrementMessageCount();
} }
}); });
} catch (error) { } catch (error) {

View file

@ -29,7 +29,6 @@
message.idForLogging() message.idForLogging()
); );
message.trigger('erased');
await message.eraseContents(); await message.eraseContents();
}) })
); );

View file

@ -1,104 +0,0 @@
// Copyright 2019-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// eslint-disable-next-line func-names
(function () {
window.Whisper = window.Whisper || {};
const messageLookup = Object.create(null);
const msgIDsBySender = new Map();
const msgIDsBySentAt = new Map();
const SECOND = 1000;
const MINUTE = SECOND * 60;
const FIVE_MINUTES = MINUTE * 5;
const HOUR = MINUTE * 60;
function register(id, message) {
if (!id || !message) {
return message;
}
const existing = messageLookup[id];
if (existing) {
messageLookup[id] = {
message: existing.message,
timestamp: Date.now(),
};
return existing.message;
}
messageLookup[id] = {
message,
timestamp: Date.now(),
};
msgIDsBySentAt.set(message.get('sent_at'), id);
msgIDsBySender.set(message.getSenderIdentifier(), id);
return message;
}
function unregister(id) {
const { message } = messageLookup[id] || {};
if (message) {
msgIDsBySender.delete(message.getSenderIdentifier());
msgIDsBySentAt.delete(message.get('sent_at'));
}
delete messageLookup[id];
}
function cleanup() {
const messages = Object.values(messageLookup);
const now = Date.now();
for (let i = 0, max = messages.length; i < max; i += 1) {
const { message, timestamp } = messages[i];
const conversation = message.getConversation();
if (
now - timestamp > FIVE_MINUTES &&
(!conversation || !conversation.messageCollection.length)
) {
unregister(message.id);
}
}
}
function getById(id) {
const existing = messageLookup[id];
return existing && existing.message ? existing.message : undefined;
}
function findBySentAt(sentAt) {
const id = msgIDsBySentAt.get(sentAt);
if (!id) {
return null;
}
return getById(id);
}
function findBySender(sender) {
const id = msgIDsBySender.get(sender);
if (!id) {
return null;
}
return getById(id);
}
function _get() {
return messageLookup;
}
setInterval(cleanup, HOUR);
window.MessageController = {
register,
unregister,
cleanup,
findBySender,
findBySentAt,
getById,
_get,
};
})();

View file

@ -281,14 +281,6 @@ async function _finishJob(message, id) {
await saveMessage(message.attributes, { await saveMessage(message.attributes, {
Message: Whisper.Message, Message: Whisper.Message,
}); });
const conversation = message.getConversation();
if (conversation) {
const fromConversation = conversation.messageCollection.get(message.id);
if (fromConversation && message !== fromConversation) {
fromConversation.set(message.attributes);
}
}
} }
await removeAttachmentDownloadJob(id); await removeAttachmentDownloadJob(id);

View file

@ -108,8 +108,11 @@
const conversation = ConversationController.get( const conversation = ConversationController.get(
message.get('conversationId') message.get('conversationId')
); );
if (conversation) { const updateLeftPane = conversation
conversation.trigger('read', message); ? conversation.debouncedUpdateLastMessage
: undefined;
if (updateLeftPane) {
updateLeftPane();
} }
this.remove(receipt); this.remove(receipt);

View file

@ -75,6 +75,8 @@ function deleteIndexedDB() {
/* Delete the database before running any tests */ /* Delete the database before running any tests */
before(async () => { before(async () => {
window.Signal.Util.MessageController.install();
await deleteIndexedDB(); await deleteIndexedDB();
try { try {
window.log.info('Initializing SQL in renderer'); window.log.info('Initializing SQL in renderer');

View file

@ -615,11 +615,11 @@ describe('Backup', () => {
); );
console.log('Backup test: Check messages'); console.log('Backup test: Check messages');
const messageCollection = await window.Signal.Data._getAllMessages({ const messages = await window.Signal.Data._getAllMessages({
MessageCollection: Whisper.MessageCollection, MessageCollection: Whisper.MessageCollection,
}); });
assert.strictEqual(messageCollection.length, MESSAGE_COUNT); assert.strictEqual(messages.length, MESSAGE_COUNT);
const messageFromDB = removeId(messageCollection.at(0).attributes); const messageFromDB = removeId(messages.at(0).attributes);
const expectedMessage = messageFromDB; const expectedMessage = messageFromDB;
console.log({ messageFromDB, expectedMessage }); console.log({ messageFromDB, expectedMessage });
assert.deepEqual(messageFromDB, expectedMessage); assert.deepEqual(messageFromDB, expectedMessage);

View file

@ -43,6 +43,7 @@ import * as universalExpireTimer from './util/universalExpireTimer';
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation'; import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
import { getSendOptions } from './util/getSendOptions'; import { getSendOptions } from './util/getSendOptions';
import { BackOff } from './util/BackOff'; import { BackOff } from './util/BackOff';
import { AppViewType } from './state/ducks/app';
import { actionCreators } from './state/actions'; import { actionCreators } from './state/actions';
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
@ -70,6 +71,7 @@ export async function cleanupSessionResets(): Promise<void> {
} }
export async function startApp(): Promise<void> { export async function startApp(): Promise<void> {
window.Signal.Util.MessageController.install();
window.startupProcessingQueue = new window.Signal.Util.StartupQueue(); window.startupProcessingQueue = new window.Signal.Util.StartupQueue();
window.attachmentDownloadQueue = []; window.attachmentDownloadQueue = [];
try { try {
@ -1712,7 +1714,7 @@ export async function startApp(): Promise<void> {
} }
window.Whisper.events.on('contactsync', () => { window.Whisper.events.on('contactsync', () => {
if (window.reduxStore.getState().app.isShowingInstaller) { if (window.reduxStore.getState().app.appView === AppViewType.Installer) {
window.reduxActions.app.openInbox(); window.reduxActions.app.openInbox();
} }
}); });

View file

@ -47,8 +47,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
}), }),
isOutgoingKeyError: false, isOutgoingKeyError: false,
isUnidentifiedDelivery: false, isUnidentifiedDelivery: false,
onSendAnyway: action('onSendAnyway'),
onShowSafetyNumber: action('onShowSafetyNumber'),
status: 'delivered', status: 'delivered',
}, },
], ],
@ -60,6 +58,9 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
i18n, i18n,
interactionMode: 'keyboard', interactionMode: 'keyboard',
sendAnyway: action('onSendAnyway'),
showSafetyNumber: action('onShowSafetyNumber'),
clearSelectedMessage: () => null, clearSelectedMessage: () => null,
deleteMessage: action('deleteMessage'), deleteMessage: action('deleteMessage'),
deleteMessageForEveryone: action('deleteMessageForEveryone'), deleteMessageForEveryone: action('deleteMessageForEveryone'),
@ -108,8 +109,6 @@ story.add('Message Statuses', () => {
}), }),
isOutgoingKeyError: false, isOutgoingKeyError: false,
isUnidentifiedDelivery: false, isUnidentifiedDelivery: false,
onSendAnyway: action('onSendAnyway'),
onShowSafetyNumber: action('onShowSafetyNumber'),
status: 'sent', status: 'sent',
}, },
{ {
@ -119,8 +118,6 @@ story.add('Message Statuses', () => {
}), }),
isOutgoingKeyError: false, isOutgoingKeyError: false,
isUnidentifiedDelivery: false, isUnidentifiedDelivery: false,
onSendAnyway: action('onSendAnyway'),
onShowSafetyNumber: action('onShowSafetyNumber'),
status: 'sending', status: 'sending',
}, },
{ {
@ -130,8 +127,6 @@ story.add('Message Statuses', () => {
}), }),
isOutgoingKeyError: false, isOutgoingKeyError: false,
isUnidentifiedDelivery: false, isUnidentifiedDelivery: false,
onSendAnyway: action('onSendAnyway'),
onShowSafetyNumber: action('onShowSafetyNumber'),
status: 'partial-sent', status: 'partial-sent',
}, },
{ {
@ -141,8 +136,6 @@ story.add('Message Statuses', () => {
}), }),
isOutgoingKeyError: false, isOutgoingKeyError: false,
isUnidentifiedDelivery: false, isUnidentifiedDelivery: false,
onSendAnyway: action('onSendAnyway'),
onShowSafetyNumber: action('onShowSafetyNumber'),
status: 'delivered', status: 'delivered',
}, },
{ {
@ -152,8 +145,6 @@ story.add('Message Statuses', () => {
}), }),
isOutgoingKeyError: false, isOutgoingKeyError: false,
isUnidentifiedDelivery: false, isUnidentifiedDelivery: false,
onSendAnyway: action('onSendAnyway'),
onShowSafetyNumber: action('onShowSafetyNumber'),
status: 'read', status: 'read',
}, },
], ],
@ -211,8 +202,6 @@ story.add('All Errors', () => {
}), }),
isOutgoingKeyError: true, isOutgoingKeyError: true,
isUnidentifiedDelivery: false, isUnidentifiedDelivery: false,
onSendAnyway: action('onSendAnyway'),
onShowSafetyNumber: action('onShowSafetyNumber'),
status: 'error', status: 'error',
}, },
{ {
@ -228,8 +217,6 @@ story.add('All Errors', () => {
], ],
isOutgoingKeyError: false, isOutgoingKeyError: false,
isUnidentifiedDelivery: true, isUnidentifiedDelivery: true,
onSendAnyway: action('onSendAnyway'),
onShowSafetyNumber: action('onShowSafetyNumber'),
status: 'error', status: 'error',
}, },
{ {
@ -239,8 +226,6 @@ story.add('All Errors', () => {
}), }),
isOutgoingKeyError: true, isOutgoingKeyError: true,
isUnidentifiedDelivery: true, isUnidentifiedDelivery: true,
onSendAnyway: action('onSendAnyway'),
onShowSafetyNumber: action('onShowSafetyNumber'),
status: 'error', status: 'error',
}, },
], ],

View file

@ -24,6 +24,7 @@ export type Contact = Pick<
| 'acceptedMessageRequest' | 'acceptedMessageRequest'
| 'avatarPath' | 'avatarPath'
| 'color' | 'color'
| 'id'
| 'isMe' | 'isMe'
| 'name' | 'name'
| 'phoneNumber' | 'phoneNumber'
@ -39,9 +40,6 @@ export type Contact = Pick<
unblurredAvatarPath?: string; unblurredAvatarPath?: string;
errors?: Array<Error>; errors?: Array<Error>;
onSendAnyway: () => void;
onShowSafetyNumber: () => void;
}; };
export type Props = { export type Props = {
@ -52,6 +50,8 @@ export type Props = {
receivedAt: number; receivedAt: number;
sentAt: number; sentAt: number;
sendAnyway: (contactId: string, messageId: string) => unknown;
showSafetyNumber: (contactId: string) => void;
i18n: LocalizerType; i18n: LocalizerType;
} & Pick< } & Pick<
MessagePropsType, MessagePropsType,
@ -148,7 +148,7 @@ export class MessageDetail extends React.Component<Props> {
} }
public renderContact(contact: Contact): JSX.Element { public renderContact(contact: Contact): JSX.Element {
const { i18n } = this.props; const { i18n, message, showSafetyNumber, sendAnyway } = this.props;
const errors = contact.errors || []; const errors = contact.errors || [];
const errorComponent = contact.isOutgoingKeyError ? ( const errorComponent = contact.isOutgoingKeyError ? (
@ -156,14 +156,14 @@ export class MessageDetail extends React.Component<Props> {
<button <button
type="button" type="button"
className="module-message-detail__contact__show-safety-number" className="module-message-detail__contact__show-safety-number"
onClick={contact.onShowSafetyNumber} onClick={() => showSafetyNumber(contact.id)}
> >
{i18n('showSafetyNumber')} {i18n('showSafetyNumber')}
</button> </button>
<button <button
type="button" type="button"
className="module-message-detail__contact__send-anyway" className="module-message-detail__contact__send-anyway"
onClick={contact.onSendAnyway} onClick={() => sendAnyway(contact.id, message.id)}
> >
{i18n('sendAnyway')} {i18n('sendAnyway')}
</button> </button>

View file

@ -136,8 +136,6 @@ export class ConversationModel extends window.Backbone
jobQueue?: typeof window.PQueueType; jobQueue?: typeof window.PQueueType;
messageCollection?: MessageModelCollectionType;
ourNumber?: string; ourNumber?: string;
ourUuid?: string; ourUuid?: string;
@ -168,6 +166,8 @@ export class ConversationModel extends window.Backbone
private isFetchingUUID?: boolean; private isFetchingUUID?: boolean;
private hasAddedHistoryDisclaimer?: boolean;
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
defaults(): Partial<ConversationAttributesType> { defaults(): Partial<ConversationAttributesType> {
return { return {
@ -198,10 +198,6 @@ export class ConversationModel extends window.Backbone
return this.get('uuid') || this.get('e164'); return this.get('uuid') || this.get('e164');
} }
handleMessageError(message: unknown, errors: unknown): void {
this.trigger('messageError', message, errors);
}
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
getContactCollection(): Backbone.Collection<ConversationModel> { getContactCollection(): Backbone.Collection<ConversationModel> {
const collection = new window.Backbone.Collection<ConversationModel>(); const collection = new window.Backbone.Collection<ConversationModel>();
@ -252,30 +248,9 @@ export class ConversationModel extends window.Backbone
); );
} }
this.messageCollection = new window.Whisper.MessageCollection([], {
conversation: this,
});
this.messageCollection.on('change:errors', this.handleMessageError, this);
this.messageCollection.on('send-error', this.onMessageError, this);
this.listenTo(
this.messageCollection,
'add remove destroy content-changed',
this.debouncedUpdateLastMessage
);
this.listenTo(this.messageCollection, 'sent', this.updateLastMessage);
this.listenTo(this.messageCollection, 'send-error', this.updateLastMessage);
this.on('newmessage', this.onNewMessage); this.on('newmessage', this.onNewMessage);
this.on('change:profileKey', this.onChangeProfileKey); this.on('change:profileKey', this.onChangeProfileKey);
// Listening for out-of-band data updates
this.on('delivered', this.updateAndMerge);
this.on('read', this.updateAndMerge);
this.on('expiration-change', this.updateAndMerge);
this.on('expired', this.onExpired);
const sealedSender = this.get('sealedSender'); const sealedSender = this.get('sealedSender');
if (sealedSender === undefined) { if (sealedSender === undefined) {
this.set({ sealedSender: SEALED_SENDER.UNKNOWN }); this.set({ sealedSender: SEALED_SENDER.UNKNOWN });
@ -1238,64 +1213,10 @@ export class ConversationModel extends window.Backbone
); );
} }
async updateAndMerge(message: MessageModel): Promise<void> { async onNewMessage(message: MessageModel): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion const uuid = message.get('sourceUuid');
this.debouncedUpdateLastMessage!(); const e164 = message.get('source');
const sourceDevice = message.get('sourceDevice');
const mergeMessage = () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const existing = this.messageCollection!.get(message.id);
if (!existing) {
return;
}
existing.merge(message.attributes);
};
await this.inProgressFetch;
mergeMessage();
}
async onExpired(message: MessageModel): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.debouncedUpdateLastMessage!();
const removeMessage = () => {
const { id } = message;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const existing = this.messageCollection!.get(id);
if (!existing) {
return;
}
window.log.info('Remove expired message from collection', {
sentAt: existing.get('sent_at'),
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.messageCollection!.remove(id);
existing.trigger('expired');
existing.cleanup();
// An expired message only counts as decrementing the message count, not
// the sent message count
this.decrementMessageCount();
};
// If a fetch is in progress, then we need to wait until that's complete to
// do this removal. Otherwise we could remove from messageCollection, then
// the async database fetch could include the removed message.
await this.inProgressFetch;
removeMessage();
}
async onNewMessage(message: WhatIsThis): Promise<void> {
const uuid = message.get ? message.get('sourceUuid') : message.sourceUuid;
const e164 = message.get ? message.get('source') : message.source;
const sourceDevice = message.get
? message.get('sourceDevice')
: message.sourceDevice;
const sourceId = window.ConversationController.ensureContactIds({ const sourceId = window.ConversationController.ensureContactIds({
uuid, uuid,
@ -1306,33 +1227,26 @@ export class ConversationModel extends window.Backbone
// Clear typing indicator for a given contact if we receive a message from them // Clear typing indicator for a given contact if we receive a message from them
this.clearContactTypingTimer(typingToken); this.clearContactTypingTimer(typingToken);
this.addSingleMessage(message);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.debouncedUpdateLastMessage!(); this.debouncedUpdateLastMessage!();
} }
// For outgoing messages, we can call this directly. We're already loaded.
addSingleMessage(message: MessageModel): MessageModel { addSingleMessage(message: MessageModel): MessageModel {
const { id } = message;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const existing = this.messageCollection!.get(id);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const model = this.messageCollection!.add(message, { merge: true });
// TODO use MessageUpdater.setToExpire // TODO use MessageUpdater.setToExpire
model.setToExpire(); message.setToExpire();
if (!existing) { const { messagesAdded } = window.reduxActions.conversations;
const { messagesAdded } = window.reduxActions.conversations; const isNewMessage = true;
const isNewMessage = true; messagesAdded(
messagesAdded( this.id,
this.id, [message.getReduxData()],
[model.getReduxData()], isNewMessage,
isNewMessage, window.isActive()
window.isActive() );
);
}
return model; return message;
} }
// For incoming messages, they might arrive while we're in the middle of a bulk fetch // For incoming messages, they might arrive while we're in the middle of a bulk fetch
@ -2119,10 +2033,6 @@ export class ConversationModel extends window.Backbone
return true; return true;
} }
onMessageError(): void {
this.updateVerified();
}
async safeGetVerified(): Promise<number> { async safeGetVerified(): Promise<number> {
const promise = window.textsecure.storage.protocol.getVerified(this.id); const promise = window.textsecure.storage.protocol.getVerified(this.id);
return promise.catch( return promise.catch(
@ -3629,12 +3539,14 @@ export class ConversationModel extends window.Backbone
if (isDirectConversation(this.attributes)) { if (isDirectConversation(this.attributes)) {
messageWithSchema.destination = destination; messageWithSchema.destination = destination;
} }
const attributes: MessageModel = { const attributes: MessageAttributesType = {
...messageWithSchema, ...messageWithSchema,
id: window.getGuid(), id: window.getGuid(),
}; };
const model = this.addSingleMessage(attributes); const model = this.addSingleMessage(
new window.Whisper.Message(attributes)
);
if (sticker) { if (sticker) {
await addStickerPackReference(model.id, sticker.packId); await addStickerPackReference(model.id, sticker.packId);
} }
@ -4205,16 +4117,17 @@ export class ConversationModel extends window.Backbone
return message; return message;
} }
async addMessageHistoryDisclaimer(): Promise<MessageModel> { async addMessageHistoryDisclaimer(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const lastMessage = this.messageCollection!.last();
if (lastMessage && lastMessage.get('type') === 'message-history-unsynced') {
// We do not need another message history disclaimer
return lastMessage;
}
const timestamp = Date.now(); const timestamp = Date.now();
if (this.hasAddedHistoryDisclaimer) {
window.log.warn(
`addMessageHistoryDisclaimer/${this.idForLogging()}: Refusing to add another this session`
);
return;
}
this.hasAddedHistoryDisclaimer = true;
const model = new window.Whisper.Message(({ const model = new window.Whisper.Message(({
type: 'message-history-unsynced', type: 'message-history-unsynced',
// Even though this isn't reflected to the user, we want to place the last seen // Even though this isn't reflected to the user, we want to place the last seen
@ -4238,8 +4151,6 @@ export class ConversationModel extends window.Backbone
const message = window.MessageController.register(id, model); const message = window.MessageController.register(id, model);
this.addSingleMessage(message); this.addSingleMessage(message);
return message;
} }
isSearchable(): boolean { isSearchable(): boolean {
@ -4816,9 +4727,6 @@ export class ConversationModel extends window.Backbone
} }
async destroyMessages(): Promise<void> { async destroyMessages(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.messageCollection!.reset([]);
this.set({ this.set({
lastMessage: null, lastMessage: null,
timestamp: null, timestamp: null,

View file

@ -284,7 +284,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
this.on('change:expirationStartTimestamp', this.setToExpire); this.on('change:expirationStartTimestamp', this.setToExpire);
this.on('change:expireTimer', this.setToExpire); this.on('change:expireTimer', this.setToExpire);
this.on('unload', this.unload); this.on('unload', this.unload);
this.on('expired', this.onExpired);
this.setToExpire(); this.setToExpire();
this.on('change', this.notifyRedux); this.on('change', this.notifyRedux);
@ -517,9 +516,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
errors: errorsForContact, errors: errorsForContact,
isOutgoingKeyError, isOutgoingKeyError,
isUnidentifiedDelivery, isUnidentifiedDelivery,
onSendAnyway: () =>
this.trigger('force-send', { contactId: id, messageId: this.id }),
onShowSafetyNumber: () => this.trigger('show-identity', id),
}; };
} }
); );
@ -1797,6 +1793,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
const { messageDeleted } = window.reduxActions.conversations; const { messageDeleted } = window.reduxActions.conversations;
messageDeleted(this.id, this.get('conversationId')); messageDeleted(this.id, this.get('conversationId'));
this.getConversation()?.debouncedUpdateLastMessage?.();
window.MessageController.unregister(this.id); window.MessageController.unregister(this.id);
this.unload(); this.unload();
await this.deleteData(); await this.deleteData();
@ -1953,7 +1952,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
preview: [], preview: [],
...additionalProperties, ...additionalProperties,
}); });
this.trigger('content-changed'); this.getConversation()?.debouncedUpdateLastMessage?.();
if (shouldPersist) { if (shouldPersist) {
await window.Signal.Data.saveMessage(this.attributes, { await window.Signal.Data.saveMessage(this.attributes, {
@ -2625,10 +2624,17 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
async send( async send(
promise: Promise<CallbackResultType | void | null> promise: Promise<CallbackResultType | void | null>
): Promise<void | Array<void>> { ): Promise<void | Array<void>> {
this.trigger('pending'); const conversation = this.getConversation();
const updateLeftPane = conversation?.debouncedUpdateLastMessage;
if (updateLeftPane) {
updateLeftPane();
}
return (promise as Promise<CallbackResultType>) return (promise as Promise<CallbackResultType>)
.then(async result => { .then(async result => {
this.trigger('done'); if (updateLeftPane) {
updateLeftPane();
}
// This is used by sendSyncMessage, then set to null // This is used by sendSyncMessage, then set to null
if (result.dataMessage) { if (result.dataMessage) {
@ -2652,11 +2658,15 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}); });
} }
this.trigger('sent', this); if (updateLeftPane) {
updateLeftPane();
}
this.sendSyncMessage(); this.sendSyncMessage();
}) })
.catch((result: CustomError | CallbackResultType) => { .catch((result: CustomError | CallbackResultType) => {
this.trigger('done'); if (updateLeftPane) {
updateLeftPane();
}
if ('dataMessage' in result && result.dataMessage) { if ('dataMessage' in result && result.dataMessage) {
this.set({ dataMessage: result.dataMessage }); this.set({ dataMessage: result.dataMessage });
@ -2756,7 +2766,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
); );
} }
this.trigger('send-error', this.get('errors')); if (updateLeftPane) {
updateLeftPane();
}
return Promise.all(promises); return Promise.all(promises);
}); });
@ -2820,6 +2832,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const conv = this.getConversation()!; const conv = this.getConversation()!;
this.set({ dataMessage }); this.set({ dataMessage });
const updateLeftPane = conv?.debouncedUpdateLastMessage;
try { try {
this.set({ this.set({
// These are the same as a normal send() // These are the same as a normal send()
@ -2846,13 +2860,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
await window.Signal.Data.saveMessage(this.attributes, { await window.Signal.Data.saveMessage(this.attributes, {
Message: window.Whisper.Message, Message: window.Whisper.Message,
}); });
this.trigger('done');
const errors = this.get('errors'); if (updateLeftPane) {
if (errors) { updateLeftPane();
this.trigger('send-error', errors);
} else {
this.trigger('sent');
} }
} }
} }

View file

@ -27,6 +27,9 @@ export type OwnProps = {
message: MessagePropsDataType; message: MessagePropsDataType;
receivedAt: number; receivedAt: number;
sentAt: number; sentAt: number;
sendAnyway: (contactId: string, messageId: string) => unknown;
showSafetyNumber: (contactId: string) => void;
} & Pick< } & Pick<
MessageDetailProps, MessageDetailProps,
| 'clearSelectedMessage' | 'clearSelectedMessage'
@ -60,6 +63,9 @@ const mapStateToProps = (
receivedAt, receivedAt,
sentAt, sentAt,
sendAnyway,
showSafetyNumber,
clearSelectedMessage, clearSelectedMessage,
deleteMessage, deleteMessage,
deleteMessageForEveryone, deleteMessageForEveryone,
@ -99,6 +105,9 @@ const mapStateToProps = (
i18n: getIntl(state), i18n: getIntl(state),
interactionMode: getInteractionMode(state), interactionMode: getInteractionMode(state),
sendAnyway,
showSafetyNumber,
clearSelectedMessage, clearSelectedMessage,
deleteMessage, deleteMessage,
deleteMessageForEveryone, deleteMessageForEveryone,

View file

@ -75,42 +75,6 @@ describe('Message', () => {
assert.isTrue(message.get('sent')); assert.isTrue(message.get('sent'));
}); });
it("triggers the 'done' event on success", async () => {
const message = createMessage({ type: 'outgoing', source });
let callCount = 0;
message.on('done', () => {
callCount += 1;
});
await message.send(Promise.resolve({}));
assert.strictEqual(callCount, 1);
});
it("triggers the 'sent' event on success", async () => {
const message = createMessage({ type: 'outgoing', source });
const listener = sinon.spy();
message.on('sent', listener);
await message.send(Promise.resolve({}));
sinon.assert.calledOnce(listener);
sinon.assert.calledWith(listener, message);
});
it("triggers the 'done' event on failure", async () => {
const message = createMessage({ type: 'outgoing', source });
const listener = sinon.spy();
message.on('done', listener);
await message.send(Promise.reject(new Error('something went wrong!')));
sinon.assert.calledOnce(listener);
});
it('saves errors from promise rejections with errors', async () => { it('saves errors from promise rejections with errors', async () => {
const message = createMessage({ type: 'outgoing', source }); const message = createMessage({ type: 'outgoing', source });

View file

@ -0,0 +1,113 @@
// Copyright 2019-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { MessageModel } from '../models/messages';
const SECOND = 1000;
const MINUTE = SECOND * 60;
const FIVE_MINUTES = MINUTE * 5;
const HOUR = MINUTE * 60;
type LookupItemType = {
timestamp: number;
message: MessageModel;
};
type LookupType = Record<string, LookupItemType>;
export class MessageController {
private messageLookup: LookupType = Object.create(null);
private msgIDsBySender = new Map<string, string>();
private msgIDsBySentAt = new Map<number, string>();
static install(): MessageController {
const instance = new MessageController();
window.MessageController = instance;
instance.startCleanupInterval();
return instance;
}
register(id: string, message: MessageModel): MessageModel {
if (!id || !message) {
return message;
}
const existing = this.messageLookup[id];
if (existing) {
this.messageLookup[id] = {
message: existing.message,
timestamp: Date.now(),
};
return existing.message;
}
this.messageLookup[id] = {
message,
timestamp: Date.now(),
};
this.msgIDsBySentAt.set(message.get('sent_at'), id);
this.msgIDsBySender.set(message.getSenderIdentifier(), id);
return message;
}
unregister(id: string): void {
const { message } = this.messageLookup[id] || {};
if (message) {
this.msgIDsBySender.delete(message.getSenderIdentifier());
this.msgIDsBySentAt.delete(message.get('sent_at'));
}
delete this.messageLookup[id];
}
cleanup(): void {
const messages = Object.values(this.messageLookup);
const now = Date.now();
for (let i = 0, max = messages.length; i < max; i += 1) {
const { message, timestamp } = messages[i];
const conversation = message.getConversation();
const state = window.reduxStore.getState();
const selectedId = state?.conversations?.selectedConversationId;
const inActiveConversation =
conversation && selectedId && conversation.id === selectedId;
if (now - timestamp > FIVE_MINUTES && !inActiveConversation) {
this.unregister(message.id);
}
}
}
getById(id: string): MessageModel | undefined {
const existing = this.messageLookup[id];
return existing && existing.message ? existing.message : undefined;
}
findBySentAt(sentAt: number): MessageModel | undefined {
const id = this.msgIDsBySentAt.get(sentAt);
if (!id) {
return undefined;
}
return this.getById(id);
}
findBySender(sender: string): MessageModel | undefined {
const id = this.msgIDsBySender.get(sender);
if (!id) {
return undefined;
}
return this.getById(id);
}
_get(): LookupType {
return this.messageLookup;
}
startCleanupInterval(): NodeJS.Timeout | number {
return setInterval(this.cleanup.bind(this), HOUR);
}
}

View file

@ -38,6 +38,7 @@ import { postLinkExperience } from './postLinkExperience';
import { sendToGroup, sendContentMessageToGroup } from './sendToGroup'; import { sendToGroup, sendContentMessageToGroup } from './sendToGroup';
import { RetryPlaceholders } from './retryPlaceholders'; import { RetryPlaceholders } from './retryPlaceholders';
import * as expirationTimer from './expirationTimer'; import * as expirationTimer from './expirationTimer';
import { MessageController } from './MessageController';
export { export {
GoogleChrome, GoogleChrome,
@ -60,6 +61,7 @@ export {
longRunningTaskWrapper, longRunningTaskWrapper,
makeLookup, makeLookup,
mapToSupportLocale, mapToSupportLocale,
MessageController,
missingCaseError, missingCaseError,
parseRemoteClientExpiration, parseRemoteClientExpiration,
postLinkExperience, postLinkExperience,

View file

@ -5,7 +5,10 @@
import { AttachmentType } from '../types/Attachment'; import { AttachmentType } from '../types/Attachment';
import { ConversationModel } from '../models/conversations'; import { ConversationModel } from '../models/conversations';
import { GroupV2PendingMemberType } from '../model-types.d'; import {
GroupV2PendingMemberType,
MessageModelCollectionType,
} from '../model-types.d';
import { LinkPreviewType } from '../types/message/LinkPreviews'; import { LinkPreviewType } from '../types/message/LinkPreviews';
import { MediaItemType } from '../components/LightboxGallery'; import { MediaItemType } from '../components/LightboxGallery';
import { MessageModel } from '../models/messages'; import { MessageModel } from '../models/messages';
@ -356,40 +359,38 @@ Whisper.ConversationView = Whisper.View.extend({
const { model }: { model: ConversationModel } = this; const { model }: { model: ConversationModel } = this;
// Events on Conversation model // Events on Conversation model
this.listenTo(model, 'destroy', this.stopListening); this.listenTo(this.model, 'destroy', this.stopListening);
this.listenTo(model, 'change:verified', this.onVerifiedChange); this.listenTo(this.model, 'change:verified', this.onVerifiedChange);
this.listenTo(model, 'newmessage', this.addMessage); this.listenTo(this.model, 'newmessage', this.lazyUpdateVerified);
this.listenTo(model, 'opened', this.onOpened);
this.listenTo(model, 'backgrounded', this.resetEmojiResults); // These are triggered by InboxView
this.listenTo(model, 'scroll-to-message', this.scrollToMessage); this.listenTo(this.model, 'opened', this.onOpened);
this.listenTo(model, 'unload', (reason: string) => this.listenTo(this.model, 'scroll-to-message', this.scrollToMessage);
this.listenTo(this.model, 'unload', (reason: string) =>
this.unload(`model trigger - ${reason}`) this.unload(`model trigger - ${reason}`)
); );
this.listenTo(model, 'focus-composer', this.focusMessageField);
this.listenTo(model, 'open-all-media', this.showAllMedia); // These are triggered by background.ts for keyboard handling
this.listenTo(model, 'begin-recording', this.captureAudio); this.listenTo(this.model, 'focus-composer', this.focusMessageField);
this.listenTo(model, 'attach-file', this.onChooseAttachment); this.listenTo(this.model, 'open-all-media', this.showAllMedia);
this.listenTo(model, 'escape-pressed', this.resetPanel); this.listenTo(this.model, 'begin-recording', this.captureAudio);
this.listenTo(model, 'show-message-details', this.showMessageDetail); this.listenTo(this.model, 'attach-file', this.onChooseAttachment);
this.listenTo(model, 'show-contact-modal', this.showContactModal); this.listenTo(this.model, 'escape-pressed', this.resetPanel);
this.listenTo(model, 'toggle-reply', (messageId: string | undefined) => { this.listenTo(this.model, 'show-message-details', this.showMessageDetail);
const target = this.quote || !messageId ? null : messageId; this.listenTo(this.model, 'show-contact-modal', this.showContactModal);
this.setQuoteMessage(target); this.listenTo(
}); this.model,
'toggle-reply',
(messageId: string | undefined) => {
const target = this.quote || !messageId ? null : messageId;
this.setQuoteMessage(target);
}
);
this.listenTo(model, 'save-attachment', this.downloadAttachmentWrapper); this.listenTo(model, 'save-attachment', this.downloadAttachmentWrapper);
this.listenTo(model, 'delete-message', this.deleteMessage); this.listenTo(model, 'delete-message', this.deleteMessage);
this.listenTo(model, 'remove-link-review', this.removeLinkPreview); this.listenTo(model, 'remove-link-review', this.removeLinkPreview);
this.listenTo(model, 'remove-all-draft-attachments', this.clearAttachments); this.listenTo(model, 'remove-all-draft-attachments', this.clearAttachments);
// Events on Message models - we still listen to these here because they
// can be emitted by the non-reduxified MessageDetail pane
this.listenTo(
model.messageCollection,
'show-identity',
this.showSafetyNumber
);
this.listenTo(model.messageCollection, 'force-send', this.forceSend);
this.lazyUpdateVerified = window._.debounce( this.lazyUpdateVerified = window._.debounce(
model.updateVerified.bind(model), model.updateVerified.bind(model),
1000 // one second 1000 // one second
@ -718,7 +719,6 @@ Whisper.ConversationView = Whisper.View.extend({
}, },
getMessageActions() { getMessageActions() {
const { model }: { model: ConversationModel } = this;
const reactToMessage = ( const reactToMessage = (
messageId: string, messageId: string,
reaction: { emoji: string; remove: boolean } reaction: { emoji: string; remove: boolean }
@ -750,17 +750,21 @@ Whisper.ConversationView = Whisper.View.extend({
this.showContactDetail(options); this.showContactDetail(options);
}; };
const kickOffAttachmentDownload = async (options: any) => { const kickOffAttachmentDownload = async (options: any) => {
if (!model.messageCollection) { const message = window.MessageController.getById(options.messageId);
throw new Error('Message collection does not exist'); if (!message) {
throw new Error(
`kickOffAttachmentDownload: Message ${options.messageId} missing!`
);
} }
const message = model.messageCollection.get(options.messageId);
await message.queueAttachmentDownloads(); await message.queueAttachmentDownloads();
}; };
const markAttachmentAsCorrupted = (options: AttachmentOptions) => { const markAttachmentAsCorrupted = (options: AttachmentOptions) => {
const message: MessageModel = this.model.messageCollection.get( const message = window.MessageController.getById(options.messageId);
options.messageId if (!message) {
); throw new Error(
assert(message, 'Message not found'); `markAttachmentAsCorrupted: Message ${options.messageId} missing!`
);
}
message.markAttachmentAsCorrupted(options.attachment); message.markAttachmentAsCorrupted(options.attachment);
}; };
const showVisualAttachment = (options: { const showVisualAttachment = (options: {
@ -784,6 +788,12 @@ Whisper.ConversationView = Whisper.View.extend({
const downloadNewVersion = () => { const downloadNewVersion = () => {
this.downloadNewVersion(); this.downloadNewVersion();
}; };
const sendAnyway = (contactId: string, messageId: string) => {
this.forceSend(contactId, messageId);
};
const showSafetyNumber = (contactId: string) => {
this.showSafetyNumber(contactId);
};
const showExpiredIncomingTapToViewToast = () => { const showExpiredIncomingTapToViewToast = () => {
this.showToast(Whisper.TapToViewExpiredIncomingToast); this.showToast(Whisper.TapToViewExpiredIncomingToast);
}; };
@ -805,8 +815,10 @@ Whisper.ConversationView = Whisper.View.extend({
reactToMessage, reactToMessage,
replyToMessage, replyToMessage,
retrySend, retrySend,
sendAnyway,
showContactDetail, showContactDetail,
showContactModal, showContactModal,
showSafetyNumber,
showExpiredIncomingTapToViewToast, showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
showForwardMessageModal, showForwardMessageModal,
@ -895,12 +907,12 @@ Whisper.ConversationView = Whisper.View.extend({
} }
const cleaned = await this.cleanModels(models); const cleaned = await this.cleanModels(models);
this.model.messageCollection.add(cleaned);
const isNewMessage = false; const isNewMessage = false;
messagesAdded( messagesAdded(
id, id,
models.map(messageModel => messageModel.getReduxData()), cleaned.map((messageModel: MessageModel) =>
messageModel.getReduxData()
),
isNewMessage, isNewMessage,
window.isActive() window.isActive()
); );
@ -950,12 +962,12 @@ Whisper.ConversationView = Whisper.View.extend({
} }
const cleaned = await this.cleanModels(models); const cleaned = await this.cleanModels(models);
this.model.messageCollection.add(cleaned);
const isNewMessage = false; const isNewMessage = false;
messagesAdded( messagesAdded(
id, id,
models.map(messageModel => messageModel.getReduxData()), cleaned.map((messageModel: MessageModel) =>
messageModel.getReduxData()
),
isNewMessage, isNewMessage,
window.isActive() window.isActive()
); );
@ -1078,7 +1090,9 @@ Whisper.ConversationView = Whisper.View.extend({
toast.render(); toast.render();
}, },
async cleanModels(collection: any) { async cleanModels(
collection: MessageModelCollectionType | Array<MessageModel>
): Promise<Array<MessageModel>> {
const result = collection const result = collection
.filter((message: any) => Boolean(message.id)) .filter((message: any) => Boolean(message.id))
.map((message: any) => .map((message: any) =>
@ -1121,7 +1135,9 @@ Whisper.ConversationView = Whisper.View.extend({
throw new Error(`scrollToMessage: failed to load message ${messageId}`); throw new Error(`scrollToMessage: failed to load message ${messageId}`);
} }
if (this.model.messageCollection.get(messageId)) { const isInMemory = Boolean(window.MessageController.getById(messageId));
if (isInMemory) {
const { scrollToMessage } = window.reduxActions.conversations; const { scrollToMessage } = window.reduxActions.conversations;
scrollToMessage(model.id, messageId); scrollToMessage(model.id, messageId);
return; return;
@ -1189,13 +1205,14 @@ Whisper.ConversationView = Whisper.View.extend({
const all = [...older.models, message, ...newer.models]; const all = [...older.models, message, ...newer.models];
const cleaned: Array<MessageModel> = await this.cleanModels(all); const cleaned: Array<MessageModel> = await this.cleanModels(all);
this.model.messageCollection.reset(cleaned);
const scrollToMessageId = const scrollToMessageId =
options && options.disableScroll ? undefined : messageId; options && options.disableScroll ? undefined : messageId;
messagesReset( messagesReset(
conversationId, conversationId,
cleaned.map(messageModel => messageModel.getReduxData()), cleaned.map((messageModel: MessageModel) =>
messageModel.getReduxData()
),
metrics, metrics,
scrollToMessageId scrollToMessageId
); );
@ -1266,12 +1283,6 @@ Whisper.ConversationView = Whisper.View.extend({
}); });
const cleaned: Array<MessageModel> = await this.cleanModels(messages); const cleaned: Array<MessageModel> = await this.cleanModels(messages);
assert(
model.messageCollection,
'loadNewestMessages: model must have messageCollection'
);
model.messageCollection.reset(cleaned);
const scrollToMessageId = const scrollToMessageId =
setFocus && metrics.newest ? metrics.newest.id : undefined; setFocus && metrics.newest ? metrics.newest.id : undefined;
@ -1283,7 +1294,9 @@ Whisper.ConversationView = Whisper.View.extend({
const unboundedFetch = true; const unboundedFetch = true;
messagesReset( messagesReset(
conversationId, conversationId,
cleaned.map(messageModel => messageModel.getReduxData()), cleaned.map((messageModel: MessageModel) =>
messageModel.getReduxData()
),
metrics, metrics,
scrollToMessageId, scrollToMessageId,
unboundedFetch unboundedFetch
@ -1453,8 +1466,6 @@ Whisper.ConversationView = Whisper.View.extend({
} }
this.remove(); this.remove();
this.model.messageCollection.reset([]);
}, },
navigateTo(url: any) { navigateTo(url: any) {
@ -2303,19 +2314,17 @@ Whisper.ConversationView = Whisper.View.extend({
}, },
async retrySend(messageId: string) { async retrySend(messageId: string) {
const message = this.model.messageCollection.get(messageId); const message = window.MessageController.getById(messageId);
if (!message) { if (!message) {
throw new Error(`retrySend: Did not find message for id ${messageId}`); throw new Error(`retrySend: Message ${messageId} missing!`);
} }
await message.retrySend(); await message.retrySend();
}, },
showForwardMessageModal(messageId: string) { showForwardMessageModal(messageId: string) {
const message = this.model.messageCollection.get(messageId); const message = window.MessageController.getById(messageId);
if (!message) { if (!message) {
throw new Error( throw new Error(`showForwardMessageModal: Message ${messageId} missing!`);
`showForwardMessageModal: Did not find message for id ${messageId}`
);
} }
const attachments = message.getAttachmentsForMessage(); const attachments = message.getAttachmentsForMessage();
@ -2658,7 +2667,7 @@ Whisper.ConversationView = Whisper.View.extend({
Component: window.Signal.Components.MediaGallery, Component: window.Signal.Components.MediaGallery,
props: await getProps(), props: await getProps(),
onClose: () => { onClose: () => {
this.stopListening(model.messageCollection, 'remove', update); unsubscribe();
}, },
}); });
view.headerTitle = window.i18n('allMedia'); view.headerTitle = window.i18n('allMedia');
@ -2667,7 +2676,28 @@ Whisper.ConversationView = Whisper.View.extend({
view.update(await getProps()); view.update(await getProps());
}; };
this.listenTo(model.messageCollection, 'remove', update); function getMessageIds(): Array<string | undefined> | undefined {
const state = window.reduxStore.getState();
const byConversation = state?.conversations?.messagesByConversation;
const messages = byConversation && byConversation[conversationId];
if (!messages || !messages.messageIds) {
return undefined;
}
return messages.messageIds;
}
// Detect message changes in the current conversation
let previousMessageList: Array<string | undefined> | undefined;
previousMessageList = getMessageIds();
const unsubscribe = window.reduxStore.subscribe(() => {
const currentMessageList = getMessageIds();
if (currentMessageList !== previousMessageList) {
update();
previousMessageList = currentMessageList;
}
});
this.listenBack(view); this.listenBack(view);
}, },
@ -2696,25 +2726,12 @@ Whisper.ConversationView = Whisper.View.extend({
this.compositionApi.current.resetEmojiResults(false); this.compositionApi.current.resetEmojiResults(false);
}, },
async addMessage(message: MessageModel) {
const { model }: { model: ConversationModel } = this;
// This is debounced, so it won't hit the database too often.
this.lazyUpdateVerified();
// We do this here because we don't want convo.messageCollection to have
// anything in it unless it has an associated view. This is so, when we
// fetch on open, it's clean.
model.addIncomingMessage(message);
},
async showMembers( async showMembers(
_e: unknown, _e: unknown,
providedMembers: void | Backbone.Collection<ConversationModel>, providedMembers: void | Backbone.Collection<ConversationModel>,
options: any = {} options: any = {}
) { ) {
const { model }: { model: ConversationModel } = this; const { model }: { model: ConversationModel } = this;
window._.defaults(options, { needVerify: false }); window._.defaults(options, { needVerify: false });
let contactCollection = providedMembers || model.contactCollection; let contactCollection = providedMembers || model.contactCollection;
@ -2746,9 +2763,9 @@ Whisper.ConversationView = Whisper.View.extend({
}: Readonly<{ contactId: string; messageId: string }>) { }: Readonly<{ contactId: string; messageId: string }>) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const contact = window.ConversationController.get(contactId)!; const contact = window.ConversationController.get(contactId)!;
const message = this.model.messageCollection.get(messageId); const message = window.MessageController.getById(messageId);
if (!message) { if (!message) {
throw new Error(`forceSend: Did not find message for id ${messageId}`); throw new Error(`forceSend: Message ${messageId} missing!`);
} }
window.showConfirmationDialog({ window.showConfirmationDialog({
@ -2770,7 +2787,14 @@ Whisper.ConversationView = Whisper.View.extend({
contact.setApproved(); contact.setApproved();
} }
message.resend(contact.getSendTarget()); const sendTarget = contact.getSendTarget();
if (!sendTarget) {
throw new Error(
`forceSend: Contact ${contact.idForLogging()} had no sendTarget!`
);
}
message.resend(sendTarget);
}, },
}); });
}, },
@ -2795,10 +2819,10 @@ Whisper.ConversationView = Whisper.View.extend({
}, },
downloadAttachmentWrapper(messageId: string) { downloadAttachmentWrapper(messageId: string) {
const message = this.model.messageCollection.get(messageId); const message = window.MessageController.getById(messageId);
if (!message) { if (!message) {
throw new Error( throw new Error(
`downloadAttachmentWrapper: Did not find message for id ${messageId}` `downloadAttachmentWrapper: Message ${messageId} missing!`
); );
} }
@ -2842,11 +2866,9 @@ Whisper.ConversationView = Whisper.View.extend({
}, },
async displayTapToViewMessage(messageId: string) { async displayTapToViewMessage(messageId: string) {
const message = this.model.messageCollection.get(messageId); const message = window.MessageController.getById(messageId);
if (!message) { if (!message) {
throw new Error( throw new Error(`displayTapToViewMessage: Message ${messageId} missing!`);
`displayTapToViewMessage: Did not find message for id ${messageId}`
);
} }
if (!message.isTapToView()) { if (!message.isTapToView()) {
@ -2919,11 +2941,9 @@ Whisper.ConversationView = Whisper.View.extend({
}, },
deleteMessage(messageId: string) { deleteMessage(messageId: string) {
const message = this.model.messageCollection.get(messageId); const message = window.MessageController.getById(messageId);
if (!message) { if (!message) {
throw new Error( throw new Error(`deleteMessage: Message ${messageId} missing!`);
`deleteMessage: Did not find message for id ${messageId}`
);
} }
window.showConfirmationDialog({ window.showConfirmationDialog({
@ -2934,8 +2954,7 @@ Whisper.ConversationView = Whisper.View.extend({
window.Signal.Data.removeMessage(message.id, { window.Signal.Data.removeMessage(message.id, {
Message: Whisper.Message, Message: Whisper.Message,
}); });
message.trigger('unload'); message.cleanup();
this.model.messageCollection.remove(message.id);
if (message.isOutgoing()) { if (message.isOutgoing()) {
this.model.decrementSentMessageCount(); this.model.decrementSentMessageCount();
} else { } else {
@ -2947,10 +2966,10 @@ Whisper.ConversationView = Whisper.View.extend({
}, },
deleteMessageForEveryone(messageId: string) { deleteMessageForEveryone(messageId: string) {
const message = this.model.messageCollection.get(messageId); const message = window.MessageController.getById(messageId);
if (!message) { if (!message) {
throw new Error( throw new Error(
`deleteMessageForEveryone: Did not find message for id ${messageId}` `deleteMessageForEveryone: Message ${messageId} missing!`
); );
} }
@ -3038,9 +3057,9 @@ Whisper.ConversationView = Whisper.View.extend({
messageId: string; messageId: string;
showSingle?: boolean; showSingle?: boolean;
}) { }) {
const message = this.model.messageCollection.get(messageId); const message = window.MessageController.getById(messageId);
if (!message) { if (!message) {
throw new Error(`showLightbox: did not find message for id ${messageId}`); throw new Error(`showLightbox: Message ${messageId} missing!`);
} }
const sticker = message.get('sticker'); const sticker = message.get('sticker');
if (sticker) { if (sticker) {
@ -3365,12 +3384,9 @@ Whisper.ConversationView = Whisper.View.extend({
}, },
showMessageDetail(messageId: string) { showMessageDetail(messageId: string) {
const { model }: { model: ConversationModel } = this; const message = window.MessageController.getById(messageId);
const message = model.messageCollection?.get(messageId);
if (!message) { if (!message) {
throw new Error( throw new Error(`showMessageDetail: Message ${messageId} missing!`);
`showMessageDetail: Did not find message for id ${messageId}`
);
} }
if (!message.isNormalBubble()) { if (!message.isNormalBubble()) {

15
ts/window.d.ts vendored
View file

@ -3,6 +3,7 @@
// Captures the globals put in place by preload.js, background.js and others // Captures the globals put in place by preload.js, background.js and others
import { DeepPartial, Store } from 'redux';
import * as Backbone from 'backbone'; import * as Backbone from 'backbone';
import * as Underscore from 'underscore'; import * as Underscore from 'underscore';
import moment from 'moment'; import moment from 'moment';
@ -113,6 +114,8 @@ import * as synchronousCrypto from './util/synchronousCrypto';
import { SocketStatus } from './types/SocketStatus'; import { SocketStatus } from './types/SocketStatus';
import SyncRequest from './textsecure/SyncRequest'; import SyncRequest from './textsecure/SyncRequest';
import { ConversationColorType, CustomColorType } from './types/Colors'; import { ConversationColorType, CustomColorType } from './types/Colors';
import { MessageController } from './util/MessageController';
import { StateType } from './state/reducer';
export { Long } from 'long'; export { Long } from 'long';
@ -235,7 +238,7 @@ declare global {
platform: string; platform: string;
preloadedImages: Array<WhatIsThis>; preloadedImages: Array<WhatIsThis>;
reduxActions: ReduxActions; reduxActions: ReduxActions;
reduxStore: WhatIsThis; reduxStore: Store<StateType>;
registerForActive: (handler: () => void) => void; registerForActive: (handler: () => void) => void;
restart: () => void; restart: () => void;
setImmediate: typeof setImmediate; setImmediate: typeof setImmediate;
@ -537,7 +540,7 @@ declare global {
ConversationController: ConversationController; ConversationController: ConversationController;
Events: WhatIsThis; Events: WhatIsThis;
MessageController: MessageControllerType; MessageController: MessageController;
SignalProtocolStore: typeof SignalProtocolStore; SignalProtocolStore: typeof SignalProtocolStore;
WebAPI: WebAPIConnectType; WebAPI: WebAPIConnectType;
Whisper: WhisperType; Whisper: WhisperType;
@ -604,14 +607,6 @@ export type DCodeIOType = {
ProtoBuf: WhatIsThis; ProtoBuf: WhatIsThis;
}; };
type MessageControllerType = {
findBySender: (sender: string) => MessageModel | null;
findBySentAt: (sentAt: number) => MessageModel | null;
getById: (id: string) => MessageModel | undefined;
register: (id: string, model: MessageModel) => MessageModel;
unregister: (id: string) => void;
};
export class CertificateValidatorType { export class CertificateValidatorType {
validate: (cerficate: any, certificateTime: number) => Promise<void>; validate: (cerficate: any, certificateTime: number) => Promise<void>;
} }