// 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, getSelectedMessageIds, getTargetedMessage, } 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; 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(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 | 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; const tooBigAttachmentCount = attachments.filter( item => item.wasTooBig ).length; if (tooBigAttachmentCount === attachments.length) { return { emoji: '📎', text: window.i18n('icu:message--attachmentTooBig--one'), }; } 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), selectedMessageIds: getSelectedMessageIds(state), targetedMessageId: getTargetedMessage(state)?.id, }); 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: '' }; }