"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "js/models/messages.js" between
Signal-Desktop-1.35.2.tar.gz and Signal-Desktop-1.36.1.tar.gz

About: Signal-Desktop is a cross-platform encrypted messaging service (also available for mobile devices).

messages.js  (Signal-Desktop-1.35.2):messages.js  (Signal-Desktop-1.36.1)
skipping to change at line 19 skipping to change at line 19
i18n, i18n,
Signal, Signal,
textsecure, textsecure,
Whisper Whisper
*/ */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function() { (function() {
'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
const { Message: TypedMessage, Contact, PhoneNumber, Errors } = Signal.Types; const {
Message: TypedMessage,
Attachment,
MIME,
Contact,
PhoneNumber,
Errors,
} = Signal.Types;
const { const {
deleteExternalMessageFiles, deleteExternalMessageFiles,
getAbsoluteAttachmentPath, getAbsoluteAttachmentPath,
loadAttachmentData, loadAttachmentData,
loadQuoteData, loadQuoteData,
loadPreviewData, loadPreviewData,
loadStickerData, loadStickerData,
upgradeMessageSchema, upgradeMessageSchema,
} = window.Signal.Migrations; } = window.Signal.Migrations;
const { const {
copyStickerToAttachments, copyStickerToAttachments,
deletePackReference, deletePackReference,
savePackMetadata, savePackMetadata,
getStickerPackStatus, getStickerPackStatus,
} = window.Signal.Stickers; } = window.Signal.Stickers;
const { GoogleChrome } = window.Signal.Util; const { GoogleChrome } = window.Signal.Util;
const { addStickerPackReference, getMessageBySender } = window.Signal.Data; const { addStickerPackReference, getMessageBySender } = window.Signal.Data;
const { bytesFromString } = window.Signal.Crypto; const { bytesFromString } = window.Signal.Crypto;
const PLACEHOLDER_CONTACT = {
title: i18n('unknownContact'),
};
window.AccountCache = Object.create(null); window.AccountCache = Object.create(null);
window.AccountJobs = Object.create(null); window.AccountJobs = Object.create(null);
window.doesAccountCheckJobExist = number => window.doesAccountCheckJobExist = number =>
Boolean(window.AccountJobs[number]); Boolean(window.AccountJobs[number]);
window.checkForSignalAccount = number => { window.checkForSignalAccount = number => {
if (window.AccountJobs[number]) { if (window.AccountJobs[number]) {
return window.AccountJobs[number]; return window.AccountJobs[number];
} }
skipping to change at line 138 skipping to change at line 146
hasSignalAccount: contact ? Boolean(contact.signalAccount) : null, hasSignalAccount: contact ? Boolean(contact.signalAccount) : null,
}; };
}, },
isNormalBubble() { isNormalBubble() {
return ( return (
!this.isCallHistory() && !this.isCallHistory() &&
!this.isEndSession() && !this.isEndSession() &&
!this.isExpirationTimerUpdate() && !this.isExpirationTimerUpdate() &&
!this.isGroupUpdate() && !this.isGroupUpdate() &&
!this.isGroupV2Change() &&
!this.isKeyChange() && !this.isKeyChange() &&
!this.isMessageHistoryUnsynced() && !this.isMessageHistoryUnsynced() &&
!this.isProfileChange() && !this.isProfileChange() &&
!this.isUnsupportedMessage() && !this.isUnsupportedMessage() &&
!this.isVerifiedChange() !this.isVerifiedChange()
); );
}, },
// Top-level prop generation for the message bubble // Top-level prop generation for the message bubble
getPropsForBubble() { getPropsForBubble() {
if (this.isUnsupportedMessage()) { if (this.isUnsupportedMessage()) {
return { return {
type: 'unsupportedMessage', type: 'unsupportedMessage',
data: this.getPropsForUnsupportedMessage(), data: this.getPropsForUnsupportedMessage(),
}; };
} else if (this.isMessageHistoryUnsynced()) { }
if (this.isGroupV2Change()) {
return {
type: 'groupV2Change',
data: this.getPropsForGroupV2Change(),
};
}
if (this.isMessageHistoryUnsynced()) {
return { return {
type: 'linkNotification', type: 'linkNotification',
data: null, data: null,
}; };
} else if (this.isExpirationTimerUpdate()) { }
if (this.isExpirationTimerUpdate()) {
return { return {
type: 'timerNotification', type: 'timerNotification',
data: this.getPropsForTimerNotification(), data: this.getPropsForTimerNotification(),
}; };
} else if (this.isKeyChange()) { }
if (this.isKeyChange()) {
return { return {
type: 'safetyNumberNotification', type: 'safetyNumberNotification',
data: this.getPropsForSafetyNumberNotification(), data: this.getPropsForSafetyNumberNotification(),
}; };
} else if (this.isVerifiedChange()) { }
if (this.isVerifiedChange()) {
return { return {
type: 'verificationNotification', type: 'verificationNotification',
data: this.getPropsForVerificationNotification(), data: this.getPropsForVerificationNotification(),
}; };
} else if (this.isGroupUpdate()) { }
if (this.isGroupUpdate()) {
return { return {
type: 'groupNotification', type: 'groupNotification',
data: this.getPropsForGroupNotification(), data: this.getPropsForGroupNotification(),
}; };
} else if (this.isEndSession()) { }
if (this.isEndSession()) {
return { return {
type: 'resetSessionNotification', type: 'resetSessionNotification',
data: this.getPropsForResetSessionNotification(), data: this.getPropsForResetSessionNotification(),
}; };
} else if (this.isCallHistory()) { }
if (this.isCallHistory()) {
return { return {
type: 'callHistory', type: 'callHistory',
data: this.getPropsForCallHistory(), data: this.getPropsForCallHistory(),
}; };
} else if (this.isProfileChange()) { }
if (this.isProfileChange()) {
return { return {
type: 'profileChange', type: 'profileChange',
data: this.getPropsForProfileChange(), data: this.getPropsForProfileChange(),
}; };
} }
return { return {
type: 'message', type: 'message',
data: this.getPropsForMessage(), data: this.getPropsForMessage(),
}; };
}, },
// Other top-level prop-generation // Other top-level prop-generation
getPropsForSearchResult() { getPropsForSearchResult() {
const ourId = ConversationController.getOurConversationId();
const sourceId = this.getContactId(); const sourceId = this.getContactId();
const fromContact = this.findAndFormatContact(sourceId); const from = this.findAndFormatContact(sourceId);
if (ourId === sourceId) {
fromContact.isMe = true;
}
const convo = this.getConversation(); const convo = this.getConversation();
const to = this.findAndFormatContact(convo.get('id'));
const to = convo ? this.findAndFormatContact(convo.get('id')) : {};
if (to && convo && convo.isMe()) {
to.isMe = true;
}
return { return {
from: fromContact || {}, from,
to, to,
isSelected: this.isSelected, isSelected: this.isSelected,
id: this.id, id: this.id,
conversationId: this.get('conversationId'), conversationId: this.get('conversationId'),
sentAt: this.get('sent_at'), sentAt: this.get('sent_at'),
snippet: this.get('snippet'), snippet: this.get('snippet'),
}; };
}, },
skipping to change at line 348 skipping to change at line 360
isUnsupportedMessage() { isUnsupportedMessage() {
const versionAtReceive = this.get('supportedVersionAtReceive'); const versionAtReceive = this.get('supportedVersionAtReceive');
const requiredVersion = this.get('requiredProtocolVersion'); const requiredVersion = this.get('requiredProtocolVersion');
return ( return (
_.isNumber(versionAtReceive) && _.isNumber(versionAtReceive) &&
_.isNumber(requiredVersion) && _.isNumber(requiredVersion) &&
versionAtReceive < requiredVersion versionAtReceive < requiredVersion
); );
}, },
isGroupV2Change() {
return Boolean(this.get('groupV2Change'));
},
isExpirationTimerUpdate() { isExpirationTimerUpdate() {
const flag = const flag =
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
return Boolean(this.get('flags') & flag); return Boolean(this.get('flags') & flag);
}, },
isKeyChange() { isKeyChange() {
return this.get('type') === 'keychange'; return this.get('type') === 'keychange';
}, },
isVerifiedChange() { isVerifiedChange() {
skipping to change at line 389 skipping to change at line 404
getPropsForUnsupportedMessage() { getPropsForUnsupportedMessage() {
const requiredVersion = this.get('requiredProtocolVersion'); const requiredVersion = this.get('requiredProtocolVersion');
const canProcessNow = this.CURRENT_PROTOCOL_VERSION >= requiredVersion; const canProcessNow = this.CURRENT_PROTOCOL_VERSION >= requiredVersion;
const sourceId = this.getContactId(); const sourceId = this.getContactId();
return { return {
canProcessNow, canProcessNow,
contact: this.findAndFormatContact(sourceId), contact: this.findAndFormatContact(sourceId),
}; };
}, },
getPropsForGroupV2Change() {
const { protobuf } = window.textsecure;
return {
AccessControlEnum: protobuf.AccessControl.AccessRequired,
RoleEnum: protobuf.Member.Role,
ourConversationId: window.ConversationController.getOurConversationId(),
change: this.get('groupV2Change'),
};
},
getPropsForTimerNotification() { getPropsForTimerNotification() {
const timerUpdate = this.get('expirationTimerUpdate'); const timerUpdate = this.get('expirationTimerUpdate');
if (!timerUpdate) { if (!timerUpdate) {
return null; return null;
} }
const { expireTimer, fromSync, source, sourceUuid } = timerUpdate; const { expireTimer, fromSync, source, sourceUuid } = timerUpdate;
const timespan = Whisper.ExpirationTimerOptions.getName(expireTimer || 0); const timespan = Whisper.ExpirationTimerOptions.getName(expireTimer || 0);
const disabled = !expireTimer; const disabled = !expireTimer;
const sourceId = ConversationController.ensureContactIds({ const sourceId = ConversationController.ensureContactIds({
e164: source, e164: source,
uuid: sourceUuid, uuid: sourceUuid,
}); });
const ourId = ConversationController.getOurConversationId(); const ourId = ConversationController.getOurConversationId();
const formattedContact = this.findAndFormatContact(sourceId);
const basicProps = { const basicProps = {
...this.findAndFormatContact(sourceId), ...formattedContact,
type: 'fromOther', type: 'fromOther',
timespan, timespan,
disabled, disabled,
}; };
if (fromSync) { if (fromSync) {
return { return {
...basicProps, ...basicProps,
type: 'fromSync', type: 'fromSync',
}; };
} else if (sourceId && sourceId === ourId) { }
if (sourceId && sourceId === ourId) {
return { return {
...basicProps, ...basicProps,
type: 'fromMe', type: 'fromMe',
}; };
} }
if (!sourceId) {
return {
...basicProps,
type: 'fromMember',
};
}
return basicProps; return basicProps;
}, },
getPropsForSafetyNumberNotification() { getPropsForSafetyNumberNotification() {
const conversation = this.getConversation(); const conversation = this.getConversation();
const isGroup = conversation && !conversation.isPrivate(); const isGroup = conversation && !conversation.isPrivate();
const identifier = this.get('key_changed'); const identifier = this.get('key_changed');
return { return {
isGroup, isGroup,
skipping to change at line 462 skipping to change at line 495
!groupUpdate.avatarUpdated && !groupUpdate.avatarUpdated &&
!groupUpdate.left && !groupUpdate.left &&
!groupUpdate.joined && !groupUpdate.joined &&
!groupUpdate.name !groupUpdate.name
) { ) {
changes.push({ changes.push({
type: 'general', type: 'general',
}); });
} }
const placeholderContact = {
title: i18n('unknownContact'),
};
if (groupUpdate.joined) { if (groupUpdate.joined) {
changes.push({ changes.push({
type: 'add', type: 'add',
contacts: _.map( contacts: _.map(
Array.isArray(groupUpdate.joined) Array.isArray(groupUpdate.joined)
? groupUpdate.joined ? groupUpdate.joined
: [groupUpdate.joined], : [groupUpdate.joined],
identifier => identifier => this.findAndFormatContact(identifier)
this.findAndFormatContact(identifier) || placeholderContact
), ),
}); });
} }
if (groupUpdate.left === 'You') { if (groupUpdate.left === 'You') {
changes.push({ changes.push({
type: 'remove', type: 'remove',
isMe: true, isMe: true,
}); });
} else if (groupUpdate.left) { } else if (groupUpdate.left) {
changes.push({ changes.push({
type: 'remove', type: 'remove',
contacts: _.map( contacts: _.map(
Array.isArray(groupUpdate.left) Array.isArray(groupUpdate.left)
? groupUpdate.left ? groupUpdate.left
: [groupUpdate.left], : [groupUpdate.left],
identifier => identifier => this.findAndFormatContact(identifier)
this.findAndFormatContact(identifier) || placeholderContact
), ),
}); });
} }
if (groupUpdate.name) { if (groupUpdate.name) {
changes.push({ changes.push({
type: 'name', type: 'name',
newName: groupUpdate.name, newName: groupUpdate.name,
}); });
} }
skipping to change at line 589 skipping to change at line 616
const conversationAccepted = Boolean( const conversationAccepted = Boolean(
conversation && conversation.getAccepted() conversation && conversation.getAccepted()
); );
const sticker = this.get('sticker'); const sticker = this.get('sticker');
const isTapToView = this.isTapToView(); const isTapToView = this.isTapToView();
const reactions = (this.get('reactions') || []).map(re => { const reactions = (this.get('reactions') || []).map(re => {
const c = this.findAndFormatContact(re.fromId); const c = this.findAndFormatContact(re.fromId);
if (!c) {
return {
emoji: re.emoji,
from: {
id: re.fromId,
},
};
}
return { return {
emoji: re.emoji, emoji: re.emoji,
timestamp: re.timestamp, timestamp: re.timestamp,
from: c, from: c,
}; };
}); });
const selectedReaction = ( const selectedReaction = (
(this.get('reactions') || []).find( (this.get('reactions') || []).find(
re => re.fromId === ConversationController.getOurConversationId() re => re.fromId === ConversationController.getOurConversationId()
skipping to change at line 650 skipping to change at line 668
isTapToViewExpired: isTapToView && this.get('isErased'), isTapToViewExpired: isTapToView && this.get('isErased'),
isTapToViewError: isTapToViewError:
isTapToView && this.isIncoming() && this.get('isTapToViewInvalid'), isTapToView && this.isIncoming() && this.get('isTapToViewInvalid'),
deletedForEveryone: this.get('deletedForEveryone') || false, deletedForEveryone: this.get('deletedForEveryone') || false,
}; };
}, },
// Dependencies of prop-generation functions // Dependencies of prop-generation functions
findAndFormatContact(identifier) { findAndFormatContact(identifier) {
if (!identifier) {
return PLACEHOLDER_CONTACT;
}
const contactModel = this.findContact(identifier); const contactModel = this.findContact(identifier);
if (contactModel) { if (contactModel) {
return contactModel.format(); return contactModel.format();
} }
const { format } = PhoneNumber; const { format, isValidNumber } = PhoneNumber;
const regionCode = storage.get('regionCode'); const regionCode = storage.get('regionCode');
if (!isValidNumber(identifier, { regionCode })) {
return PLACEHOLDER_CONTACT;
}
const phoneNumber = format(identifier, {
ourRegionCode: regionCode,
});
return { return {
phoneNumber: format(identifier, { title: phoneNumber,
ourRegionCode: regionCode, phoneNumber,
}),
}; };
}, },
findContact(identifier) { findContact(identifier) {
return ConversationController.get(identifier); return ConversationController.get(identifier);
}, },
getConversation() { getConversation() {
return ConversationController.get(this.get('conversationId')); return ConversationController.get(this.get('conversationId'));
}, },
createNonBreakingLastSeparator(text) { createNonBreakingLastSeparator(text) {
if (!text) { if (!text) {
skipping to change at line 871 skipping to change at line 901
const { thumbnail } = attachment; const { thumbnail } = attachment;
const path = const path =
thumbnail && thumbnail &&
thumbnail.path && thumbnail.path &&
getAbsoluteAttachmentPath(thumbnail.path); getAbsoluteAttachmentPath(thumbnail.path);
const objectUrl = thumbnail && thumbnail.objectUrl; const objectUrl = thumbnail && thumbnail.objectUrl;
const thumbnailWithObjectUrl = const thumbnailWithObjectUrl =
!path && !objectUrl !path && !objectUrl
? null ? null
: Object.assign({}, attachment.thumbnail || {}, { : { ...(attachment.thumbnail || {}), objectUrl: path || objectUrl };
objectUrl: path || objectUrl,
});
return Object.assign({}, attachment, { return {
...attachment,
isVoiceMessage: Signal.Types.Attachment.isVoiceMessage(attachment), isVoiceMessage: Signal.Types.Attachment.isVoiceMessage(attachment),
thumbnail: thumbnailWithObjectUrl, thumbnail: thumbnailWithObjectUrl,
}); };
}, },
// More display logic getNotificationData() /* : { text: string, emoji?: string } */ {
getDescription() {
if (this.isUnsupportedMessage()) { if (this.isUnsupportedMessage()) {
return i18n('message--getDescription--unsupported-message'); return { text: i18n('message--getDescription--unsupported-message') };
} }
if (this.isProfileChange()) { if (this.isProfileChange()) {
const change = this.get('profileChange'); const change = this.get('profileChange');
const changedId = this.get('changedId'); const changedId = this.get('changedId');
const changedContact = this.findAndFormatContact(changedId); const changedContact = this.findAndFormatContact(changedId);
return Signal.Util.getStringForProfileChange( return {
change, text: Signal.Util.getStringForProfileChange(
changedContact, change,
i18n changedContact,
); i18n
),
};
} }
if (this.isGroupV2Change()) {
const { protobuf } = window.textsecure;
const change = this.get('groupV2Change');
const lines = window.Signal.GroupChange.renderChange(change, {
AccessControlEnum: protobuf.AccessControl.AccessRequired,
i18n: window.i18n,
ourConversationId: window.ConversationController.getOurConversationId(
),
renderContact: conversationId => {
const conversation = window.ConversationController.get(
conversationId
);
return conversation
? conversation.getTitle()
: window.i18n('unknownUser');
},
renderString: (key, i18n, placeholders) => i18n(key, placeholders),
RoleEnum: protobuf.Member.Role,
});
return { text: lines.join(' ') };
}
const attachments = this.get('attachments') || [];
if (this.isTapToView()) { if (this.isTapToView()) {
if (this.isErased()) { if (this.isErased()) {
return i18n('message--getDescription--disappearing-media'); return { text: i18n('message--getDescription--disappearing-media') };
} }
const attachments = this.get('attachments'); if (Attachment.isImage(attachments)) {
if (!attachments || !attachments[0]) { return {
return i18n('mediaMessage'); text: i18n('message--getDescription--disappearing-photo'),
emoji: '📷',
};
} }
if (Attachment.isVideo(attachments)) {
const { contentType } = attachments[0]; return {
if (GoogleChrome.isImageTypeSupported(contentType)) { text: i18n('message--getDescription--disappearing-video'),
return i18n('message--getDescription--disappearing-photo'); emoji: '🎥',
} else if (GoogleChrome.isVideoTypeSupported(contentType)) { };
return i18n('message--getDescription--disappearing-video');
} }
// There should be an image or video attachment, but we have a fallback
return i18n('mediaMessage'); just in
// case.
return { text: i18n('mediaMessage'), emoji: '📎' };
} }
if (this.isGroupUpdate()) { if (this.isGroupUpdate()) {
const groupUpdate = this.get('group_update'); const groupUpdate = this.get('group_update');
const fromContact = this.getContact(); const fromContact = this.getContact();
const messages = []; const messages = [];
if (groupUpdate.left === 'You') { if (groupUpdate.left === 'You') {
return i18n('youLeftTheGroup'); return { text: i18n('youLeftTheGroup') };
} else if (groupUpdate.left) { }
return i18n('leftTheGroup', [ if (groupUpdate.left) {
this.getNameForNumber(groupUpdate.left), return {
]); text: i18n('leftTheGroup', [
this.getNameForNumber(groupUpdate.left),
]),
};
} }
if (!fromContact) { if (!fromContact) {
return ''; return { text: '' };
} }
if (fromContact.isMe()) { if (fromContact.isMe()) {
messages.push(i18n('youUpdatedTheGroup')); messages.push(i18n('youUpdatedTheGroup'));
} else { } else {
messages.push(i18n('updatedTheGroup', [fromContact.getTitle()])); messages.push(i18n('updatedTheGroup', [fromContact.getTitle()]));
} }
if (groupUpdate.joined && groupUpdate.joined.length) { if (groupUpdate.joined && groupUpdate.joined.length) {
const joinedContacts = _.map(groupUpdate.joined, item => const joinedContacts = _.map(groupUpdate.joined, item =>
skipping to change at line 982 skipping to change at line 1044
} }
} }
if (groupUpdate.name) { if (groupUpdate.name) {
messages.push(i18n('titleIsNow', [groupUpdate.name])); messages.push(i18n('titleIsNow', [groupUpdate.name]));
} }
if (groupUpdate.avatarUpdated) { if (groupUpdate.avatarUpdated) {
messages.push(i18n('updatedGroupAvatar')); messages.push(i18n('updatedGroupAvatar'));
} }
return messages.join(' '); return { text: messages.join(' ') };
} }
if (this.isEndSession()) { if (this.isEndSession()) {
return i18n('sessionEnded'); return { text: i18n('sessionEnded') };
} }
if (this.isIncoming() && this.hasErrors()) { if (this.isIncoming() && this.hasErrors()) {
return i18n('incomingError'); return { text: i18n('incomingError') };
}
return this.get('body');
},
getNotificationText() {
const description = this.getDescription();
if (description) {
return description;
} }
if (this.get('attachments').length > 0) {
return i18n('mediaMessage'); const body = (this.get('body') || '').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) {
return {
text: body || i18n('message--getNotificationText--gif'),
emoji: '🎡',
};
}
if (Attachment.isImage(attachments)) {
return {
text: body || i18n('message--getNotificationText--photo'),
emoji: '📷',
};
}
if (Attachment.isVideo(attachments)) {
return {
text: body || i18n('message--getNotificationText--video'),
emoji: '🎥',
};
}
if (Attachment.isVoiceMessage(attachment)) {
return {
text: body || i18n('message--getNotificationText--voice-message'),
emoji: '🎤',
};
}
if (Attachment.isAudio(attachments)) {
return {
text: body || i18n('message--getNotificationText--audio-message'),
emoji: '🔈',
};
}
return {
text: body || i18n('message--getNotificationText--file'),
emoji: '📎',
};
} }
if (this.get('sticker')) {
return i18n('message--getNotificationText--stickers'); const stickerData = this.get('sticker');
if (stickerData) {
const sticker = Signal.Stickers.getSticker(
stickerData.packId,
stickerData.stickerId
);
const { emoji } = sticker || {};
if (!emoji) {
window.log.warn('Unable to get emoji for sticker');
}
return {
text: i18n('message--getNotificationText--stickers'),
emoji,
};
} }
if (this.isCallHistory()) { if (this.isCallHistory()) {
return window.Signal.Components.getCallingNotificationText( return {
this.get('callHistoryDetails'), text: window.Signal.Components.getCallingNotificationText(
window.i18n this.get('callHistoryDetails'),
); window.i18n
),
};
} }
if (this.isExpirationTimerUpdate()) { if (this.isExpirationTimerUpdate()) {
const { expireTimer } = this.get('expirationTimerUpdate'); const { expireTimer } = this.get('expirationTimerUpdate');
if (!expireTimer) { if (!expireTimer) {
return i18n('disappearingMessagesDisabled'); return { text: i18n('disappearingMessagesDisabled') };
} }
return i18n('timerSetTo', [ return {
Whisper.ExpirationTimerOptions.getAbbreviated(expireTimer || 0), text: i18n('timerSetTo', [
]); Whisper.ExpirationTimerOptions.getAbbreviated(expireTimer || 0),
]),
};
} }
if (this.isKeyChange()) { if (this.isKeyChange()) {
const identifier = this.get('key_changed'); const identifier = this.get('key_changed');
const conversation = this.findContact(identifier); const conversation = this.findContact(identifier);
return i18n('safetyNumberChangedGroup', [ return {
conversation ? conversation.getTitle() : null, text: i18n('safetyNumberChangedGroup', [
]); conversation ? conversation.getTitle() : null,
]),
};
} }
const contacts = this.get('contact'); const contacts = this.get('contact');
if (contacts && contacts.length) { if (contacts && contacts.length) {
return Contact.getName(contacts[0]); return { text: Contact.getName(contacts[0]), emoji: '👤' };
} }
return ''; if (body) {
return { text: body };
}
return { text: '' };
},
getNotificationText() /* : string */ {
const { text, emoji } = this.getNotificationData();
// 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) && !Signal.OS.isLinux();
if (shouldIncludeEmoji) {
return i18n('message--getNotificationText--text-with-emoji', {
text,
emoji,
});
}
return text;
}, },
// General // General
idForLogging() { idForLogging() {
const source = this.getSource(); const source = this.getSource();
const device = this.getSourceDevice(); const device = this.getSourceDevice();
const timestamp = this.get('sent_at'); const timestamp = this.get('sent_at');
return `${source}.${device} ${timestamp}`; return `${source}.${device} ${timestamp}`;
}, },
skipping to change at line 1222 skipping to change at line 1357
isEmpty() { isEmpty() {
// Core message types - we check for all four because they can each stand alone // Core message types - we check for all four because they can each stand alone
const hasBody = Boolean(this.get('body')); const hasBody = Boolean(this.get('body'));
const hasAttachment = (this.get('attachments') || []).length > 0; const hasAttachment = (this.get('attachments') || []).length > 0;
const hasEmbeddedContact = (this.get('contact') || []).length > 0; const hasEmbeddedContact = (this.get('contact') || []).length > 0;
const isSticker = Boolean(this.get('sticker')); const isSticker = Boolean(this.get('sticker'));
// Rendered sync messages // Rendered sync messages
const isCallHistory = this.isCallHistory(); const isCallHistory = this.isCallHistory();
const isGroupUpdate = this.isGroupUpdate(); const isGroupUpdate = this.isGroupUpdate();
const isGroupV2Change = this.isGroupV2Change();
const isEndSession = this.isEndSession(); const isEndSession = this.isEndSession();
const isExpirationTimerUpdate = this.isExpirationTimerUpdate(); const isExpirationTimerUpdate = this.isExpirationTimerUpdate();
const isVerifiedChange = this.isVerifiedChange(); const isVerifiedChange = this.isVerifiedChange();
// Placeholder messages // Placeholder messages
const isUnsupportedMessage = this.isUnsupportedMessage(); const isUnsupportedMessage = this.isUnsupportedMessage();
const isTapToView = this.isTapToView(); const isTapToView = this.isTapToView();
// Errors // Errors
const hasErrors = this.hasErrors(); const hasErrors = this.hasErrors();
skipping to change at line 1249 skipping to change at line 1385
const hasSomethingToDisplay = const hasSomethingToDisplay =
// Core message types // Core message types
hasBody || hasBody ||
hasAttachment || hasAttachment ||
hasEmbeddedContact || hasEmbeddedContact ||
isSticker || isSticker ||
// Rendered sync messages // Rendered sync messages
isCallHistory || isCallHistory ||
isGroupUpdate || isGroupUpdate ||
isGroupV2Change ||
isEndSession || isEndSession ||
isExpirationTimerUpdate || isExpirationTimerUpdate ||
isVerifiedChange || isVerifiedChange ||
// Placeholder messages // Placeholder messages
isUnsupportedMessage || isUnsupportedMessage ||
isTapToView || isTapToView ||
// Errors // Errors
hasErrors || hasErrors ||
// Locally-generated notifications // Locally-generated notifications
isKeyChange || isKeyChange ||
skipping to change at line 1371 skipping to change at line 1508
this.unset('unread'); this.unset('unread');
if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) { if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) {
const expirationStartTimestamp = Math.min( const expirationStartTimestamp = Math.min(
Date.now(), Date.now(),
readAt || Date.now() readAt || Date.now()
); );
this.set({ expirationStartTimestamp }); this.set({ expirationStartTimestamp });
} }
Whisper.Notifications.remove( Whisper.Notifications.removeBy({ messageId: this.id });
Whisper.Notifications.where({
messageId: this.id,
})
);
if (!skipSave) { if (!skipSave) {
await window.Signal.Data.saveMessage(this.attributes, { await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message, Message: Whisper.Message,
}); });
} }
}, },
isExpiring() { isExpiring() {
return this.get('expireTimer') && this.get('expirationStartTimestamp'); return this.get('expireTimer') && this.get('expirationStartTimestamp');
}, },
skipping to change at line 1545 skipping to change at line 1678
null, null,
this.get('sent_at'), this.get('sent_at'),
this.get('expireTimer'), this.get('expireTimer'),
profileKey, profileKey,
options options
); );
} else { } else {
// Because this is a partial group send, we manually construct the reque st like // Because this is a partial group send, we manually construct the reque st like
// sendMessageToGroup does. // sendMessageToGroup does.
const groupV2 = conversation.getGroupV2Info();
promise = textsecure.messaging.sendMessage( promise = textsecure.messaging.sendMessage(
{ {
recipients, recipients,
body, body,
timestamp: this.get('sent_at'), timestamp: this.get('sent_at'),
attachments, attachments,
quote: quoteWithData, quote: quoteWithData,
preview: previewWithData, preview: previewWithData,
sticker: stickerWithData, sticker: stickerWithData,
expireTimer: this.get('expireTimer'), expireTimer: this.get('expireTimer'),
profileKey, profileKey,
group: { groupV2,
id: this.getConversation().get('groupId'), group: groupV2
type: textsecure.protobuf.GroupContext.Type.DELIVER, ? null
}, : {
id: this.getConversation().get('groupId'),
type: textsecure.protobuf.GroupContext.Type.DELIVER,
},
}, },
options options
); );
} }
return this.send(conversation.wrapSend(promise)); return this.send(conversation.wrapSend(promise));
}, },
isReplayableError(e) { isReplayableError(e) {
return ( return (
e.name === 'MessageError' || e.name === 'MessageError' ||
skipping to change at line 1719 skipping to change at line 1857
}) })
.catch(result => { .catch(result => {
this.trigger('done'); this.trigger('done');
if (result.dataMessage) { if (result.dataMessage) {
this.set({ dataMessage: result.dataMessage }); this.set({ dataMessage: result.dataMessage });
} }
let promises = []; let promises = [];
// If we successfully sent to a user, we can remove our unregistered f
lag.
result.successfulIdentifiers.forEach(identifier => {
const c = ConversationController.get(identifier);
if (c && c.isEverUnregistered()) {
c.setRegistered();
}
});
if (result instanceof Error) { if (result instanceof Error) {
this.saveErrors(result); this.saveErrors(result);
if (result.name === 'SignedPreKeyRotationError') { if (result.name === 'SignedPreKeyRotationError') {
promises.push(getAccountManager().rotateSignedPreKey()); promises.push(getAccountManager().rotateSignedPreKey());
} else if (result.name === 'OutgoingIdentityKeyError') { } else if (result.name === 'OutgoingIdentityKeyError') {
const c = ConversationController.get(result.number); const c = ConversationController.get(result.number);
promises.push(c.getProfiles()); promises.push(c.getProfiles());
} }
} else { } else {
if (result.successfulIdentifiers.length > 0) { if (result.successfulIdentifiers.length > 0) {
const sentTo = this.get('sent_to') || []; const sentTo = this.get('sent_to') || [];
// If we just found out that we couldn't send to a user because th
ey are no
// longer registered, we will update our unregistered flag. In g
roups we
// will not event try to send to them for 6 hours. And we will n
ever try
// to fetch them on startup again.
// The way to discover registration once more is:
// 1) any attempt to send to them in 1:1 conversation
// 2) the six-hour time period has passed and we send in a group
again
const unregisteredUserErrors = _.filter(
result.errors,
error => error.name === 'UnregisteredUserError'
);
unregisteredUserErrors.forEach(error => {
const c = ConversationController.get(error.identifier);
if (c) {
c.setUnregistered();
}
});
// In groups, we don't treat unregistered users as a user-visible // In groups, we don't treat unregistered users as a user-visible
// error. The message will look successful, but the details // error. The message will look successful, but the details
// screen will show that we didn't send to these unregistered us ers. // screen will show that we didn't send to these unregistered us ers.
const filteredErrors = _.reject( const filteredErrors = _.reject(
result.errors, result.errors,
error => error.name === 'UnregisteredUserError' error => error.name === 'UnregisteredUserError'
); );
// We don't start the expiration timer if there are real errors // We don't start the expiration timer if there are real errors
// left after filtering out all of the unregistered user errors. // left after filtering out all of the unregistered user errors.
skipping to change at line 2258 skipping to change at line 2422
toUpdate.get('unidentifiedDeliveries'), toUpdate.get('unidentifiedDeliveries'),
unidentifiedDeliveries unidentifiedDeliveries
), ),
}); });
await window.Signal.Data.saveMessage(toUpdate.attributes, { await window.Signal.Data.saveMessage(toUpdate.attributes, {
Message: Whisper.Message, Message: Whisper.Message,
}); });
confirm(); confirm();
return; return;
} else if (isUpdate) { }
if (isUpdate) {
window.log.warn( window.log.warn(
`handleDataMessage: Received update transcript, but no existing en try for message ${message.idForLogging()}. Dropping.` `handleDataMessage: Received update transcript, but no existing en try for message ${message.idForLogging()}. Dropping.`
); );
confirm(); confirm();
return; return;
} else if (existingMessage) { }
if (existingMessage) {
window.log.warn( window.log.warn(
`handleDataMessage: Received duplicate transcript for message ${me ssage.idForLogging()}, but it was not an update transcript. Dropping.` `handleDataMessage: Received duplicate transcript for message ${me ssage.idForLogging()}, but it was not an update transcript. Dropping.`
); );
confirm(); confirm();
return; return;
} }
} }
// We drop incoming messages for groups we already know about, which we' const existingRevision = conversation.get('revision');
re not a const isGroupV2 = Boolean(initialMessage.groupV2);
// part of, except for group updates. const isV2GroupUpdate =
const ourUuid = textsecure.storage.user.getUuid(); initialMessage.groupV2 &&
const ourNumber = textsecure.storage.user.getNumber(); (!existingRevision ||
const isGroupUpdate = initialMessage.groupV2.revision > existingRevision);
// GroupV2
if (isGroupV2) {
conversation.maybeRepairGroupV2(
_.pick(initialMessage.groupV2, [
'masterKey',
'secretParams',
'publicParams',
])
);
}
if (isV2GroupUpdate) {
const { revision, groupChange } = initialMessage.groupV2;
try {
await window.Signal.Groups.maybeUpdateGroup({
conversation,
groupChangeBase64: groupChange,
newRevision: revision,
receivedAt: message.get('received_at'),
sentAt: message.get('sent_at'),
});
} catch (error) {
const errorText = error && error.stack ? error.stack : error;
window.log.error(
`handleDataMessage: Failed to process group update for ${conversat
ion.idForLogging()} as part of message ${message.idForLogging()}: ${errorText}`
);
throw error;
}
}
const ourConversationId = ConversationController.getOurConversationId();
const senderId = ConversationController.ensureContactIds({
e164: source,
uuid: sourceUuid,
});
const isV1GroupUpdate =
initialMessage.group && initialMessage.group &&
initialMessage.group.type !== initialMessage.group.type !==
textsecure.protobuf.GroupContext.Type.DELIVER; textsecure.protobuf.GroupContext.Type.DELIVER;
// Drop an incoming GroupV2 message if we or the sender are not part of
the group
// after applying the message's associated group chnages.
if (
type === 'incoming' &&
!conversation.isPrivate() &&
isGroupV2 &&
(conversation.get('left') ||
!conversation.hasMember(ourConversationId) ||
!conversation.hasMember(senderId))
) {
window.log.warn(
`Received message destined for group ${conversation.idForLogging()},
which we or the sender are not a part of. Dropping.`
);
confirm();
return;
}
// We drop incoming messages for v1 groups we already know about, which
we're not
// a part of, except for group updates. Because group v1 updates haven
't been
// applied by this point.
if ( if (
type === 'incoming' && type === 'incoming' &&
!conversation.isPrivate() && !conversation.isPrivate() &&
!conversation.hasMember(ourNumber || ourUuid) && !isGroupV2 &&
!isGroupUpdate !isV1GroupUpdate &&
(conversation.get('left') ||
!conversation.hasMember(ourConversationId))
) { ) {
window.log.warn( window.log.warn(
`Received message destined for group ${conversation.idForLogging()}, which we're not a part of. Dropping.` `Received message destined for group ${conversation.idForLogging()}, which we're not a part of. Dropping.`
); );
confirm(); confirm();
return; return;
} }
// Send delivery receipts, but only for incoming sealed sender messages // Send delivery receipts, but only for incoming sealed sender messages
// and not for messages from unaccepted conversations // and not for messages from unaccepted conversations
skipping to change at line 2371 skipping to change at line 2598
const isSupported = !message.isUnsupportedMessage(); const isSupported = !message.isUnsupportedMessage();
if (!isSupported) { if (!isSupported) {
await message.eraseContents(); await message.eraseContents();
} }
if (isSupported) { if (isSupported) {
let attributes = { let attributes = {
...conversation.attributes, ...conversation.attributes,
}; };
if (dataMessage.group) {
// GroupV1
if (!isGroupV2 && dataMessage.group) {
const pendingGroupUpdate = []; const pendingGroupUpdate = [];
const memberConversations = await Promise.all( const memberConversations = await Promise.all(
dataMessage.group.membersE164.map(e164 => dataMessage.group.membersE164.map(e164 =>
ConversationController.getOrCreateAndWait(e164, 'private') ConversationController.getOrCreateAndWait(e164, 'private')
) )
); );
const members = memberConversations.map(c => c.get('id')); const members = memberConversations.map(c => c.get('id'));
attributes = { attributes = {
...attributes, ...attributes,
type: 'group', type: 'group',
skipping to change at line 2480 skipping to change at line 2709
return c ? c.get('e164') : null; return c ? c.get('e164') : null;
}); });
pendingGroupUpdate.push(['joined', e164s]); pendingGroupUpdate.push(['joined', e164s]);
} }
if (conversation.get('left')) { if (conversation.get('left')) {
window.log.warn('re-added to a left group'); window.log.warn('re-added to a left group');
attributes.left = false; attributes.left = false;
conversation.set({ addedBy: message.getContactId() }); conversation.set({ addedBy: message.getContactId() });
} }
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) { } else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
const senderId = ConversationController.ensureContactIds({
e164: source,
uuid: sourceUuid,
});
const sender = ConversationController.get(senderId); const sender = ConversationController.get(senderId);
const inGroup = Boolean( const inGroup = Boolean(
sender && sender &&
(conversation.get('members') || []).includes(sender.id) (conversation.get('members') || []).includes(sender.id)
); );
if (!inGroup) { if (!inGroup) {
const senderString = sender ? sender.idForLogging() : null; const senderString = sender ? sender.idForLogging() : null;
window.log.info( window.log.info(
`Got 'left' message from someone not in group: ${senderStrin g}. Dropping.` `Got 'left' message from someone not in group: ${senderStrin g}. Dropping.`
); );
skipping to change at line 2521 skipping to change at line 2746
(acc, [key, value]) => { (acc, [key, value]) => {
acc[key] = value; acc[key] = value;
return acc; return acc;
}, },
{} {}
); );
message.set({ group_update: groupUpdate }); message.set({ group_update: groupUpdate });
} }
} }
// Drop empty messages after. This needs to happen after the initial
// message.set call and after GroupV1 processing to make sure all po
ssible
// properties are set before we determine that a message is empty.
if (message.isEmpty()) {
window.log.info(
`handleDataMessage: Dropping empty message ${message.idForLoggin
g()} in conversation ${conversation.idForLogging()}`
);
confirm();
return;
}
if (type === 'outgoing') { if (type === 'outgoing') {
const receipts = Whisper.DeliveryReceipts.forMessage( const receipts = Whisper.DeliveryReceipts.forMessage(
conversation, conversation,
message message
); );
receipts.forEach(receipt => receipts.forEach(receipt =>
message.set({ message.set({
delivered: (message.get('delivered') || 0) + 1, delivered: (message.get('delivered') || 0) + 1,
delivered_to: _.union(message.get('delivered_to') || [], [ delivered_to: _.union(message.get('delivered_to') || [], [
receipt.get('deliveredTo'), receipt.get('deliveredTo'),
]), ]),
}) })
); );
} }
attributes.active_at = now; attributes.active_at = now;
conversation.set(attributes); conversation.set(attributes);
if (message.isExpirationTimerUpdate()) { if (dataMessage.expireTimer) {
message.set({
expirationTimerUpdate: {
source,
sourceUuid,
expireTimer: dataMessage.expireTimer,
},
});
conversation.set({ expireTimer: dataMessage.expireTimer });
} else if (dataMessage.expireTimer) {
message.set({ expireTimer: dataMessage.expireTimer }); message.set({ expireTimer: dataMessage.expireTimer });
} }
// NOTE: Remove once the above uses if (!isGroupV2) {
// `Conversation::updateExpirationTimer`: if (message.isExpirationTimerUpdate()) {
const { expireTimer } = dataMessage; message.set({
const shouldLogExpireTimerChange = expirationTimerUpdate: {
message.isExpirationTimerUpdate() || expireTimer; source,
if (shouldLogExpireTimerChange) { sourceUuid,
window.log.info("Update conversation 'expireTimer'", { expireTimer: dataMessage.expireTimer,
id: conversation.idForLogging(), },
expireTimer, });
source: 'handleDataMessage', conversation.set({ expireTimer: dataMessage.expireTimer });
}); }
}
if (!message.isEndSession()) { // NOTE: Remove once the above calls this.model.updateExpirationTi
if (dataMessage.expireTimer) { mer()
if ( const { expireTimer } = dataMessage;
dataMessage.expireTimer !== conversation.get('expireTimer') const shouldLogExpireTimerChange =
message.isExpirationTimerUpdate() || expireTimer;
if (shouldLogExpireTimerChange) {
window.log.info("Update conversation 'expireTimer'", {
id: conversation.idForLogging(),
expireTimer,
source: 'handleDataMessage',
});
}
if (!message.isEndSession()) {
if (dataMessage.expireTimer) {
if (
dataMessage.expireTimer !== conversation.get('expireTimer')
) {
conversation.updateExpirationTimer(
dataMessage.expireTimer,
source,
message.get('received_at'),
{
fromGroupUpdate: message.isGroupUpdate(),
}
);
}
} else if (
conversation.get('expireTimer') &&
// We only turn off timers if it's not a group update
!message.isGroupUpdate()
) { ) {
conversation.updateExpirationTimer( conversation.updateExpirationTimer(
dataMessage.expireTimer, null,
source, source,
message.get('received_at'), message.get('received_at')
{
fromGroupUpdate: message.isGroupUpdate(),
}
); );
} }
} else if (
conversation.get('expireTimer') &&
// We only turn off timers if it's not a group update
!message.isGroupUpdate()
) {
conversation.updateExpirationTimer(
null,
source,
message.get('received_at')
);
} }
} }
if (type === 'incoming') { if (type === 'incoming') {
const readSync = Whisper.ReadSyncs.forMessage(message); const readSync = Whisper.ReadSyncs.forMessage(message);
if (readSync) { if (readSync) {
if ( if (
message.get('expireTimer') && message.get('expireTimer') &&
!message.get('expirationStartTimestamp') !message.get('expirationStartTimestamp')
) { ) {
message.set( message.set(
'expirationStartTimestamp', 'expirationStartTimestamp',
Math.min(readSync.get('read_at'), Date.now()) Math.min(readSync.get('read_at'), Date.now())
skipping to change at line 2687 skipping to change at line 2928
} }
// Check for out-of-order view syncs // Check for out-of-order view syncs
if (type === 'incoming' && message.isTapToView()) { if (type === 'incoming' && message.isTapToView()) {
const viewSync = Whisper.ViewSyncs.forMessage(message); const viewSync = Whisper.ViewSyncs.forMessage(message);
if (viewSync) { if (viewSync) {
await message.markViewed({ fromSync: true }); await message.markViewed({ fromSync: true });
} }
} }
} }
// Drop empty messages. This needs to happen after the initial
// message.set call to make sure all possible properties are set
// before we determine that a message is empty.
if (message.isEmpty()) {
window.log.info(
`Dropping empty datamessage ${message.idForLogging()} in conversat
ion ${conversation.idForLogging()}`
);
confirm();
return;
}
const conversationTimestamp = conversation.get('timestamp'); const conversationTimestamp = conversation.get('timestamp');
if ( if (
!conversationTimestamp || !conversationTimestamp ||
message.get('sent_at') > conversationTimestamp message.get('sent_at') > conversationTimestamp
) { ) {
conversation.set({ conversation.set({
lastMessage: message.getNotificationText(), lastMessage: message.getNotificationText(),
timestamp: message.get('sent_at'), timestamp: message.get('sent_at'),
}); });
} }
skipping to change at line 2819 skipping to change at line 3049
async handleDeleteForEveryone(del, shouldPersist = true) { async handleDeleteForEveryone(del, shouldPersist = true) {
window.log.info('Handling DOE.', { window.log.info('Handling DOE.', {
fromId: del.get('fromId'), fromId: del.get('fromId'),
targetSentTimestamp: del.get('targetSentTimestamp'), targetSentTimestamp: del.get('targetSentTimestamp'),
messageServerTimestamp: this.get('serverTimestamp'), messageServerTimestamp: this.get('serverTimestamp'),
deleteServerTimestamp: del.get('serverTimestamp'), deleteServerTimestamp: del.get('serverTimestamp'),
}); });
// Remove any notifications for this message // Remove any notifications for this message
const notificationForMessage = Whisper.Notifications.findWhere({ Whisper.Notifications.removeBy({ messageId: this.get('id') });
messageId: this.get('id'),
});
Whisper.Notifications.remove(notificationForMessage);
// Erase the contents of this message // Erase the contents of this message
await this.eraseContents( await this.eraseContents(
{ deletedForEveryone: true, reactions: [] }, { deletedForEveryone: true, reactions: [] },
shouldPersist shouldPersist
); );
// Update the conversation's last message in case this was the last messag e // Update the conversation's last message in case this was the last messag e
this.getConversation().updateLastMessage(); this.getConversation().updateLastMessage();
}, },
 End of changes. 82 change blocks. 
187 lines changed or deleted 428 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)