Replace MessageController with MessageCache

This commit is contained in:
Josh Perez 2023-10-03 20:12:57 -04:00 committed by GitHub
parent ba1a8aad09
commit 7d35216fda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 2237 additions and 1229 deletions

View File

@ -20,6 +20,16 @@ const rules = {
'brace-style': ['error', '1tbs', { allowSingleLine: false }],
curly: ['error', 'all'],
// Immer support
'no-param-reassign': [
'error',
{
props: true,
ignorePropertyModificationsForRegex: ['^draft'],
ignorePropertyModificationsFor: ['acc', 'ctx', 'context'],
},
],
// Always use === and !== except when directly comparing to null
// (which only will equal null or undefined)
eqeqeq: ['error', 'always', { null: 'never' }],

View File

@ -776,8 +776,9 @@ async function createWindow() {
}
const startInTray =
isTestEnvironment(getEnvironment()) ||
(await systemTraySettingCache.get()) ===
SystemTraySetting.MinimizeToAndStartInSystemTray;
SystemTraySetting.MinimizeToAndStartInSystemTray;
const visibleOnAnyScreen = some(screen.getAllDisplays(), display => {
if (
@ -2882,6 +2883,10 @@ async function showStickerCreatorWindow() {
}
if (isTestEnvironment(getEnvironment())) {
ipc.handle('ci:test-electron:debug', async (_event, info) => {
process.stdout.write(`ci:test-electron:debug=${JSON.stringify(info)}\n`);
});
ipc.handle('ci:test-electron:done', async (_event, info) => {
if (!process.env.TEST_QUIT_ON_COMPLETE) {
return;

View File

@ -1,33 +0,0 @@
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// For reference: https://github.com/airbnb/javascript
module.exports = {
env: {
mocha: true,
browser: true,
},
globals: {
check: true,
gen: true,
},
parserOptions: {
sourceType: 'module',
},
rules: {
// We still get the value of this rule, it just allows for dev deps
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true,
},
],
// We want to keep each test structured the same, even if its contents are tiny
'arrow-body-style': 'off',
},
};

View File

@ -1,8 +1,6 @@
// Copyright 2014 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global Whisper, _, Backbone */
/*
* global helpers for tests
*/
@ -18,20 +16,21 @@ function deleteIndexedDB() {
});
}
window.Events = {
getThemeSetting: () => 'light',
};
/* Delete the database before running any tests */
before(async () => {
window.testUtilities.installMessageController();
await window.testUtilities.initialize();
await deleteIndexedDB();
await window.testUtilities.initializeMessageCounter();
await window.Signal.Data.removeAll();
await window.storage.fetch();
});
window.textsecure.storage.protocol = window.getSignalProtocolStore();
window.testUtilities.prepareTests();
delete window.testUtilities.prepareTests;
window.textsecure.storage.protocol = window.getSignalProtocolStore();
!(function () {
const passed = [];

View File

@ -120,7 +120,12 @@ export function getCI(deviceName: string): CIType {
[sentAt]
);
return messages.map(
m => window.MessageController.register(m.id, m).attributes
m =>
window.MessageCache.__DEPRECATED$register(
m.id,
m,
'CI.getMessagesBySentAt'
).attributes
);
}

View File

@ -12,7 +12,6 @@ import type {
ConversationRenderInfoType,
} from './model-types.d';
import type { ConversationModel } from './models/conversations';
import type { MessageModel } from './models/messages';
import dataInterface from './sql/Client';
import * as log from './logging/log';
@ -1127,13 +1126,11 @@ export class ConversationController {
});
}
log.warn(`${logId}: Update cached messages in MessageController`);
window.MessageController.update((message: MessageModel) => {
if (message.get('conversationId') === obsoleteId) {
message.set({ conversationId: currentId });
}
log.warn(`${logId}: Update cached messages in MessageCache`);
window.MessageCache.replaceAllObsoleteConversationIds({
conversationId: currentId,
obsoleteId,
});
log.warn(`${logId}: Update messages table`);
await migrateConversationMessages(obsoleteId, currentId);

View File

@ -3,7 +3,6 @@
import { webFrame } from 'electron';
import { isNumber, throttle, groupBy } from 'lodash';
import { bindActionCreators } from 'redux';
import { render } from 'react-dom';
import { batch as batchDispatch } from 'react-redux';
import PQueue from 'p-queue';
@ -108,7 +107,6 @@ import { AppViewType } from './state/ducks/app';
import type { BadgesStateType } from './state/ducks/badges';
import { areAnyCallsActiveOrRinging } from './state/selectors/calling';
import { badgeImageFileDownloader } from './badges/badgeImageFileDownloader';
import { actionCreators } from './state/actions';
import * as Deletes from './messageModifiers/Deletes';
import type { EditAttributesType } from './messageModifiers/Edits';
import * as Edits from './messageModifiers/Edits';
@ -154,7 +152,6 @@ import { startInteractionMode } from './services/InteractionMode';
import type { MainWindowStatsType } from './windows/context';
import { ReactionSource } from './reactions/ReactionSource';
import { singleProtoJobQueue } from './jobs/singleProtoJobQueue';
import { getInitialState } from './state/getInitialState';
import {
conversationJobQueue,
conversationQueueJobEnum,
@ -164,6 +161,7 @@ import MessageSender from './textsecure/SendMessage';
import type AccountManager from './textsecure/AccountManager';
import { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate';
import { flushAttachmentDownloadQueue } from './util/attachmentDownloadQueue';
import { initializeRedux } from './state/initializeRedux';
import { StartupQueue } from './util/StartupQueue';
import { showConfirmationDialog } from './util/showConfirmationDialog';
import { onCallEventSync } from './util/onCallEventSync';
@ -1151,7 +1149,7 @@ export async function startApp(): Promise<void> {
Errors.toLogFormat(error)
);
} finally {
initializeRedux({ mainWindowStats, menuOptions });
setupAppState({ mainWindowStats, menuOptions });
drop(start());
window.Signal.Services.initializeNetworkObserver(
window.reduxActions.network
@ -1173,89 +1171,24 @@ export async function startApp(): Promise<void> {
}
});
function initializeRedux({
function setupAppState({
mainWindowStats,
menuOptions,
}: {
mainWindowStats: MainWindowStatsType;
menuOptions: MenuOptionsType;
}) {
// Here we set up a full redux store with initial state for our LeftPane Root
const convoCollection = window.getConversations();
const initialState = getInitialState({
badges: initialBadgesState,
initializeRedux({
callsHistory: getCallsHistoryForRedux(),
initialBadgesState,
mainWindowStats,
menuOptions,
stories: getStoriesForRedux(),
storyDistributionLists: getDistributionListsForRedux(),
callsHistory: getCallsHistoryForRedux(),
});
const store = window.Signal.State.createStore(initialState);
window.reduxStore = store;
// Binding these actions to our redux store and exposing them allows us to update
// redux when things change in the backbone world.
window.reduxActions = {
accounts: bindActionCreators(actionCreators.accounts, store.dispatch),
app: bindActionCreators(actionCreators.app, store.dispatch),
audioPlayer: bindActionCreators(
actionCreators.audioPlayer,
store.dispatch
),
audioRecorder: bindActionCreators(
actionCreators.audioRecorder,
store.dispatch
),
badges: bindActionCreators(actionCreators.badges, store.dispatch),
callHistory: bindActionCreators(
actionCreators.callHistory,
store.dispatch
),
calling: bindActionCreators(actionCreators.calling, store.dispatch),
composer: bindActionCreators(actionCreators.composer, store.dispatch),
conversations: bindActionCreators(
actionCreators.conversations,
store.dispatch
),
crashReports: bindActionCreators(
actionCreators.crashReports,
store.dispatch
),
inbox: bindActionCreators(actionCreators.inbox, store.dispatch),
emojis: bindActionCreators(actionCreators.emojis, store.dispatch),
expiration: bindActionCreators(actionCreators.expiration, store.dispatch),
globalModals: bindActionCreators(
actionCreators.globalModals,
store.dispatch
),
items: bindActionCreators(actionCreators.items, store.dispatch),
lightbox: bindActionCreators(actionCreators.lightbox, store.dispatch),
linkPreviews: bindActionCreators(
actionCreators.linkPreviews,
store.dispatch
),
mediaGallery: bindActionCreators(
actionCreators.mediaGallery,
store.dispatch
),
network: bindActionCreators(actionCreators.network, store.dispatch),
safetyNumber: bindActionCreators(
actionCreators.safetyNumber,
store.dispatch
),
search: bindActionCreators(actionCreators.search, store.dispatch),
stickers: bindActionCreators(actionCreators.stickers, store.dispatch),
stories: bindActionCreators(actionCreators.stories, store.dispatch),
storyDistributionLists: bindActionCreators(
actionCreators.storyDistributionLists,
store.dispatch
),
toast: bindActionCreators(actionCreators.toast, store.dispatch),
updates: bindActionCreators(actionCreators.updates, store.dispatch),
user: bindActionCreators(actionCreators.user, store.dispatch),
username: bindActionCreators(actionCreators.username, store.dispatch),
};
// Here we set up a full redux store with initial state for our LeftPane Root
const convoCollection = window.getConversations();
const {
conversationAdded,

View File

@ -2008,8 +2008,12 @@ export async function createGroupV2(
forceSave: true,
ourAci,
});
const model = new window.Whisper.Message(createdTheGroupMessage);
window.MessageController.register(model.id, model);
let model = new window.Whisper.Message(createdTheGroupMessage);
model = window.MessageCache.__DEPRECATED$register(
model.id,
model,
'createGroupV2'
);
conversation.trigger('newmessage', model);
if (expireTimer) {
@ -3371,7 +3375,7 @@ async function appendChangeMessages(
let newMessages = 0;
for (const changeMessage of mergedMessages) {
const existing = window.MessageController.getById(changeMessage.id);
const existing = window.MessageCache.__DEPRECATED$getById(changeMessage.id);
// Update existing message
if (existing) {
@ -3383,8 +3387,12 @@ async function appendChangeMessages(
continue;
}
const model = new window.Whisper.Message(changeMessage);
window.MessageController.register(model.id, model);
let model = new window.Whisper.Message(changeMessage);
model = window.MessageCache.__DEPRECATED$register(
model.id,
model,
'appendChangeMessages'
);
conversation.trigger('newmessage', model);
newMessages += 1;
}

View File

@ -27,7 +27,7 @@ import { getUntrustedConversationServiceIds } from './getUntrustedConversationSe
import { handleMessageSend } from '../../util/handleMessageSend';
import { isConversationAccepted } from '../../util/isConversationAccepted';
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
import { getMessageById } from '../../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
import { isNotNil } from '../../util/isNotNil';
import type { CallbackResultType } from '../../textsecure/Types.d';
import type { MessageModel } from '../../models/messages';
@ -59,7 +59,7 @@ export async function sendDeleteForEveryone(
const logId = `sendDeleteForEveryone(${conversation.idForLogging()}, ${messageId})`;
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
log.error(`${logId}: Failed to fetch message. Failing job.`);
return;

View File

@ -20,7 +20,7 @@ import { getUntrustedConversationServiceIds } from './getUntrustedConversationSe
import { handleMessageSend } from '../../util/handleMessageSend';
import { isConversationAccepted } from '../../util/isConversationAccepted';
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
import { getMessageById } from '../../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
import { isNotNil } from '../../util/isNotNil';
import type { CallbackResultType } from '../../textsecure/Types.d';
import type { MessageModel } from '../../models/messages';
@ -45,7 +45,7 @@ export async function sendDeleteStoryForEveryone(
const logId = `sendDeleteStoryForEveryone(${storyId})`;
const message = await getMessageById(storyId);
const message = await __DEPRECATED$getMessageById(storyId);
if (!message) {
log.error(`${logId}: Failed to fetch message. Failing job.`);
return;

View File

@ -7,7 +7,7 @@ import PQueue from 'p-queue';
import * as Errors from '../../types/errors';
import { strictAssert } from '../../util/assert';
import type { MessageModel } from '../../models/messages';
import { getMessageById } from '../../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
import type { ConversationModel } from '../../models/conversations';
import { isGroup, isGroupV2, isMe } from '../../util/whatTypeOfConversation';
import { getSendOptions } from '../../util/getSendOptions';
@ -72,7 +72,7 @@ export async function sendNormalMessage(
const { Message } = window.Signal.Types;
const { messageId, revision, editedMessageTimestamp } = data;
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
log.info(
`message ${messageId} was not found, maybe because it was deleted. Giving up on sending it`
@ -551,7 +551,7 @@ async function getMessageSendData({
uploadMessagePreviews(message, uploadQueue),
uploadMessageQuote(message, uploadQueue),
uploadMessageSticker(message, uploadQueue),
storyId ? getMessageById(storyId) : undefined,
storyId ? __DEPRECATED$getMessageById(storyId) : undefined,
]);
// Save message after uploading attachments

View File

@ -14,7 +14,7 @@ import type { ConversationModel } from '../../models/conversations';
import * as reactionUtil from '../../reactions/util';
import { isSent, SendStatus } from '../../messages/MessageSendState';
import { getMessageById } from '../../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
import { isIncoming } from '../../messages/helpers';
import {
isMe,
@ -60,7 +60,7 @@ export async function sendReaction(
const ourConversationId =
window.ConversationController.getOurConversationIdOrThrow();
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
log.info(
`message ${messageId} was not found, maybe because it was deleted. Giving up on sending its reactions`
@ -334,7 +334,11 @@ export async function sendReaction(
});
void conversation.addSingleMessage(
window.MessageController.register(reactionMessage.id, reactionMessage)
window.MessageCache.__DEPRECATED$register(
reactionMessage.id,
reactionMessage,
'sendReaction'
)
);
}
}

View File

@ -391,7 +391,7 @@ async function _getMessageById(
id: string,
messageId: string
): Promise<MessageModel | undefined> {
const message = window.MessageController.getById(messageId);
const message = window.MessageCache.__DEPRECATED$getById(messageId);
if (message) {
return message;
@ -408,7 +408,11 @@ async function _getMessageById(
}
strictAssert(messageId === messageAttributes.id, 'message id mismatch');
return window.MessageController.register(messageId, messageAttributes);
return window.MessageCache.__DEPRECATED$register(
messageId,
messageAttributes,
'AttachmentDownloads._getMessageById'
);
}
async function _finishJob(

View File

@ -85,9 +85,10 @@ export async function onDelete(del: DeleteAttributesType): Promise<void> {
return;
}
const message = window.MessageController.register(
const message = window.MessageCache.__DEPRECATED$register(
targetMessage.id,
targetMessage
targetMessage,
'Deletes.onDelete'
);
await deleteForEveryone(message, del);

View File

@ -106,9 +106,10 @@ export async function onEdit(edit: EditAttributesType): Promise<void> {
return;
}
const message = window.MessageController.register(
const message = window.MessageCache.__DEPRECATED$register(
targetMessage.id,
targetMessage
targetMessage,
'Edits.onEdit'
);
await handleEditMessage(message.attributes, edit);

View File

@ -95,7 +95,11 @@ async function getTargetMessage(
(isOutgoing(item) || isStory(item)) && sourceId === item.conversationId
);
if (message) {
return window.MessageController.register(message.id, message);
return window.MessageCache.__DEPRECATED$register(
message.id,
message,
'MessageReceipts.getTargetMessage 1'
);
}
const groups = await window.Signal.Data.getAllGroupsInvolvingServiceId(
@ -113,7 +117,11 @@ async function getTargetMessage(
return null;
}
return window.MessageController.register(target.id, target);
return window.MessageCache.__DEPRECATED$register(
target.id,
target,
'MessageReceipts.getTargetMessage 2'
);
}
const wasDeliveredWithSealedSender = (
@ -376,7 +384,11 @@ export async function onReceipt(
await Promise.all(
targetMessages.map(msg => {
const model = window.MessageController.register(msg.id, msg);
const model = window.MessageCache.__DEPRECATED$register(
msg.id,
msg,
'MessageReceipts.onReceipt'
);
return updateMessageSendState(receipt, model);
})
);

View File

@ -196,9 +196,10 @@ export async function onReaction(
return;
}
const message = window.MessageController.register(
const message = window.MessageCache.__DEPRECATED$register(
targetMessage.id,
targetMessage
targetMessage,
'Reactions.onReaction'
);
// Use the generated message in ts/background.ts to create a message

View File

@ -125,7 +125,11 @@ export async function onSync(sync: ReadSyncAttributesType): Promise<void> {
notificationService.removeBy({ messageId: found.id });
const message = window.MessageController.register(found.id, found);
const message = window.MessageCache.__DEPRECATED$register(
found.id,
found,
'ReadSyncs.onSync'
);
const readAt = Math.min(sync.readAt, Date.now());
const newestSentAt = sync.timestamp;

View File

@ -97,7 +97,11 @@ export async function onSync(
return;
}
const message = window.MessageController.register(found.id, found);
const message = window.MessageCache.__DEPRECATED$register(
found.id,
found,
'ViewOnceOpenSyncs.onSync'
);
await message.markViewOnceMessageViewed({ fromSync: true });
viewOnceSyncs.delete(sync.timestamp);

View File

@ -99,7 +99,11 @@ export async function onSync(sync: ViewSyncAttributesType): Promise<void> {
notificationService.removeBy({ messageId: found.id });
const message = window.MessageController.register(found.id, found);
const message = window.MessageCache.__DEPRECATED$register(
found.id,
found,
'ViewSyncs.onSync'
);
let didChangeMessage = false;
if (message.get('readStatus') !== ReadStatus.Viewed) {

View File

@ -3,13 +3,13 @@
import * as log from '../logging/log';
import type { MessageAttributesType } from '../model-types.d';
import type { MessageModel } from '../models/messages';
import * as Errors from '../types/errors';
import type { MessageModel } from '../models/messages';
export async function getMessageById(
export async function __DEPRECATED$getMessageById(
messageId: string
): Promise<MessageModel | undefined> {
const message = window.MessageController.getById(messageId);
const message = window.MessageCache.__DEPRECATED$getById(messageId);
if (message) {
return message;
}
@ -28,5 +28,9 @@ export async function getMessageById(
return undefined;
}
return window.MessageController.register(found.id, found);
return window.MessageCache.__DEPRECATED$register(
found.id,
found,
'__DEPRECATED$getMessageById'
);
}

View File

@ -13,7 +13,7 @@ export async function getMessagesById(
const messageIdsToLookUpInDatabase: Array<string> = [];
for (const messageId of messageIds) {
const message = window.MessageController.getById(messageId);
const message = window.MessageCache.__DEPRECATED$getById(messageId);
if (message) {
messagesFromMemory.push(message);
} else {
@ -39,7 +39,11 @@ export async function getMessagesById(
// We use `window.Whisper.Message` instead of `MessageModel` here to avoid a circular
// import.
const message = new window.Whisper.Message(rawMessage);
return window.MessageController.register(message.id, message);
return window.MessageCache.__DEPRECATED$register(
message.id,
message,
'getMessagesById'
);
});
return [...messagesFromMemory, ...messagesFromDatabase];

5
ts/model-types.d.ts vendored
View File

@ -1,15 +1,12 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import * as Backbone from 'backbone';
import type { GroupV2ChangeType } from './groups';
import type { DraftBodyRanges, RawBodyRange } from './types/BodyRange';
import type { CustomColorType, ConversationColorType } from './types/Colors';
import type { SendMessageChallengeData } from './textsecure/Errors';
import type { MessageModel } from './models/messages';
import type { ConversationModel } from './models/conversations';
import type { ProfileNameChangeType } from './util/getStringForProfileChange';
import type { CapabilitiesType } from './textsecure/WebAPI';
@ -503,5 +500,3 @@ export type ShallowChallengeError = CustomError & {
export declare class ConversationModelCollectionType extends Backbone.Collection<ConversationModel> {
resetLookups(): void;
}
export declare class MessageModelCollectionType extends Backbone.Collection<MessageModel> {}

View File

@ -160,6 +160,7 @@ import { getQuoteAttachment } from '../util/makeQuote';
import { deriveProfileKeyVersion } from '../util/zkgroup';
import { incrementMessageCounter } from '../util/incrementMessageCounter';
import OS from '../util/os/osMain';
import { getMessageAuthorText } from '../util/getMessageAuthorText';
/* eslint-disable more/no-then */
window.Whisper = window.Whisper || {};
@ -1765,7 +1766,13 @@ export class ConversationModel extends window.Backbone
): Promise<Array<MessageModel>> {
const result = messages
.filter(message => Boolean(message.id))
.map(message => window.MessageController.register(message.id, message));
.map(message =>
window.MessageCache.__DEPRECATED$register(
message.id,
message,
'cleanModels'
)
);
const eliminated = messages.length - result.length;
if (eliminated > 0) {
@ -2078,7 +2085,11 @@ export class ConversationModel extends window.Backbone
// eslint-disable-next-line no-await-in-loop
await Promise.all(
readMessages.map(async m => {
const registered = window.MessageController.register(m.id, m);
const registered = window.MessageCache.__DEPRECATED$register(
m.id,
m,
'handleReadAndDownloadAttachments'
);
const shouldSave = await registered.queueAttachmentDownloads();
if (shouldSave) {
await window.Signal.Data.saveMessage(registered.attributes, {
@ -2824,12 +2835,13 @@ export class ConversationModel extends window.Backbone
const id = await window.Signal.Data.saveMessage(message, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
});
const model = window.MessageController.register(
const model = window.MessageCache.__DEPRECATED$register(
id,
new window.Whisper.Message({
...message,
id,
})
}),
'addChatSessionRefreshed'
);
this.trigger('newmessage', model);
@ -2868,12 +2880,13 @@ export class ConversationModel extends window.Backbone
const id = await window.Signal.Data.saveMessage(message, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
});
const model = window.MessageController.register(
const model = window.MessageCache.__DEPRECATED$register(
id,
new window.Whisper.Message({
...message,
id,
})
}),
'addDeliveryIssue'
);
this.trigger('newmessage', model);
@ -2922,9 +2935,10 @@ export class ConversationModel extends window.Backbone
ourAci: window.textsecure.storage.user.getCheckedAci(),
forceSave: true,
});
const model = window.MessageController.register(
const model = window.MessageCache.__DEPRECATED$register(
message.id,
new window.Whisper.Message(message)
new window.Whisper.Message(message),
'addKeyChange'
);
const isUntrusted = await this.isUntrusted();
@ -3001,12 +3015,13 @@ export class ConversationModel extends window.Backbone
ourAci: window.textsecure.storage.user.getCheckedAci(),
forceSave: true,
});
const model = window.MessageController.register(
const model = window.MessageCache.__DEPRECATED$register(
id,
new window.Whisper.Message({
...message,
id,
})
}),
'addConversationMerge'
);
this.trigger('newmessage', model);
@ -3052,9 +3067,10 @@ export class ConversationModel extends window.Backbone
ourAci: window.textsecure.storage.user.getCheckedAci(),
forceSave: true,
});
const model = window.MessageController.register(
const model = window.MessageCache.__DEPRECATED$register(
message.id,
new window.Whisper.Message(message)
new window.Whisper.Message(message),
'addVerifiedChange'
);
this.trigger('newmessage', model);
@ -3093,12 +3109,13 @@ export class ConversationModel extends window.Backbone
const id = await window.Signal.Data.saveMessage(message, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
});
const model = window.MessageController.register(
const model = window.MessageCache.__DEPRECATED$register(
id,
new window.Whisper.Message({
...message,
id,
})
}),
'addProfileChange'
);
this.trigger('newmessage', model);
@ -3139,12 +3156,13 @@ export class ConversationModel extends window.Backbone
ourAci: window.textsecure.storage.user.getCheckedAci(),
}
);
const model = window.MessageController.register(
const model = window.MessageCache.__DEPRECATED$register(
id,
new window.Whisper.Message({
...(message as MessageAttributesType),
id,
})
}),
'addNotification'
);
this.trigger('newmessage', model);
@ -3224,7 +3242,7 @@ export class ConversationModel extends window.Backbone
`maybeRemoveUniversalTimer(${this.idForLogging()}): removed notification`
);
const message = window.MessageController.getById(notificationId);
const message = window.MessageCache.__DEPRECATED$getById(notificationId);
if (message) {
await window.Signal.Data.removeMessage(message.id);
}
@ -3261,7 +3279,7 @@ export class ConversationModel extends window.Backbone
`maybeClearContactRemoved(${this.idForLogging()}): removed notification`
);
const message = window.MessageController.getById(notificationId);
const message = window.MessageCache.__DEPRECATED$getById(notificationId);
if (message) {
await window.Signal.Data.removeMessage(message.id);
}
@ -3643,7 +3661,7 @@ export class ConversationModel extends window.Backbone
draftBodyRanges: [],
draftTimestamp: null,
quotedMessageId: undefined,
lastMessageAuthor: message.getAuthorText(),
lastMessageAuthor: getMessageAuthorText(message.attributes),
lastMessageBodyRanges: message.get('bodyRanges'),
lastMessage:
notificationData?.text || message.getNotificationText() || '',
@ -3795,7 +3813,11 @@ export class ConversationModel extends window.Backbone
});
const model = new window.Whisper.Message(attributes);
const message = window.MessageController.register(model.id, model);
const message = window.MessageCache.__DEPRECATED$register(
model.id,
model,
'enqueueMessageForSend'
);
message.cachedOutgoingContactData = contact;
// Attach path to preview images so that sendNormalMessage can use them to
@ -3985,17 +4007,22 @@ export class ConversationModel extends window.Backbone
let previewMessage: MessageModel | undefined;
let activityMessage: MessageModel | undefined;
// Register the message with MessageController so that if it already exists
// Register the message with MessageCache so that if it already exists
// in memory we use that data instead of the data from the db which may
// be out of date.
if (preview) {
previewMessage = window.MessageController.register(preview.id, preview);
previewMessage = window.MessageCache.__DEPRECATED$register(
preview.id,
preview,
'previewMessage'
);
}
if (activity) {
activityMessage = window.MessageController.register(
activityMessage = window.MessageCache.__DEPRECATED$register(
activity.id,
activity
activity,
'activityMessage'
);
}
@ -4027,7 +4054,7 @@ export class ConversationModel extends window.Backbone
notificationData?.text || previewMessage?.getNotificationText() || '',
lastMessageBodyRanges: notificationData?.bodyRanges,
lastMessagePrefix: notificationData?.emoji,
lastMessageAuthor: previewMessage?.getAuthorText(),
lastMessageAuthor: getMessageAuthorText(previewMessage?.attributes),
lastMessageStatus:
(previewMessage
? getMessagePropStatus(previewMessage.attributes, ourConversationId)
@ -4394,7 +4421,11 @@ export class ConversationModel extends window.Backbone
model.set({ id });
const message = window.MessageController.register(id, model);
const message = window.MessageCache.__DEPRECATED$register(
id,
model,
'updateExpirationTimer'
);
void this.addSingleMessage(message);
void this.updateUnread();

View File

@ -27,11 +27,10 @@ import type { DeleteAttributesType } from '../messageModifiers/Deletes';
import type { SentEventData } from '../textsecure/messageReceiverEvents';
import { isNotNil } from '../util/isNotNil';
import { isNormalNumber } from '../util/isNormalNumber';
import { softAssert, strictAssert } from '../util/assert';
import { strictAssert } from '../util/assert';
import { hydrateStoryContext } from '../util/hydrateStoryContext';
import { drop } from '../util/drop';
import { dropNull } from '../util/dropNull';
import type { ConversationModel } from './conversations';
import { getCallingNotificationText } from '../util/callingNotification';
import type {
ProcessedDataMessage,
ProcessedQuote,
@ -39,7 +38,6 @@ import type {
CallbackResultType,
} from '../textsecure/Types.d';
import { SendMessageProtoError } from '../textsecure/Errors';
import * as expirationTimer from '../util/expirationTimer';
import { getUserLanguages } from '../util/userLanguages';
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp';
import { copyCdnFields } from '../util/attachments';
@ -49,15 +47,11 @@ import type { ServiceIdString } from '../types/ServiceId';
import { normalizeServiceId } from '../types/ServiceId';
import { isAciString } from '../util/isAciString';
import * as reactionUtil from '../reactions/util';
import * as Stickers from '../types/Stickers';
import * as Errors from '../types/errors';
import * as EmbeddedContact from '../types/EmbeddedContact';
import type { AttachmentType } from '../types/Attachment';
import { isImage, isVideo } from '../types/Attachment';
import * as Attachment from '../types/Attachment';
import { stringToMIMEType } from '../types/MIME';
import * as MIME from '../types/MIME';
import * as GroupChange from '../groupChange';
import { ReadStatus } from '../messages/MessageReadStatus';
import type { SendStateByConversationId } from '../messages/MessageSendState';
import {
@ -79,12 +73,9 @@ import {
} from '../util/whatTypeOfConversation';
import { handleMessageSend } from '../util/handleMessageSend';
import { getSendOptions } from '../util/getSendOptions';
import { findAndFormatContact } from '../util/findAndFormatContact';
import { modifyTargetMessage } from '../util/modifyTargetMessage';
import {
getAttachmentsForMessage,
getMessagePropStatus,
getPropsForCallHistory,
hasErrors,
isCallHistory,
isChatSessionRefreshed,
@ -106,14 +97,9 @@ import {
isUnsupportedMessage,
isVerifiedChange,
isConversationMerge,
extractHydratedMentions,
} from '../state/selectors/message';
import {
isInCall,
getCallSelector,
getActiveCall,
} from '../state/selectors/calling';
import type { ReactionAttributesType } from '../messageModifiers/Reactions';
import { isInCall } from '../state/selectors/calling';
import { ReactionSource } from '../reactions/ReactionSource';
import * as LinkPreview from '../types/LinkPreview';
import { SignalService as Proto } from '../protobuf';
@ -138,9 +124,7 @@ import {
isCustomError,
messageHasPaymentEvent,
isQuoteAMatch,
getPaymentEventNotificationText,
} from '../messages/helpers';
import type { ReplacementValuesType } from '../types/I18N';
import { viewOnceOpenJobQueue } from '../jobs/viewOnceOpenJobQueue';
import { getMessageIdForLogging } from '../util/idForLogging';
import { hasAttachmentDownloads } from '../util/hasAttachmentDownloads';
@ -148,33 +132,29 @@ import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads';
import { findStoryMessages } from '../util/findStoryMessage';
import { getStoryDataFromMessageAttributes } from '../services/storyLoader';
import type { ConversationQueueJobData } from '../jobs/conversationJobQueue';
import { getMessageById } from '../messages/getMessageById';
import { shouldDownloadStory } from '../util/shouldDownloadStory';
import type { EmbeddedContactWithHydratedAvatar } from '../types/EmbeddedContact';
import { SeenStatus } from '../MessageSeenStatus';
import { isNewReactionReplacingPrevious } from '../reactions/util';
import { parseBoostBadgeListFromServer } from '../badges/parseBadgesFromServer';
import { GiftBadgeStates } from '../components/conversation/Message';
import type { StickerWithHydratedData } from '../types/Stickers';
import { getStringForConversationMerge } from '../util/getStringForConversationMerge';
import {
addToAttachmentDownloadQueue,
shouldUseAttachmentDownloadQueue,
} from '../util/attachmentDownloadQueue';
import { getTitleNoDefault, getNumber } from '../util/getTitle';
import dataInterface from '../sql/Client';
import { getQuoteBodyText } from '../util/getQuoteBodyText';
import { shouldReplyNotifyUser } from '../util/shouldReplyNotifyUser';
import { isConversationAccepted } from '../util/isConversationAccepted';
import type { RawBodyRange } from '../types/BodyRange';
import { BodyRange, applyRangesForText } from '../types/BodyRange';
import { getStringForProfileChange } from '../util/getStringForProfileChange';
import { BodyRange } from '../types/BodyRange';
import {
queueUpdateMessage,
saveNewMessageBatcher,
} from '../util/messageBatcher';
import { getCallHistorySelector } from '../state/selectors/callHistory';
import { getConversationSelector } from '../state/selectors/conversations';
import { getSenderIdentifier } from '../util/getSenderIdentifier';
import { getNotificationDataForMessage } from '../util/getNotificationDataForMessage';
import { getNotificationTextForMessage } from '../util/getNotificationTextForMessage';
import { getMessageAuthorText } from '../util/getMessageAuthorText';
/* eslint-disable more/no-then */
@ -212,9 +192,17 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
cachedOutgoingStickerData?: StickerWithHydratedData;
public registerLocations: Set<string>;
constructor(attributes: MessageAttributesType) {
super(attributes);
if (!this.id && attributes.id) {
this.id = attributes.id;
}
this.registerLocations = new Set();
// Note that we intentionally don't use `initialize()` method because it
// isn't compatible with esnext output of esbuild.
if (isObject(attributes)) {
@ -266,6 +254,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return;
}
window.MessageCache.setAttributes({
messageId: this.id,
messageAttributes: this.attributes,
skipSaveToDatabase: true,
});
const { storyChanged } = window.reduxActions.stories;
if (isStory(this.attributes)) {
@ -293,19 +287,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
getSenderIdentifier(): string {
const sentAt = this.get('sent_at');
const source = this.get('source');
const sourceServiceId = this.get('sourceServiceId');
const sourceDevice = this.get('sourceDevice');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const conversation = window.ConversationController.lookupOrCreate({
e164: source,
serviceId: sourceServiceId,
reason: 'MessageModel.getSenderIdentifier',
})!;
return `${conversation?.id}.${sourceDevice}-${sentAt}`;
return getSenderIdentifier(this.attributes);
}
getReceivedAt(): number {
@ -345,63 +327,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
shouldSave?: boolean;
} = {}
): Promise<void> {
const ourAci = window.textsecure.storage.user.getCheckedAci();
const storyId = this.get('storyId');
if (!storyId) {
return;
}
const context = this.get('storyReplyContext');
// We'll continue trying to get the attachment as long as the message still exists
if (context && (context.attachment?.url || !context.messageId)) {
return;
}
const message =
inMemoryMessage === undefined
? (await getMessageById(storyId))?.attributes
: inMemoryMessage;
if (!message) {
const conversation = this.getConversation();
softAssert(
conversation && isDirectConversation(conversation.attributes),
'hydrateStoryContext: Not a type=direct conversation'
);
this.set({
storyReplyContext: {
attachment: undefined,
// This is ok to do because story replies only show in 1:1 conversations
// so the story that was quoted should be from the same conversation.
authorAci: conversation?.getAci(),
// No messageId, referenced story not found!
messageId: '',
},
});
if (shouldSave) {
await window.Signal.Data.saveMessage(this.attributes, { ourAci });
}
return;
}
const attachments = getAttachmentsForMessage({ ...message });
let attachment: AttachmentType | undefined = attachments?.[0];
if (attachment && !attachment.url && !attachment.textAttachment) {
attachment = undefined;
}
const { sourceServiceId: authorAci } = message;
strictAssert(isAciString(authorAci), 'Story message from pni');
this.set({
storyReplyContext: {
attachment: omit(attachment, 'screenshotData'),
authorAci,
messageId: message.id,
},
});
if (shouldSave) {
await window.Signal.Data.saveMessage(this.attributes, { ourAci });
}
await hydrateStoryContext(this.id, inMemoryMessage, { shouldSave });
}
// Dependencies of prop-generation functions
@ -414,486 +340,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
text: string;
bodyRanges?: ReadonlyArray<RawBodyRange>;
} {
// eslint-disable-next-line prefer-destructuring
const attributes: MessageAttributesType = this.attributes;
if (isDeliveryIssue(attributes)) {
return {
emoji: '⚠️',
text: window.i18n('icu:DeliveryIssue--preview'),
};
}
if (isConversationMerge(attributes)) {
const conversation = this.getConversation();
strictAssert(
conversation,
'getNotificationData/isConversationMerge/conversation'
);
strictAssert(
attributes.conversationMerge,
'getNotificationData/isConversationMerge/conversationMerge'
);
return {
text: getStringForConversationMerge({
obsoleteConversationTitle: getTitleNoDefault(
attributes.conversationMerge.renderInfo
),
obsoleteConversationNumber: getNumber(
attributes.conversationMerge.renderInfo
),
conversationTitle: conversation.getTitle(),
i18n: window.i18n,
}),
};
}
if (isChatSessionRefreshed(attributes)) {
return {
emoji: '🔁',
text: window.i18n('icu:ChatRefresh--notification'),
};
}
if (isUnsupportedMessage(attributes)) {
return {
text: window.i18n('icu:message--getDescription--unsupported-message'),
};
}
if (isGroupV1Migration(attributes)) {
return {
text: window.i18n('icu:GroupV1--Migration--was-upgraded'),
};
}
if (isProfileChange(attributes)) {
const change = this.get('profileChange');
const changedId = this.get('changedId');
const changedContact = findAndFormatContact(changedId);
if (!change) {
throw new Error('getNotificationData: profileChange was missing!');
}
return {
text: getStringForProfileChange(change, changedContact, window.i18n),
};
}
if (isGroupV2Change(attributes)) {
const change = this.get('groupV2Change');
strictAssert(
change,
'getNotificationData: isGroupV2Change true, but no groupV2Change!'
);
const changes = GroupChange.renderChange<string>(change, {
i18n: window.i18n,
ourAci: window.textsecure.storage.user.getCheckedAci(),
ourPni: window.textsecure.storage.user.getCheckedPni(),
renderContact: (conversationId: string) => {
const conversation =
window.ConversationController.get(conversationId);
return conversation
? conversation.getTitle()
: window.i18n('icu:unknownContact');
},
renderString: (
key: string,
_i18n: unknown,
components: ReplacementValuesType<string | number> | undefined
) => {
// eslint-disable-next-line local-rules/valid-i18n-keys
return window.i18n(key, components);
},
});
return { text: changes.map(({ text }) => text).join(' ') };
}
if (messageHasPaymentEvent(attributes)) {
const sender = findAndFormatContact(attributes.sourceServiceId);
const conversation = findAndFormatContact(attributes.conversationId);
return {
text: getPaymentEventNotificationText(
attributes.payment,
sender.title,
conversation.title,
sender.isMe,
window.i18n
),
emoji: '💳',
};
}
const attachments = this.get('attachments') || [];
if (isTapToView(attributes)) {
if (this.isErased()) {
return {
text: window.i18n('icu:message--getDescription--disappearing-media'),
};
}
if (Attachment.isImage(attachments)) {
return {
text: window.i18n('icu:message--getDescription--disappearing-photo'),
emoji: '📷',
};
}
if (Attachment.isVideo(attachments)) {
return {
text: window.i18n('icu:message--getDescription--disappearing-video'),
emoji: '🎥',
};
}
// There should be an image or video attachment, but we have a fallback just in
// case.
return { text: window.i18n('icu:mediaMessage'), emoji: '📎' };
}
if (isGroupUpdate(attributes)) {
const groupUpdate = this.get('group_update');
const fromContact = getContact(this.attributes);
const messages = [];
if (!groupUpdate) {
throw new Error('getNotificationData: Missing group_update');
}
if (groupUpdate.left === 'You') {
return { text: window.i18n('icu:youLeftTheGroup') };
}
if (groupUpdate.left) {
return {
text: window.i18n('icu:leftTheGroup', {
name: this.getNameForNumber(groupUpdate.left),
}),
};
}
if (!fromContact) {
return { text: '' };
}
if (isMe(fromContact.attributes)) {
messages.push(window.i18n('icu:youUpdatedTheGroup'));
} else {
messages.push(
window.i18n('icu:updatedTheGroup', {
name: fromContact.getTitle(),
})
);
}
if (groupUpdate.joined && groupUpdate.joined.length) {
const joinedContacts = groupUpdate.joined.map(item =>
window.ConversationController.getOrCreate(item, 'private')
);
const joinedWithoutMe = joinedContacts.filter(
contact => !isMe(contact.attributes)
);
if (joinedContacts.length > 1) {
messages.push(
window.i18n('icu:multipleJoinedTheGroup', {
names: joinedWithoutMe
.map(contact => contact.getTitle())
.join(', '),
})
);
if (joinedWithoutMe.length < joinedContacts.length) {
messages.push(window.i18n('icu:youJoinedTheGroup'));
}
} else {
const joinedContact = window.ConversationController.getOrCreate(
groupUpdate.joined[0],
'private'
);
if (isMe(joinedContact.attributes)) {
messages.push(window.i18n('icu:youJoinedTheGroup'));
} else {
messages.push(
window.i18n('icu:joinedTheGroup', {
name: joinedContacts[0].getTitle(),
})
);
}
}
}
if (groupUpdate.name) {
messages.push(
window.i18n('icu:titleIsNow', {
name: groupUpdate.name,
})
);
}
if (groupUpdate.avatarUpdated) {
messages.push(window.i18n('icu:updatedGroupAvatar'));
}
return { text: messages.join(' ') };
}
if (isEndSession(attributes)) {
return { text: window.i18n('icu:sessionEnded') };
}
if (isIncoming(attributes) && hasErrors(attributes)) {
return { text: window.i18n('icu:incomingError') };
}
const body = (this.get('body') || '').trim();
const bodyRanges = this.get('bodyRanges') || [];
if (attachments.length) {
// This should never happen but we want to be extra-careful.
const attachment = attachments[0] || {};
const { contentType } = attachment;
if (contentType === MIME.IMAGE_GIF || Attachment.isGIF(attachments)) {
return {
bodyRanges,
emoji: '🎡',
text: body || window.i18n('icu:message--getNotificationText--gif'),
};
}
if (Attachment.isImage(attachments)) {
return {
bodyRanges,
emoji: '📷',
text: body || window.i18n('icu:message--getNotificationText--photo'),
};
}
if (Attachment.isVideo(attachments)) {
return {
bodyRanges,
emoji: '🎥',
text: body || window.i18n('icu:message--getNotificationText--video'),
};
}
if (Attachment.isVoiceMessage(attachment)) {
return {
bodyRanges,
emoji: '🎤',
text:
body ||
window.i18n('icu:message--getNotificationText--voice-message'),
};
}
if (Attachment.isAudio(attachments)) {
return {
bodyRanges,
emoji: '🔈',
text:
body ||
window.i18n('icu:message--getNotificationText--audio-message'),
};
}
return {
bodyRanges,
text: body || window.i18n('icu:message--getNotificationText--file'),
emoji: '📎',
};
}
const stickerData = this.get('sticker');
if (stickerData) {
const emoji =
Stickers.getSticker(stickerData.packId, stickerData.stickerId)?.emoji ||
stickerData?.emoji;
if (!emoji) {
log.warn('Unable to get emoji for sticker');
}
return {
text: window.i18n('icu:message--getNotificationText--stickers'),
emoji: dropNull(emoji),
};
}
if (isCallHistory(attributes)) {
const state = window.reduxStore.getState();
const callingNotification = getPropsForCallHistory(attributes, {
callSelector: getCallSelector(state),
activeCall: getActiveCall(state),
callHistorySelector: getCallHistorySelector(state),
conversationSelector: getConversationSelector(state),
});
if (callingNotification) {
const text = getCallingNotificationText(
callingNotification,
window.i18n
);
if (text != null) {
return {
text,
};
}
}
log.error("This call history message doesn't have valid call history");
}
if (isExpirationTimerUpdate(attributes)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { expireTimer } = this.get('expirationTimerUpdate')!;
if (!expireTimer) {
return { text: window.i18n('icu:disappearingMessagesDisabled') };
}
return {
text: window.i18n('icu:timerSetTo', {
time: expirationTimer.format(window.i18n, expireTimer),
}),
};
}
if (isKeyChange(attributes)) {
const identifier = this.get('key_changed');
const conversation = window.ConversationController.get(identifier);
return {
text: window.i18n('icu:safetyNumberChangedGroup', {
name: conversation ? conversation.getTitle() : '',
}),
};
}
const contacts = this.get('contact');
if (contacts && contacts.length) {
return {
text:
EmbeddedContact.getName(contacts[0]) ||
window.i18n('icu:unknownContact'),
emoji: '👤',
};
}
const giftBadge = this.get('giftBadge');
if (giftBadge) {
const emoji = '✨';
if (isOutgoing(this.attributes)) {
const toContact = window.ConversationController.get(
this.attributes.conversationId
);
const recipient =
toContact?.getTitle() ?? window.i18n('icu:unknownContact');
return {
emoji,
text: window.i18n('icu:message--donation--preview--sent', {
recipient,
}),
};
}
const fromContact = getContact(this.attributes);
const sender =
fromContact?.getTitle() ?? window.i18n('icu:unknownContact');
return {
emoji,
text:
giftBadge.state === GiftBadgeStates.Unopened
? window.i18n('icu:message--donation--preview--unopened', {
sender,
})
: window.i18n('icu:message--donation--preview--redeemed'),
};
}
if (body) {
return {
text: body,
bodyRanges,
};
}
return { text: '' };
}
getAuthorText(): string | undefined {
// if it's outgoing, it must be self-authored
const selfAuthor = isOutgoing(this.attributes)
? window.i18n('icu:you')
: undefined;
// if it's not selfAuthor and there's no incoming contact,
// it might be a group notification, so we return undefined
return selfAuthor ?? this.getIncomingContact()?.getTitle({ isShort: true });
return getNotificationDataForMessage(this.attributes);
}
getNotificationText(): string {
const { text, emoji } = this.getNotificationData();
const { attributes } = this;
const conversation = this.getConversation();
strictAssert(
conversation != null,
'Conversation not found in ConversationController'
);
if (!isConversationAccepted(conversation.attributes)) {
return window.i18n('icu:message--getNotificationText--messageRequest');
}
if (attributes.storyReaction) {
if (attributes.type === 'outgoing') {
const name = this.getConversation()?.get('profileName');
if (!name) {
return window.i18n(
'icu:Quote__story-reaction-notification--outgoing--nameless',
{
emoji: attributes.storyReaction.emoji,
}
);
}
return window.i18n('icu:Quote__story-reaction-notification--outgoing', {
emoji: attributes.storyReaction.emoji,
name,
});
}
const ourAci = window.textsecure.storage.user.getCheckedAci();
if (
attributes.type === 'incoming' &&
attributes.storyReaction.targetAuthorAci === ourAci
) {
return window.i18n('icu:Quote__story-reaction-notification--incoming', {
emoji: attributes.storyReaction.emoji,
});
}
if (!window.Signal.OS.isLinux()) {
return attributes.storyReaction.emoji;
}
return window.i18n('icu:Quote__story-reaction--single');
}
const mentions =
extractHydratedMentions(attributes, {
conversationSelector: findAndFormatContact,
}) || [];
const spoilers = (attributes.bodyRanges || []).filter(
range =>
BodyRange.isFormatting(range) && range.style === BodyRange.Style.SPOILER
) as Array<BodyRange<BodyRange.Formatting>>;
const modifiedText = applyRangesForText({ text, mentions, spoilers });
// Linux emoji support is mixed, so we disable it. (Note that this doesn't touch
// the `text`, which can contain emoji.)
const shouldIncludeEmoji = Boolean(emoji) && !window.Signal.OS.isLinux();
if (shouldIncludeEmoji) {
return window.i18n('icu:message--getNotificationText--text-with-emoji', {
text: modifiedText,
emoji,
});
}
return modifiedText || '';
return getNotificationTextForMessage(this.attributes);
}
// General
@ -921,14 +372,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
this.set(attributes);
}
getNameForNumber(number: string): string {
const conversation = window.ConversationController.get(number);
if (!conversation) {
return number;
}
return conversation.getTitle();
}
async cleanup(): Promise<void> {
await cleanupMessage(this.attributes);
}
@ -1041,7 +484,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
`doubleCheckMissingQuoteReference/${logId}: missing story reference`
);
const message = window.MessageController.getById(storyId);
const message = window.MessageCache.__DEPRECATED$getById(storyId);
if (!message) {
return;
}
@ -1068,7 +511,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
log.info(
`doubleCheckMissingQuoteReference/${logId}: Verifying reference to ${sentAt}`
);
const inMemoryMessages = window.MessageController.filterBySentAt(
const inMemoryMessages = window.MessageCache.__DEPRECATED$filterBySentAt(
Number(sentAt)
);
let matchingMessage = find(inMemoryMessages, message =>
@ -1082,7 +525,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
isQuoteAMatch(item, this.get('conversationId'), quote)
);
if (found) {
matchingMessage = window.MessageController.register(found.id, found);
matchingMessage = window.MessageCache.__DEPRECATED$register(
found.id,
found,
'doubleCheckMissingQuoteReference'
);
}
}
@ -1292,21 +739,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
this.set(markRead(this.attributes, readAt, options));
}
getIncomingContact(): ConversationModel | undefined | null {
if (!isIncoming(this.attributes)) {
return null;
}
const sourceServiceId = this.get('sourceServiceId');
if (!sourceServiceId) {
return null;
}
return window.ConversationController.getOrCreate(
sourceServiceId,
'private'
);
}
async retrySend(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const conversation = this.getConversation()!;
@ -1971,7 +1403,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
messageId: '',
};
const inMemoryMessages = window.MessageController.filterBySentAt(id);
const inMemoryMessages =
window.MessageCache.__DEPRECATED$filterBySentAt(id);
const matchingMessage = find(inMemoryMessages, item =>
isQuoteAMatch(item.attributes, conversationId, result)
);
@ -1992,7 +1425,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return result;
}
queryMessage = window.MessageController.register(found.id, found);
queryMessage = window.MessageCache.__DEPRECATED$register(
found.id,
found,
'copyFromQuotedMessage'
);
}
if (queryMessage) {
@ -2146,7 +1583,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// 3. in rare cases, an incoming message can be retried, though it will
// still go through one of the previous two codepaths
// eslint-disable-next-line @typescript-eslint/no-this-alias
const message = this;
let message: MessageModel = this;
const source = message.get('source');
const sourceServiceId = message.get('sourceServiceId');
const type = message.get('type');
@ -2164,9 +1601,9 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
log.info(`${idLog}: starting processing in queue`);
// First, check for duplicates. If we find one, stop processing here.
const inMemoryMessage = window.MessageController.findBySender(
const inMemoryMessage = window.MessageCache.findBySender(
this.getSenderIdentifier()
)?.attributes;
);
if (inMemoryMessage) {
log.info(`${idLog}: cache hit`, this.getSenderIdentifier());
} else {
@ -2197,9 +1634,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
`${idLog}: Updating message ${message.idForLogging()} with received transcript`
);
const toUpdate = window.MessageController.register(
const toUpdate = window.MessageCache.__DEPRECATED$register(
existingMessage.id,
existingMessage
existingMessage,
'handleDataMessage/outgoing/toUpdate'
);
const unidentifiedDeliveriesSet = new Set<string>(
@ -2594,6 +2032,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const ourPni = window.textsecure.storage.user.getCheckedPni();
const ourServiceIds: Set<ServiceIdString> = new Set([ourAci, ourPni]);
window.MessageCache.toMessageAttributes(this.attributes);
message.set({
id: messageId,
attachments: dataMessage.attachments,
@ -2786,12 +2225,16 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
) {
conversation.set({
lastMessage: message.getNotificationText(),
lastMessageAuthor: message.getAuthorText(),
lastMessageAuthor: getMessageAuthorText(message.attributes),
timestamp: message.get('sent_at'),
});
}
window.MessageController.register(message.id, message);
message = window.MessageCache.__DEPRECATED$register(
message.id,
message,
'handleDataMessage/message'
);
conversation.incrementMessageCount();
// If we sent a message in a given conversation, unarchive it!
@ -2892,7 +2335,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const isFirstRun = false;
await this.modifyTargetMessage(conversation, isFirstRun);
if (await shouldReplyNotifyUser(this, conversation)) {
if (await shouldReplyNotifyUser(this.attributes, conversation)) {
await conversation.notify(this);
}
@ -3042,9 +2485,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
timestamp: reaction.timestamp,
});
const messageToAdd = window.MessageController.register(
const messageToAdd = window.MessageCache.__DEPRECATED$register(
generatedMessage.id,
generatedMessage
generatedMessage,
'generatedMessage'
);
if (isDirectConversation(targetConversation.attributes)) {
await targetConversation.addSingleMessage(messageToAdd);
@ -3063,7 +2507,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
'handleReaction: notifying for story reaction to ' +
`${getMessageIdForLogging(storyMessage)} from someone else`
);
if (await shouldReplyNotifyUser(messageToAdd, targetConversation)) {
if (
await shouldReplyNotifyUser(
messageToAdd.attributes,
targetConversation
)
) {
drop(targetConversation.notify(messageToAdd));
}
}
@ -3188,9 +2637,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
});
void conversation.addSingleMessage(
window.MessageController.register(
window.MessageCache.__DEPRECATED$register(
generatedMessage.id,
generatedMessage
generatedMessage,
'generatedMessage2'
)
);
@ -3285,14 +2735,3 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
window.Whisper.Message = MessageModel;
window.Whisper.MessageCollection = window.Backbone.Collection.extend({
model: window.Whisper.Message,
comparator(left: Readonly<MessageModel>, right: Readonly<MessageModel>) {
if (left.get('received_at') === right.get('received_at')) {
return (left.get('sent_at') || 0) - (right.get('sent_at') || 0);
}
return (left.get('received_at') || 0) - (right.get('received_at') || 0);
},
});

View File

@ -6,7 +6,7 @@ import { v4 as generateUuid } from 'uuid';
import type { ReactionAttributesType } from '../messageModifiers/Reactions';
import { ReactionSource } from './ReactionSource';
import { getMessageById } from '../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
import { getSourceServiceId, isStory } from '../messages/helpers';
import { strictAssert } from '../util/assert';
import { isDirectConversation } from '../util/whatTypeOfConversation';
@ -26,7 +26,7 @@ export async function enqueueReactionForSend({
messageId: string;
remove: boolean;
}>): Promise<void> {
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
strictAssert(message, 'enqueueReactionForSend: no message found');
const targetAuthorAci = getSourceServiceId(message.attributes);

View File

@ -31,6 +31,16 @@ try {
process.exit(1);
}
const debugMatch = stdout.matchAll(/ci:test-electron:debug=(.*)?\n/g);
Array.from(debugMatch).forEach(info => {
try {
const args = JSON.parse(info[1]);
console.log('DEBUG:', args);
} catch {
// this section intentionally left blank
}
});
const match = stdout.match(/ci:test-electron:done=(.*)?\n/);
if (!match) {

410
ts/services/MessageCache.ts Normal file
View File

@ -0,0 +1,410 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import cloneDeep from 'lodash/cloneDeep';
import type { MessageAttributesType } from '../model-types.d';
import type { MessageModel } from '../models/messages';
import * as Errors from '../types/errors';
import * as log from '../logging/log';
import { drop } from '../util/drop';
import { getEnvironment, Environment } from '../environment';
import { getMessageConversation } from '../util/getMessageConversation';
import { getMessageModelLogger } from '../util/MessageModelLogger';
import { getSenderIdentifier } from '../util/getSenderIdentifier';
import { isNotNil } from '../util/isNotNil';
import { map } from '../util/iterables';
import { softAssert, strictAssert } from '../util/assert';
export class MessageCache {
private state = {
messages: new Map<string, MessageAttributesType>(),
messageIdsBySender: new Map<string, string>(),
messageIdsBySentAt: new Map<number, Array<string>>(),
lastAccessedAt: new Map<string, number>(),
};
// Stores the models so that __DEPRECATED$register always returns the existing
// copy instead of a new model.
private modelCache = new Map<string, MessageModel>();
// Synchronously access a message's attributes from internal cache. Will
// return undefined if the message does not exist in memory.
public accessAttributes(
messageId: string
): Readonly<MessageAttributesType> | undefined {
const messageAttributes = this.state.messages.get(messageId);
return messageAttributes
? this.freezeAttributes(messageAttributes)
: undefined;
}
// Synchronously access a message's attributes from internal cache. Throws
// if the message does not exist in memory.
public accessAttributesOrThrow(
source: string,
messageId: string
): Readonly<MessageAttributesType> {
const messageAttributes = this.accessAttributes(messageId);
strictAssert(
messageAttributes,
`MessageCache.accessAttributesOrThrow/${source}: no message`
);
return messageAttributes;
}
// Evicts messages from the message cache if they have not been accessed past
// the expiry time.
public deleteExpiredMessages(expiryTime: number): void {
const now = Date.now();
for (const [messageId, messageAttributes] of this.state.messages) {
const timeLastAccessed = this.state.lastAccessedAt.get(messageId) ?? 0;
const conversation = getMessageConversation(messageAttributes);
const state = window.reduxStore.getState();
const selectedId = state?.conversations?.selectedConversationId;
const inActiveConversation =
conversation && selectedId && conversation.id === selectedId;
if (now - timeLastAccessed > expiryTime && !inActiveConversation) {
this.__DEPRECATED$unregister(messageId);
}
}
}
// Finds a message in the cache by sender identifier
public findBySender(
senderIdentifier: string
): Readonly<MessageAttributesType> | undefined {
const id = this.state.messageIdsBySender.get(senderIdentifier);
if (!id) {
return undefined;
}
return this.accessAttributes(id);
}
public replaceAllObsoleteConversationIds({
conversationId,
obsoleteId,
}: {
conversationId: string;
obsoleteId: string;
}): void {
for (const [messageId, messageAttributes] of this.state.messages) {
if (messageAttributes.conversationId !== obsoleteId) {
continue;
}
this.setAttributes({
messageId,
messageAttributes: { conversationId },
skipSaveToDatabase: true,
});
}
}
// Find the message's attributes whether in memory or in the database.
// Refresh the attributes in the cache if they exist. Throw if we cannot find
// a matching message.
public async resolveAttributes(
source: string,
messageId: string
): Promise<Readonly<MessageAttributesType>> {
const inMemoryMessageAttributes = this.accessAttributes(messageId);
if (inMemoryMessageAttributes) {
return inMemoryMessageAttributes;
}
let messageAttributesFromDatabase: MessageAttributesType | undefined;
try {
messageAttributesFromDatabase = await window.Signal.Data.getMessageById(
messageId
);
} catch (err: unknown) {
log.error(
`MessageCache.resolveAttributes(${messageId}): db error ${Errors.toLogFormat(
err
)}`
);
}
strictAssert(
messageAttributesFromDatabase,
`MessageCache.resolveAttributes/${source}: no message`
);
return this.freezeAttributes(messageAttributesFromDatabase);
}
// Updates a message's attributes and saves the message to cache and to the
// database. Option to skip the save to the database.
public setAttributes({
messageId,
messageAttributes: partialMessageAttributes,
skipSaveToDatabase = false,
}: {
messageId: string;
messageAttributes: Partial<MessageAttributesType>;
skipSaveToDatabase: boolean;
}): void {
let messageAttributes = this.accessAttributes(messageId);
softAssert(messageAttributes, 'could not find message attributes');
if (!messageAttributes) {
// We expect message attributes to be defined in cache if one is trying to
// set new attributes. In the case that the attributes are missing in cache
// we'll add whatever we currently have to cache as a defensive measure so
// that the code continues to work properly downstream. The softAssert above
// that logs/debugger should be addressed upstream immediately by ensuring
// that message is in cache.
const partiallyCachedMessage = {
id: messageId,
...partialMessageAttributes,
} as MessageAttributesType;
this.addMessageToCache(partiallyCachedMessage);
messageAttributes = partiallyCachedMessage;
}
this.state.messageIdsBySender.delete(
getSenderIdentifier(messageAttributes)
);
const nextMessageAttributes = {
...messageAttributes,
...partialMessageAttributes,
};
const { id, sent_at: sentAt } = nextMessageAttributes;
const previousIdsBySentAt = this.state.messageIdsBySentAt.get(sentAt);
let nextIdsBySentAtSet: Set<string>;
if (previousIdsBySentAt) {
nextIdsBySentAtSet = new Set(previousIdsBySentAt);
nextIdsBySentAtSet.add(id);
} else {
nextIdsBySentAtSet = new Set([id]);
}
this.state.messages.set(id, nextMessageAttributes);
this.state.lastAccessedAt.set(id, Date.now());
this.state.messageIdsBySender.set(
getSenderIdentifier(messageAttributes),
id
);
this.markModelStale(nextMessageAttributes);
if (window.reduxActions) {
window.reduxActions.conversations.messageChanged(
messageId,
nextMessageAttributes.conversationId,
nextMessageAttributes
);
}
if (skipSaveToDatabase) {
return;
}
drop(
window.Signal.Data.saveMessage(messageAttributes, {
ourAci: window.textsecure.storage.user.getCheckedAci(),
})
);
}
// When you already have the message attributes from the db and want to
// ensure that they're added to the cache. The latest attributes from cache
// are returned if they exist, if not the attributes passed in are returned.
public toMessageAttributes(
messageAttributes: MessageAttributesType
): Readonly<MessageAttributesType> {
this.addMessageToCache(messageAttributes);
const nextMessageAttributes = this.state.messages.get(messageAttributes.id);
strictAssert(
nextMessageAttributes,
`MessageCache.toMessageAttributes: no message for id ${messageAttributes.id}`
);
if (getEnvironment() === Environment.Development) {
return Object.freeze(cloneDeep(nextMessageAttributes));
}
return nextMessageAttributes;
}
static install(): MessageCache {
const instance = new MessageCache();
window.MessageCache = instance;
return instance;
}
private addMessageToCache(messageAttributes: MessageAttributesType): void {
if (!messageAttributes.id) {
return;
}
if (this.state.messages.has(messageAttributes.id)) {
this.state.lastAccessedAt.set(messageAttributes.id, Date.now());
return;
}
const { id, sent_at: sentAt } = messageAttributes;
const previousIdsBySentAt = this.state.messageIdsBySentAt.get(sentAt);
let nextIdsBySentAtSet: Set<string>;
if (previousIdsBySentAt) {
nextIdsBySentAtSet = new Set(previousIdsBySentAt);
nextIdsBySentAtSet.add(id);
} else {
nextIdsBySentAtSet = new Set([id]);
}
this.state.messages.set(messageAttributes.id, { ...messageAttributes });
this.state.lastAccessedAt.set(messageAttributes.id, Date.now());
this.state.messageIdsBySentAt.set(sentAt, Array.from(nextIdsBySentAtSet));
this.state.messageIdsBySender.set(
getSenderIdentifier(messageAttributes),
id
);
}
private freezeAttributes(
messageAttributes: MessageAttributesType
): Readonly<MessageAttributesType> {
this.addMessageToCache(messageAttributes);
if (getEnvironment() === Environment.Development) {
return Object.freeze(cloneDeep(messageAttributes));
}
return messageAttributes;
}
private removeMessage(messageId: string): void {
const messageAttributes = this.state.messages.get(messageId);
if (!messageAttributes) {
return;
}
const { id, sent_at: sentAt } = messageAttributes;
const nextIdsBySentAtSet =
new Set(this.state.messageIdsBySentAt.get(sentAt)) || new Set();
nextIdsBySentAtSet.delete(id);
if (nextIdsBySentAtSet.size) {
this.state.messageIdsBySentAt.set(sentAt, Array.from(nextIdsBySentAtSet));
} else {
this.state.messageIdsBySentAt.delete(sentAt);
}
this.state.messages.delete(messageId);
this.state.lastAccessedAt.delete(messageId);
this.state.messageIdsBySender.delete(
getSenderIdentifier(messageAttributes)
);
}
// Deprecated methods below
// Adds the message into the cache and eturns a Proxy that resembles
// a MessageModel
public __DEPRECATED$register(
id: string,
data: MessageModel | MessageAttributesType,
location: string
): MessageModel {
if (!id || !data) {
throw new Error(
'MessageCache.__DEPRECATED$register: Got falsey id or message'
);
}
const existing = this.__DEPRECATED$getById(id);
if (existing) {
this.addMessageToCache(existing.attributes);
return existing;
}
const modelProxy = this.toModel(data);
const messageAttributes = 'attributes' in data ? data.attributes : data;
this.addMessageToCache(messageAttributes);
modelProxy.registerLocations.add(location);
return modelProxy;
}
// Deletes the message from our cache
public __DEPRECATED$unregister(id: string): void {
const model = this.modelCache.get(id);
if (!model) {
return;
}
this.removeMessage(id);
this.modelCache.delete(id);
}
// Finds a message in the cache by Id
public __DEPRECATED$getById(id: string): MessageModel | undefined {
const data = this.state.messages.get(id);
if (!data) {
return undefined;
}
return this.toModel(data);
}
// Finds a message in the cache by sentAt/timestamp
public __DEPRECATED$filterBySentAt(sentAt: number): Iterable<MessageModel> {
const items = this.state.messageIdsBySentAt.get(sentAt) ?? [];
const attrs = items.map(id => this.accessAttributes(id)).filter(isNotNil);
return map(attrs, data => this.toModel(data));
}
// Marks cached model as "should be stale" to discourage continued use.
// The model's attributes are directly updated so that the model is in sync
// with the in-memory attributes.
private markModelStale(messageAttributes: MessageAttributesType): void {
const { id } = messageAttributes;
const model = this.modelCache.get(id);
if (!model) {
return;
}
model.attributes = { ...messageAttributes };
if (getEnvironment() === Environment.Development) {
log.warn('MessageCache: stale model', {
cid: model.cid,
locations: Array.from(model.registerLocations).join('+'),
});
}
}
// Creates a proxy object for MessageModel which logs usage in development
// so that we're able to migrate off of models
private toModel(
messageAttributes: MessageAttributesType | MessageModel
): MessageModel {
const existingModel = this.modelCache.get(messageAttributes.id);
if (existingModel) {
return existingModel;
}
const model =
'attributes' in messageAttributes
? messageAttributes
: new window.Whisper.Message(messageAttributes);
const proxy = getMessageModelLogger(model);
this.modelCache.set(messageAttributes.id, proxy);
return proxy;
}
}

View File

@ -33,9 +33,10 @@ class ExpiringMessagesDeletionService {
const inMemoryMessages: Array<MessageModel> = [];
messages.forEach(dbMessage => {
const message = window.MessageController.register(
const message = window.MessageCache.__DEPRECATED$register(
dbMessage.id,
dbMessage
dbMessage,
'destroyExpiredMessages'
);
messageIds.push(message.id);
inMemoryMessages.push(message);

View File

@ -0,0 +1,17 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as durations from '../util/durations';
import { isEnabled } from '../RemoteConfig';
import { MessageCache } from './MessageCache';
const TEN_MINUTES = 10 * durations.MINUTE;
export function initMessageCleanup(): void {
setInterval(
() => window.MessageCache.deleteExpiredMessages(TEN_MINUTES),
isEnabled('desktop.messageCleanup') ? TEN_MINUTES : durations.HOUR
);
MessageCache.install();
}

View File

@ -15,7 +15,11 @@ async function eraseTapToViewMessages() {
await window.Signal.Data.getTapToViewMessagesNeedingErase();
await Promise.all(
messages.map(async fromDB => {
const message = window.MessageController.register(fromDB.id, fromDB);
const message = window.MessageCache.__DEPRECATED$register(
fromDB.id,
fromDB,
'eraseTapToViewMessages'
);
window.SignalContext.log.info(
'eraseTapToViewMessages: erasing message contents',

View File

@ -18,8 +18,6 @@ import { ConfirmationDialog } from './components/ConfirmationDialog';
import { createApp } from './state/roots/createApp';
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
import { createStore } from './state/createStore';
// Types
import * as TypesAttachment from './types/Attachment';
import * as VisualAttachment from './types/VisualAttachment';
@ -379,7 +377,6 @@ export const setup = (options: {
};
const State = {
createStore,
Roots,
};

View File

@ -67,7 +67,7 @@ import { resolveAttachmentDraftData } from '../../util/resolveAttachmentDraftDat
import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk';
import { shouldShowInvalidMessageToast } from '../../util/shouldShowInvalidMessageToast';
import { writeDraftAttachment } from '../../util/writeDraftAttachment';
import { getMessageById } from '../../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
import { canReply } from '../selectors/message';
import { getContactId } from '../../messages/helpers';
import { getConversationSelector } from '../selectors/conversations';
@ -747,7 +747,9 @@ export function setQuoteByMessageId(
return;
}
const message = messageId ? await getMessageById(messageId) : undefined;
const message = messageId
? await __DEPRECATED$getMessageById(messageId)
: undefined;
const state = getState();
if (

View File

@ -137,7 +137,7 @@ import {
buildUpdateAttributesChange,
initiateMigrationToGroupV2 as doInitiateMigrationToGroupV2,
} from '../../groups';
import { getMessageById } from '../../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
import type { PanelRenderType, PanelRequestType } from '../../types/Panels';
import type { ConversationQueueJobData } from '../../jobs/conversationJobQueue';
import { isOlderThan } from '../../util/timestamp';
@ -1329,7 +1329,7 @@ function markMessageRead(
return;
}
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
throw new Error(`markMessageRead: failed to load message ${messageId}`);
}
@ -1674,7 +1674,7 @@ function deleteMessages({
await Promise.all(
messageIds.map(async messageId => {
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
throw new Error(`deleteMessages: Message ${messageId} missing!`);
}
@ -1778,7 +1778,7 @@ function setMessageToEdit(
return;
}
const message = (await getMessageById(messageId))?.attributes;
const message = (await __DEPRECATED$getMessageById(messageId))?.attributes;
if (!message) {
return;
}
@ -1855,7 +1855,7 @@ function generateNewGroupLink(
* replace it with an actual action that fits in with the redux approach.
*/
export const markViewed = (messageId: string): void => {
const message = window.MessageController.getById(messageId);
const message = window.MessageCache.__DEPRECATED$getById(messageId);
if (!message) {
throw new Error(`markViewed: Message ${messageId} missing!`);
}
@ -2126,7 +2126,7 @@ function kickOffAttachmentDownload(
options: Readonly<{ messageId: string }>
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => {
const message = await getMessageById(options.messageId);
const message = await __DEPRECATED$getMessageById(options.messageId);
if (!message) {
throw new Error(
`kickOffAttachmentDownload: Message ${options.messageId} missing!`
@ -2158,7 +2158,7 @@ function markAttachmentAsCorrupted(
options: AttachmentOptions
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => {
const message = await getMessageById(options.messageId);
const message = await __DEPRECATED$getMessageById(options.messageId);
if (!message) {
throw new Error(
`markAttachmentAsCorrupted: Message ${options.messageId} missing!`
@ -2177,7 +2177,7 @@ function openGiftBadge(
messageId: string
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
return async dispatch => {
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
throw new Error(`openGiftBadge: Message ${messageId} missing!`);
}
@ -2197,7 +2197,7 @@ function retryMessageSend(
messageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => {
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
throw new Error(`retryMessageSend: Message ${messageId} missing!`);
}
@ -2214,7 +2214,7 @@ export function copyMessageText(
messageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => {
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
throw new Error(`copy: Message ${messageId} missing!`);
}
@ -2233,7 +2233,7 @@ export function retryDeleteForEveryone(
messageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => {
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
throw new Error(`retryDeleteForEveryone: Message ${messageId} missing!`);
}
@ -2737,7 +2737,7 @@ function conversationStoppedByMissingVerification(payload: {
};
}
function messageChanged(
export function messageChanged(
id: string,
conversationId: string,
data: MessageAttributesType
@ -2962,7 +2962,7 @@ function pushPanelForConversation(
const message =
state.conversations.messagesLookup[messageId] ||
(await getMessageById(messageId))?.attributes;
(await __DEPRECATED$getMessageById(messageId))?.attributes;
if (!message) {
throw new Error(
'pushPanelForConversation: could not find message for MessageDetails'
@ -3038,7 +3038,7 @@ function deleteMessagesForEveryone(
await Promise.all(
messageIds.map(async messageId => {
try {
const message = window.MessageController.getById(messageId);
const message = window.MessageCache.__DEPRECATED$getById(messageId);
if (!message) {
throw new Error(
`deleteMessageForEveryone: Message ${messageId} missing!`
@ -3394,7 +3394,11 @@ function loadRecentMediaItems(
// Cache these messages in memory to ensure Lightbox can find them
messages.forEach(message => {
window.MessageController.register(message.id, message);
window.MessageCache.__DEPRECATED$register(
message.id,
message,
'loadRecentMediaItems'
);
});
const recentMediaItems = messages
@ -3492,7 +3496,7 @@ export function saveAttachmentFromMessage(
providedAttachment?: AttachmentType
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
return async (dispatch, getState) => {
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
throw new Error(
`saveAttachmentFromMessage: Message ${messageId} missing!`
@ -3585,7 +3589,7 @@ export function scrollToMessage(
throw new Error('scrollToMessage: No conversation found');
}
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
throw new Error(`scrollToMessage: failed to load message ${messageId}`);
}
@ -3599,7 +3603,7 @@ export function scrollToMessage(
let isInMemory = true;
if (!window.MessageController.getById(messageId)) {
if (!window.MessageCache.__DEPRECATED$getById(messageId)) {
isInMemory = false;
}
@ -4009,7 +4013,7 @@ function onConversationOpened(
conversation.onOpenStart();
if (messageId) {
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (message) {
drop(conversation.loadAndScroll(messageId));
@ -4138,7 +4142,7 @@ function showArchivedConversations(): ShowArchivedConversationsActionType {
}
function doubleCheckMissingQuoteReference(messageId: string): NoopActionType {
const message = window.MessageController.getById(messageId);
const message = window.MessageCache.__DEPRECATED$getById(messageId);
if (message) {
void message.doubleCheckMissingQuoteReference();
}

View File

@ -21,7 +21,6 @@ import * as Errors from '../../types/errors';
import * as SingleServePromise from '../../services/singleServePromise';
import * as Stickers from '../../types/Stickers';
import * as log from '../../logging/log';
import { getMessageById } from '../../messages/getMessageById';
import { getMessagePropsSelector } from '../selectors/message';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper';
@ -559,15 +558,12 @@ function toggleForwardMessagesModal(
const messagesProps = await Promise.all(
messageIds.map(async messageId => {
const message = await getMessageById(messageId);
const messageAttributes = await window.MessageCache.resolveAttributes(
'toggleForwardMessagesModal',
messageId
);
if (!message) {
throw new Error(
`toggleForwardMessagesModal: no message found for ${messageId}`
);
}
const attachments = message.get('attachments') ?? [];
const { attachments = [] } = messageAttributes;
if (!attachments.every(isDownloaded)) {
dispatch(
@ -576,7 +572,7 @@ function toggleForwardMessagesModal(
}
const messagePropsSelector = getMessagePropsSelector(getState());
const messageProps = messagePropsSelector(message.attributes);
const messageProps = messagePropsSelector(messageAttributes);
return messageProps;
})
@ -765,14 +761,10 @@ function showEditHistoryModal(
messageId: string
): ThunkAction<void, RootStateType, unknown, ShowEditHistoryModalActionType> {
return async dispatch => {
const message = await getMessageById(messageId);
if (!message) {
log.warn('showEditHistoryModal: no message found');
return;
}
const messageAttributes = message.attributes;
const messageAttributes = await window.MessageCache.resolveAttributes(
'showEditHistoryModal',
messageId
);
const nextEditHistoryMessages =
copyOverMessageAttributesIntoEditHistory(messageAttributes);

View File

@ -17,7 +17,7 @@ import type { ShowToastActionType } from './toast';
import type { StateType as RootStateType } from '../reducer';
import * as log from '../../logging/log';
import { getMessageById } from '../../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
import type { MessageAttributesType } from '../../model-types.d';
import { isGIF } from '../../types/Attachment';
import {
@ -137,7 +137,7 @@ function showLightboxForViewOnceMedia(
return async dispatch => {
log.info('showLightboxForViewOnceMedia: attempting to display message');
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
throw new Error(
`showLightboxForViewOnceMedia: Message ${messageId} missing!`
@ -232,7 +232,7 @@ function showLightbox(opts: {
return async (dispatch, getState) => {
const { attachment, messageId } = opts;
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
throw new Error(`showLightbox: Message ${messageId} missing!`);
}
@ -373,7 +373,7 @@ function showLightboxForAdjacentMessage(
sent_at: sentAt,
} = media.message;
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
log.warn('showLightboxForAdjacentMessage: original message is gone');
dispatch({

View File

@ -101,7 +101,11 @@ function loadMediaItems(
await Promise.all(
rawMedia.map(async message => {
const { schemaVersion } = message;
const model = window.MessageController.register(message.id, message);
const model = window.MessageCache.__DEPRECATED$register(
message.id,
message,
'loadMediaItems'
);
if (schemaVersion && schemaVersion < VERSION_NEEDED_FOR_DISPLAY) {
const upgradedMsgAttributes = await upgradeMessageSchema(message);

View File

@ -36,7 +36,7 @@ import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUnti
import { deleteStoryForEveryone as doDeleteStoryForEveryone } from '../../util/deleteStoryForEveryone';
import { deleteGroupStoryReplyForEveryone as doDeleteGroupStoryReplyForEveryone } from '../../util/deleteGroupStoryReplyForEveryone';
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
import { getMessageById } from '../../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
import { markOnboardingStoryAsRead } from '../../util/markOnboardingStoryAsRead';
import { markViewed } from '../../services/MessageUpdater';
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
@ -387,7 +387,7 @@ function markStoryRead(
return;
}
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
log.warn(`markStoryRead: no message found ${messageId}`);
@ -520,7 +520,7 @@ function queueStoryDownload(
return;
}
const message = await getMessageById(storyId);
const message = await __DEPRECATED$getMessageById(storyId);
if (message) {
// We want to ensure that we re-hydrate the story reply context with the
@ -1395,7 +1395,7 @@ function removeAllContactStories(
const messages = (
await Promise.all(
messageIds.map(async messageId => {
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
log.warn(`${logId}: no message found ${messageId}`);

View File

@ -0,0 +1,98 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { bindActionCreators } from 'redux';
import type { BadgesStateType } from './ducks/badges';
import type { CallHistoryDetails } from '../types/CallDisposition';
import type { MainWindowStatsType } from '../windows/context';
import type { MenuOptionsType } from '../types/menu';
import type { StoryDataType } from './ducks/stories';
import type { StoryDistributionListDataType } from './ducks/storyDistributionLists';
import { actionCreators } from './actions';
import { createStore } from './createStore';
import { getInitialState } from './getInitialState';
export function initializeRedux({
callsHistory,
initialBadgesState,
mainWindowStats,
menuOptions,
stories,
storyDistributionLists,
}: {
callsHistory: ReadonlyArray<CallHistoryDetails>;
initialBadgesState: BadgesStateType;
mainWindowStats: MainWindowStatsType;
menuOptions: MenuOptionsType;
stories: Array<StoryDataType>;
storyDistributionLists: Array<StoryDistributionListDataType>;
}): void {
const initialState = getInitialState({
badges: initialBadgesState,
callsHistory,
mainWindowStats,
menuOptions,
stories,
storyDistributionLists,
});
const store = createStore(initialState);
window.reduxStore = store;
// Binding these actions to our redux store and exposing them allows us to update
// redux when things change in the backbone world.
window.reduxActions = {
accounts: bindActionCreators(actionCreators.accounts, store.dispatch),
app: bindActionCreators(actionCreators.app, store.dispatch),
audioPlayer: bindActionCreators(actionCreators.audioPlayer, store.dispatch),
audioRecorder: bindActionCreators(
actionCreators.audioRecorder,
store.dispatch
),
badges: bindActionCreators(actionCreators.badges, store.dispatch),
callHistory: bindActionCreators(actionCreators.callHistory, store.dispatch),
calling: bindActionCreators(actionCreators.calling, store.dispatch),
composer: bindActionCreators(actionCreators.composer, store.dispatch),
conversations: bindActionCreators(
actionCreators.conversations,
store.dispatch
),
crashReports: bindActionCreators(
actionCreators.crashReports,
store.dispatch
),
inbox: bindActionCreators(actionCreators.inbox, store.dispatch),
emojis: bindActionCreators(actionCreators.emojis, store.dispatch),
expiration: bindActionCreators(actionCreators.expiration, store.dispatch),
globalModals: bindActionCreators(
actionCreators.globalModals,
store.dispatch
),
items: bindActionCreators(actionCreators.items, store.dispatch),
lightbox: bindActionCreators(actionCreators.lightbox, store.dispatch),
linkPreviews: bindActionCreators(
actionCreators.linkPreviews,
store.dispatch
),
mediaGallery: bindActionCreators(
actionCreators.mediaGallery,
store.dispatch
),
network: bindActionCreators(actionCreators.network, store.dispatch),
safetyNumber: bindActionCreators(
actionCreators.safetyNumber,
store.dispatch
),
search: bindActionCreators(actionCreators.search, store.dispatch),
stickers: bindActionCreators(actionCreators.stickers, store.dispatch),
stories: bindActionCreators(actionCreators.stories, store.dispatch),
storyDistributionLists: bindActionCreators(
actionCreators.storyDistributionLists,
store.dispatch
),
toast: bindActionCreators(actionCreators.toast, store.dispatch),
updates: bindActionCreators(actionCreators.updates, store.dispatch),
user: bindActionCreators(actionCreators.user, store.dispatch),
username: bindActionCreators(actionCreators.username, store.dispatch),
};
}

View File

@ -19,7 +19,6 @@ import {
} from '../selectors/conversations';
import { getIntl, getTheme, getRegionCode } from '../selectors/user';
import { getLinkPreview } from '../selectors/linkPreviews';
import { getMessageById } from '../../messages/getMessageById';
import { getPreferredBadgeSelector } from '../selectors/badges';
import type {
ForwardMessageData,
@ -36,6 +35,8 @@ import { SmartCompositionTextArea } from './CompositionTextArea';
import { useToastActions } from '../ducks/toast';
import { hydrateRanges } from '../../types/BodyRange';
import { isDownloaded } from '../../types/Attachment';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
import { strictAssert } from '../../util/assert';
function toMessageForwardDraft(
props: ForwardMessagePropsType,
@ -119,10 +120,10 @@ function SmartForwardMessagesModalInner({
try {
const messages = await Promise.all(
finalDrafts.map(async (draft): Promise<ForwardMessageData> => {
const message = await getMessageById(draft.originalMessageId);
if (message == null) {
throw new Error('No message found');
}
const message = await __DEPRECATED$getMessageById(
draft.originalMessageId
);
strictAssert(message, 'no message found');
return {
draft,
originalMessage: message.attributes,

View File

@ -83,7 +83,11 @@ describe('Conversations', () => {
forceSave: true,
ourAci,
});
message = window.MessageController.register(message.id, message);
message = window.MessageCache.__DEPRECATED$register(
message.id,
message,
'test'
);
await window.Signal.Data.updateConversation(conversation.attributes);
await conversation.updateLastMessage();

View File

@ -5,17 +5,30 @@ import { assert } from 'chai';
import * as sinon from 'sinon';
import { v4 as generateUuid } from 'uuid';
import { setupI18n } from '../../util/setupI18n';
import type { AttachmentType } from '../../types/Attachment';
import type { CallbackResultType } from '../../textsecure/Types.d';
import type { ConversationModel } from '../../models/conversations';
import type { MessageAttributesType } from '../../model-types.d';
import type { MessageModel } from '../../models/messages';
import type { RawBodyRange } from '../../types/BodyRange';
import type { StorageAccessType } from '../../types/Storage.d';
import type { WebAPIType } from '../../textsecure/WebAPI';
import MessageSender from '../../textsecure/SendMessage';
import enMessages from '../../../_locales/en/messages.json';
import { SendStatus } from '../../messages/MessageSendState';
import MessageSender from '../../textsecure/SendMessage';
import type { WebAPIType } from '../../textsecure/WebAPI';
import type { CallbackResultType } from '../../textsecure/Types.d';
import type { StorageAccessType } from '../../types/Storage.d';
import { generateAci } from '../../types/ServiceId';
import { SignalService as Proto } from '../../protobuf';
import { generateAci } from '../../types/ServiceId';
import { getContact } from '../../messages/helpers';
import type { ConversationModel } from '../../models/conversations';
import { setupI18n } from '../../util/setupI18n';
import {
APPLICATION_JSON,
AUDIO_MP3,
IMAGE_GIF,
IMAGE_PNG,
LONG_MESSAGE,
TEXT_ATTACHMENT,
VIDEO_MP4,
} from '../../types/MIME';
describe('Message', () => {
const STORAGE_KEYS_TO_RESTORE: Array<keyof StorageAccessType> = [
@ -28,7 +41,7 @@ describe('Message', () => {
const i18n = setupI18n('en', enMessages);
const attributes = {
type: 'outgoing',
type: 'outgoing' as const,
body: 'hi',
conversationId: 'foo',
attachments: [],
@ -39,12 +52,12 @@ describe('Message', () => {
const me = '+14155555556';
const ourServiceId = generateAci();
function createMessage(attrs: { [key: string]: unknown }) {
const messages = new window.Whisper.MessageCollection();
return messages.add({
received_at: Date.now(),
function createMessage(attrs: Partial<MessageAttributesType>): MessageModel {
return new window.Whisper.Message({
id: generateUuid(),
...attrs,
});
received_at: Date.now(),
} as MessageAttributesType);
}
function createMessageAndGetNotificationData(attrs: {
@ -214,14 +227,12 @@ describe('Message', () => {
describe('getContact', () => {
it('gets outgoing contact', () => {
const messages = new window.Whisper.MessageCollection();
const message = messages.add(attributes);
const message = createMessage(attributes);
assert.exists(getContact(message.attributes));
});
it('gets incoming contact', () => {
const messages = new window.Whisper.MessageCollection();
const message = messages.add({
const message = createMessage({
type: 'incoming',
source,
});
@ -287,7 +298,8 @@ describe('Message', () => {
isErased: false,
attachments: [
{
contentType: 'image/png',
contentType: IMAGE_PNG,
size: 0,
},
],
}),
@ -302,7 +314,8 @@ describe('Message', () => {
isErased: false,
attachments: [
{
contentType: 'video/mp4',
contentType: VIDEO_MP4,
size: 0,
},
],
}),
@ -317,7 +330,8 @@ describe('Message', () => {
isErased: false,
attachments: [
{
contentType: 'text/plain',
contentType: LONG_MESSAGE,
size: 0,
},
],
}),
@ -482,7 +496,7 @@ describe('Message', () => {
createMessageAndGetNotificationData({
type: 'incoming',
source,
flags: true,
flags: 1,
}),
{ text: i18n('icu:sessionEnded') }
);
@ -493,17 +507,26 @@ describe('Message', () => {
createMessageAndGetNotificationData({
type: 'incoming',
source,
errors: [{}],
errors: [new Error()],
}),
{ text: i18n('icu:incomingError') }
);
});
const attachmentTestCases = [
const attachmentTestCases: Array<{
title: string;
attachment: AttachmentType;
expectedResult: {
text: string;
emoji: string;
bodyRanges?: Array<RawBodyRange>;
};
}> = [
{
title: 'GIF',
attachment: {
contentType: 'image/gif',
contentType: IMAGE_GIF,
size: 0,
},
expectedResult: {
text: 'GIF',
@ -514,7 +537,8 @@ describe('Message', () => {
{
title: 'photo',
attachment: {
contentType: 'image/png',
contentType: IMAGE_PNG,
size: 0,
},
expectedResult: {
text: 'Photo',
@ -525,7 +549,8 @@ describe('Message', () => {
{
title: 'video',
attachment: {
contentType: 'video/mp4',
contentType: VIDEO_MP4,
size: 0,
},
expectedResult: {
text: 'Video',
@ -536,8 +561,9 @@ describe('Message', () => {
{
title: 'voice message',
attachment: {
contentType: 'audio/ogg',
contentType: AUDIO_MP3,
flags: Proto.AttachmentPointer.Flags.VOICE_MESSAGE,
size: 0,
},
expectedResult: {
text: 'Voice Message',
@ -548,8 +574,9 @@ describe('Message', () => {
{
title: 'audio message',
attachment: {
contentType: 'audio/ogg',
fileName: 'audio.ogg',
contentType: AUDIO_MP3,
fileName: 'audio.mp3',
size: 0,
},
expectedResult: {
text: 'Audio Message',
@ -560,7 +587,8 @@ describe('Message', () => {
{
title: 'plain text',
attachment: {
contentType: 'text/plain',
contentType: LONG_MESSAGE,
size: 0,
},
expectedResult: {
text: 'File',
@ -571,7 +599,8 @@ describe('Message', () => {
{
title: 'unspecified-type',
attachment: {
contentType: null,
contentType: APPLICATION_JSON,
size: 0,
},
expectedResult: {
text: 'File',
@ -600,7 +629,8 @@ describe('Message', () => {
attachments: [
attachment,
{
contentType: 'text/html',
contentType: TEXT_ATTACHMENT,
size: 0,
},
],
}),
@ -671,7 +701,8 @@ describe('Message', () => {
source,
attachments: [
{
contentType: 'image/png',
contentType: IMAGE_PNG,
size: 0,
},
],
}).getNotificationText(),
@ -699,7 +730,8 @@ describe('Message', () => {
source,
attachments: [
{
contentType: 'image/png',
contentType: IMAGE_PNG,
size: 0,
},
],
}).getNotificationText(),
@ -708,26 +740,3 @@ describe('Message', () => {
});
});
});
describe('MessageCollection', () => {
it('should be ordered oldest to newest', () => {
const messages = new window.Whisper.MessageCollection();
// Timestamps
const today = Date.now();
const tomorrow = today + 12345;
// Add threads
messages.add({ received_at: today });
messages.add({ received_at: tomorrow });
const { models } = messages;
const firstTimestamp = models[0].get('received_at');
const secondTimestamp = models[1].get('received_at');
// Compare timestamps
assert(typeof firstTimestamp === 'number');
assert(typeof secondTimestamp === 'number');
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
assert(firstTimestamp! < secondTimestamp!);
});
});

View File

@ -0,0 +1,382 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { v4 as uuid } from 'uuid';
import type { MessageAttributesType } from '../../model-types.d';
import { MessageModel } from '../../models/messages';
import { strictAssert } from '../../util/assert';
import { MessageCache } from '../../services/MessageCache';
describe('MessageCache', () => {
describe('filterBySentAt', () => {
it('returns an empty iterable if no messages match', () => {
const mc = new MessageCache();
assert.isEmpty([...mc.__DEPRECATED$filterBySentAt(123)]);
});
it('returns all messages that match the timestamp', () => {
const mc = new MessageCache();
let message1 = new MessageModel({
conversationId: 'xyz',
body: 'message1',
id: uuid(),
received_at: 1,
sent_at: 1234,
timestamp: 9999,
type: 'incoming',
});
let message2 = new MessageModel({
conversationId: 'xyz',
body: 'message2',
id: uuid(),
received_at: 2,
sent_at: 1234,
timestamp: 9999,
type: 'outgoing',
});
const message3 = new MessageModel({
conversationId: 'xyz',
body: 'message3',
id: uuid(),
received_at: 3,
sent_at: 5678,
timestamp: 9999,
type: 'outgoing',
});
message1 = mc.__DEPRECATED$register(message1.id, message1, 'test');
message2 = mc.__DEPRECATED$register(message2.id, message2, 'test');
// We deliberately register this message twice for testing.
message2 = mc.__DEPRECATED$register(message2.id, message2, 'test');
mc.__DEPRECATED$register(message3.id, message3, 'test');
const filteredMessages = Array.from(
mc.__DEPRECATED$filterBySentAt(1234)
).map(x => x.attributes);
assert.deepEqual(
filteredMessages,
[message1.attributes, message2.attributes],
'first'
);
mc.__DEPRECATED$unregister(message2.id);
const filteredMessages2 = Array.from(
mc.__DEPRECATED$filterBySentAt(1234)
).map(x => x.attributes);
assert.deepEqual(filteredMessages2, [message1.attributes], 'second');
});
});
describe('__DEPRECATED$register: syncing with backbone', () => {
it('backbone to redux', () => {
const message1 = new MessageModel({
conversationId: 'xyz',
id: uuid(),
body: 'test1',
received_at: 1,
sent_at: Date.now(),
timestamp: Date.now(),
type: 'outgoing',
});
const messageFromController = window.MessageCache.__DEPRECATED$register(
message1.id,
message1,
'test'
);
assert.strictEqual(
message1,
messageFromController,
'same objects from mc.__DEPRECATED$register'
);
const messageById = window.MessageCache.__DEPRECATED$getById(message1.id);
assert.strictEqual(message1, messageById, 'same objects from mc.getById');
const messageInCache = window.MessageCache.accessAttributes(message1.id);
strictAssert(messageInCache, 'no message found');
assert.deepEqual(
message1.attributes,
messageInCache,
'same attributes as in cache'
);
message1.set({ body: 'test2' });
assert.equal(message1.attributes.body, 'test2', 'message model updated');
assert.equal(
messageById?.attributes.body,
'test2',
'old reference from messageById was updated'
);
assert.equal(
messageInCache.body,
'test1',
'old cache reference not updated'
);
const newMessageById = window.MessageCache.__DEPRECATED$getById(
message1.id
);
assert.deepEqual(
message1.attributes,
newMessageById?.attributes,
'same attributes from mc.getById (2)'
);
const newMessageInCache = window.MessageCache.accessAttributes(
message1.id
);
strictAssert(newMessageInCache, 'no message found');
assert.deepEqual(
message1.attributes,
newMessageInCache,
'same attributes as in cache (2)'
);
});
it('redux to backbone (working with models)', () => {
const message = new MessageModel({
conversationId: 'xyz',
id: uuid(),
body: 'test1',
received_at: 1,
sent_at: Date.now(),
timestamp: Date.now(),
type: 'outgoing',
});
window.MessageCache.toMessageAttributes(message.attributes);
const messageFromController = window.MessageCache.__DEPRECATED$register(
message.id,
message,
'test'
);
assert.notStrictEqual(
message,
messageFromController,
'mc.__DEPRECATED$register returns existing but it is not the same reference'
);
assert.deepEqual(
message.attributes,
messageFromController.attributes,
'mc.__DEPRECATED$register returns existing and is the same attributes'
);
messageFromController.set({ body: 'test2' });
assert.notEqual(
message.get('body'),
messageFromController.get('body'),
'new model is not equal to old model'
);
const messageInCache = window.MessageCache.accessAttributes(message.id);
strictAssert(messageInCache, 'no message found');
assert.equal(
messageFromController.get('body'),
messageInCache.body,
'new update is in cache'
);
assert.isUndefined(
messageFromController.get('storyReplyContext'),
'storyReplyContext is undefined'
);
window.MessageCache.setAttributes({
messageId: message.id,
messageAttributes: {
storyReplyContext: {
attachment: undefined,
authorAci: undefined,
messageId: 'test123',
},
},
skipSaveToDatabase: true,
});
// This works because we refresh the model whenever an attribute changes
// but this should log a warning.
assert.equal(
messageFromController.get('storyReplyContext')?.messageId,
'test123',
'storyReplyContext was updated (stale model)'
);
const newMessageFromController =
window.MessageCache.__DEPRECATED$register(message.id, message, 'test');
assert.equal(
newMessageFromController.get('storyReplyContext')?.messageId,
'test123',
'storyReplyContext was updated (not stale)'
);
});
it('redux to backbone (working with attributes)', () => {
it('sets the attributes and returns a fresh copy', () => {
const mc = new MessageCache();
const messageAttributes: MessageAttributesType = {
conversationId: uuid(),
id: uuid(),
received_at: 1,
sent_at: Date.now(),
timestamp: Date.now(),
type: 'incoming',
};
const messageModel = mc.__DEPRECATED$register(
messageAttributes.id,
messageAttributes,
'test/updateAttributes'
);
assert.deepEqual(
messageAttributes,
messageModel.attributes,
'initial attributes matches message model'
);
const proposedStoryReplyContext = {
attachment: undefined,
authorAci: undefined,
messageId: 'test123',
};
assert.notDeepEqual(
messageModel.attributes.storyReplyContext,
proposedStoryReplyContext,
'attributes were changed outside of the message model'
);
mc.setAttributes({
messageId: messageAttributes.id,
messageAttributes: {
storyReplyContext: proposedStoryReplyContext,
},
skipSaveToDatabase: true,
});
const nextMessageAttributes = mc.accessAttributesOrThrow(
'test',
messageAttributes.id
);
assert.notDeepEqual(
messageAttributes,
nextMessageAttributes,
'initial attributes are stale'
);
assert.notDeepEqual(
messageAttributes.storyReplyContext,
proposedStoryReplyContext,
'initial attributes are stale 2'
);
assert.deepEqual(
nextMessageAttributes.storyReplyContext,
proposedStoryReplyContext,
'fresh attributes match what was proposed'
);
assert.notStrictEqual(
nextMessageAttributes.storyReplyContext,
proposedStoryReplyContext,
'fresh attributes are not the same reference as proposed attributes'
);
assert.deepEqual(
messageModel.attributes,
nextMessageAttributes,
'model was updated'
);
assert.equal(
messageModel.get('storyReplyContext')?.messageId,
'test123',
'storyReplyContext in model is set correctly'
);
});
});
});
describe('accessAttributes', () => {
it('gets the attributes if they exist', () => {
const mc = new MessageCache();
const messageAttributes: MessageAttributesType = {
conversationId: uuid(),
id: uuid(),
received_at: 1,
sent_at: Date.now(),
timestamp: Date.now(),
type: 'incoming',
};
mc.toMessageAttributes(messageAttributes);
const accessAttributes = mc.accessAttributes(messageAttributes.id);
assert.deepEqual(
accessAttributes,
messageAttributes,
'attributes returned have the same values'
);
assert.notStrictEqual(
accessAttributes,
messageAttributes,
'attributes returned are not the same references'
);
const undefinedMessage = mc.accessAttributes(uuid());
assert.isUndefined(undefinedMessage, 'access did not find message');
});
});
describe('accessAttributesOrThrow', () => {
it('accesses the attributes or throws if they do not exist', () => {
const mc = new MessageCache();
const messageAttributes: MessageAttributesType = {
conversationId: uuid(),
id: uuid(),
received_at: 1,
sent_at: Date.now(),
timestamp: Date.now(),
type: 'incoming',
};
mc.toMessageAttributes(messageAttributes);
const accessAttributes = mc.accessAttributesOrThrow(
'tests.1',
messageAttributes.id
);
assert.deepEqual(
accessAttributes,
messageAttributes,
'attributes returned have the same values'
);
assert.notStrictEqual(
accessAttributes,
messageAttributes,
'attributes returned are not the same references'
);
assert.throws(() => {
mc.accessAttributesOrThrow('tests.2', uuid());
});
});
});
});

View File

@ -861,7 +861,11 @@ describe('both/state/ducks/stories', () => {
const storyId = generateUuid();
const messageAttributes = getStoryMessage(storyId);
window.MessageController.register(storyId, messageAttributes);
window.MessageCache.__DEPRECATED$register(
storyId,
messageAttributes,
'test'
);
const dispatch = sinon.spy();
await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null);
@ -883,7 +887,11 @@ describe('both/state/ducks/stories', () => {
],
};
window.MessageController.register(storyId, messageAttributes);
window.MessageCache.__DEPRECATED$register(
storyId,
messageAttributes,
'test'
);
const dispatch = sinon.spy();
await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null);
@ -905,7 +913,11 @@ describe('both/state/ducks/stories', () => {
],
};
window.MessageController.register(storyId, messageAttributes);
window.MessageCache.__DEPRECATED$register(
storyId,
messageAttributes,
'test'
);
const dispatch = sinon.spy();
await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null);
@ -947,7 +959,11 @@ describe('both/state/ducks/stories', () => {
},
});
window.MessageController.register(storyId, messageAttributes);
window.MessageCache.__DEPRECATED$register(
storyId,
messageAttributes,
'test'
);
const dispatch = sinon.spy();
await queueStoryDownload(storyId)(dispatch, getState, null);
@ -1004,7 +1020,11 @@ describe('both/state/ducks/stories', () => {
},
});
window.MessageController.register(storyId, messageAttributes);
window.MessageCache.__DEPRECATED$register(
storyId,
messageAttributes,
'test'
);
const dispatch = sinon.spy();
await queueStoryDownload(storyId)(dispatch, getState, null);

View File

@ -1,56 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { MessageModel } from '../../models/messages';
import { MessageController } from '../../util/MessageController';
describe('MessageController', () => {
describe('filterBySentAt', () => {
it('returns an empty iterable if no messages match', () => {
const mc = new MessageController();
assert.isEmpty([...mc.filterBySentAt(123)]);
});
it('returns all messages that match the timestamp', () => {
const mc = new MessageController();
const message1 = new MessageModel({
conversationId: 'xyz',
id: 'abc',
received_at: 1,
sent_at: 1234,
timestamp: 9999,
type: 'incoming',
});
const message2 = new MessageModel({
conversationId: 'xyz',
id: 'def',
received_at: 2,
sent_at: 1234,
timestamp: 9999,
type: 'outgoing',
});
const message3 = new MessageModel({
conversationId: 'xyz',
id: 'ignored',
received_at: 3,
sent_at: 5678,
timestamp: 9999,
type: 'outgoing',
});
mc.register(message1.id, message1);
mc.register(message2.id, message2);
// We deliberately register this message twice for testing.
mc.register(message2.id, message2);
mc.register(message3.id, message3);
assert.sameMembers([...mc.filterBySentAt(1234)], [message1, message2]);
mc.unregister(message2.id);
assert.sameMembers([...mc.filterBySentAt(1234)], [message1]);
});
});
});

View File

@ -13,7 +13,7 @@ import { generateStoryDistributionId } from '../../types/StoryDistributionId';
import type { App } from '../playwright';
import { Bootstrap } from '../bootstrap';
export const debug = createDebug('mock:test:edit');
export const debug = createDebug('mock:test:stories');
const IdentifierType = Proto.ManifestRecord.Identifier.Type;
@ -236,7 +236,10 @@ describe('story/messaging', function unknownContacts() {
}
const sentAt = new Date(time).valueOf();
debug('Contact sends reply to group story');
debug('Contact sends reply to group story', {
story: sentAt,
reply: sentAt + 1,
});
await contacts[0].sendRaw(
desktop,
{

View File

@ -1,143 +0,0 @@
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MessageModel } from '../models/messages';
import * as durations from './durations';
import * as log from '../logging/log';
import { map, filter } from './iterables';
import { isNotNil } from './isNotNil';
import type { MessageAttributesType } from '../model-types.d';
import { isEnabled } from '../RemoteConfig';
const FIVE_MINUTES = 5 * durations.MINUTE;
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, Set<string>>();
static install(): MessageController {
const instance = new MessageController();
window.MessageController = instance;
instance.startCleanupInterval();
return instance;
}
register(
id: string,
data: MessageModel | MessageAttributesType
): MessageModel {
if (!id || !data) {
throw new Error('MessageController.register: Got falsey id or message');
}
const existing = this.messageLookup[id];
if (existing) {
this.messageLookup[id] = {
message: existing.message,
timestamp: Date.now(),
};
return existing.message;
}
const message =
'attributes' in data ? data : new window.Whisper.Message(data);
this.messageLookup[id] = {
message,
timestamp: Date.now(),
};
const sentAt = message.get('sent_at');
const previousIdsBySentAt = this.msgIDsBySentAt.get(sentAt);
if (previousIdsBySentAt) {
previousIdsBySentAt.add(id);
} else {
this.msgIDsBySentAt.set(sentAt, new Set([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());
const sentAt = message.get('sent_at');
const idsBySentAt = this.msgIDsBySentAt.get(sentAt) || new Set();
idsBySentAt.delete(id);
if (!idsBySentAt.size) {
this.msgIDsBySentAt.delete(sentAt);
}
}
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;
}
filterBySentAt(sentAt: number): Iterable<MessageModel> {
const ids = this.msgIDsBySentAt.get(sentAt) || [];
const maybeMessages = map(ids, id => this.getById(id));
return filter(maybeMessages, isNotNil);
}
findBySender(sender: string): MessageModel | undefined {
const id = this.msgIDsBySender.get(sender);
if (!id) {
return undefined;
}
return this.getById(id);
}
update(predicate: (message: MessageModel) => void): void {
const values = Object.values(this.messageLookup);
log.info(
`MessageController.update: About to process ${values.length} messages`
);
values.forEach(({ message }) => predicate(message));
}
_get(): LookupType {
return this.messageLookup;
}
startCleanupInterval(): NodeJS.Timeout | number {
return setInterval(
this.cleanup.bind(this),
isEnabled('desktop.messageCleanup') ? FIVE_MINUTES : durations.HOUR
);
}
}

View File

@ -0,0 +1,55 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MessageModel } from '../models/messages';
import * as log from '../logging/log';
import { getEnvironment, Environment } from '../environment';
export function getMessageModelLogger(model: MessageModel): MessageModel {
const { id } = model;
if (getEnvironment() !== Environment.Development) {
return model;
}
const proxyHandler: ProxyHandler<MessageModel> = {
get(target: MessageModel, property: keyof MessageModel) {
// Allowed set of attributes & methods
if (property === 'attributes') {
return model.attributes;
}
if (property === 'id') {
return id;
}
if (property === 'get') {
return model.get.bind(model);
}
if (property === 'set') {
return model.set.bind(model);
}
if (property === 'registerLocations') {
return target.registerLocations;
}
// Disallowed set of methods & attributes
log.warn(`MessageModelLogger: model.${property}`, new Error().stack);
if (typeof target[property] === 'function') {
return target[property].bind(target);
}
if (typeof target[property] !== 'undefined') {
return target[property];
}
return undefined;
},
};
return new Proxy(model, proxyHandler);
}

View File

@ -883,12 +883,13 @@ async function saveCallHistory(
});
log.info('saveCallHistory: Saved call history message:', id);
const model = window.MessageController.register(
const model = window.MessageCache.__DEPRECATED$register(
id,
new window.Whisper.Message({
...message,
id,
})
}),
'callDisposition'
);
if (callHistory.direction === CallDirection.Outgoing) {
@ -986,7 +987,7 @@ export async function clearCallHistoryDataAndSync(): Promise<void> {
const messageIds = await window.Signal.Data.clearCallHistory(timestamp);
messageIds.forEach(messageId => {
const message = window.MessageController.getById(messageId);
const message = window.MessageCache.__DEPRECATED$getById(messageId);
const conversation = message?.getConversation();
if (message == null || conversation == null) {
return;
@ -996,7 +997,7 @@ export async function clearCallHistoryDataAndSync(): Promise<void> {
message.get('conversationId')
);
conversation.debouncedUpdateLastMessage();
window.MessageController.unregister(messageId);
window.MessageCache.__DEPRECATED$unregister(messageId);
});
const ourAci = window.textsecure.storage.user.getCheckedAci();

View File

@ -25,7 +25,7 @@ export function cleanupMessageFromMemory(message: MessageAttributesType): void {
const parentConversation = window.ConversationController.get(conversationId);
parentConversation?.debouncedUpdateLastMessage();
window.MessageController.unregister(id);
window.MessageCache.__DEPRECATED$unregister(id);
}
async function cleanupStoryReplies(
@ -72,9 +72,10 @@ async function cleanupStoryReplies(
// Cleanup all group replies
await Promise.all(
replies.map(reply => {
const replyMessageModel = window.MessageController.register(
const replyMessageModel = window.MessageCache.__DEPRECATED$register(
reply.id,
reply
reply,
'cleanupStoryReplies/group'
);
return replyMessageModel.eraseContents();
})
@ -83,7 +84,11 @@ async function cleanupStoryReplies(
// Refresh the storyReplyContext data for 1:1 conversations
await Promise.all(
replies.map(async reply => {
const model = window.MessageController.register(reply.id, reply);
const model = window.MessageCache.__DEPRECATED$register(
reply.id,
reply,
'cleanupStoryReplies/1:1'
);
model.unset('storyReplyContext');
await model.hydrateStoryContext(story, { shouldSave: true });
})

View File

@ -3,13 +3,13 @@
import { DAY } from './durations';
import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage';
import { getMessageById } from '../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
import * as log from '../logging/log';
export async function deleteGroupStoryReplyForEveryone(
replyMessageId: string
): Promise<void> {
const messageModel = await getMessageById(replyMessageId);
const messageModel = await __DEPRECATED$getMessageById(replyMessageId);
if (!messageModel) {
log.warn(

View File

@ -19,7 +19,7 @@ import {
import { onStoryRecipientUpdate } from './onStoryRecipientUpdate';
import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage';
import { isGroupV2 } from './whatTypeOfConversation';
import { getMessageById } from '../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
import { strictAssert } from './assert';
import { repeat, zipObject } from './iterables';
import { isOlderThan } from './timestamp';
@ -46,7 +46,7 @@ export async function deleteStoryForEveryone(
}
const logId = `deleteStoryForEveryone(${story.messageId})`;
const message = await getMessageById(story.messageId);
const message = await __DEPRECATED$getMessageById(story.messageId);
if (!message) {
throw new Error('Story not found');
}

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import * as log from '../logging/log';
import { getMessageById } from '../messages/getMessageById';
import { calculateExpirationTimestamp } from './expirationTimer';
import { DAY } from './durations';
@ -16,23 +15,23 @@ export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
}
const hasExpired = await (async () => {
for (const id of existingOnboardingStoryMessageIds) {
// eslint-disable-next-line no-await-in-loop
const message = await getMessageById(id);
if (!message) {
continue;
}
const [storyId] = existingOnboardingStoryMessageIds;
try {
const messageAttributes = await window.MessageCache.resolveAttributes(
'findAndDeleteOnboardingStoryIfExists',
storyId
);
const expires = calculateExpirationTimestamp(message.attributes) ?? 0;
const expires = calculateExpirationTimestamp(messageAttributes) ?? 0;
const now = Date.now();
const isExpired = expires < now;
const needsRepair = expires > now + 2 * DAY;
return isExpired || needsRepair;
} catch {
return true;
}
return true;
})();
if (!hasExpired) {

View File

@ -31,7 +31,8 @@ export async function findStoryMessages(
const ourConversationId =
window.ConversationController.getOurConversationIdOrThrow();
const inMemoryMessages = window.MessageController.filterBySentAt(sentAt);
const inMemoryMessages =
window.MessageCache.__DEPRECATED$filterBySentAt(sentAt);
const matchingMessages = [
...filter(inMemoryMessages, item =>
isStoryAMatch(
@ -60,7 +61,11 @@ export async function findStoryMessages(
}
const result = found.map(attributes =>
window.MessageController.register(attributes.id, attributes)
window.MessageCache.__DEPRECATED$register(
attributes.id,
attributes,
'findStoryMessages'
)
);
return result;
}

View File

@ -0,0 +1,50 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type {
ConversationAttributesType,
MessageAttributesType,
} from '../model-types.d';
import { isIncoming, isOutgoing } from '../state/selectors/message';
import { getTitle } from './getTitle';
function getIncomingContact(
messageAttributes: MessageAttributesType
): ConversationAttributesType | undefined {
if (!isIncoming(messageAttributes)) {
return undefined;
}
const { sourceServiceId } = messageAttributes;
if (!sourceServiceId) {
return undefined;
}
return window.ConversationController.getOrCreate(sourceServiceId, 'private')
.attributes;
}
export function getMessageAuthorText(
messageAttributes?: MessageAttributesType
): string | undefined {
if (!messageAttributes) {
return undefined;
}
// if it's outgoing, it must be self-authored
const selfAuthor = isOutgoing(messageAttributes)
? window.i18n('icu:you')
: undefined;
if (selfAuthor) {
return selfAuthor;
}
const incomingContact = getIncomingContact(messageAttributes);
if (incomingContact) {
return getTitle(incomingContact, { isShort: true });
}
// if it's not selfAuthor and there's no incoming contact,
// it might be a group notification, so we return undefined
return undefined;
}

View File

@ -0,0 +1,13 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MessageAttributesType } from '../model-types.d';
import type { ConversationModel } from '../models/conversations';
export function getMessageConversation({
conversationId,
}: Pick<MessageAttributesType, 'conversationId'>):
| ConversationModel
| undefined {
return window.ConversationController.get(conversationId);
}

View File

@ -0,0 +1,452 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { RawBodyRange } from '../types/BodyRange';
import type { MessageAttributesType } from '../model-types.d';
import type { ReplacementValuesType } from '../types/I18N';
import * as Attachment from '../types/Attachment';
import * as EmbeddedContact from '../types/EmbeddedContact';
import * as GroupChange from '../groupChange';
import * as MIME from '../types/MIME';
import * as Stickers from '../types/Stickers';
import * as expirationTimer from './expirationTimer';
import * as log from '../logging/log';
import { GiftBadgeStates } from '../components/conversation/Message';
import { dropNull } from './dropNull';
import { getCallHistorySelector } from '../state/selectors/callHistory';
import { getCallSelector, getActiveCall } from '../state/selectors/calling';
import { getCallingNotificationText } from './callingNotification';
import { getConversationSelector } from '../state/selectors/conversations';
import { getStringForConversationMerge } from './getStringForConversationMerge';
import { getStringForProfileChange } from './getStringForProfileChange';
import { getTitleNoDefault, getNumber } from './getTitle';
import { findAndFormatContact } from './findAndFormatContact';
import { isMe } from './whatTypeOfConversation';
import { strictAssert } from './assert';
import {
getPropsForCallHistory,
hasErrors,
isCallHistory,
isChatSessionRefreshed,
isDeliveryIssue,
isEndSession,
isExpirationTimerUpdate,
isGroupUpdate,
isGroupV1Migration,
isGroupV2Change,
isIncoming,
isKeyChange,
isOutgoing,
isProfileChange,
isTapToView,
isUnsupportedMessage,
isConversationMerge,
} from '../state/selectors/message';
import {
getContact,
messageHasPaymentEvent,
getPaymentEventNotificationText,
} from '../messages/helpers';
function getNameForNumber(e164: string): string {
const conversation = window.ConversationController.get(e164);
if (!conversation) {
return e164;
}
return conversation.getTitle();
}
export function getNotificationDataForMessage(
attributes: MessageAttributesType
): {
bodyRanges?: ReadonlyArray<RawBodyRange>;
emoji?: string;
text: string;
} {
if (isDeliveryIssue(attributes)) {
return {
emoji: '⚠️',
text: window.i18n('icu:DeliveryIssue--preview'),
};
}
if (isConversationMerge(attributes)) {
const conversation = window.ConversationController.get(
attributes.conversationId
);
strictAssert(
conversation,
'getNotificationData/isConversationMerge/conversation'
);
strictAssert(
attributes.conversationMerge,
'getNotificationData/isConversationMerge/conversationMerge'
);
return {
text: getStringForConversationMerge({
obsoleteConversationTitle: getTitleNoDefault(
attributes.conversationMerge.renderInfo
),
obsoleteConversationNumber: getNumber(
attributes.conversationMerge.renderInfo
),
conversationTitle: conversation.getTitle(),
i18n: window.i18n,
}),
};
}
if (isChatSessionRefreshed(attributes)) {
return {
emoji: '🔁',
text: window.i18n('icu:ChatRefresh--notification'),
};
}
if (isUnsupportedMessage(attributes)) {
return {
text: window.i18n('icu:message--getDescription--unsupported-message'),
};
}
if (isGroupV1Migration(attributes)) {
return {
text: window.i18n('icu:GroupV1--Migration--was-upgraded'),
};
}
if (isProfileChange(attributes)) {
const { profileChange: change, changedId } = attributes;
const changedContact = findAndFormatContact(changedId);
if (!change) {
throw new Error('getNotificationData: profileChange was missing!');
}
return {
text: getStringForProfileChange(change, changedContact, window.i18n),
};
}
if (isGroupV2Change(attributes)) {
const { groupV2Change: change } = attributes;
strictAssert(
change,
'getNotificationData: isGroupV2Change true, but no groupV2Change!'
);
const changes = GroupChange.renderChange<string>(change, {
i18n: window.i18n,
ourAci: window.textsecure.storage.user.getCheckedAci(),
ourPni: window.textsecure.storage.user.getCheckedPni(),
renderContact: (conversationId: string) => {
const conversation = window.ConversationController.get(conversationId);
return conversation
? conversation.getTitle()
: window.i18n('icu:unknownContact');
},
renderString: (
key: string,
_i18n: unknown,
components: ReplacementValuesType<string | number> | undefined
) => {
// eslint-disable-next-line local-rules/valid-i18n-keys
return window.i18n(key, components);
},
});
return { text: changes.map(({ text }) => text).join(' ') };
}
if (messageHasPaymentEvent(attributes)) {
const sender = findAndFormatContact(attributes.sourceServiceId);
const conversation = findAndFormatContact(attributes.conversationId);
return {
text: getPaymentEventNotificationText(
attributes.payment,
sender.title,
conversation.title,
sender.isMe,
window.i18n
),
emoji: '💳',
};
}
const { attachments = [] } = attributes;
if (isTapToView(attributes)) {
if (attributes.isErased) {
return {
text: window.i18n('icu:message--getDescription--disappearing-media'),
};
}
if (Attachment.isImage(attachments)) {
return {
text: window.i18n('icu:message--getDescription--disappearing-photo'),
emoji: '📷',
};
}
if (Attachment.isVideo(attachments)) {
return {
text: window.i18n('icu:message--getDescription--disappearing-video'),
emoji: '🎥',
};
}
// There should be an image or video attachment, but we have a fallback just in
// case.
return { text: window.i18n('icu:mediaMessage'), emoji: '📎' };
}
if (isGroupUpdate(attributes)) {
const { group_update: groupUpdate } = attributes;
const fromContact = getContact(attributes);
const messages = [];
if (!groupUpdate) {
throw new Error('getNotificationData: Missing group_update');
}
if (groupUpdate.left === 'You') {
return { text: window.i18n('icu:youLeftTheGroup') };
}
if (groupUpdate.left) {
return {
text: window.i18n('icu:leftTheGroup', {
name: getNameForNumber(groupUpdate.left),
}),
};
}
if (!fromContact) {
return { text: '' };
}
if (isMe(fromContact.attributes)) {
messages.push(window.i18n('icu:youUpdatedTheGroup'));
} else {
messages.push(
window.i18n('icu:updatedTheGroup', {
name: fromContact.getTitle(),
})
);
}
if (groupUpdate.joined && groupUpdate.joined.length) {
const joinedContacts = groupUpdate.joined.map(item =>
window.ConversationController.getOrCreate(item, 'private')
);
const joinedWithoutMe = joinedContacts.filter(
contact => !isMe(contact.attributes)
);
if (joinedContacts.length > 1) {
messages.push(
window.i18n('icu:multipleJoinedTheGroup', {
names: joinedWithoutMe
.map(contact => contact.getTitle())
.join(', '),
})
);
if (joinedWithoutMe.length < joinedContacts.length) {
messages.push(window.i18n('icu:youJoinedTheGroup'));
}
} else {
const joinedContact = window.ConversationController.getOrCreate(
groupUpdate.joined[0],
'private'
);
if (isMe(joinedContact.attributes)) {
messages.push(window.i18n('icu:youJoinedTheGroup'));
} else {
messages.push(
window.i18n('icu:joinedTheGroup', {
name: joinedContacts[0].getTitle(),
})
);
}
}
}
if (groupUpdate.name) {
messages.push(
window.i18n('icu:titleIsNow', {
name: groupUpdate.name,
})
);
}
if (groupUpdate.avatarUpdated) {
messages.push(window.i18n('icu:updatedGroupAvatar'));
}
return { text: messages.join(' ') };
}
if (isEndSession(attributes)) {
return { text: window.i18n('icu:sessionEnded') };
}
if (isIncoming(attributes) && hasErrors(attributes)) {
return { text: window.i18n('icu:incomingError') };
}
const { body: untrimmedBody = '', bodyRanges = [] } = attributes;
const body = untrimmedBody.trim();
if (attachments.length) {
// This should never happen but we want to be extra-careful.
const attachment = attachments[0] || {};
const { contentType } = attachment;
if (contentType === MIME.IMAGE_GIF || Attachment.isGIF(attachments)) {
return {
bodyRanges,
emoji: '🎡',
text: body || window.i18n('icu:message--getNotificationText--gif'),
};
}
if (Attachment.isImage(attachments)) {
return {
bodyRanges,
emoji: '📷',
text: body || window.i18n('icu:message--getNotificationText--photo'),
};
}
if (Attachment.isVideo(attachments)) {
return {
bodyRanges,
emoji: '🎥',
text: body || window.i18n('icu:message--getNotificationText--video'),
};
}
if (Attachment.isVoiceMessage(attachment)) {
return {
bodyRanges,
emoji: '🎤',
text:
body ||
window.i18n('icu:message--getNotificationText--voice-message'),
};
}
if (Attachment.isAudio(attachments)) {
return {
bodyRanges,
emoji: '🔈',
text:
body ||
window.i18n('icu:message--getNotificationText--audio-message'),
};
}
return {
bodyRanges,
text: body || window.i18n('icu:message--getNotificationText--file'),
emoji: '📎',
};
}
const { sticker: stickerData } = attributes;
if (stickerData) {
const emoji =
Stickers.getSticker(stickerData.packId, stickerData.stickerId)?.emoji ||
stickerData?.emoji;
if (!emoji) {
log.warn('Unable to get emoji for sticker');
}
return {
text: window.i18n('icu:message--getNotificationText--stickers'),
emoji: dropNull(emoji),
};
}
if (isCallHistory(attributes)) {
const state = window.reduxStore.getState();
const callingNotification = getPropsForCallHistory(attributes, {
callSelector: getCallSelector(state),
activeCall: getActiveCall(state),
callHistorySelector: getCallHistorySelector(state),
conversationSelector: getConversationSelector(state),
});
if (callingNotification) {
const text = getCallingNotificationText(callingNotification, window.i18n);
if (text != null) {
return {
text,
};
}
}
log.error("This call history message doesn't have valid call history");
}
if (isExpirationTimerUpdate(attributes)) {
const { expireTimer } = attributes.expirationTimerUpdate ?? {};
if (!expireTimer) {
return { text: window.i18n('icu:disappearingMessagesDisabled') };
}
return {
text: window.i18n('icu:timerSetTo', {
time: expirationTimer.format(window.i18n, expireTimer),
}),
};
}
if (isKeyChange(attributes)) {
const { key_changed: identifier } = attributes;
const conversation = window.ConversationController.get(identifier);
return {
text: window.i18n('icu:safetyNumberChangedGroup', {
name: conversation ? conversation.getTitle() : '',
}),
};
}
const { contact: contacts } = attributes;
if (contacts && contacts.length) {
return {
text:
EmbeddedContact.getName(contacts[0]) ||
window.i18n('icu:unknownContact'),
emoji: '👤',
};
}
const { giftBadge } = attributes;
if (giftBadge) {
const emoji = '✨';
if (isOutgoing(attributes)) {
const toContact = window.ConversationController.get(
attributes.conversationId
);
const recipient =
toContact?.getTitle() ?? window.i18n('icu:unknownContact');
return {
emoji,
text: window.i18n('icu:message--donation--preview--sent', {
recipient,
}),
};
}
const fromContact = getContact(attributes);
const sender = fromContact?.getTitle() ?? window.i18n('icu:unknownContact');
return {
emoji,
text:
giftBadge.state === GiftBadgeStates.Unopened
? window.i18n('icu:message--donation--preview--unopened', {
sender,
})
: window.i18n('icu:message--donation--preview--redeemed'),
};
}
if (body) {
return {
text: body,
bodyRanges,
};
}
return { text: '' };
}

View File

@ -0,0 +1,88 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MessageAttributesType } from '../model-types.d';
import { BodyRange, applyRangesForText } from '../types/BodyRange';
import { extractHydratedMentions } from '../state/selectors/message';
import { findAndFormatContact } from './findAndFormatContact';
import { getNotificationDataForMessage } from './getNotificationDataForMessage';
import { isConversationAccepted } from './isConversationAccepted';
import { strictAssert } from './assert';
export function getNotificationTextForMessage(
attributes: MessageAttributesType
): string {
const { text, emoji } = getNotificationDataForMessage(attributes);
const conversation = window.ConversationController.get(
attributes.conversationId
);
strictAssert(
conversation != null,
'Conversation not found in ConversationController'
);
if (!isConversationAccepted(conversation.attributes)) {
return window.i18n('icu:message--getNotificationText--messageRequest');
}
if (attributes.storyReaction) {
if (attributes.type === 'outgoing') {
const { profileName: name } = conversation.attributes;
if (!name) {
return window.i18n(
'icu:Quote__story-reaction-notification--outgoing--nameless',
{
emoji: attributes.storyReaction.emoji,
}
);
}
return window.i18n('icu:Quote__story-reaction-notification--outgoing', {
emoji: attributes.storyReaction.emoji,
name,
});
}
const ourAci = window.textsecure.storage.user.getCheckedAci();
if (
attributes.type === 'incoming' &&
attributes.storyReaction.targetAuthorAci === ourAci
) {
return window.i18n('icu:Quote__story-reaction-notification--incoming', {
emoji: attributes.storyReaction.emoji,
});
}
if (!window.Signal.OS.isLinux()) {
return attributes.storyReaction.emoji;
}
return window.i18n('icu:Quote__story-reaction--single');
}
const mentions =
extractHydratedMentions(attributes, {
conversationSelector: findAndFormatContact,
}) || [];
const spoilers = (attributes.bodyRanges || []).filter(
range =>
BodyRange.isFormatting(range) && range.style === BodyRange.Style.SPOILER
) as Array<BodyRange<BodyRange.Formatting>>;
const modifiedText = applyRangesForText({ text, mentions, spoilers });
// Linux emoji support is mixed, so we disable it. (Note that this doesn't touch
// the `text`, which can contain emoji.)
const shouldIncludeEmoji = Boolean(emoji) && !window.Signal.OS.isLinux();
if (shouldIncludeEmoji) {
return window.i18n('icu:message--getNotificationText--text-with-emoji', {
text: modifiedText,
emoji,
});
}
return modifiedText || '';
}

View File

@ -0,0 +1,23 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { MessageAttributesType } from '../model-types.d';
export function getSenderIdentifier({
sent_at: sentAt,
source,
sourceServiceId,
sourceDevice,
}: Pick<
MessageAttributesType,
'sent_at' | 'source' | 'sourceServiceId' | 'sourceDevice'
>): string {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const conversation = window.ConversationController.lookupOrCreate({
e164: source,
serviceId: sourceServiceId,
reason: 'MessageModel.getSenderIdentifier',
})!;
return `${conversation?.id}.${sourceDevice}-${sentAt}`;
}

View File

@ -89,9 +89,10 @@ export async function handleEditMessage(
return;
}
const mainMessageModel = window.MessageController.register(
const mainMessageModel = window.MessageCache.__DEPRECATED$register(
mainMessage.id,
mainMessage
mainMessage,
'handleEditMessage'
);
// Pull out the edit history from the main message. If this is the first edit

View File

@ -0,0 +1,89 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import omit from 'lodash/omit';
import type { AttachmentType } from '../types/Attachment';
import type { MessageAttributesType } from '../model-types.d';
import { getAttachmentsForMessage } from '../state/selectors/message';
import { isAciString } from './isAciString';
import { isDirectConversation } from './whatTypeOfConversation';
import { softAssert, strictAssert } from './assert';
export async function hydrateStoryContext(
messageId: string,
storyMessageParam?: MessageAttributesType,
{
shouldSave,
}: {
shouldSave?: boolean;
} = {}
): Promise<void> {
const messageAttributes = await window.MessageCache.resolveAttributes(
'hydrateStoryContext',
messageId
);
const { storyId } = messageAttributes;
if (!storyId) {
return;
}
const { storyReplyContext: context } = messageAttributes;
// We'll continue trying to get the attachment as long as the message still exists
if (context && (context.attachment?.url || !context.messageId)) {
return;
}
const storyMessage =
storyMessageParam === undefined
? await window.MessageCache.resolveAttributes(
'hydrateStoryContext/story',
storyId
)
: window.MessageCache.toMessageAttributes(storyMessageParam);
if (!storyMessage) {
const conversation = window.ConversationController.get(
messageAttributes.conversationId
);
softAssert(
conversation && isDirectConversation(conversation.attributes),
'hydrateStoryContext: Not a type=direct conversation'
);
window.MessageCache.setAttributes({
messageId,
messageAttributes: {
storyReplyContext: {
attachment: undefined,
// This is ok to do because story replies only show in 1:1 conversations
// so the story that was quoted should be from the same conversation.
authorAci: conversation?.getAci(),
// No messageId = referenced story not found
messageId: '',
},
},
skipSaveToDatabase: !shouldSave,
});
return;
}
const attachments = getAttachmentsForMessage({ ...storyMessage });
let attachment: AttachmentType | undefined = attachments?.[0];
if (attachment && !attachment.url && !attachment.textAttachment) {
attachment = undefined;
}
const { sourceServiceId: authorAci } = storyMessage;
strictAssert(isAciString(authorAci), 'Story message from pni');
window.MessageCache.setAttributes({
messageId,
messageAttributes: {
storyReplyContext: {
attachment: omit(attachment, 'screenshotData'),
authorAci,
messageId: storyMessage.id,
},
},
skipSaveToDatabase: !shouldSave,
});
}

View File

@ -1351,27 +1351,6 @@
"updated": "2020-10-13T18:36:57.012Z",
"reasonDetail": "necessary for quill"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/quill/dist/quill.js",
"line": " // this.container.innerHTML = html.replace(/\\>\\r?\\n +\\</g, '><'); // Remove spaces between tags",
"reasonCategory": "falseMatch",
"updated": "2023-05-17T01:41:49.734Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/quill/dist/quill.js",
"line": " // this.container.innerHTML = '';",
"reasonCategory": "falseMatch",
"updated": "2023-05-17T01:41:49.734Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/quill/dist/quill.js",
"line": " // this.container.innerHTML = '';",
"reasonCategory": "falseMatch",
"updated": "2023-05-17T01:41:49.734Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/quill/dist/quill.js",
@ -1468,6 +1447,27 @@
"updated": "2020-10-13T18:36:57.012Z",
"reasonDetail": "necessary for quill"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/quill/dist/quill.js",
"line": " // this.container.innerHTML = html.replace(/\\>\\r?\\n +\\</g, '><'); // Remove spaces between tags",
"reasonCategory": "usageTrusted",
"updated": "2023-09-28T00:50:24.377Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/quill/dist/quill.js",
"line": " // this.container.innerHTML = '';",
"reasonCategory": "usageTrusted",
"updated": "2023-09-28T00:50:24.377Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/quill/dist/quill.js",
"line": " // this.container.innerHTML = '';",
"reasonCategory": "usageTrusted",
"updated": "2023-09-28T00:50:24.377Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/quill/dist/quill.min.js",

View File

@ -102,7 +102,9 @@ export async function markConversationRead(
const allReadMessagesSync = allUnreadMessages
.map(messageSyncData => {
const message = window.MessageController.getById(messageSyncData.id);
const message = window.MessageCache.__DEPRECATED$getById(
messageSyncData.id
);
// we update the in-memory MessageModel with the fresh database call data
if (message) {
message.set(omit(messageSyncData, 'originalReadStatus'));

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import * as log from '../logging/log';
import { getMessageById } from '../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
import { isNotNil } from './isNotNil';
import { DurationInSeconds } from './durations';
import { markViewed } from '../services/MessageUpdater';
@ -19,7 +19,7 @@ export async function markOnboardingStoryAsRead(): Promise<boolean> {
}
const messages = await Promise.all(
existingOnboardingStoryMessageIds.map(getMessageById)
existingOnboardingStoryMessageIds.map(__DEPRECATED$getMessageById)
);
const storyReadDate = Date.now();

View File

@ -161,7 +161,11 @@ export async function onStoryRecipientUpdate(
return true;
}
const message = window.MessageController.register(item.id, item);
const message = window.MessageCache.__DEPRECATED$register(
item.id,
item,
'onStoryRecipientUpdate'
);
const sendStateConversationIds = new Set(
Object.keys(nextSendStateByConversationId)

View File

@ -30,7 +30,7 @@ import type { StickerType } from '../types/Stickers';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import { isNotNil } from './isNotNil';
type ReturnType = {
export type MessageAttachmentsDownloadedType = {
bodyAttachment?: AttachmentType;
attachments: Array<AttachmentType>;
editHistory?: Array<EditHistoryType>;
@ -45,7 +45,7 @@ type ReturnType = {
// count then you'll also have to modify ./hasAttachmentsDownloads
export async function queueAttachmentDownloads(
message: MessageAttributesType
): Promise<ReturnType | undefined> {
): Promise<MessageAttachmentsDownloadedType | undefined> {
const attachmentsToQueue = message.attachments || [];
const messageId = message.id;
const idForLogging = getMessageIdForLogging(message);

View File

@ -15,7 +15,7 @@ import {
getConversationIdForLogging,
getMessageIdForLogging,
} from './idForLogging';
import { getMessageById } from '../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
import { getRecipientConversationIds } from './getRecipientConversationIds';
import { getRecipients } from './getRecipients';
import { repeat, zipObject } from './iterables';
@ -35,7 +35,7 @@ export async function sendDeleteForEveryoneMessage(
timestamp: targetTimestamp,
id: messageId,
} = options;
const message = await getMessageById(messageId);
const message = await __DEPRECATED$getMessageById(messageId);
if (!message) {
throw new Error('sendDeleteForEveryoneMessage: Cannot find message!');
}

View File

@ -23,7 +23,7 @@ import {
import { concat, filter, map, repeat, zipObject, find } from './iterables';
import { getConversationIdForLogging } from './idForLogging';
import { isQuoteAMatch } from '../messages/helpers';
import { getMessageById } from '../messages/getMessageById';
import { __DEPRECATED$getMessageById } from '../messages/getMessageById';
import { handleEditMessage } from './handleEditMessage';
import { incrementMessageCounter } from './incrementMessageCounter';
import { isGroupV1 } from './whatTypeOfConversation';
@ -64,7 +64,7 @@ export async function sendEditedMessage(
conversation.attributes
)})`;
const targetMessage = await getMessageById(targetMessageId);
const targetMessage = await __DEPRECATED$getMessageById(targetMessageId);
strictAssert(targetMessage, 'could not find message to edit');
if (isGroupV1(conversation.attributes)) {

View File

@ -311,7 +311,11 @@ export async function sendStoryMessage(
await Promise.all(
distributionListMessages.map(messageAttributes => {
const model = new window.Whisper.Message(messageAttributes);
const message = window.MessageController.register(model.id, model);
const message = window.MessageCache.__DEPRECATED$register(
model.id,
model,
'sendStoryMessage'
);
void ourConversation.addSingleMessage(model, { isJustSent: true });
@ -361,7 +365,11 @@ export async function sendStoryMessage(
},
async jobToInsert => {
const model = new window.Whisper.Message(messageAttributes);
const message = window.MessageController.register(model.id, model);
const message = window.MessageCache.__DEPRECATED$register(
model.id,
model,
'sendStoryMessage'
);
const conversation = message.getConversation();
void conversation?.addSingleMessage(model, { isJustSent: true });

View File

@ -2,22 +2,24 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { ConversationModel } from '../models/conversations';
import type { MessageModel } from '../models/messages';
import type { MessageAttributesType } from '../model-types.d';
import * as log from '../logging/log';
import dataInterface from '../sql/Client';
import { isGroup } from './whatTypeOfConversation';
import { isMessageUnread } from './isMessageUnread';
export async function shouldReplyNotifyUser(
message: MessageModel,
messageAttributes: Readonly<
Pick<MessageAttributesType, 'readStatus' | 'storyId'>
>,
conversation: ConversationModel
): Promise<boolean> {
// Don't notify if the message has already been read
if (!isMessageUnread(message.attributes)) {
if (!isMessageUnread(messageAttributes)) {
return false;
}
const storyId = message.get('storyId');
const { storyId } = messageAttributes;
// If this is not a reply to a story, always notify.
if (storyId == null) {

16
ts/window.d.ts vendored
View File

@ -9,10 +9,7 @@ import type PQueue from 'p-queue/dist';
import type { assert } from 'chai';
import type { PhoneNumber, PhoneNumberFormat } from 'google-libphonenumber';
import type {
ConversationModelCollectionType,
MessageModelCollectionType,
} from './model-types.d';
import type { ConversationModelCollectionType } from './model-types.d';
import type { textsecure } from './textsecure';
import type { Storage } from './textsecure/Storage';
import type {
@ -33,7 +30,6 @@ import type { LocalizerType, ThemeType } from './types/Util';
import type { Receipt } from './types/Receipt';
import type { ConversationController } from './ConversationController';
import type { ReduxActions } from './state/types';
import type { createStore } from './state/createStore';
import type { createApp } from './state/roots/createApp';
import type Data from './sql/Client';
import type { MessageModel } from './models/messages';
@ -43,7 +39,7 @@ import type { ConfirmationDialog } from './components/ConfirmationDialog';
import type { SignalProtocolStore } from './SignalProtocolStore';
import type { SocketStatus } from './types/SocketStatus';
import type SyncRequest from './textsecure/SyncRequest';
import type { MessageController } from './util/MessageController';
import type { MessageCache } from './services/MessageCache';
import type { StateType } from './state/reducer';
import type { SystemTraySetting } from './types/SystemTraySetting';
import type { Address } from './types/Address';
@ -164,7 +160,6 @@ export type SignalCoreType = {
};
OS: OSType;
State: {
createStore: typeof createStore;
Roots: {
createApp: typeof createApp;
};
@ -235,7 +230,7 @@ declare global {
ConversationController: ConversationController;
Events: IPCEventsType;
FontFace: typeof FontFace;
MessageController: MessageController;
MessageCache: MessageCache;
SignalProtocolStore: typeof SignalProtocolStore;
WebAPI: WebAPIConnectType;
Whisper: WhisperType;
@ -277,10 +272,10 @@ declare global {
RETRY_DELAY: boolean;
assert: typeof assert;
testUtilities: {
debug: (info: unknown) => void;
initialize: () => Promise<void>;
onComplete: (info: unknown) => void;
prepareTests: () => void;
installMessageController: () => void;
initializeMessageCounter: () => Promise<void>;
};
}
@ -308,7 +303,6 @@ export type WhisperType = {
Conversation: typeof ConversationModel;
ConversationCollection: typeof ConversationModelCollectionType;
Message: typeof MessageModel;
MessageCollection: typeof MessageModelCollectionType;
deliveryReceiptQueue: PQueue;
deliveryReceiptBatcher: BatcherType<Receipt>;

View File

@ -10,18 +10,49 @@ import { sync } from 'fast-glob';
import { assert } from 'chai';
import { getSignalProtocolStore } from '../../SignalProtocolStore';
import { MessageController } from '../../util/MessageController';
import { initMessageCleanup } from '../../services/messageStateCleanup';
import { initializeMessageCounter } from '../../util/incrementMessageCounter';
import { initializeRedux } from '../../state/initializeRedux';
import * as Stickers from '../../types/Stickers';
window.assert = assert;
// This is a hack to let us run TypeScript tests in the renderer process. See the
// code in `test/index.html`.
// code in `test/test.js`.
window.testUtilities = {
debug(info) {
return ipc.invoke('ci:test-electron:debug', info);
},
onComplete(info) {
return ipc.invoke('ci:test-electron:done', info);
},
async initialize() {
initMessageCleanup();
await initializeMessageCounter();
await Stickers.load();
initializeRedux({
callsHistory: [],
initialBadgesState: { byId: {} },
mainWindowStats: {
isFullScreen: false,
isMaximized: false,
},
menuOptions: {
development: false,
devTools: false,
includeSetup: false,
isProduction: false,
platform: 'test',
},
stories: [],
storyDistributionLists: [],
});
},
prepareTests() {
console.log('Preparing tests...');
sync('../../test-{both,electron}/**/*_test.js', {
@ -29,12 +60,6 @@ window.testUtilities = {
cwd: __dirname,
}).forEach(require);
},
installMessageController() {
MessageController.install();
},
initializeMessageCounter() {
return initializeMessageCounter();
},
};
window.getSignalProtocolStore = getSignalProtocolStore;

View File

@ -13,11 +13,11 @@ import './phase3-post-signal';
import './phase4-test';
import '../../backbone/reliable_trigger';
import type { CdsLookupOptionsType } from '../../textsecure/WebAPI';
import type { FeatureFlagType } from '../../window.d';
import type { StorageAccessType } from '../../types/Storage.d';
import type { CdsLookupOptionsType } from '../../textsecure/WebAPI';
import { start as startConversationController } from '../../ConversationController';
import { MessageController } from '../../util/MessageController';
import { initMessageCleanup } from '../../services/messageStateCleanup';
import { Environment, getEnvironment } from '../../environment';
import { isProduction } from '../../util/version';
import { ipcInvoke } from '../../sql/channels';
@ -43,7 +43,7 @@ if (window.SignalContext.config.proxyUrl) {
}
window.Whisper.events = clone(window.Backbone.Events);
MessageController.install();
initMessageCleanup();
startConversationController();
if (!isProduction(window.SignalContext.getVersion())) {
@ -51,7 +51,8 @@ if (!isProduction(window.SignalContext.getVersion())) {
cdsLookup: (options: CdsLookupOptionsType) =>
window.textsecure.server?.cdsLookup(options),
getConversation: (id: string) => window.ConversationController.get(id),
getMessageById: (id: string) => window.MessageController.getById(id),
getMessageById: (id: string) =>
window.MessageCache.__DEPRECATED$getById(id),
getReduxState: () => window.reduxStore.getState(),
getSfuUrl: () => window.Signal.Services.calling._sfuUrl,
getStorageItem: (name: keyof StorageAccessType) => window.storage.get(name),