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
73 changed files with 2237 additions and 1229 deletions

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) {