"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "js/models/conversations.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).

conversations.js  (Signal-Desktop-1.35.2):conversations.js  (Signal-Desktop-1.36.1)
skipping to change at line 19 skipping to change at line 19
storage, storage,
textsecure, textsecure,
Whisper, Whisper,
Signal Signal
*/ */
/* 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 SEALED_SENDER = { const SEALED_SENDER = {
UNKNOWN: 0, UNKNOWN: 0,
ENABLED: 1, ENABLED: 1,
DISABLED: 2, DISABLED: 2,
UNRESTRICTED: 3, UNRESTRICTED: 3,
}; };
const { Util } = window.Signal; const { Services, Util } = window.Signal;
const { Contact, Message } = window.Signal.Types; const { Contact, Message } = window.Signal.Types;
const { const {
deleteAttachmentData, deleteAttachmentData,
doesAttachmentExist, doesAttachmentExist,
getAbsoluteAttachmentPath, getAbsoluteAttachmentPath,
loadAttachmentData, loadAttachmentData,
readStickerData, readStickerData,
upgradeMessageSchema, upgradeMessageSchema,
writeNewAttachmentData, writeNewAttachmentData,
} = window.Signal.Migrations; } = window.Signal.Migrations;
skipping to change at line 83 skipping to change at line 81
sentMessageCount: 0, sentMessageCount: 0,
}; };
}, },
idForLogging() { idForLogging() {
if (this.isPrivate()) { if (this.isPrivate()) {
const uuid = this.get('uuid'); const uuid = this.get('uuid');
const e164 = this.get('e164'); const e164 = this.get('e164');
return `${uuid || e164} (${this.id})`; return `${uuid || e164} (${this.id})`;
} }
if (this.get('groupVersion') > 1) {
return `groupv2(${this.get('groupId')})`;
}
const groupId = this.get('groupId'); const groupId = this.get('groupId');
return `group(${groupId})`; return `group(${groupId})`;
}, },
debugID() {
const uuid = this.get('uuid');
const e164 = this.get('e164');
const groupId = this.get('groupId');
return `group(${groupId}), sender(${uuid || e164}), id(${this.id})`;
},
// This is one of the few times that we want to collapse our uuid/e164 pair down into // This is one of the few times that we want to collapse our uuid/e164 pair down into
// just one bit of data. If we have a UUID, we'll send using it. // just one bit of data. If we have a UUID, we'll send using it.
getSendTarget() { getSendTarget() {
return this.get('uuid') || this.get('e164'); return this.get('uuid') || this.get('e164');
}, },
handleMessageError(message, errors) { handleMessageError(message, errors) {
this.trigger('messageError', message, errors); this.trigger('messageError', message, errors);
}, },
skipping to change at line 187 skipping to change at line 195
}, },
isMe() { isMe() {
const e164 = this.get('e164'); const e164 = this.get('e164');
const uuid = this.get('uuid'); const uuid = this.get('uuid');
return ( return (
(e164 && e164 === this.ourNumber) || (uuid && uuid === this.ourUuid) (e164 && e164 === this.ourNumber) || (uuid && uuid === this.ourUuid)
); );
}, },
isEverUnregistered() {
return Boolean(this.get('discoveredUnregisteredAt'));
},
isUnregistered() {
const now = Date.now();
const sixHoursAgo = now - 1000 * 60 * 60 * 6;
const discoveredUnregisteredAt = this.get('discoveredUnregisteredAt');
if (discoveredUnregisteredAt && discoveredUnregisteredAt > sixHoursAgo) {
return true;
}
return false;
},
setUnregistered() {
window.log.info(
`Conversation ${this.idForLogging()} is now unregistered`
);
this.set({
discoveredUnregisteredAt: Date.now(),
});
window.Signal.Data.updateConversation(this.attributes);
},
setRegistered() {
window.log.info(
`Conversation ${this.idForLogging()} is registered once again`
);
this.set({
discoveredUnregisteredAt: undefined,
});
window.Signal.Data.updateConversation(this.attributes);
},
isBlocked() { isBlocked() {
const uuid = this.get('uuid'); const uuid = this.get('uuid');
if (uuid) { if (uuid) {
return window.storage.isUuidBlocked(uuid); return window.storage.isUuidBlocked(uuid);
} }
const e164 = this.get('e164'); const e164 = this.get('e164');
if (e164) { if (e164) {
return window.storage.isBlocked(e164); return window.storage.isBlocked(e164);
} }
const groupId = this.get('groupId'); const groupId = this.get('groupId');
if (groupId) { if (groupId) {
return window.storage.isGroupBlocked(groupId); return window.storage.isGroupBlocked(groupId);
} }
return false; return false;
}, },
block() { block({ viaStorageServiceSync = false } = {}) {
let blocked = false;
const isBlocked = this.isBlocked();
const uuid = this.get('uuid'); const uuid = this.get('uuid');
if (uuid) { if (uuid) {
window.storage.addBlockedUuid(uuid); window.storage.addBlockedUuid(uuid);
blocked = true;
} }
const e164 = this.get('e164'); const e164 = this.get('e164');
if (e164) { if (e164) {
window.storage.addBlockedNumber(e164); window.storage.addBlockedNumber(e164);
blocked = true;
} }
const groupId = this.get('groupId'); const groupId = this.get('groupId');
if (groupId) { if (groupId) {
window.storage.addBlockedGroup(groupId); window.storage.addBlockedGroup(groupId);
blocked = true;
}
if (!viaStorageServiceSync && !isBlocked && blocked) {
this.captureChange();
} }
}, },
unblock() { unblock({ viaStorageServiceSync = false } = {}) {
let unblocked = false;
const isBlocked = this.isBlocked();
const uuid = this.get('uuid'); const uuid = this.get('uuid');
if (uuid) { if (uuid) {
window.storage.removeBlockedUuid(uuid); window.storage.removeBlockedUuid(uuid);
unblocked = true;
} }
const e164 = this.get('e164'); const e164 = this.get('e164');
if (e164) { if (e164) {
window.storage.removeBlockedNumber(e164); window.storage.removeBlockedNumber(e164);
unblocked = true;
} }
const groupId = this.get('groupId'); const groupId = this.get('groupId');
if (groupId) { if (groupId) {
window.storage.removeBlockedGroup(groupId); window.storage.removeBlockedGroup(groupId);
unblocked = true;
} }
return false; if (!viaStorageServiceSync && isBlocked && unblocked) {
this.captureChange();
}
return unblocked;
}, },
enableProfileSharing() { enableProfileSharing({ viaStorageServiceSync = false } = {}) {
const before = this.get('profileSharing');
this.set({ profileSharing: true }); this.set({ profileSharing: true });
const after = this.get('profileSharing');
if (!viaStorageServiceSync && Boolean(before) !== Boolean(after)) {
this.captureChange();
}
}, },
disableProfileSharing() { disableProfileSharing({ viaStorageServiceSync = false } = {}) {
const before = this.get('profileSharing');
this.set({ profileSharing: false }); this.set({ profileSharing: false });
const after = this.get('profileSharing');
if (!viaStorageServiceSync && Boolean(before) !== Boolean(after)) {
this.captureChange();
}
}, },
hasDraft() { hasDraft() {
const draftAttachments = this.get('draftAttachments') || []; const draftAttachments = this.get('draftAttachments') || [];
return ( return (
this.get('draft') || this.get('draft') ||
this.get('quotedMessageId') || this.get('quotedMessageId') ||
draftAttachments.length > 0 draftAttachments.length > 0
); );
}, },
skipping to change at line 339 skipping to change at line 416
if (this.typingPauseTimer) { if (this.typingPauseTimer) {
clearTimeout(this.typingPauseTimer); clearTimeout(this.typingPauseTimer);
this.typingPauseTimer = null; this.typingPauseTimer = null;
} }
if (this.typingRefreshTimer) { if (this.typingRefreshTimer) {
clearTimeout(this.typingRefreshTimer); clearTimeout(this.typingRefreshTimer);
this.typingRefreshTimer = null; this.typingRefreshTimer = null;
} }
}, },
async fetchLatestGroupV2Data() {
if (this.get('groupVersion') !== 2) {
return;
}
await window.Signal.Groups.waitThenMaybeUpdateGroup({
conversation: this,
});
},
maybeRepairGroupV2(data) {
if (
this.get('groupVersion') &&
this.get('masterKey') &&
this.get('secretParams') &&
this.get('publicParams')
) {
return;
}
window.log.info(`Repairing GroupV2 conversation ${this.idForLogging()}`);
const { masterKey, secretParams, publicParams } = data;
this.set({ masterKey, secretParams, publicParams, groupVersion: 2 });
window.Signal.Data.updateConversation(this.attributes);
},
getGroupV2Info(groupChange) {
if (this.isPrivate() || this.get('groupVersion') !== 2) {
return null;
}
return {
masterKey: window.Signal.Crypto.base64ToArrayBuffer(
this.get('masterKey')
),
revision: this.get('revision'),
members: this.getRecipients(),
groupChange,
};
},
getGroupV1Info() {
if (this.isPrivate() || this.get('groupVersion') > 0) {
return null;
}
return {
id: this.get('groupId'),
members: this.getRecipients(),
};
},
sendTypingMessage(isTyping) { sendTypingMessage(isTyping) {
if (!textsecure.messaging) { if (!textsecure.messaging) {
return; return;
} }
const groupId = !this.isPrivate() ? this.get('groupId') : null; // We don't send typing messages to our other devices
const groupNumbers = this.getRecipients(); if (this.isMe()) {
return;
}
const recipientId = this.isPrivate() ? this.getSendTarget() : null; const recipientId = this.isPrivate() ? this.getSendTarget() : null;
const groupId = !this.isPrivate() ? this.get('groupId') : null;
const groupMembers = this.getRecipients();
const sendOptions = this.getSendOptions(); // We don't send typing messages if our recipients list is empty
if (!this.isPrivate() && !groupMembers.length) {
return;
}
const sendOptions = this.getSendOptions();
this.wrapSend( this.wrapSend(
textsecure.messaging.sendTypingMessage( textsecure.messaging.sendTypingMessage(
{ {
isTyping, isTyping,
recipientId, recipientId,
groupId, groupId,
groupNumbers, groupMembers,
}, },
sendOptions sendOptions
) )
); );
}, },
async cleanup() { async cleanup() {
await window.Signal.Types.Conversation.deleteExternalFiles( await window.Signal.Types.Conversation.deleteExternalFiles(
this.attributes, this.attributes,
{ {
skipping to change at line 420 skipping to change at line 556
// If a fetch is in progress, then we need to wait until that's complete t o // If a fetch is in progress, then we need to wait until that's complete t o
// do this removal. Otherwise we could remove from messageCollection, th en // do this removal. Otherwise we could remove from messageCollection, th en
// the async database fetch could include the removed message. // the async database fetch could include the removed message.
await this.inProgressFetch; await this.inProgressFetch;
removeMessage(); removeMessage();
}, },
async onNewMessage(message) { async onNewMessage(message) {
const uuid = message.get ? message.get('sourceUuid') : message.sourceUuid;
const e164 = message.get ? message.get('source') : message.source;
const sourceDevice = message.get
? message.get('sourceDevice')
: message.sourceDevice;
const sourceId = window.ConversationController.ensureContactIds({
uuid,
e164,
});
const typingToken = `${sourceId}.${sourceDevice}`;
// Clear typing indicator for a given contact if we receive a message from them // Clear typing indicator for a given contact if we receive a message from them
const deviceId = message.get this.clearContactTypingTimer(typingToken);
? `${message.get('conversationId')}.${message.get('sourceDevice')}`
: `${message.conversationId}.${message.sourceDevice}`;
this.clearContactTypingTimer(deviceId);
this.debouncedUpdateLastMessage(); this.debouncedUpdateLastMessage();
}, },
// For outgoing messages, we can call this directly. We're already loaded. // For outgoing messages, we can call this directly. We're already loaded.
addSingleMessage(message) { addSingleMessage(message) {
const { id } = message; const { id } = message;
const existing = this.messageCollection.get(id); const existing = this.messageCollection.get(id);
const model = this.messageCollection.add(message, { merge: true }); const model = this.messageCollection.add(message, { merge: true });
skipping to change at line 517 skipping to change at line 662
isMe: this.isMe(), isMe: this.isMe(),
isVerified: this.isVerified(), isVerified: this.isVerified(),
lastMessage: { lastMessage: {
status: this.get('lastMessageStatus'), status: this.get('lastMessageStatus'),
text: this.get('lastMessage'), text: this.get('lastMessage'),
deletedForEveryone: this.get('lastMessageDeletedForEveryone'), deletedForEveryone: this.get('lastMessageDeletedForEveryone'),
}, },
lastUpdated: this.get('timestamp'), lastUpdated: this.get('timestamp'),
membersCount: this.isPrivate() membersCount: this.isPrivate()
? undefined ? undefined
: (this.get('members') || []).length, : (this.get('membersV2') || this.get('members') || []).length,
messageRequestsEnabled, messageRequestsEnabled,
muteExpiresAt: this.get('muteExpiresAt'), muteExpiresAt: this.get('muteExpiresAt'),
name: this.get('name'), name: this.get('name'),
phoneNumber: this.getNumber(), phoneNumber: this.getNumber(),
profileName: this.getProfileName(), profileName: this.getProfileName(),
sharedGroupNames: this.get('sharedGroupNames'), sharedGroupNames: this.get('sharedGroupNames'),
shouldShowDraft, shouldShowDraft,
timestamp, timestamp,
title: this.getTitle(), title: this.getTitle(),
type: this.isPrivate() ? 'direct' : 'group', type: this.isPrivate() ? 'direct' : 'group',
skipping to change at line 634 skipping to change at line 779
timestamp: m.get('sent_at'), timestamp: m.get('sent_at'),
hasErrors: m.hasErrors(), hasErrors: m.hasErrors(),
})); }));
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await this.sendReadReceiptsFor(receiptSpecs); await this.sendReadReceiptsFor(receiptSpecs);
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await Promise.all(readMessages.map(m => m.queueAttachmentDownloads())); await Promise.all(readMessages.map(m => m.queueAttachmentDownloads()));
} while (messages.length > 0); } while (messages.length > 0);
}, },
async applyMessageRequestResponse(response, { fromSync = false } = {}) { async applyMessageRequestResponse(
response,
{ fromSync = false, viaStorageServiceSync = false } = {}
) {
// Apply message request response locally // Apply message request response locally
this.set({ this.set({
messageRequestResponseType: response, messageRequestResponseType: response,
}); });
window.Signal.Data.updateConversation(this.attributes); window.Signal.Data.updateConversation(this.attributes);
if (response === this.messageRequestEnum.ACCEPT) { if (response === this.messageRequestEnum.ACCEPT) {
this.unblock(); this.unblock({ viaStorageServiceSync });
this.enableProfileSharing(); this.enableProfileSharing({ viaStorageServiceSync });
if (!fromSync) { if (!fromSync) {
this.sendProfileKeyUpdate(); this.sendProfileKeyUpdate();
// Locally accepted // Locally accepted
await this.handleReadAndDownloadAttachments(); await this.handleReadAndDownloadAttachments();
} }
} else if (response === this.messageRequestEnum.BLOCK) { } else if (response === this.messageRequestEnum.BLOCK) {
// Block locally, other devices should block upon receiving the sync mes sage // Block locally, other devices should block upon receiving the sync mes sage
this.block(); this.block({ viaStorageServiceSync });
this.disableProfileSharing(); this.disableProfileSharing({ viaStorageServiceSync });
} else if (response === this.messageRequestEnum.DELETE) { } else if (response === this.messageRequestEnum.DELETE) {
// Delete messages locally, other devices should delete upon receiving // Delete messages locally, other devices should delete upon receiving
// the sync message // the sync message
this.destroyMessages(); this.destroyMessages();
this.disableProfileSharing(); this.disableProfileSharing({ viaStorageServiceSync });
this.updateLastMessage(); this.updateLastMessage();
if (!fromSync) { if (!fromSync) {
this.trigger('unload', 'deleted from message request'); this.trigger('unload', 'deleted from message request');
} }
} else if (response === this.messageRequestEnum.BLOCK_AND_DELETE) { } else if (response === this.messageRequestEnum.BLOCK_AND_DELETE) {
// Delete messages locally, other devices should delete upon receiving // Delete messages locally, other devices should delete upon receiving
// the sync message // the sync message
this.destroyMessages(); this.destroyMessages();
this.disableProfileSharing(); this.disableProfileSharing({ viaStorageServiceSync });
this.updateLastMessage(); this.updateLastMessage();
// Block locally, other devices should block upon receiving the sync mes sage // Block locally, other devices should block upon receiving the sync mes sage
this.block(); this.block({ viaStorageServiceSync });
// Leave group if this was a local action // Leave group if this was a local action
if (!fromSync) { if (!fromSync) {
this.leaveGroup(); this.leaveGroup();
this.trigger('unload', 'blocked and deleted from message request'); this.trigger('unload', 'blocked and deleted from message request');
} }
} }
}, },
async syncMessageRequestResponse(response) { async syncMessageRequestResponse(response) {
// Let this run, no await // Let this run, no await
skipping to change at line 726 skipping to change at line 874
const verified = await this.safeGetVerified(); const verified = await this.safeGetVerified();
if (this.get('verified') !== verified) { if (this.get('verified') !== verified) {
this.set({ verified }); this.set({ verified });
window.Signal.Data.updateConversation(this.attributes); window.Signal.Data.updateConversation(this.attributes);
} }
return; return;
} }
await this.fetchContacts(); this.fetchContacts();
await Promise.all( await Promise.all(
this.contactCollection.map(async contact => { this.contactCollection.map(async contact => {
if (!contact.isMe()) { if (!contact.isMe()) {
await contact.updateVerified(); await contact.updateVerified();
} }
}) })
); );
this.onMemberVerifiedChange(); this.onMemberVerifiedChange();
}, },
skipping to change at line 752 skipping to change at line 901
const { VERIFIED } = this.verifiedEnum; const { VERIFIED } = this.verifiedEnum;
return this.queueJob(() => this._setVerified(VERIFIED, options)); return this.queueJob(() => this._setVerified(VERIFIED, options));
}, },
setUnverified(options) { setUnverified(options) {
const { UNVERIFIED } = this.verifiedEnum; const { UNVERIFIED } = this.verifiedEnum;
return this.queueJob(() => this._setVerified(UNVERIFIED, options)); return this.queueJob(() => this._setVerified(UNVERIFIED, options));
}, },
async _setVerified(verified, providedOptions) { async _setVerified(verified, providedOptions) {
const options = providedOptions || {}; const options = providedOptions || {};
_.defaults(options, { _.defaults(options, {
viaStorageServiceSync: false,
viaSyncMessage: false, viaSyncMessage: false,
viaContactSync: false, viaContactSync: false,
key: null, key: null,
}); });
const { VERIFIED, UNVERIFIED } = this.verifiedEnum; const { VERIFIED, UNVERIFIED } = this.verifiedEnum;
if (!this.isPrivate()) { if (!this.isPrivate()) {
throw new Error( throw new Error(
'You cannot verify a group conversation. ' + 'You cannot verify a group conversation. ' +
skipping to change at line 786 skipping to change at line 936
} else { } else {
keyChange = await textsecure.storage.protocol.setVerified( keyChange = await textsecure.storage.protocol.setVerified(
this.id, this.id,
verified verified
); );
} }
this.set({ verified }); this.set({ verified });
window.Signal.Data.updateConversation(this.attributes); window.Signal.Data.updateConversation(this.attributes);
if (
!options.viaStorageServiceSync &&
!keyChange &&
beginningVerified !== verified
) {
this.captureChange();
}
// Three situations result in a verification notice in the conversation: // Three situations result in a verification notice in the conversation:
// 1) The message came from an explicit verification in another client ( not // 1) The message came from an explicit verification in another client ( not
// a contact sync) // a contact sync)
// 2) The verification value received by the contact sync is different // 2) The verification value received by the contact sync is different
// from what we have on record (and it's not a transition to UNVERIFI ED) // from what we have on record (and it's not a transition to UNVERIFI ED)
// 3) Our local verification status is VERIFIED and it hasn't changed, // 3) Our local verification status is VERIFIED and it hasn't changed,
// but the key did change (Key1/VERIFIED to Key2/VERIFIED - but we do n't // but the key did change (Key1/VERIFIED to Key2/VERIFIED - but we do n't
// want to show DEFAULT->DEFAULT or UNVERIFIED->UNVERIFIED) // want to show DEFAULT->DEFAULT or UNVERIFIED->UNVERIFIED)
if ( if (
!options.viaContactSync || !options.viaContactSync ||
skipping to change at line 822 skipping to change at line 980
}, },
sendVerifySyncMessage(e164, uuid, state) { sendVerifySyncMessage(e164, uuid, state) {
// Because syncVerification sends a (null) message to the target of the ve rify and // Because syncVerification sends a (null) message to the target of the ve rify and
// a sync message to our own devices, we need to send the accessKeys dow n for both // a sync message to our own devices, we need to send the accessKeys dow n for both
// contacts. So we merge their sendOptions. // contacts. So we merge their sendOptions.
const { sendOptions } = ConversationController.prepareForSend( const { sendOptions } = ConversationController.prepareForSend(
this.ourNumber || this.ourUuid, this.ourNumber || this.ourUuid,
{ syncMessage: true } { syncMessage: true }
); );
const contactSendOptions = this.getSendOptions(); const contactSendOptions = this.getSendOptions();
const options = Object.assign({}, sendOptions, contactSendOptions); const options = { ...sendOptions, ...contactSendOptions };
const promise = textsecure.storage.protocol.loadIdentityKey(e164); const promise = textsecure.storage.protocol.loadIdentityKey(e164);
return promise.then(key => return promise.then(key =>
this.wrapSend( this.wrapSend(
textsecure.messaging.syncVerification(e164, uuid, state, key, options) textsecure.messaging.syncVerification(e164, uuid, state, key, options)
) )
); );
}, },
isVerified() { isVerified() {
if (this.isPrivate()) { if (this.isPrivate()) {
skipping to change at line 1248 skipping to change at line 1406
this.jobQueue = this.jobQueue || new window.PQueue({ concurrency: 1 }); this.jobQueue = this.jobQueue || new window.PQueue({ concurrency: 1 });
const taskWithTimeout = textsecure.createTaskWithTimeout( const taskWithTimeout = textsecure.createTaskWithTimeout(
callback, callback,
`conversation ${this.idForLogging()}` `conversation ${this.idForLogging()}`
); );
return this.jobQueue.add(taskWithTimeout); return this.jobQueue.add(taskWithTimeout);
}, },
getRecipients() { getMembers() {
if (this.isPrivate()) { if (this.isPrivate()) {
return [this.getSendTarget()]; return [this];
}
if (this.get('membersV2')) {
return _.compact(
this.get('membersV2').map(member => {
const c = ConversationController.get(member.conversationId);
// In groups we won't sent to contacts we believe are unregistered
if (c && c.isUnregistered()) {
return null;
}
return c;
})
);
} }
const me = ConversationController.getOurConversationId();
// The list of members might not always be conversationIds for old groups. if (this.get('members')) {
return _.compact(
this.get('members').map(id => {
const c = ConversationController.get(id);
// In groups we won't sent to contacts we believe are unregistered
if (c && c.isUnregistered()) {
return null;
}
return c;
})
);
}
window.log.warn(
'getMembers: Group conversation had neither membersV2 nor members'
);
return [];
},
getMemberIds() {
const members = this.getMembers();
return members.map(member => member.id);
},
getRecipients() {
const members = this.getMembers();
// Eliminate our
return _.compact( return _.compact(
this.get('members').map(memberId => { members.map(member => (member.isMe() ? null : member.getSendTarget()))
const c = ConversationController.get(memberId);
if (c.id === me) {
return null;
}
return c.getSendTarget();
})
); );
}, },
async getQuoteAttachment(attachments, preview, sticker) { async getQuoteAttachment(attachments, preview, sticker) {
if (attachments && attachments.length) { if (attachments && attachments.length) {
return Promise.all( return Promise.all(
attachments attachments
.filter( .filter(
attachment => attachment =>
attachment && attachment &&
skipping to change at line 1425 skipping to change at line 1621
const expireTimer = this.get('expireTimer'); const expireTimer = this.get('expireTimer');
const reactionModel = Whisper.Reactions.add({ const reactionModel = Whisper.Reactions.add({
...outgoingReaction, ...outgoingReaction,
fromId: ConversationController.getOurConversationId(), fromId: ConversationController.getOurConversationId(),
timestamp, timestamp,
fromSync: true, fromSync: true,
}); });
Whisper.Reactions.onReaction(reactionModel); Whisper.Reactions.onReaction(reactionModel);
const destination = this.get('e164'); const destination = this.getSendTarget();
const recipients = this.getRecipients(); const recipients = this.getRecipients();
let profileKey; let profileKey;
if (this.get('profileSharing')) { if (this.get('profileSharing')) {
profileKey = storage.get('profileKey'); profileKey = storage.get('profileKey');
} }
return this.queueJob(async () => { return this.queueJob(async () => {
window.log.info( window.log.info(
'Sending reaction to conversation', 'Sending reaction to conversation',
skipping to change at line 1468 skipping to change at line 1664
// We're offline! // We're offline!
if (!textsecure.messaging) { if (!textsecure.messaging) {
throw new Error('Cannot send reaction while offline!'); throw new Error('Cannot send reaction while offline!');
} }
// Special-case the self-send case - we send only a sync message // Special-case the self-send case - we send only a sync message
if (this.isMe()) { if (this.isMe()) {
const dataMessage = await textsecure.messaging.getMessageProto( const dataMessage = await textsecure.messaging.getMessageProto(
destination, destination,
null, null, // body
null, null, // attachments
null, null, // quote
null, null, // preview
null, null, // sticker
outgoingReaction, outgoingReaction,
timestamp, timestamp,
expireTimer, expireTimer,
profileKey profileKey
); );
return message.sendSyncMessageOnly(dataMessage); return message.sendSyncMessageOnly(dataMessage);
} }
const options = this.getSendOptions(); const options = this.getSendOptions();
const promise = (() => { const promise = (() => {
if (this.isPrivate()) { if (this.isPrivate()) {
return textsecure.messaging.sendMessageToIdentifier( return textsecure.messaging.sendMessageToIdentifier(
destination, destination,
null, null, // body
null, null, // attachments
null, null, // quote
null, null, // preview
null, null, // sticker
outgoingReaction, outgoingReaction,
timestamp, timestamp,
expireTimer, expireTimer,
profileKey, profileKey,
options options
); );
} }
return textsecure.messaging.sendMessageToGroup( return textsecure.messaging.sendMessageToGroup(
this.get('groupId'), {
this.getRecipients(), groupV1: this.getGroupV1Info(),
null, groupV2: this.getGroupV2Info(),
null, reaction: outgoingReaction,
null, timestamp,
null, expireTimer,
null, profileKey,
outgoingReaction, },
timestamp,
expireTimer,
profileKey,
options options
); );
})(); })();
return message.send(this.wrapSend(promise)); return message.send(this.wrapSend(promise));
}).catch(error => { }).catch(error => {
window.log.error('Error sending reaction', reaction, target, error); window.log.error('Error sending reaction', reaction, target, error);
const reverseReaction = reactionModel.clone(); const reverseReaction = reactionModel.clone();
reverseReaction.set('remove', !reverseReaction.get('remove')); reverseReaction.set('remove', !reverseReaction.get('remove'));
skipping to change at line 1660 skipping to change at line 1853
// Special-case the self-send case - we send only a sync message // Special-case the self-send case - we send only a sync message
if (this.isMe()) { if (this.isMe()) {
const dataMessage = await textsecure.messaging.getMessageProto( const dataMessage = await textsecure.messaging.getMessageProto(
destination, destination,
messageBody, messageBody,
finalAttachments, finalAttachments,
quote, quote,
preview, preview,
sticker, sticker,
null, null, // reaction
now, now,
expireTimer, expireTimer,
profileKey profileKey
); );
return message.sendSyncMessageOnly(dataMessage); return message.sendSyncMessageOnly(dataMessage);
} }
const conversationType = this.get('type'); const conversationType = this.get('type');
const options = this.getSendOptions(); const options = this.getSendOptions();
const promise = (() => { let promise;
switch (conversationType) { if (conversationType === Message.GROUP) {
case Message.PRIVATE: promise = textsecure.messaging.sendMessageToGroup(
return textsecure.messaging.sendMessageToIdentifier( {
destination, attachments: finalAttachments,
messageBody, expireTimer,
finalAttachments, groupV1: this.getGroupV1Info(),
quote, groupV2: this.getGroupV2Info(),
preview, messageText: messageBody,
sticker, preview,
null, profileKey,
now, quote,
expireTimer, sticker,
profileKey, timestamp: now,
options },
); options
case Message.GROUP: );
return textsecure.messaging.sendMessageToGroup( } else {
this.get('groupId'), promise = textsecure.messaging.sendMessageToIdentifier(
this.getRecipients(), destination,
messageBody, messageBody,
finalAttachments, finalAttachments,
quote, quote,
preview, preview,
sticker, sticker,
null, null, // reaction
now, now,
expireTimer, expireTimer,
profileKey, profileKey,
options options
); );
default: }
throw new TypeError(
`Invalid conversation type: '${conversationType}'`
);
}
})();
return message.send(this.wrapSend(promise)); return message.send(this.wrapSend(promise));
}); });
}, },
wrapSend(promise) { wrapSend(promise) {
return promise.then( return promise.then(
async result => { async result => {
// success // success
if (result) { if (result) {
await this.handleMessageSendResult( await this.handleMessageSendResult(
result.failoverIdentifiers, result.failoverIdentifiers,
result.unidentifiedDeliveries result.unidentifiedDeliveries,
result.discoveredIdentifierPairs
); );
} }
return result; return result;
}, },
async result => { async result => {
// failure // failure
if (result) { if (result) {
await this.handleMessageSendResult( await this.handleMessageSendResult(
result.failoverIdentifiers, result.failoverIdentifiers,
result.unidentifiedDeliveries result.unidentifiedDeliveries,
result.discoveredIdentifierPairs
); );
} }
throw result; throw result;
} }
); );
}, },
async handleMessageSendResult(failoverIdentifiers, unidentifiedDeliveries) { async handleMessageSendResult(
failoverIdentifiers,
unidentifiedDeliveries,
discoveredIdentifierPairs
) {
discoveredIdentifierPairs.forEach(item => {
const { uuid, e164 } = item;
window.ConversationController.ensureContactIds({
uuid,
e164,
highTrust: true,
});
});
await Promise.all( await Promise.all(
(failoverIdentifiers || []).map(async identifier => { (failoverIdentifiers || []).map(async identifier => {
const conversation = ConversationController.get(identifier); const conversation = ConversationController.get(identifier);
if ( if (
conversation && conversation &&
conversation.get('sealedSender') !== SEALED_SENDER.DISABLED conversation.get('sealedSender') !== SEALED_SENDER.DISABLED
) { ) {
window.log.info( window.log.info(
`Setting sealedSender to DISABLED for conversation ${conversation. idForLogging()}` `Setting sealedSender to DISABLED for conversation ${conversation. idForLogging()}`
skipping to change at line 1916 skipping to change at line 2119
this.hasDraft() && this.hasDraft() &&
this.get('draftTimestamp') && this.get('draftTimestamp') &&
(!previewMessage || (!previewMessage ||
previewMessage.get('sent_at') < this.get('draftTimestamp')) previewMessage.get('sent_at') < this.get('draftTimestamp'))
) { ) {
return; return;
} }
const currentTimestamp = this.get('timestamp') || null; const currentTimestamp = this.get('timestamp') || null;
const timestamp = activityMessage const timestamp = activityMessage
? activityMessage.get('sent_at') || currentTimestamp ? activityMessage.get('sent_at') ||
activityMessage.get('received_at') ||
currentTimestamp
: currentTimestamp; : currentTimestamp;
this.set({ this.set({
lastMessage: lastMessage:
(previewMessage ? previewMessage.getNotificationText() : '') || '', (previewMessage ? previewMessage.getNotificationText() : '') || '',
lastMessageStatus: lastMessageStatus:
(previewMessage ? previewMessage.getMessagePropStatus() : null) || (previewMessage ? previewMessage.getMessagePropStatus() : null) ||
null, null,
timestamp, timestamp,
lastMessageDeletedForEveryone: previewMessage lastMessageDeletedForEveryone: previewMessage
? previewMessage.deletedForEveryone ? previewMessage.deletedForEveryone
: false, : false,
}); });
window.Signal.Data.updateConversation(this.attributes); window.Signal.Data.updateConversation(this.attributes);
}, },
async setArchived(isArchived) { setArchived(isArchived) {
const before = this.get('isArchived');
this.set({ isArchived }); this.set({ isArchived });
window.Signal.Data.updateConversation(this.attributes); window.Signal.Data.updateConversation(this.attributes);
const after = this.get('isArchived');
if (Boolean(before) !== Boolean(after)) {
this.captureChange();
}
},
async updateExpirationTimerInGroupV2(seconds) {
// Make change on the server
const actions = window.Signal.Groups.buildDisappearingMessagesTimerChange(
{
expireTimer: seconds || 0,
group: this.attributes,
}
);
let signedGroupChange;
try {
signedGroupChange = await window.Signal.Groups.uploadGroupChange({
actions,
group: this.attributes,
serverPublicParamsBase64: window.getServerPublicParams(),
});
} catch (error) {
// Get latest GroupV2 data, since we ran into trouble updating it
this.fetchLatestGroupV2Data();
throw error;
}
// Update local conversation
this.set({
expireTimer: seconds || 0,
revision: actions.version,
});
window.Signal.Data.updateConversation(this.attributes);
// Create local notification
const timestamp = Date.now();
const id = window.getGuid();
const message = MessageController.register(
id,
new Whisper.Message({
id,
conversationId: this.id,
sent_at: timestamp,
received_at: timestamp,
flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
expirationTimerUpdate: {
expireTimer: seconds,
sourceUuid: this.ourUuid,
},
})
);
await window.Signal.Data.saveMessage(message.attributes, {
Message: Whisper.Message,
forceSave: true,
});
this.trigger('newmessage', message);
// Send message to all group members
const profileKey = this.get('profileSharing')
? storage.get('profileKey')
: undefined;
const sendOptions = this.getSendOptions();
const promise = textsecure.messaging.sendMessageToGroup(
{
groupV2: this.getGroupV2Info(signedGroupChange.toArrayBuffer()),
timestamp,
profileKey,
},
sendOptions
);
message.send(promise);
}, },
async updateExpirationTimer( async updateExpirationTimer(
providedExpireTimer, providedExpireTimer,
providedSource, providedSource,
receivedAt, receivedAt,
options = {} options = {}
) { ) {
if (this.get('groupVersion') === 2) {
if (providedSource || receivedAt) {
throw new Error(
'updateExpirationTimer: GroupV2 timers are not updated this way'
);
}
await this.updateExpirationTimerInGroupV2(providedExpireTimer);
return false;
}
let expireTimer = providedExpireTimer; let expireTimer = providedExpireTimer;
let source = providedSource; let source = providedSource;
if (this.get('left')) { if (this.get('left')) {
return false; return false;
} }
_.defaults(options, { fromSync: false, fromGroupUpdate: false }); _.defaults(options, { fromSync: false, fromGroupUpdate: false });
if (!expireTimer) { if (!expireTimer) {
expireTimer = null; expireTimer = null;
skipping to change at line 2027 skipping to change at line 2318
profileKey = storage.get('profileKey'); profileKey = storage.get('profileKey');
} }
const sendOptions = this.getSendOptions(); const sendOptions = this.getSendOptions();
let promise; let promise;
if (this.isMe()) { if (this.isMe()) {
const flags = const flags =
textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
const dataMessage = await textsecure.messaging.getMessageProto( const dataMessage = await textsecure.messaging.getMessageProto(
this.getSendTarget(), this.getSendTarget(),
null, null, // body
[], [], // attachments
null, null, // quote
[], [], // preview
null, null, // sticker
null, null, // reaction
message.get('sent_at'), message.get('sent_at'),
expireTimer, expireTimer,
profileKey, profileKey,
flags flags
); );
return message.sendSyncMessageOnly(dataMessage); return message.sendSyncMessageOnly(dataMessage);
} }
if (this.get('type') === 'private') { if (this.get('type') === 'private') {
promise = textsecure.messaging.sendExpirationTimerUpdateToIdentifier( promise = textsecure.messaging.sendExpirationTimerUpdateToIdentifier(
skipping to change at line 2146 skipping to change at line 2437
this.get('uuid'), this.get('uuid'),
this.get('e164'), this.get('e164'),
now, now,
options options
) )
) )
); );
} }
}, },
async updateGroup(providedGroupUpdate) {
let groupUpdate = providedGroupUpdate;
if (this.isPrivate()) {
throw new Error('Called update group on private conversation');
}
if (groupUpdate === undefined) {
groupUpdate = this.pick(['name', 'avatar', 'members']);
}
const now = Date.now();
const model = new Whisper.Message({
conversationId: this.id,
type: 'outgoing',
sent_at: now,
received_at: now,
group_update: groupUpdate,
});
const id = await window.Signal.Data.saveMessage(model.attributes, {
Message: Whisper.Message,
});
model.set({ id });
const message = MessageController.register(model.id, model);
this.addSingleMessage(message);
const options = this.getSendOptions();
message.send(
this.wrapSend(
textsecure.messaging.updateGroup(
this.id,
this.get('name'),
this.get('avatar'),
this.get('members'),
options
)
)
);
},
async leaveGroup() {
const now = Date.now();
if (this.get('type') === 'group') {
const groupIdentifiers = this.getRecipients();
this.set({ left: true });
window.Signal.Data.updateConversation(this.attributes);
const model = new Whisper.Message({
group_update: { left: 'You' },
conversationId: this.id,
type: 'outgoing',
sent_at: now,
received_at: now,
});
const id = await window.Signal.Data.saveMessage(model.attributes, {
Message: Whisper.Message,
});
model.set({ id });
const message = MessageController.register(model.id, model);
this.addSingleMessage(message);
const options = this.getSendOptions();
message.send(
this.wrapSend(
textsecure.messaging.leaveGroup(this.id, groupIdentifiers, options)
)
);
}
},
async markRead(newestUnreadDate, providedOptions) { async markRead(newestUnreadDate, providedOptions) {
const options = providedOptions || {}; const options = providedOptions || {};
_.defaults(options, { sendReadReceipts: true }); _.defaults(options, { sendReadReceipts: true });
const conversationId = this.id; const conversationId = this.id;
Whisper.Notifications.remove( Whisper.Notifications.removeBy({ conversationId });
Whisper.Notifications.where({
conversationId,
})
);
let unreadMessages = await this.getUnread(); let unreadMessages = await this.getUnread();
const oldUnread = unreadMessages.filter( const oldUnread = unreadMessages.filter(
message => message.get('received_at') <= newestUnreadDate message => message.get('received_at') <= newestUnreadDate
); );
let read = await Promise.all( let read = await Promise.all(
_.map(oldUnread, async providedM => { _.map(oldUnread, async providedM => {
const m = MessageController.register(providedM.id, providedM); const m = MessageController.register(providedM.id, providedM);
skipping to change at line 2344 skipping to change at line 2558
}, },
onChangeProfileKey() { onChangeProfileKey() {
if (this.isPrivate()) { if (this.isPrivate()) {
this.getProfiles(); this.getProfiles();
} }
}, },
getProfiles() { getProfiles() {
// request all conversation members' keys // request all conversation members' keys
let conversations = []; const conversations = this.getMembers();
if (this.isPrivate()) {
conversations = [this];
} else {
conversations = this.get('members')
.map(id => ConversationController.get(id))
.filter(Boolean);
}
return Promise.all( return Promise.all(
_.map(conversations, conversation => { _.map(conversations, conversation => {
this.getProfile(conversation.get('uuid'), conversation.get('e164')); this.getProfile(conversation.get('uuid'), conversation.get('e164'));
}) })
); );
}, },
async getProfile(providedUuid, providedE164) { async getProfile(providedUuid, providedE164) {
if (!textsecure.messaging) { if (!textsecure.messaging) {
throw new Error( throw new Error(
skipping to change at line 2529 skipping to change at line 2736
c.idForLogging(), c.idForLogging(),
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
} else { } else {
await c.dropProfileKey(); await c.dropProfileKey();
} }
return; return;
} }
try { try {
await c.setProfileName(profile.name); await c.setEncryptedProfileName(profile.name);
} catch (error) { } catch (error) {
window.log.warn( window.log.warn(
'getProfile decryption failure:', 'getProfile decryption failure:',
c.idForLogging(), c.idForLogging(),
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );
await c.dropProfileKey(); await c.dropProfileKey();
} }
try { try {
skipping to change at line 2554 skipping to change at line 2761
`Clearing profile avatar for conversation ${c.idForLogging()}` `Clearing profile avatar for conversation ${c.idForLogging()}`
); );
c.set({ c.set({
profileAvatar: null, profileAvatar: null,
}); });
} }
} }
window.Signal.Data.updateConversation(c.attributes); window.Signal.Data.updateConversation(c.attributes);
}, },
async setProfileName(encryptedName) { async setEncryptedProfileName(encryptedName) {
if (!encryptedName) { if (!encryptedName) {
return; return;
} }
const key = this.get('profileKey'); const key = this.get('profileKey');
if (!key) { if (!key) {
return; return;
} }
// decode // decode
const keyBuffer = base64ToArrayBuffer(key); const keyBuffer = base64ToArrayBuffer(key);
skipping to change at line 2604 skipping to change at line 2811
}; };
await this.addProfileChange(change); await this.addProfileChange(change);
} }
}, },
async setProfileAvatar(avatarPath) { async setProfileAvatar(avatarPath) {
if (!avatarPath) { if (!avatarPath) {
return; return;
} }
if (this.isMe()) {
window.storage.put('avatarUrl', avatarPath);
}
const avatar = await textsecure.messaging.getAvatar(avatarPath); const avatar = await textsecure.messaging.getAvatar(avatarPath);
const key = this.get('profileKey'); const key = this.get('profileKey');
if (!key) { if (!key) {
return; return;
} }
const keyBuffer = base64ToArrayBuffer(key); const keyBuffer = base64ToArrayBuffer(key);
// decrypt // decrypt
const decrypted = await textsecure.crypto.decryptProfile( const decrypted = await textsecure.crypto.decryptProfile(
avatar, avatar,
skipping to change at line 2631 skipping to change at line 2842
decrypted, decrypted,
{ {
writeNewAttachmentData, writeNewAttachmentData,
deleteAttachmentData, deleteAttachmentData,
doesAttachmentExist, doesAttachmentExist,
} }
); );
this.set(newAttributes); this.set(newAttributes);
} }
}, },
async setProfileKey(profileKey) { async setProfileKey(profileKey, { viaStorageServiceSync = false } = {}) {
// profileKey is a string so we can compare it directly // profileKey is a string so we can compare it directly
if (this.get('profileKey') !== profileKey) { if (this.get('profileKey') !== profileKey) {
window.log.info( window.log.info(
`Setting sealedSender to UNKNOWN for conversation ${this.idForLogging( )}` `Setting sealedSender to UNKNOWN for conversation ${this.idForLogging( )}`
); );
this.set({ this.set({
profileKey, profileKey,
profileKeyVersion: null, profileKeyVersion: null,
profileKeyCredential: null, profileKeyCredential: null,
accessKey: null, accessKey: null,
sealedSender: SEALED_SENDER.UNKNOWN, sealedSender: SEALED_SENDER.UNKNOWN,
}); });
if (!viaStorageServiceSync) {
this.captureChange();
}
await Promise.all([ await Promise.all([
this.deriveAccessKeyIfNeeded(), this.deriveAccessKeyIfNeeded(),
this.deriveProfileKeyVersionIfNeeded(), this.deriveProfileKeyVersionIfNeeded(),
]); ]);
window.Signal.Data.updateConversation(this.attributes, { window.Signal.Data.updateConversation(this.attributes, {
Conversation: Whisper.Conversation, Conversation: Whisper.Conversation,
}); });
} }
}, },
skipping to change at line 2714 skipping to change at line 2929
const profileKeyVersion = Util.zkgroup.deriveProfileKeyVersion( const profileKeyVersion = Util.zkgroup.deriveProfileKeyVersion(
profileKey, profileKey,
uuid uuid
); );
this.set({ profileKeyVersion }); this.set({ profileKeyVersion });
}, },
hasMember(identifier) { hasMember(identifier) {
const cid = ConversationController.getConversationId(identifier); const id = ConversationController.getConversationId(identifier);
return cid && _.contains(this.get('members'), cid); const memberIds = this.getMemberIds();
return _.contains(memberIds, id);
}, },
fetchContacts() { fetchContacts() {
if (this.isPrivate()) { if (this.isPrivate()) {
this.contactCollection.reset([this]); this.contactCollection.reset([this]);
return Promise.resolve();
} }
const members = this.get('members') || []; const members = this.getMembers();
const promises = members.map(identifier => _.forEach(members, member => {
ConversationController.getOrCreateAndWait(identifier, 'private') this.listenTo(member, 'change:verified', this.onMemberVerifiedChange);
);
return Promise.all(promises).then(contacts => {
_.forEach(contacts, contact => {
this.listenTo(
contact,
'change:verified',
this.onMemberVerifiedChange
);
});
this.contactCollection.reset(contacts);
}); });
this.contactCollection.reset(members);
}, },
async destroyMessages() { async destroyMessages() {
this.messageCollection.reset([]); this.messageCollection.reset([]);
this.set({ this.set({
lastMessage: null, lastMessage: null,
timestamp: null, timestamp: null,
active_at: null, active_at: null,
}); });
skipping to change at line 2838 skipping to change at line 3044
const avatar = this.isMe() const avatar = this.isMe()
? this.get('profileAvatar') || this.get('avatar') ? this.get('profileAvatar') || this.get('avatar')
: this.get('avatar') || this.get('profileAvatar'); : this.get('avatar') || this.get('profileAvatar');
if (avatar && avatar.path) { if (avatar && avatar.path) {
return getAbsoluteAttachmentPath(avatar.path); return getAbsoluteAttachmentPath(avatar.path);
} }
return null; return null;
}, },
getAvatar() { canChangeTimer() {
const title = this.get('name'); if (this.isPrivate()) {
const color = this.getColor(); return true;
const avatar = this.get('avatar') || this.get('profileAvatar'); }
if (avatar && avatar.path) { if (this.get('groupVersion') !== 2) {
return { url: getAbsoluteAttachmentPath(avatar.path), color }; return true;
} else if (this.isPrivate()) {
return {
color,
content: this.getInitials(title) || '#',
};
} }
return { url: 'images/group_default.png', color };
const accessControlEnum =
textsecure.protobuf.AccessControl.AccessRequired;
const accessControl = this.get('accessControl');
const canAnyoneChangeTimer =
accessControl &&
(accessControl.attributes === accessControlEnum.ANY ||
accessControl.attributes === accessControlEnum.MEMBER);
if (canAnyoneChangeTimer) {
return true;
}
const memberEnum = textsecure.protobuf.Member.Role;
const members = this.get('membersV2') || [];
const myId = ConversationController.getConversationId(
textsecure.storage.user.getUuid() || textsecure.storage.user.getNumber()
);
const me = members.find(item => item.conversationId === myId);
if (!me) {
return false;
}
const isAdministrator = me.role === memberEnum.ADMINISTRATOR;
if (isAdministrator) {
return true;
}
return false;
}, },
getNotificationIcon() { // Set of items to captureChanges on:
return new Promise(resolve => { // [-] uuid
const avatar = this.getAvatar(); // [-] e164
if (avatar.url) { // [X] profileKey
resolve(avatar.url); // [-] identityKey
} else { // [X] verified!
resolve(new Whisper.IdenticonSVGView(avatar).getDataUrl()); // [-] profileName
} // [-] profileFamilyName
// [X] blocked
// [X] whitelisted
// [X] archived
captureChange() {
if (!window.Signal.RemoteConfig.isEnabled('desktop.storageWrite')) {
window.log.info(
'conversation.captureChange: Returning early; desktop.storageWrite is
falsey'
);
return;
}
this.set({ needsStorageServiceSync: true });
this.queueJob(() => {
Services.storageServiceUploadJob();
}); });
}, },
async notify(message, reaction) { async notify(message, reaction) {
if (this.get('muteExpiresAt') && Date.now() < this.get('muteExpiresAt')) { if (this.get('muteExpiresAt') && Date.now() < this.get('muteExpiresAt')) {
return; return;
} }
if (!message.isIncoming() && !reaction) { if (!message.isIncoming() && !reaction) {
return; return;
} }
const conversationId = this.id; const conversationId = this.id;
const sender = reaction const sender = reaction
? ConversationController.get(reaction.get('fromId')) ? ConversationController.get(reaction.get('fromId'))
: message.getContact(); : message.getContact();
const senderName = sender ? sender.getTitle() : i18n('unknownContact');
const senderTitle = this.isPrivate()
? senderName
: i18n('notificationSenderInGroup', {
sender: senderName,
group: this.getTitle(),
});
const iconUrl = await sender.getNotificationIcon(); let notificationIconUrl;
const avatar = this.get('avatar') || this.get('profileAvatar');
if (avatar && avatar.path) {
notificationIconUrl = getAbsoluteAttachmentPath(avatar.path);
} else if (this.isPrivate()) {
notificationIconUrl = await new Whisper.IdenticonSVGView({
color: this.getColor(),
content: this.getInitials(this.get('name')) || '#',
}).getDataUrl();
} else {
// Not technically needed, but helps us be explicit: we don't show an ic
on for a
// group that doesn't have an icon.
notificationIconUrl = undefined;
}
const messageJSON = message.toJSON(); const messageJSON = message.toJSON();
const messageSentAt = messageJSON.sent_at;
const messageId = message.id; const messageId = message.id;
const isExpiringMessage = Message.hasExpiration(messageJSON); const isExpiringMessage = Message.hasExpiration(messageJSON);
Whisper.Notifications.add({ Whisper.Notifications.add({
senderTitle,
conversationId, conversationId,
iconUrl, notificationIconUrl,
isExpiringMessage, isExpiringMessage,
message: message.getNotificationText(), message: message.getNotificationText(),
messageId, messageId,
messageSentAt,
title: sender.getTitle(),
reaction: reaction ? reaction.toJSON() : null, reaction: reaction ? reaction.toJSON() : null,
}); });
}, },
notifyTyping(options = {}) { notifyTyping(options = {}) {
const { isTyping, senderId, isMe, senderDevice } = options; const { isTyping, senderId, isMe, senderDevice } = options;
// We don't do anything with typing messages from our other devices // We don't do anything with typing messages from our other devices
if (isMe) { if (isMe) {
return; return;
} }
const deviceId = `${senderId}.${senderDevice}`; const typingToken = `${senderId}.${senderDevice}`;
this.contactTypingTimers = this.contactTypingTimers || {}; this.contactTypingTimers = this.contactTypingTimers || {};
const record = this.contactTypingTimers[deviceId]; const record = this.contactTypingTimers[typingToken];
if (record) { if (record) {
clearTimeout(record.timer); clearTimeout(record.timer);
} }
if (isTyping) { if (isTyping) {
this.contactTypingTimers[deviceId] = this.contactTypingTimers[ this.contactTypingTimers[typingToken] = this.contactTypingTimers[
deviceId typingToken
] || { ] || {
timestamp: Date.now(), timestamp: Date.now(),
senderId, senderId,
senderDevice, senderDevice,
}; };
this.contactTypingTimers[deviceId].timer = setTimeout( this.contactTypingTimers[typingToken].timer = setTimeout(
this.clearContactTypingTimer.bind(this, deviceId), this.clearContactTypingTimer.bind(this, typingToken),
15 * 1000 15 * 1000
); );
if (!record) { if (!record) {
// User was not previously typing before. State change! // User was not previously typing before. State change!
this.trigger('change', this); this.trigger('change', this);
} }
} else { } else {
delete this.contactTypingTimers[deviceId]; delete this.contactTypingTimers[typingToken];
if (record) { if (record) {
// User was previously typing, and is no longer. State change! // User was previously typing, and is no longer. State change!
this.trigger('change', this); this.trigger('change', this);
} }
} }
}, },
clearContactTypingTimer(deviceId) { clearContactTypingTimer(typingToken) {
this.contactTypingTimers = this.contactTypingTimers || {}; this.contactTypingTimers = this.contactTypingTimers || {};
const record = this.contactTypingTimers[deviceId]; const record = this.contactTypingTimers[typingToken];
if (record) { if (record) {
clearTimeout(record.timer); clearTimeout(record.timer);
delete this.contactTypingTimers[deviceId]; delete this.contactTypingTimers[typingToken];
// User was previously typing, but timed out or we received message. Sta te change! // User was previously typing, but timed out or we received message. Sta te change!
this.trigger('change', this); this.trigger('change', this);
} }
}, },
}); });
Whisper.ConversationCollection = Backbone.Collection.extend({ Whisper.ConversationCollection = Backbone.Collection.extend({
model: Whisper.Conversation, model: Whisper.Conversation,
skipping to change at line 3065 skipping to change at line 3327
Backbone.Collection.prototype.get.call(this, id) Backbone.Collection.prototype.get.call(this, id)
); );
}, },
comparator(m) { comparator(m) {
return -m.get('timestamp'); return -m.get('timestamp');
}, },
}); });
Whisper.Conversation.COLORS = COLORS.concat(['grey', 'default']).join(' '); Whisper.Conversation.COLORS = COLORS.concat(['grey', 'default']).join(' ');
// This is a wrapper model used to display group members in the member list vi
ew, within
// the world of backbone, but layering another bit of group-specific data to
p of base
// conversation data.
Whisper.GroupMemberConversation = Backbone.Model.extend({
initialize(attributes) {
const { conversation, isAdmin } = attributes;
if (!conversation) {
throw new Error(
'GroupMemberConversation.initialze: conversation required!'
);
}
if (!_.isBoolean(isAdmin)) {
throw new Error('GroupMemberConversation.initialze: isAdmin required!');
}
// If our underlying conversation changes, we change too
this.listenTo(conversation, 'change', () => {
this.trigger('change', this);
});
this.conversation = conversation;
this.isAdmin = isAdmin;
},
format() {
return {
...this.conversation.format(),
isAdmin: this.isAdmin,
};
},
get(...params) {
return this.conversation.get(...params);
},
getTitle() {
return this.conversation.getTitle();
},
isMe() {
return this.conversation.isMe();
},
});
// We need a custom collection here to get the sorting we need
Whisper.GroupConversationCollection = Backbone.Collection.extend({
model: Whisper.GroupMemberConversation,
initialize() {
this.collator = new Intl.Collator();
},
comparator(left, right) {
if (left.isAdmin && !right.isAdmin) {
return -1;
}
if (!left.isAdmin && right.isAdmin) {
return 1;
}
const leftLower = left.getTitle().toLowerCase();
const rightLower = right.getTitle().toLowerCase();
return this.collator.compare(leftLower, rightLower);
},
});
})(); })();
 End of changes. 87 change blocks. 
251 lines changed or deleted 584 lines changed or added

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