Merge contacts when we discover split or duplicated contacts
This commit is contained in:
parent
68e432188b
commit
901179440f
32 changed files with 1199 additions and 824 deletions
|
@ -348,7 +348,6 @@
|
||||||
<script type='text/javascript' src='js/expiring_tap_to_view_messages.js'></script>
|
<script type='text/javascript' src='js/expiring_tap_to_view_messages.js'></script>
|
||||||
|
|
||||||
<script type='text/javascript' src='js/chromium.js'></script>
|
<script type='text/javascript' src='js/chromium.js'></script>
|
||||||
<script type='text/javascript' src='js/conversation_controller.js'></script>
|
|
||||||
<script type='text/javascript' src='js/message_controller.js'></script>
|
<script type='text/javascript' src='js/message_controller.js'></script>
|
||||||
|
|
||||||
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
|
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
|
||||||
|
|
127
js/background.js
127
js/background.js
|
@ -25,14 +25,24 @@
|
||||||
wait: 500,
|
wait: 500,
|
||||||
maxSize: 500,
|
maxSize: 500,
|
||||||
processBatch: async items => {
|
processBatch: async items => {
|
||||||
const bySource = _.groupBy(items, item => item.source || item.sourceUuid);
|
const byConversationId = _.groupBy(items, item =>
|
||||||
const sources = Object.keys(bySource);
|
ConversationController.ensureContactIds({
|
||||||
|
e164: item.source,
|
||||||
|
uuid: item.sourceUuid,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const ids = Object.keys(byConversationId);
|
||||||
|
|
||||||
for (let i = 0, max = sources.length; i < max; i += 1) {
|
for (let i = 0, max = ids.length; i < max; i += 1) {
|
||||||
const source = sources[i];
|
const conversationId = ids[i];
|
||||||
const timestamps = bySource[source].map(item => item.timestamp);
|
const timestamps = byConversationId[conversationId].map(
|
||||||
|
item => item.timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
const c = ConversationController.get(conversationId);
|
||||||
|
const uuid = c.get('uuid');
|
||||||
|
const e164 = c.get('e164');
|
||||||
|
|
||||||
const c = ConversationController.get(source);
|
|
||||||
c.queueJob(async () => {
|
c.queueJob(async () => {
|
||||||
try {
|
try {
|
||||||
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
||||||
|
@ -41,15 +51,15 @@
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await wrap(
|
await wrap(
|
||||||
textsecure.messaging.sendDeliveryReceipt(
|
textsecure.messaging.sendDeliveryReceipt(
|
||||||
c.get('e164'),
|
e164,
|
||||||
c.get('uuid'),
|
uuid,
|
||||||
timestamps,
|
timestamps,
|
||||||
sendOptions
|
sendOptions
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
`Failed to send delivery receipt to ${source} for timestamps ${timestamps}:`,
|
`Failed to send delivery receipt to ${e164}/${uuid} for timestamps ${timestamps}:`,
|
||||||
error && error.stack ? error.stack : error
|
error && error.stack ? error.stack : error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -577,6 +587,7 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.Signal.conversationControllerStart();
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
ConversationController.load(),
|
ConversationController.load(),
|
||||||
|
@ -584,6 +595,7 @@
|
||||||
Signal.Emojis.load(),
|
Signal.Emojis.load(),
|
||||||
textsecure.storage.protocol.hydrateCaches(),
|
textsecure.storage.protocol.hydrateCaches(),
|
||||||
]);
|
]);
|
||||||
|
await ConversationController.checkForConflicts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'background.js: ConversationController failed to load:',
|
'background.js: ConversationController failed to load:',
|
||||||
|
@ -1759,6 +1771,7 @@
|
||||||
|
|
||||||
const deviceId = textsecure.storage.user.getDeviceId();
|
const deviceId = textsecure.storage.user.getDeviceId();
|
||||||
|
|
||||||
|
// If we didn't capture a UUID on registration, go get it from the server
|
||||||
if (!textsecure.storage.user.getUuid()) {
|
if (!textsecure.storage.user.getUuid()) {
|
||||||
const server = WebAPI.connect({
|
const server = WebAPI.connect({
|
||||||
username: OLD_USERNAME,
|
username: OLD_USERNAME,
|
||||||
|
@ -1954,6 +1967,7 @@
|
||||||
const senderId = ConversationController.ensureContactIds({
|
const senderId = ConversationController.ensureContactIds({
|
||||||
e164: sender,
|
e164: sender,
|
||||||
uuid: senderUuid,
|
uuid: senderUuid,
|
||||||
|
highTrust: true,
|
||||||
});
|
});
|
||||||
const conversation = ConversationController.get(groupId || senderId);
|
const conversation = ConversationController.get(groupId || senderId);
|
||||||
const ourId = ConversationController.getOurConversationId();
|
const ourId = ConversationController.getOurConversationId();
|
||||||
|
@ -2047,6 +2061,7 @@
|
||||||
const detailsId = ConversationController.ensureContactIds({
|
const detailsId = ConversationController.ensureContactIds({
|
||||||
e164: details.number,
|
e164: details.number,
|
||||||
uuid: details.uuid,
|
uuid: details.uuid,
|
||||||
|
highTrust: true,
|
||||||
});
|
});
|
||||||
const conversation = ConversationController.get(detailsId);
|
const conversation = ConversationController.get(detailsId);
|
||||||
let activeAt = conversation.get('active_at');
|
let activeAt = conversation.get('active_at');
|
||||||
|
@ -2236,12 +2251,10 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceE164 = textsecure.storage.user.getNumber();
|
|
||||||
const sourceUuid = textsecure.storage.user.getUuid();
|
|
||||||
const receivedAt = Date.now();
|
const receivedAt = Date.now();
|
||||||
await conversation.updateExpirationTimer(
|
await conversation.updateExpirationTimer(
|
||||||
expireTimer,
|
expireTimer,
|
||||||
sourceE164 || sourceUuid,
|
ConversationController.getOurConversationId(),
|
||||||
receivedAt,
|
receivedAt,
|
||||||
{
|
{
|
||||||
fromSync: true,
|
fromSync: true,
|
||||||
|
@ -2256,10 +2269,16 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
|
// Matches event data from `libtextsecure` `MessageReceiver::handleSentMessage`:
|
||||||
const getDescriptorForSent = ({ message, destination }) =>
|
const getDescriptorForSent = ({ message, destination, destinationUuid }) =>
|
||||||
message.group
|
message.group
|
||||||
? getGroupDescriptor(message.group)
|
? getGroupDescriptor(message.group)
|
||||||
: { type: Message.PRIVATE, id: destination };
|
: {
|
||||||
|
type: Message.PRIVATE,
|
||||||
|
id: ConversationController.ensureContactIds({
|
||||||
|
e164: destination,
|
||||||
|
uuid: destinationUuid,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
|
// Matches event data from `libtextsecure` `MessageReceiver::handleDataMessage`:
|
||||||
const getDescriptorForReceived = ({ message, source, sourceUuid }) =>
|
const getDescriptorForReceived = ({ message, source, sourceUuid }) =>
|
||||||
|
@ -2270,6 +2289,7 @@
|
||||||
id: ConversationController.ensureContactIds({
|
id: ConversationController.ensureContactIds({
|
||||||
e164: source,
|
e164: source,
|
||||||
uuid: sourceUuid,
|
uuid: sourceUuid,
|
||||||
|
highTrust: true,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2280,13 +2300,12 @@
|
||||||
messageDescriptor,
|
messageDescriptor,
|
||||||
}) {
|
}) {
|
||||||
const profileKey = data.message.profileKey.toString('base64');
|
const profileKey = data.message.profileKey.toString('base64');
|
||||||
const sender = await ConversationController.getOrCreateAndWait(
|
const sender = await ConversationController.get(messageDescriptor.id);
|
||||||
messageDescriptor.id,
|
|
||||||
'private'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Will do the save for us
|
if (sender) {
|
||||||
await sender.setProfileKey(profileKey);
|
// Will do the save for us
|
||||||
|
await sender.setProfileKey(profileKey);
|
||||||
|
}
|
||||||
|
|
||||||
return confirm();
|
return confirm();
|
||||||
}
|
}
|
||||||
|
@ -2357,9 +2376,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onProfileKeyUpdate({ data, confirm }) {
|
async function onProfileKeyUpdate({ data, confirm }) {
|
||||||
const conversation = ConversationController.get(
|
const conversationId = ConversationController.ensureContactIds({
|
||||||
data.source || data.sourceUuid
|
e164: data.source,
|
||||||
);
|
uuid: data.sourceUuid,
|
||||||
|
highTrust: true,
|
||||||
|
});
|
||||||
|
const conversation = ConversationController.get(conversationId);
|
||||||
|
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
|
@ -2397,11 +2419,8 @@
|
||||||
messageDescriptor,
|
messageDescriptor,
|
||||||
}) {
|
}) {
|
||||||
// First set profileSharing = true for the conversation we sent to
|
// First set profileSharing = true for the conversation we sent to
|
||||||
const { id, type } = messageDescriptor;
|
const { id } = messageDescriptor;
|
||||||
const conversation = await ConversationController.getOrCreateAndWait(
|
const conversation = await ConversationController.get(id);
|
||||||
id,
|
|
||||||
type
|
|
||||||
);
|
|
||||||
|
|
||||||
conversation.enableProfileSharing();
|
conversation.enableProfileSharing();
|
||||||
window.Signal.Data.updateConversation(conversation.attributes);
|
window.Signal.Data.updateConversation(conversation.attributes);
|
||||||
|
@ -2417,7 +2436,7 @@
|
||||||
return confirm();
|
return confirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSentMessage(data) {
|
function createSentMessage(data, descriptor) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
let sentTo = [];
|
let sentTo = [];
|
||||||
|
|
||||||
|
@ -2430,6 +2449,11 @@
|
||||||
data.unidentifiedDeliveries = unidentified.map(item => item.destination);
|
data.unidentifiedDeliveries = unidentified.map(item => item.destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isGroup = descriptor.type === Message.GROUP;
|
||||||
|
const conversationId = isGroup
|
||||||
|
? ConversationController.ensureGroup(descriptor.id)
|
||||||
|
: descriptor.id;
|
||||||
|
|
||||||
return new Whisper.Message({
|
return new Whisper.Message({
|
||||||
source: textsecure.storage.user.getNumber(),
|
source: textsecure.storage.user.getNumber(),
|
||||||
sourceUuid: textsecure.storage.user.getUuid(),
|
sourceUuid: textsecure.storage.user.getUuid(),
|
||||||
|
@ -2438,7 +2462,7 @@
|
||||||
serverTimestamp: data.serverTimestamp,
|
serverTimestamp: data.serverTimestamp,
|
||||||
sent_to: sentTo,
|
sent_to: sentTo,
|
||||||
received_at: now,
|
received_at: now,
|
||||||
conversationId: data.destination,
|
conversationId,
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
sent: true,
|
sent: true,
|
||||||
unidentifiedDeliveries: data.unidentifiedDeliveries || [],
|
unidentifiedDeliveries: data.unidentifiedDeliveries || [],
|
||||||
|
@ -2468,7 +2492,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = createSentMessage(data);
|
const message = createSentMessage(data, messageDescriptor);
|
||||||
|
|
||||||
if (data.message.reaction) {
|
if (data.message.reaction) {
|
||||||
const { reaction } = data.message;
|
const { reaction } = data.message;
|
||||||
|
@ -2502,12 +2526,7 @@
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
ConversationController.getOrCreate(
|
|
||||||
messageDescriptor.id,
|
|
||||||
messageDescriptor.type
|
|
||||||
);
|
|
||||||
// Don't wait for handleDataMessage, as it has its own per-conversation queueing
|
// Don't wait for handleDataMessage, as it has its own per-conversation queueing
|
||||||
|
|
||||||
message.handleDataMessage(data.message, event.confirm, {
|
message.handleDataMessage(data.message, event.confirm, {
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
@ -2520,12 +2539,10 @@
|
||||||
const fromContactId = ConversationController.ensureContactIds({
|
const fromContactId = ConversationController.ensureContactIds({
|
||||||
e164: data.source,
|
e164: data.source,
|
||||||
uuid: data.sourceUuid,
|
uuid: data.sourceUuid,
|
||||||
|
highTrust: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Determine if this message is in a group
|
|
||||||
const isGroup = descriptor.type === Message.GROUP;
|
const isGroup = descriptor.type === Message.GROUP;
|
||||||
|
|
||||||
// Determine the conversationId this message belongs to
|
|
||||||
const conversationId = isGroup
|
const conversationId = isGroup
|
||||||
? ConversationController.ensureGroup(descriptor.id, {
|
? ConversationController.ensureGroup(descriptor.id, {
|
||||||
addedBy: fromContactId,
|
addedBy: fromContactId,
|
||||||
|
@ -2651,10 +2668,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
const conversationId = message.get('conversationId');
|
const conversationId = message.get('conversationId');
|
||||||
const conversation = ConversationController.getOrCreate(
|
const conversation = ConversationController.get(conversationId);
|
||||||
conversationId,
|
|
||||||
'private'
|
|
||||||
);
|
|
||||||
|
|
||||||
// This matches the queueing behavior used in Message.handleDataMessage
|
// This matches the queueing behavior used in Message.handleDataMessage
|
||||||
conversation.queueJob(async () => {
|
conversation.queueJob(async () => {
|
||||||
|
@ -2791,9 +2805,13 @@
|
||||||
|
|
||||||
function onReadReceipt(ev) {
|
function onReadReceipt(ev) {
|
||||||
const readAt = ev.timestamp;
|
const readAt = ev.timestamp;
|
||||||
const { timestamp } = ev.read;
|
const { timestamp, source, sourceUuid } = ev.read;
|
||||||
const reader = ConversationController.getConversationId(ev.read.reader);
|
const reader = ConversationController.ensureContactIds({
|
||||||
window.log.info('read receipt', reader, timestamp);
|
e164: source,
|
||||||
|
uuid: sourceUuid,
|
||||||
|
highTrust: true,
|
||||||
|
});
|
||||||
|
window.log.info('read receipt', timestamp, source, sourceUuid, reader);
|
||||||
|
|
||||||
ev.confirm();
|
ev.confirm();
|
||||||
|
|
||||||
|
@ -2879,7 +2897,11 @@
|
||||||
ev.viaContactSync ? 'via contact sync' : ''
|
ev.viaContactSync ? 'via contact sync' : ''
|
||||||
);
|
);
|
||||||
|
|
||||||
const verifiedId = ConversationController.ensureContactIds({ e164, uuid });
|
const verifiedId = ConversationController.ensureContactIds({
|
||||||
|
e164,
|
||||||
|
uuid,
|
||||||
|
highTrust: true,
|
||||||
|
});
|
||||||
const contact = await ConversationController.get(verifiedId, 'private');
|
const contact = await ConversationController.get(verifiedId, 'private');
|
||||||
const options = {
|
const options = {
|
||||||
viaSyncMessage: true,
|
viaSyncMessage: true,
|
||||||
|
@ -2899,20 +2921,23 @@
|
||||||
function onDeliveryReceipt(ev) {
|
function onDeliveryReceipt(ev) {
|
||||||
const { deliveryReceipt } = ev;
|
const { deliveryReceipt } = ev;
|
||||||
const { sourceUuid, source } = deliveryReceipt;
|
const { sourceUuid, source } = deliveryReceipt;
|
||||||
const identifier = source || sourceUuid;
|
|
||||||
|
|
||||||
window.log.info(
|
window.log.info(
|
||||||
'delivery receipt from',
|
'delivery receipt from',
|
||||||
`${identifier}.${deliveryReceipt.sourceDevice}`,
|
`${source} ${sourceUuid} ${deliveryReceipt.sourceDevice}`,
|
||||||
deliveryReceipt.timestamp
|
deliveryReceipt.timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
ev.confirm();
|
ev.confirm();
|
||||||
|
|
||||||
const deliveredTo = ConversationController.getConversationId(identifier);
|
const deliveredTo = ConversationController.ensureContactIds({
|
||||||
|
e164: source,
|
||||||
|
uuid: sourceUuid,
|
||||||
|
highTrust: true,
|
||||||
|
});
|
||||||
|
|
||||||
if (!deliveredTo) {
|
if (!deliveredTo) {
|
||||||
window.log.info('no conversation for identifier', identifier);
|
window.log.info('no conversation for', source, sourceUuid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,353 +0,0 @@
|
||||||
/* global _, Whisper, Backbone, storage, textsecure */
|
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
|
|
||||||
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
|
||||||
|
|
||||||
const conversations = new Whisper.ConversationCollection();
|
|
||||||
const inboxCollection = new (Backbone.Collection.extend({
|
|
||||||
initialize() {
|
|
||||||
this.listenTo(conversations, 'add change:active_at', this.addActive);
|
|
||||||
this.listenTo(conversations, 'reset', () => this.reset([]));
|
|
||||||
|
|
||||||
this.on(
|
|
||||||
'add remove change:unreadCount',
|
|
||||||
_.debounce(this.updateUnreadCount.bind(this), 1000)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
addActive(model) {
|
|
||||||
if (model.get('active_at')) {
|
|
||||||
this.add(model);
|
|
||||||
} else {
|
|
||||||
this.remove(model);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateUnreadCount() {
|
|
||||||
const newUnreadCount = _.reduce(
|
|
||||||
this.map(m => m.get('unreadCount')),
|
|
||||||
(item, memo) => item + memo,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
storage.put('unreadCount', newUnreadCount);
|
|
||||||
|
|
||||||
if (newUnreadCount > 0) {
|
|
||||||
window.setBadgeCount(newUnreadCount);
|
|
||||||
window.document.title = `${window.getTitle()} (${newUnreadCount})`;
|
|
||||||
} else {
|
|
||||||
window.setBadgeCount(0);
|
|
||||||
window.document.title = window.getTitle();
|
|
||||||
}
|
|
||||||
window.updateTrayIcon(newUnreadCount);
|
|
||||||
},
|
|
||||||
}))();
|
|
||||||
|
|
||||||
window.getInboxCollection = () => inboxCollection;
|
|
||||||
window.getConversations = () => conversations;
|
|
||||||
|
|
||||||
window.ConversationController = {
|
|
||||||
get(id) {
|
|
||||||
if (!this._initialFetchComplete) {
|
|
||||||
throw new Error(
|
|
||||||
'ConversationController.get() needs complete initial fetch'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return conversations.get(id);
|
|
||||||
},
|
|
||||||
// Needed for some model setup which happens during the initial fetch() call below
|
|
||||||
getUnsafe(id) {
|
|
||||||
return conversations.get(id);
|
|
||||||
},
|
|
||||||
dangerouslyCreateAndAdd(attributes) {
|
|
||||||
return conversations.add(attributes);
|
|
||||||
},
|
|
||||||
getOrCreate(identifier, type, additionalInitialProps = {}) {
|
|
||||||
if (typeof identifier !== 'string') {
|
|
||||||
throw new TypeError("'id' must be a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type !== 'private' && type !== 'group') {
|
|
||||||
throw new TypeError(
|
|
||||||
`'type' must be 'private' or 'group'; got: '${type}'`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._initialFetchComplete) {
|
|
||||||
throw new Error(
|
|
||||||
'ConversationController.get() needs complete initial fetch'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let conversation = conversations.get(identifier);
|
|
||||||
if (conversation) {
|
|
||||||
return conversation;
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = window.getGuid();
|
|
||||||
|
|
||||||
if (type === 'group') {
|
|
||||||
conversation = conversations.add({
|
|
||||||
id,
|
|
||||||
uuid: null,
|
|
||||||
e164: null,
|
|
||||||
groupId: identifier,
|
|
||||||
type,
|
|
||||||
version: 2,
|
|
||||||
...additionalInitialProps,
|
|
||||||
});
|
|
||||||
} else if (window.isValidGuid(identifier)) {
|
|
||||||
conversation = conversations.add({
|
|
||||||
id,
|
|
||||||
uuid: identifier,
|
|
||||||
e164: null,
|
|
||||||
groupId: null,
|
|
||||||
type,
|
|
||||||
version: 2,
|
|
||||||
...additionalInitialProps,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
conversation = conversations.add({
|
|
||||||
id,
|
|
||||||
uuid: null,
|
|
||||||
e164: identifier,
|
|
||||||
groupId: null,
|
|
||||||
type,
|
|
||||||
version: 2,
|
|
||||||
...additionalInitialProps,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const create = async () => {
|
|
||||||
if (!conversation.isValid()) {
|
|
||||||
const validationError = conversation.validationError || {};
|
|
||||||
window.log.error(
|
|
||||||
'Contact is not valid. Not saving, but adding to collection:',
|
|
||||||
conversation.idForLogging(),
|
|
||||||
validationError.stack
|
|
||||||
);
|
|
||||||
|
|
||||||
return conversation;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await window.Signal.Data.saveConversation(conversation.attributes, {
|
|
||||||
Conversation: Whisper.Conversation,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
'Conversation save failed! ',
|
|
||||||
identifier,
|
|
||||||
type,
|
|
||||||
'Error:',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return conversation;
|
|
||||||
};
|
|
||||||
|
|
||||||
conversation.initialPromise = create();
|
|
||||||
|
|
||||||
return conversation;
|
|
||||||
},
|
|
||||||
getOrCreateAndWait(id, type, additionalInitialProps = {}) {
|
|
||||||
return this._initialPromise.then(() => {
|
|
||||||
const conversation = this.getOrCreate(id, type, additionalInitialProps);
|
|
||||||
|
|
||||||
if (conversation) {
|
|
||||||
return conversation.initialPromise.then(() => conversation);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject(
|
|
||||||
new Error('getOrCreateAndWait: did not get conversation')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getConversationId(address) {
|
|
||||||
if (!address) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [id] = textsecure.utils.unencodeNumber(address);
|
|
||||||
const conv = this.get(id);
|
|
||||||
|
|
||||||
if (conv) {
|
|
||||||
return conv.get('id');
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
getOurConversationId() {
|
|
||||||
const e164 = textsecure.storage.user.getNumber();
|
|
||||||
const uuid = textsecure.storage.user.getUuid();
|
|
||||||
return this.ensureContactIds({ e164, uuid });
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Given a UUID and/or an E164, resolves to a string representing the local
|
|
||||||
* database of the given contact. If a conversation is found it is updated
|
|
||||||
* to have the given UUID and E164. If a conversation is not found, this
|
|
||||||
* function creates a conversation with the given UUID and E164. If the
|
|
||||||
* conversation * is found in the local database it is updated.
|
|
||||||
*
|
|
||||||
* This function also additionally checks for mismatched e164/uuid pairs out
|
|
||||||
* of abundance of caution.
|
|
||||||
*/
|
|
||||||
ensureContactIds({ e164, uuid }) {
|
|
||||||
// Check for at least one parameter being provided. This is necessary
|
|
||||||
// because this path can be called on startup to resolve our own ID before
|
|
||||||
// our phone number or UUID are known. The existing behavior in these
|
|
||||||
// cases can handle a returned `undefined` id, so we do that.
|
|
||||||
if (!e164 && !uuid) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lowerUuid = uuid ? uuid.toLowerCase() : undefined;
|
|
||||||
|
|
||||||
const convoE164 = this.get(e164);
|
|
||||||
const convoUuid = this.get(lowerUuid);
|
|
||||||
|
|
||||||
// Check for mismatched UUID and E164
|
|
||||||
if (
|
|
||||||
convoE164 &&
|
|
||||||
convoUuid &&
|
|
||||||
convoE164.get('id') !== convoUuid.get('id')
|
|
||||||
) {
|
|
||||||
window.log.warn('Received a message with a mismatched UUID and E164.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const convo = convoUuid || convoE164;
|
|
||||||
|
|
||||||
const idOrIdentifier = convo ? convo.get('id') : e164 || lowerUuid;
|
|
||||||
|
|
||||||
const finalConversation = this.getOrCreate(idOrIdentifier, 'private');
|
|
||||||
finalConversation.updateE164(e164);
|
|
||||||
finalConversation.updateUuid(lowerUuid);
|
|
||||||
|
|
||||||
return finalConversation.get('id');
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Given a groupId and optional additional initialization properties,
|
|
||||||
* ensures the existence of a group conversation and returns a string
|
|
||||||
* representing the local database ID of the group conversation.
|
|
||||||
*/
|
|
||||||
ensureGroup(groupId, additionalInitProps = {}) {
|
|
||||||
return this.getOrCreate(groupId, 'group', additionalInitProps).get('id');
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Given certain metadata about a message (an identifier of who wrote the
|
|
||||||
* message and the sent_at timestamp of the message) returns the
|
|
||||||
* conversation the message belongs to OR null if a conversation isn't
|
|
||||||
* found.
|
|
||||||
* @param {string} targetFrom The E164, UUID, or Conversation ID of the message author
|
|
||||||
* @param {number} targetTimestamp The sent_at timestamp of the target message
|
|
||||||
*/
|
|
||||||
async getConversationForTargetMessage(targetFrom, targetTimestamp) {
|
|
||||||
const targetFromId = this.getConversationId(targetFrom);
|
|
||||||
|
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
|
||||||
targetTimestamp,
|
|
||||||
{
|
|
||||||
MessageCollection: Whisper.MessageCollection,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const targetMessage = messages.find(m => {
|
|
||||||
const contact = m.getContact();
|
|
||||||
|
|
||||||
if (!contact) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mcid = contact.get('id');
|
|
||||||
return mcid === targetFromId;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (targetMessage) {
|
|
||||||
return targetMessage.getConversation();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
prepareForSend(id, options) {
|
|
||||||
// id is any valid conversation identifier
|
|
||||||
const conversation = this.get(id);
|
|
||||||
const sendOptions = conversation
|
|
||||||
? conversation.getSendOptions(options)
|
|
||||||
: null;
|
|
||||||
const wrap = conversation
|
|
||||||
? conversation.wrapSend.bind(conversation)
|
|
||||||
: promise => promise;
|
|
||||||
|
|
||||||
return { wrap, sendOptions };
|
|
||||||
},
|
|
||||||
async getAllGroupsInvolvingId(conversationId) {
|
|
||||||
const groups = await window.Signal.Data.getAllGroupsInvolvingId(
|
|
||||||
conversationId,
|
|
||||||
{
|
|
||||||
ConversationCollection: Whisper.ConversationCollection,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return groups.map(group => conversations.add(group));
|
|
||||||
},
|
|
||||||
loadPromise() {
|
|
||||||
return this._initialPromise;
|
|
||||||
},
|
|
||||||
reset() {
|
|
||||||
this._initialPromise = Promise.resolve();
|
|
||||||
this._initialFetchComplete = false;
|
|
||||||
conversations.reset([]);
|
|
||||||
},
|
|
||||||
async load() {
|
|
||||||
window.log.info('ConversationController: starting initial fetch');
|
|
||||||
|
|
||||||
if (conversations.length) {
|
|
||||||
throw new Error('ConversationController: Already loaded!');
|
|
||||||
}
|
|
||||||
|
|
||||||
const load = async () => {
|
|
||||||
try {
|
|
||||||
const collection = await window.Signal.Data.getAllConversations({
|
|
||||||
ConversationCollection: Whisper.ConversationCollection,
|
|
||||||
});
|
|
||||||
|
|
||||||
conversations.add(collection.models);
|
|
||||||
|
|
||||||
this._initialFetchComplete = true;
|
|
||||||
await Promise.all(
|
|
||||||
conversations.map(async conversation => {
|
|
||||||
if (!conversation.get('lastMessage')) {
|
|
||||||
await conversation.updateLastMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
// In case a too-large draft was saved to the database
|
|
||||||
const draft = conversation.get('draft');
|
|
||||||
if (draft && draft.length > MAX_MESSAGE_BODY_LENGTH) {
|
|
||||||
this.model.set({
|
|
||||||
draft: draft.slice(0, MAX_MESSAGE_BODY_LENGTH),
|
|
||||||
});
|
|
||||||
window.Signal.Data.updateConversation(conversation.attributes);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
window.log.info('ConversationController: done with initial fetch');
|
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
'ConversationController: initial fetch failed',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this._initialPromise = load();
|
|
||||||
|
|
||||||
return this._initialPromise;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})();
|
|
|
@ -30,11 +30,10 @@
|
||||||
this.remove(receipts);
|
this.remove(receipts);
|
||||||
return receipts;
|
return receipts;
|
||||||
},
|
},
|
||||||
async getTargetMessage(source, messages) {
|
async getTargetMessage(sourceId, messages) {
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const sourceId = ConversationController.getConversationId(source);
|
|
||||||
const message = messages.find(
|
const message = messages.find(
|
||||||
item => !item.isIncoming() && sourceId === item.get('conversationId')
|
item => !item.isIncoming() && sourceId === item.get('conversationId')
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
forConversation(conversation) {
|
forConversation(conversation) {
|
||||||
if (conversation.get('e164')) {
|
if (conversation.get('e164')) {
|
||||||
const syncByE164 = this.findWhere({
|
const syncByE164 = this.findWhere({
|
||||||
e164: conversation.get('e164'),
|
threadE164: conversation.get('e164'),
|
||||||
});
|
});
|
||||||
if (syncByE164) {
|
if (syncByE164) {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
if (conversation.get('uuid')) {
|
if (conversation.get('uuid')) {
|
||||||
const syncByUuid = this.findWhere({
|
const syncByUuid = this.findWhere({
|
||||||
uuid: conversation.get('uuid'),
|
threadUuid: conversation.get('uuid'),
|
||||||
});
|
});
|
||||||
if (syncByUuid) {
|
if (syncByUuid) {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
|
|
||||||
if (conversation.get('groupId')) {
|
if (conversation.get('groupId')) {
|
||||||
const syncByGroupId = this.findWhere({
|
const syncByGroupId = this.findWhere({
|
||||||
uuid: conversation.get('groupId'),
|
groupId: conversation.get('groupId'),
|
||||||
});
|
});
|
||||||
if (syncByGroupId) {
|
if (syncByGroupId) {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
|
@ -65,12 +65,19 @@
|
||||||
const threadE164 = sync.get('threadE164');
|
const threadE164 = sync.get('threadE164');
|
||||||
const threadUuid = sync.get('threadUuid');
|
const threadUuid = sync.get('threadUuid');
|
||||||
const groupId = sync.get('groupId');
|
const groupId = sync.get('groupId');
|
||||||
const identifier = threadE164 || threadUuid || groupId;
|
|
||||||
const conversation = ConversationController.get(identifier);
|
const conversation = groupId
|
||||||
|
? ConversationController.get(groupId)
|
||||||
|
: ConversationController.get(
|
||||||
|
ConversationController.ensureContactIds({
|
||||||
|
e164: threadE164,
|
||||||
|
uuid: threadUuid,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
window.log(
|
window.log(
|
||||||
`Received message request response for unknown conversation: ${identifier}`
|
`Received message request response for unknown conversation: ${groupId} ${threadUuid} ${threadE164}`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,12 @@
|
||||||
return `group(${groupId})`;
|
return `group(${groupId})`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
getSendTarget() {
|
||||||
|
return this.get('uuid') || this.get('e164');
|
||||||
|
},
|
||||||
|
|
||||||
handleMessageError(message, errors) {
|
handleMessageError(message, errors) {
|
||||||
this.trigger('messageError', message, errors);
|
this.trigger('messageError', message, errors);
|
||||||
},
|
},
|
||||||
|
@ -318,9 +324,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupId = !this.isPrivate() ? this.get('groupId') : null;
|
const groupId = !this.isPrivate() ? this.get('groupId') : null;
|
||||||
const maybeRecipientId = this.get('uuid') || this.get('e164');
|
|
||||||
const recipientId = this.isPrivate() ? maybeRecipientId : null;
|
|
||||||
const groupNumbers = this.getRecipients();
|
const groupNumbers = this.getRecipients();
|
||||||
|
const recipientId = this.isPrivate() ? this.getSendTarget() : null;
|
||||||
|
|
||||||
const sendOptions = this.getSendOptions();
|
const sendOptions = this.getSendOptions();
|
||||||
|
|
||||||
|
@ -395,10 +400,10 @@
|
||||||
|
|
||||||
async onNewMessage(message) {
|
async onNewMessage(message) {
|
||||||
// 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 identifier = message.get
|
const deviceId = message.get
|
||||||
? `${message.get('source')}.${message.get('sourceDevice')}`
|
? `${message.get('conversationId')}.${message.get('sourceDevice')}`
|
||||||
: `${message.source}.${message.sourceDevice}`;
|
: `${message.conversationId}.${message.sourceDevice}`;
|
||||||
this.clearContactTypingTimer(identifier);
|
this.clearContactTypingTimer(deviceId);
|
||||||
|
|
||||||
this.debouncedUpdateLastMessage();
|
this.debouncedUpdateLastMessage();
|
||||||
},
|
},
|
||||||
|
@ -582,7 +587,13 @@
|
||||||
m => !m.hasErrors() && m.isIncoming()
|
m => !m.hasErrors() && m.isIncoming()
|
||||||
);
|
);
|
||||||
const receiptSpecs = readMessages.map(m => ({
|
const receiptSpecs = readMessages.map(m => ({
|
||||||
sender: m.get('source') || m.get('sourceUuid'),
|
senderE164: m.get('source'),
|
||||||
|
senderUuid: m.get('sourceUuid'),
|
||||||
|
senderId: ConversationController.get({
|
||||||
|
e164: m.get('source'),
|
||||||
|
uuid: m.get('sourceUuid'),
|
||||||
|
lowTrust: true,
|
||||||
|
}),
|
||||||
timestamp: m.get('sent_at'),
|
timestamp: m.get('sent_at'),
|
||||||
hasErrors: m.hasErrors(),
|
hasErrors: m.hasErrors(),
|
||||||
}));
|
}));
|
||||||
|
@ -1167,14 +1178,12 @@
|
||||||
|
|
||||||
getRecipients() {
|
getRecipients() {
|
||||||
if (this.isPrivate()) {
|
if (this.isPrivate()) {
|
||||||
return [this.get('uuid') || this.get('e164')];
|
return [this.getSendTarget()];
|
||||||
}
|
}
|
||||||
const me = ConversationController.getConversationId(
|
const me = ConversationController.getOurConversationId();
|
||||||
textsecure.storage.user.getUuid() || textsecure.storage.user.getNumber()
|
|
||||||
);
|
|
||||||
return _.without(this.get('members'), me).map(memberId => {
|
return _.without(this.get('members'), me).map(memberId => {
|
||||||
const c = ConversationController.get(memberId);
|
const c = ConversationController.get(memberId);
|
||||||
return c.get('uuid') || c.get('e164');
|
return c.getSendTarget();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1331,11 +1340,7 @@
|
||||||
|
|
||||||
const reactionModel = Whisper.Reactions.add({
|
const reactionModel = Whisper.Reactions.add({
|
||||||
...outgoingReaction,
|
...outgoingReaction,
|
||||||
fromId:
|
fromId: ConversationController.getOurConversationId(),
|
||||||
this.ourNumber ||
|
|
||||||
this.ourUuid ||
|
|
||||||
textsecure.storage.user.getNumber() ||
|
|
||||||
textsecure.storage.user.getUuid(),
|
|
||||||
timestamp,
|
timestamp,
|
||||||
fromSync: true,
|
fromSync: true,
|
||||||
});
|
});
|
||||||
|
@ -1446,9 +1451,7 @@
|
||||||
|
|
||||||
async sendProfileKeyUpdate() {
|
async sendProfileKeyUpdate() {
|
||||||
const id = this.get('id');
|
const id = this.get('id');
|
||||||
const recipients = this.isPrivate()
|
const recipients = this.getRecipients();
|
||||||
? [this.get('uuid') || this.get('e164')]
|
|
||||||
: this.getRecipients();
|
|
||||||
if (!this.get('profileSharing')) {
|
if (!this.get('profileSharing')) {
|
||||||
window.log.error(
|
window.log.error(
|
||||||
'Attempted to send profileKeyUpdate to conversation without profileSharing enabled',
|
'Attempted to send profileKeyUpdate to conversation without profileSharing enabled',
|
||||||
|
@ -1477,7 +1480,7 @@
|
||||||
const { clearUnreadMetrics } = window.reduxActions.conversations;
|
const { clearUnreadMetrics } = window.reduxActions.conversations;
|
||||||
clearUnreadMetrics(this.id);
|
clearUnreadMetrics(this.id);
|
||||||
|
|
||||||
const destination = this.get('uuid') || this.get('e164');
|
const destination = this.getSendTarget();
|
||||||
const expireTimer = this.get('expireTimer');
|
const expireTimer = this.get('expireTimer');
|
||||||
const recipients = this.getRecipients();
|
const recipients = this.getRecipients();
|
||||||
|
|
||||||
|
@ -1549,7 +1552,7 @@
|
||||||
).map(contact => {
|
).map(contact => {
|
||||||
const error = new Error('Network is not available');
|
const error = new Error('Network is not available');
|
||||||
error.name = 'SendMessageNetworkError';
|
error.name = 'SendMessageNetworkError';
|
||||||
error.identifier = contact.get('uuid') || contact.get('e164');
|
error.identifier = contact.get('id');
|
||||||
return error;
|
return error;
|
||||||
});
|
});
|
||||||
await message.saveErrors(errors);
|
await message.saveErrors(errors);
|
||||||
|
@ -1658,8 +1661,8 @@
|
||||||
|
|
||||||
async handleMessageSendResult(failoverIdentifiers, unidentifiedDeliveries) {
|
async handleMessageSendResult(failoverIdentifiers, unidentifiedDeliveries) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
(failoverIdentifiers || []).map(async number => {
|
(failoverIdentifiers || []).map(async identifier => {
|
||||||
const conversation = ConversationController.get(number);
|
const conversation = ConversationController.get(identifier);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
conversation &&
|
conversation &&
|
||||||
|
@ -1677,8 +1680,8 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
(unidentifiedDeliveries || []).map(async number => {
|
(unidentifiedDeliveries || []).map(async identifier => {
|
||||||
const conversation = ConversationController.get(number);
|
const conversation = ConversationController.get(identifier);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
conversation &&
|
conversation &&
|
||||||
|
@ -1722,17 +1725,10 @@
|
||||||
getSendMetadata(options = {}) {
|
getSendMetadata(options = {}) {
|
||||||
const { syncMessage, disableMeCheck } = options;
|
const { syncMessage, disableMeCheck } = options;
|
||||||
|
|
||||||
if (!this.ourNumber && !this.ourUuid) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// START: this code has an Expiration date of ~2018/11/21
|
// START: this code has an Expiration date of ~2018/11/21
|
||||||
// We don't want to enable unidentified delivery for send unless it is
|
// We don't want to enable unidentified delivery for send unless it is
|
||||||
// also enabled for our own account.
|
// also enabled for our own account.
|
||||||
const myId = ConversationController.ensureContactIds({
|
const myId = ConversationController.getOurConversationId();
|
||||||
e164: this.ourNumber,
|
|
||||||
uuid: this.ourUuid,
|
|
||||||
});
|
|
||||||
const me = ConversationController.get(myId);
|
const me = ConversationController.get(myId);
|
||||||
if (
|
if (
|
||||||
!disableMeCheck &&
|
!disableMeCheck &&
|
||||||
|
@ -1903,10 +1899,7 @@
|
||||||
source,
|
source,
|
||||||
});
|
});
|
||||||
|
|
||||||
source =
|
source = source || ConversationController.getOurConversationId();
|
||||||
source ||
|
|
||||||
textsecure.storage.user.getNumber() ||
|
|
||||||
textsecure.storage.user.getUuid();
|
|
||||||
|
|
||||||
// When we add a disappearing messages notification to the conversation, we want it
|
// When we add a disappearing messages notification to the conversation, we want it
|
||||||
// to be above the message that initiated that change, hence the subtraction.
|
// to be above the message that initiated that change, hence the subtraction.
|
||||||
|
@ -1933,7 +1926,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.isPrivate()) {
|
if (this.isPrivate()) {
|
||||||
model.set({ destination: this.get('uuid') || this.get('e164') });
|
model.set({ destination: this.getSendTarget() });
|
||||||
}
|
}
|
||||||
if (model.isOutgoing()) {
|
if (model.isOutgoing()) {
|
||||||
model.set({ recipients: this.getRecipients() });
|
model.set({ recipients: this.getRecipients() });
|
||||||
|
@ -1963,7 +1956,7 @@
|
||||||
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.get('uuid') || this.get('e164'),
|
this.getSendTarget(),
|
||||||
null,
|
null,
|
||||||
[],
|
[],
|
||||||
null,
|
null,
|
||||||
|
@ -1980,7 +1973,7 @@
|
||||||
|
|
||||||
if (this.get('type') === 'private') {
|
if (this.get('type') === 'private') {
|
||||||
promise = textsecure.messaging.sendExpirationTimerUpdateToIdentifier(
|
promise = textsecure.messaging.sendExpirationTimerUpdateToIdentifier(
|
||||||
this.get('uuid') || this.get('e164'),
|
this.getSendTarget(),
|
||||||
expireTimer,
|
expireTimer,
|
||||||
message.get('sent_at'),
|
message.get('sent_at'),
|
||||||
profileKey,
|
profileKey,
|
||||||
|
@ -2180,7 +2173,12 @@
|
||||||
await m.markRead(options.readAt);
|
await m.markRead(options.readAt);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sender: m.get('source') || m.get('sourceUuid'),
|
senderE164: m.get('source'),
|
||||||
|
senderUuid: m.get('sourceUuid'),
|
||||||
|
senderId: ConversationController.ensureContactIds({
|
||||||
|
e164: m.get('source'),
|
||||||
|
uuid: m.get('sourceUuid'),
|
||||||
|
}),
|
||||||
timestamp: m.get('sent_at'),
|
timestamp: m.get('sent_at'),
|
||||||
hasErrors: m.hasErrors(),
|
hasErrors: m.hasErrors(),
|
||||||
};
|
};
|
||||||
|
@ -2188,7 +2186,7 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
// Some messages we're marking read are local notifications with no sender
|
// Some messages we're marking read are local notifications with no sender
|
||||||
read = _.filter(read, m => Boolean(m.sender));
|
read = _.filter(read, m => Boolean(m.senderId));
|
||||||
unreadMessages = unreadMessages.filter(m => Boolean(m.isIncoming()));
|
unreadMessages = unreadMessages.filter(m => Boolean(m.isIncoming()));
|
||||||
|
|
||||||
const unreadCount = unreadMessages.length - read.length;
|
const unreadCount = unreadMessages.length - read.length;
|
||||||
|
@ -2206,8 +2204,10 @@
|
||||||
window.log.info(`Sending ${read.length} read syncs`);
|
window.log.info(`Sending ${read.length} read syncs`);
|
||||||
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes
|
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes
|
||||||
// to a contact, we need accessKeys for both.
|
// to a contact, we need accessKeys for both.
|
||||||
const { sendOptions } = ConversationController.prepareForSend(
|
const {
|
||||||
this.ourUuid || this.ourNumber,
|
sendOptions,
|
||||||
|
} = ConversationController.prepareForSend(
|
||||||
|
ConversationController.getOurConversationId(),
|
||||||
{ syncMessage: true }
|
{ syncMessage: true }
|
||||||
);
|
);
|
||||||
await this.wrapSend(
|
await this.wrapSend(
|
||||||
|
@ -2222,12 +2222,12 @@
|
||||||
if (storage.get('read-receipt-setting') && this.getAccepted()) {
|
if (storage.get('read-receipt-setting') && this.getAccepted()) {
|
||||||
window.log.info(`Sending ${items.length} read receipts`);
|
window.log.info(`Sending ${items.length} read receipts`);
|
||||||
const convoSendOptions = this.getSendOptions();
|
const convoSendOptions = this.getSendOptions();
|
||||||
const receiptsBySender = _.groupBy(items, 'sender');
|
const receiptsBySender = _.groupBy(items, 'senderId');
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
_.map(receiptsBySender, async (receipts, identifier) => {
|
_.map(receiptsBySender, async (receipts, senderId) => {
|
||||||
const timestamps = _.map(receipts, 'timestamp');
|
const timestamps = _.map(receipts, 'timestamp');
|
||||||
const c = ConversationController.get(identifier);
|
const c = ConversationController.get(senderId);
|
||||||
await this.wrapSend(
|
await this.wrapSend(
|
||||||
textsecure.messaging.sendReadReceipts(
|
textsecure.messaging.sendReadReceipts(
|
||||||
c.get('e164'),
|
c.get('e164'),
|
||||||
|
@ -2249,28 +2249,33 @@
|
||||||
|
|
||||||
getProfiles() {
|
getProfiles() {
|
||||||
// request all conversation members' keys
|
// request all conversation members' keys
|
||||||
let ids = [];
|
let conversations = [];
|
||||||
if (this.isPrivate()) {
|
if (this.isPrivate()) {
|
||||||
ids = [this.get('uuid') || this.get('e164')];
|
conversations = [this];
|
||||||
} else {
|
} else {
|
||||||
ids = this.get('members')
|
conversations = this.get('members')
|
||||||
.map(id => {
|
.map(id => ConversationController.get(id))
|
||||||
const c = ConversationController.get(id);
|
|
||||||
return c ? c.get('uuid') || c.get('e164') : null;
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
return Promise.all(_.map(ids, this.getProfile));
|
return Promise.all(
|
||||||
|
_.map(conversations, conversation => {
|
||||||
|
this.getProfile(conversation.get('uuid'), conversation.get('e164'));
|
||||||
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getProfile(id) {
|
async getProfile(providedUuid, providedE164) {
|
||||||
if (!textsecure.messaging) {
|
if (!textsecure.messaging) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Conversation.getProfile: textsecure.messaging not available'
|
'Conversation.getProfile: textsecure.messaging not available'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const c = ConversationController.getOrCreate(id, 'private');
|
const id = ConversationController.ensureContactIds({
|
||||||
|
uuid: providedUuid,
|
||||||
|
e164: providedE164,
|
||||||
|
});
|
||||||
|
const c = ConversationController.get(id);
|
||||||
const {
|
const {
|
||||||
generateProfileKeyCredentialRequest,
|
generateProfileKeyCredentialRequest,
|
||||||
getClientZkProfileOperations,
|
getClientZkProfileOperations,
|
||||||
|
@ -2294,6 +2299,7 @@
|
||||||
|
|
||||||
const profileKey = c.get('profileKey');
|
const profileKey = c.get('profileKey');
|
||||||
const uuid = c.get('uuid');
|
const uuid = c.get('uuid');
|
||||||
|
const identifier = c.getSendTarget();
|
||||||
const profileKeyVersionHex = c.get('profileKeyVersion');
|
const profileKeyVersionHex = c.get('profileKeyVersion');
|
||||||
const existingProfileKeyCredential = c.get('profileKeyCredential');
|
const existingProfileKeyCredential = c.get('profileKeyCredential');
|
||||||
|
|
||||||
|
@ -2317,11 +2323,11 @@
|
||||||
|
|
||||||
const sendMetadata = c.getSendMetadata({ disableMeCheck: true }) || {};
|
const sendMetadata = c.getSendMetadata({ disableMeCheck: true }) || {};
|
||||||
const getInfo =
|
const getInfo =
|
||||||
sendMetadata[c.get('e164')] || sendMetadata[c.get('uuid')] || {};
|
sendMetadata[c.get('uuid')] || sendMetadata[c.get('e164')] || {};
|
||||||
|
|
||||||
if (getInfo.accessKey) {
|
if (getInfo.accessKey) {
|
||||||
try {
|
try {
|
||||||
profile = await textsecure.messaging.getProfile(id, {
|
profile = await textsecure.messaging.getProfile(identifier, {
|
||||||
accessKey: getInfo.accessKey,
|
accessKey: getInfo.accessKey,
|
||||||
profileKeyVersion: profileKeyVersionHex,
|
profileKeyVersion: profileKeyVersionHex,
|
||||||
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
|
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
|
||||||
|
@ -2332,7 +2338,7 @@
|
||||||
`Setting sealedSender to DISABLED for conversation ${c.idForLogging()}`
|
`Setting sealedSender to DISABLED for conversation ${c.idForLogging()}`
|
||||||
);
|
);
|
||||||
c.set({ sealedSender: SEALED_SENDER.DISABLED });
|
c.set({ sealedSender: SEALED_SENDER.DISABLED });
|
||||||
profile = await textsecure.messaging.getProfile(id, {
|
profile = await textsecure.messaging.getProfile(identifier, {
|
||||||
profileKeyVersion: profileKeyVersionHex,
|
profileKeyVersion: profileKeyVersionHex,
|
||||||
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
|
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
|
||||||
});
|
});
|
||||||
|
@ -2341,7 +2347,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
profile = await textsecure.messaging.getProfile(id, {
|
profile = await textsecure.messaging.getProfile(identifier, {
|
||||||
profileKeyVersion: profileKeyVersionHex,
|
profileKeyVersion: profileKeyVersionHex,
|
||||||
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
|
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
|
||||||
});
|
});
|
||||||
|
@ -2349,14 +2355,14 @@
|
||||||
|
|
||||||
const identityKey = base64ToArrayBuffer(profile.identityKey);
|
const identityKey = base64ToArrayBuffer(profile.identityKey);
|
||||||
const changed = await textsecure.storage.protocol.saveIdentity(
|
const changed = await textsecure.storage.protocol.saveIdentity(
|
||||||
`${id}.1`,
|
`${identifier}.1`,
|
||||||
identityKey,
|
identityKey,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
// save identity will close all sessions except for .1, so we
|
// save identity will close all sessions except for .1, so we
|
||||||
// must close that one manually.
|
// must close that one manually.
|
||||||
const address = new libsignal.SignalProtocolAddress(id, 1);
|
const address = new libsignal.SignalProtocolAddress(identifier, 1);
|
||||||
window.log.info('closing session for', address.toString());
|
window.log.info('closing session for', address.toString());
|
||||||
const sessionCipher = new libsignal.SessionCipher(
|
const sessionCipher = new libsignal.SessionCipher(
|
||||||
textsecure.storage.protocol,
|
textsecure.storage.protocol,
|
||||||
|
@ -2421,7 +2427,7 @@
|
||||||
if (error.code !== 403 && error.code !== 404) {
|
if (error.code !== 403 && error.code !== 404) {
|
||||||
window.log.warn(
|
window.log.warn(
|
||||||
'getProfile failure:',
|
'getProfile failure:',
|
||||||
id,
|
c.idForLogging(),
|
||||||
error && error.stack ? error.stack : error
|
error && error.stack ? error.stack : error
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -2435,7 +2441,7 @@
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.log.warn(
|
window.log.warn(
|
||||||
'getProfile decryption failure:',
|
'getProfile decryption failure:',
|
||||||
id,
|
c.idForLogging(),
|
||||||
error && error.stack ? error.stack : error
|
error && error.stack ? error.stack : error
|
||||||
);
|
);
|
||||||
await c.dropProfileKey();
|
await c.dropProfileKey();
|
||||||
|
@ -2780,10 +2786,9 @@
|
||||||
|
|
||||||
const conversationId = this.id;
|
const conversationId = this.id;
|
||||||
|
|
||||||
const sender = await ConversationController.getOrCreateAndWait(
|
const sender = reaction
|
||||||
reaction ? reaction.get('fromId') : message.get('source'),
|
? ConversationController.get(reaction.get('fromId'))
|
||||||
'private'
|
: message.getContact();
|
||||||
);
|
|
||||||
|
|
||||||
const iconUrl = await sender.getNotificationIcon();
|
const iconUrl = await sender.getNotificationIcon();
|
||||||
|
|
||||||
|
@ -2805,42 +2810,33 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
notifyTyping(options = {}) {
|
notifyTyping(options = {}) {
|
||||||
const {
|
const { isTyping, senderId, isMe, senderDevice } = options;
|
||||||
isTyping,
|
|
||||||
sender,
|
|
||||||
senderUuid,
|
|
||||||
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 identifier = `${sender}.${senderDevice}`;
|
const deviceId = `${senderId}.${senderDevice}`;
|
||||||
|
|
||||||
this.contactTypingTimers = this.contactTypingTimers || {};
|
this.contactTypingTimers = this.contactTypingTimers || {};
|
||||||
const record = this.contactTypingTimers[identifier];
|
const record = this.contactTypingTimers[deviceId];
|
||||||
|
|
||||||
if (record) {
|
if (record) {
|
||||||
clearTimeout(record.timer);
|
clearTimeout(record.timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTyping) {
|
if (isTyping) {
|
||||||
this.contactTypingTimers[identifier] = this.contactTypingTimers[
|
this.contactTypingTimers[deviceId] = this.contactTypingTimers[
|
||||||
identifier
|
deviceId
|
||||||
] || {
|
] || {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
sender,
|
|
||||||
senderId,
|
senderId,
|
||||||
senderUuid,
|
|
||||||
senderDevice,
|
senderDevice,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.contactTypingTimers[identifier].timer = setTimeout(
|
this.contactTypingTimers[deviceId].timer = setTimeout(
|
||||||
this.clearContactTypingTimer.bind(this, identifier),
|
this.clearContactTypingTimer.bind(this, deviceId),
|
||||||
15 * 1000
|
15 * 1000
|
||||||
);
|
);
|
||||||
if (!record) {
|
if (!record) {
|
||||||
|
@ -2848,7 +2844,7 @@
|
||||||
this.trigger('change', this);
|
this.trigger('change', this);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
delete this.contactTypingTimers[identifier];
|
delete this.contactTypingTimers[deviceId];
|
||||||
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);
|
||||||
|
@ -2856,13 +2852,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
clearContactTypingTimer(identifier) {
|
clearContactTypingTimer(deviceId) {
|
||||||
this.contactTypingTimers = this.contactTypingTimers || {};
|
this.contactTypingTimers = this.contactTypingTimers || {};
|
||||||
const record = this.contactTypingTimers[identifier];
|
const record = this.contactTypingTimers[deviceId];
|
||||||
|
|
||||||
if (record) {
|
if (record) {
|
||||||
clearTimeout(record.timer);
|
clearTimeout(record.timer);
|
||||||
delete this.contactTypingTimers[identifier];
|
delete this.contactTypingTimers[deviceId];
|
||||||
|
|
||||||
// User was previously typing, but timed out or we received message. State change!
|
// User was previously typing, but timed out or we received message. State change!
|
||||||
this.trigger('change', this);
|
this.trigger('change', this);
|
||||||
|
@ -2879,9 +2875,9 @@
|
||||||
* than just their id.
|
* than just their id.
|
||||||
*/
|
*/
|
||||||
initialize() {
|
initialize() {
|
||||||
this._byE164 = {};
|
this._byE164 = Object.create(null);
|
||||||
this._byUuid = {};
|
this._byUuid = Object.create(null);
|
||||||
this._byGroupId = {};
|
this._byGroupId = Object.create(null);
|
||||||
this.on('idUpdated', (model, idProp, oldValue) => {
|
this.on('idUpdated', (model, idProp, oldValue) => {
|
||||||
if (oldValue) {
|
if (oldValue) {
|
||||||
if (idProp === 'e164') {
|
if (idProp === 'e164') {
|
||||||
|
@ -2908,9 +2904,9 @@
|
||||||
|
|
||||||
reset(...args) {
|
reset(...args) {
|
||||||
Backbone.Collection.prototype.reset.apply(this, args);
|
Backbone.Collection.prototype.reset.apply(this, args);
|
||||||
this._byE164 = {};
|
this._byE164 = Object.create(null);
|
||||||
this._byUuid = {};
|
this._byUuid = Object.create(null);
|
||||||
this._byGroupId = {};
|
this._byGroupId = Object.create(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
add(...models) {
|
add(...models) {
|
||||||
|
@ -2918,12 +2914,22 @@
|
||||||
[].concat(res).forEach(model => {
|
[].concat(res).forEach(model => {
|
||||||
const e164 = model.get('e164');
|
const e164 = model.get('e164');
|
||||||
if (e164) {
|
if (e164) {
|
||||||
this._byE164[e164] = model;
|
const existing = this._byE164[e164];
|
||||||
|
|
||||||
|
// Prefer the contact with both e164 and uuid
|
||||||
|
if (!existing || (existing && !existing.get('uuid'))) {
|
||||||
|
this._byE164[e164] = model;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const uuid = model.get('uuid');
|
const uuid = model.get('uuid');
|
||||||
if (uuid) {
|
if (uuid) {
|
||||||
this._byUuid[uuid] = model;
|
const existing = this._byUuid[uuid];
|
||||||
|
|
||||||
|
// Prefer the contact with both e164 and uuid
|
||||||
|
if (!existing || (existing && !existing.get('e164'))) {
|
||||||
|
this._byUuid[uuid] = model;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupId = model.get('groupId');
|
const groupId = model.get('groupId');
|
||||||
|
|
|
@ -193,30 +193,20 @@
|
||||||
|
|
||||||
// Other top-level prop-generation
|
// Other top-level prop-generation
|
||||||
getPropsForSearchResult() {
|
getPropsForSearchResult() {
|
||||||
const sourceE164 = this.getSource();
|
const ourId = ConversationController.getOurConversationId();
|
||||||
const sourceUuid = this.getSourceUuid();
|
const sourceId = this.getContactId();
|
||||||
const fromContact = this.findAndFormatContact(sourceE164 || sourceUuid);
|
const fromContact = this.findAndFormatContact(sourceId);
|
||||||
|
|
||||||
if (
|
if (ourId === sourceId) {
|
||||||
(sourceE164 && sourceE164 === this.OUR_NUMBER) ||
|
|
||||||
(sourceUuid && sourceUuid === this.OUR_UUID)
|
|
||||||
) {
|
|
||||||
fromContact.isMe = true;
|
fromContact.isMe = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const convo = this.getConversation();
|
const convo = this.getConversation();
|
||||||
|
|
||||||
let to = convo ? this.findAndFormatContact(convo.get('id')) : {};
|
const to = convo ? this.findAndFormatContact(convo.get('id')) : {};
|
||||||
|
|
||||||
if (convo && convo.isMe()) {
|
if (to && convo && convo.isMe()) {
|
||||||
to.isMe = true;
|
to.isMe = true;
|
||||||
} else if (
|
|
||||||
(sourceE164 && convo && sourceE164 === convo.get('e164')) ||
|
|
||||||
(sourceUuid && convo && sourceUuid === convo.get('uuid'))
|
|
||||||
) {
|
|
||||||
to = {
|
|
||||||
isMe: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -237,10 +227,10 @@
|
||||||
|
|
||||||
const unidentifiedLookup = (
|
const unidentifiedLookup = (
|
||||||
this.get('unidentifiedDeliveries') || []
|
this.get('unidentifiedDeliveries') || []
|
||||||
).reduce((accumulator, uuidOrE164) => {
|
).reduce((accumulator, identifier) => {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
accumulator[
|
accumulator[
|
||||||
ConversationController.getConversationId(uuidOrE164)
|
ConversationController.getConversationId(identifier)
|
||||||
] = true;
|
] = true;
|
||||||
return accumulator;
|
return accumulator;
|
||||||
}, Object.create(null));
|
}, Object.create(null));
|
||||||
|
@ -249,7 +239,7 @@
|
||||||
// Older messages don't have the recipients included on the message, so we fall
|
// Older messages don't have the recipients included on the message, so we fall
|
||||||
// back to the conversation's current recipients
|
// back to the conversation's current recipients
|
||||||
const conversationIds = this.isIncoming()
|
const conversationIds = this.isIncoming()
|
||||||
? [this.getContact().get('id')]
|
? [this.getContactId()]
|
||||||
: _.union(
|
: _.union(
|
||||||
(this.get('sent_to') || []).map(id =>
|
(this.get('sent_to') || []).map(id =>
|
||||||
ConversationController.getConversationId(id)
|
ConversationController.getConversationId(id)
|
||||||
|
@ -379,11 +369,11 @@
|
||||||
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 phoneNumber = this.getSource();
|
const sourceId = this.getContactId();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canProcessNow,
|
canProcessNow,
|
||||||
contact: this.findAndFormatContact(phoneNumber),
|
contact: this.findAndFormatContact(sourceId),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getPropsForTimerNotification() {
|
getPropsForTimerNotification() {
|
||||||
|
@ -396,8 +386,14 @@
|
||||||
const timespan = Whisper.ExpirationTimerOptions.getName(expireTimer || 0);
|
const timespan = Whisper.ExpirationTimerOptions.getName(expireTimer || 0);
|
||||||
const disabled = !expireTimer;
|
const disabled = !expireTimer;
|
||||||
|
|
||||||
|
const sourceId = ConversationController.ensureContactIds({
|
||||||
|
e164: source,
|
||||||
|
uuid: sourceUuid,
|
||||||
|
});
|
||||||
|
const ourId = ConversationController.getOurConversationId();
|
||||||
|
|
||||||
const basicProps = {
|
const basicProps = {
|
||||||
...this.findAndFormatContact(source),
|
...this.findAndFormatContact(sourceId),
|
||||||
type: 'fromOther',
|
type: 'fromOther',
|
||||||
timespan,
|
timespan,
|
||||||
disabled,
|
disabled,
|
||||||
|
@ -408,7 +404,7 @@
|
||||||
...basicProps,
|
...basicProps,
|
||||||
type: 'fromSync',
|
type: 'fromSync',
|
||||||
};
|
};
|
||||||
} else if (source === this.OUR_NUMBER || sourceUuid === this.OUR_UUID) {
|
} else if (sourceId && sourceId === ourId) {
|
||||||
return {
|
return {
|
||||||
...basicProps,
|
...basicProps,
|
||||||
type: 'fromMe',
|
type: 'fromMe',
|
||||||
|
@ -430,12 +426,12 @@
|
||||||
getPropsForVerificationNotification() {
|
getPropsForVerificationNotification() {
|
||||||
const type = this.get('verified') ? 'markVerified' : 'markNotVerified';
|
const type = this.get('verified') ? 'markVerified' : 'markNotVerified';
|
||||||
const isLocal = this.get('local');
|
const isLocal = this.get('local');
|
||||||
const phoneNumber = this.get('verifiedChanged');
|
const identifier = this.get('verifiedChanged');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
isLocal,
|
isLocal,
|
||||||
contact: this.findAndFormatContact(phoneNumber),
|
contact: this.findAndFormatContact(identifier),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getPropsForGroupNotification() {
|
getPropsForGroupNotification() {
|
||||||
|
@ -460,7 +456,7 @@
|
||||||
Array.isArray(groupUpdate.joined)
|
Array.isArray(groupUpdate.joined)
|
||||||
? groupUpdate.joined
|
? groupUpdate.joined
|
||||||
: [groupUpdate.joined],
|
: [groupUpdate.joined],
|
||||||
phoneNumber => this.findAndFormatContact(phoneNumber)
|
identifier => this.findAndFormatContact(identifier)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -477,7 +473,7 @@
|
||||||
Array.isArray(groupUpdate.left)
|
Array.isArray(groupUpdate.left)
|
||||||
? groupUpdate.left
|
? groupUpdate.left
|
||||||
: [groupUpdate.left],
|
: [groupUpdate.left],
|
||||||
phoneNumber => this.findAndFormatContact(phoneNumber)
|
identifier => this.findAndFormatContact(identifier)
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -495,9 +491,8 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceE164 = this.getSource();
|
const sourceId = this.getContactId();
|
||||||
const sourceUuid = this.getSourceUuid();
|
const from = this.findAndFormatContact(sourceId);
|
||||||
const from = this.findAndFormatContact(sourceE164 || sourceUuid);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from,
|
from,
|
||||||
|
@ -537,10 +532,9 @@
|
||||||
.map(attachment => this.getPropsForAttachment(attachment));
|
.map(attachment => this.getPropsForAttachment(attachment));
|
||||||
},
|
},
|
||||||
getPropsForMessage() {
|
getPropsForMessage() {
|
||||||
const sourceE164 = this.getSource();
|
const sourceId = this.getContactId();
|
||||||
const sourceUuid = this.getSourceUuid();
|
const contact = this.findAndFormatContact(sourceId);
|
||||||
const contact = this.findAndFormatContact(sourceE164 || sourceUuid);
|
const contactModel = this.findContact(sourceId);
|
||||||
const contactModel = this.findContact(sourceE164 || sourceUuid);
|
|
||||||
|
|
||||||
const authorColor = contactModel ? contactModel.getColor() : null;
|
const authorColor = contactModel ? contactModel.getColor() : null;
|
||||||
const authorAvatarPath = contactModel
|
const authorAvatarPath = contactModel
|
||||||
|
@ -774,7 +768,13 @@
|
||||||
referencedMessageNotFound,
|
referencedMessageNotFound,
|
||||||
} = quote;
|
} = quote;
|
||||||
const contact =
|
const contact =
|
||||||
author && ConversationController.get(author || authorUuid);
|
(author || authorUuid) &&
|
||||||
|
ConversationController.get(
|
||||||
|
ConversationController.ensureContactIds({
|
||||||
|
e164: author,
|
||||||
|
uuid: authorUuid,
|
||||||
|
})
|
||||||
|
);
|
||||||
const authorColor = contact ? contact.getColor() : 'grey';
|
const authorColor = contact ? contact.getColor() : 'grey';
|
||||||
|
|
||||||
const authorPhoneNumber = format(author, {
|
const authorPhoneNumber = format(author, {
|
||||||
|
@ -810,17 +810,18 @@
|
||||||
|
|
||||||
const e164 = conversation.get('e164');
|
const e164 = conversation.get('e164');
|
||||||
const uuid = conversation.get('uuid');
|
const uuid = conversation.get('uuid');
|
||||||
|
const conversationId = conversation.get('id');
|
||||||
|
|
||||||
const readBy = this.get('read_by') || [];
|
const readBy = this.get('read_by') || [];
|
||||||
if (includesAny(readBy, identifier, e164, uuid)) {
|
if (includesAny(readBy, conversationId, e164, uuid)) {
|
||||||
return 'read';
|
return 'read';
|
||||||
}
|
}
|
||||||
const deliveredTo = this.get('delivered_to') || [];
|
const deliveredTo = this.get('delivered_to') || [];
|
||||||
if (includesAny(deliveredTo, identifier, e164, uuid)) {
|
if (includesAny(deliveredTo, conversationId, e164, uuid)) {
|
||||||
return 'delivered';
|
return 'delivered';
|
||||||
}
|
}
|
||||||
const sentTo = this.get('sent_to') || [];
|
const sentTo = this.get('sent_to') || [];
|
||||||
if (includesAny(sentTo, identifier, e164, uuid)) {
|
if (includesAny(sentTo, conversationId, e164, uuid)) {
|
||||||
return 'sent';
|
return 'sent';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1220,19 +1221,22 @@
|
||||||
|
|
||||||
return this.OUR_UUID;
|
return this.OUR_UUID;
|
||||||
},
|
},
|
||||||
getContact() {
|
getContactId() {
|
||||||
const source = this.getSource();
|
const source = this.getSource();
|
||||||
const sourceUuid = this.getSourceUuid();
|
const sourceUuid = this.getSourceUuid();
|
||||||
|
|
||||||
if (!source && !sourceUuid) {
|
if (!source && !sourceUuid) {
|
||||||
return null;
|
return ConversationController.getOurConversationId();
|
||||||
}
|
}
|
||||||
|
|
||||||
const contactId = ConversationController.ensureContactIds({
|
return ConversationController.ensureContactIds({
|
||||||
e164: source,
|
e164: source,
|
||||||
uuid: sourceUuid,
|
uuid: sourceUuid,
|
||||||
});
|
});
|
||||||
return ConversationController.get(contactId, 'private');
|
},
|
||||||
|
getContact() {
|
||||||
|
const id = this.getContactId();
|
||||||
|
return ConversationController.get(id);
|
||||||
},
|
},
|
||||||
isOutgoing() {
|
isOutgoing() {
|
||||||
return this.get('type') === 'outgoing';
|
return this.get('type') === 'outgoing';
|
||||||
|
@ -1395,7 +1399,7 @@
|
||||||
let recipients = _.intersection(intendedRecipients, currentRecipients);
|
let recipients = _.intersection(intendedRecipients, currentRecipients);
|
||||||
recipients = _.without(recipients, successfulRecipients).map(id => {
|
recipients = _.without(recipients, successfulRecipients).map(id => {
|
||||||
const c = ConversationController.get(id);
|
const c = ConversationController.get(id);
|
||||||
return c.get('uuid') || c.get('e164');
|
return c.getSendTarget();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!recipients.length) {
|
if (!recipients.length) {
|
||||||
|
@ -1699,7 +1703,7 @@
|
||||||
try {
|
try {
|
||||||
this.set({
|
this.set({
|
||||||
// These are the same as a normal send()
|
// These are the same as a normal send()
|
||||||
sent_to: [conv.get('uuid') || conv.get('e164')],
|
sent_to: [conv.getSendTarget()],
|
||||||
sent: true,
|
sent: true,
|
||||||
expirationStartTimestamp: Date.now(),
|
expirationStartTimestamp: Date.now(),
|
||||||
});
|
});
|
||||||
|
@ -1709,8 +1713,8 @@
|
||||||
unidentifiedDeliveries: result ? result.unidentifiedDeliveries : null,
|
unidentifiedDeliveries: result ? result.unidentifiedDeliveries : null,
|
||||||
|
|
||||||
// These are unique to a Note to Self message - immediately read/delivered
|
// These are unique to a Note to Self message - immediately read/delivered
|
||||||
delivered_to: [this.OUR_UUID || this.OUR_NUMBER],
|
delivered_to: [ConversationController.getOurConversationId()],
|
||||||
read_by: [this.OUR_UUID || this.OUR_NUMBER],
|
read_by: [ConversationController.getOurConversationId()],
|
||||||
});
|
});
|
||||||
} catch (result) {
|
} catch (result) {
|
||||||
const errors = (result && result.errors) || [
|
const errors = (result && result.errors) || [
|
||||||
|
@ -2004,20 +2008,20 @@
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { attachments, id, author } = quote;
|
const { attachments, id, author, authorUuid } = quote;
|
||||||
const firstAttachment = attachments[0];
|
const firstAttachment = attachments[0];
|
||||||
|
const authorConversationId = ConversationController.ensureContactIds({
|
||||||
|
e164: author,
|
||||||
|
uuid: authorUuid,
|
||||||
|
});
|
||||||
|
|
||||||
const collection = await window.Signal.Data.getMessagesBySentAt(id, {
|
const collection = await window.Signal.Data.getMessagesBySentAt(id, {
|
||||||
MessageCollection: Whisper.MessageCollection,
|
MessageCollection: Whisper.MessageCollection,
|
||||||
});
|
});
|
||||||
const found = collection.find(item => {
|
const found = collection.find(item => {
|
||||||
const messageAuthor = item.getContact();
|
const messageAuthorId = item.getContactId();
|
||||||
|
|
||||||
return (
|
return authorConversationId === messageAuthorId;
|
||||||
messageAuthor &&
|
|
||||||
ConversationController.getConversationId(author) ===
|
|
||||||
messageAuthor.get('id')
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
|
@ -2119,10 +2123,7 @@
|
||||||
const source = message.get('source');
|
const source = message.get('source');
|
||||||
const sourceUuid = message.get('sourceUuid');
|
const sourceUuid = message.get('sourceUuid');
|
||||||
const type = message.get('type');
|
const type = message.get('type');
|
||||||
let conversationId = message.get('conversationId');
|
const conversationId = message.get('conversationId');
|
||||||
if (initialMessage.group) {
|
|
||||||
conversationId = initialMessage.group.id;
|
|
||||||
}
|
|
||||||
const GROUP_TYPES = textsecure.protobuf.GroupContext.Type;
|
const GROUP_TYPES = textsecure.protobuf.GroupContext.Type;
|
||||||
|
|
||||||
const conversation = ConversationController.get(conversationId);
|
const conversation = ConversationController.get(conversationId);
|
||||||
|
@ -2392,10 +2393,13 @@
|
||||||
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.getContact().get('id') });
|
conversation.set({ addedBy: message.getContactId() });
|
||||||
}
|
}
|
||||||
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
|
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
|
||||||
const sender = ConversationController.get(source || sourceUuid);
|
const sender = ConversationController.ensureContactIds({
|
||||||
|
e164: source,
|
||||||
|
uuid: sourceUuid,
|
||||||
|
});
|
||||||
const inGroup = Boolean(
|
const inGroup = Boolean(
|
||||||
sender &&
|
sender &&
|
||||||
(conversation.get('members') || []).includes(sender.id)
|
(conversation.get('members') || []).includes(sender.id)
|
||||||
|
@ -2453,6 +2457,7 @@
|
||||||
message.set({
|
message.set({
|
||||||
expirationTimerUpdate: {
|
expirationTimerUpdate: {
|
||||||
source,
|
source,
|
||||||
|
sourceUuid,
|
||||||
expireTimer: dataMessage.expireTimer,
|
expireTimer: dataMessage.expireTimer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -2567,9 +2572,7 @@
|
||||||
e164: source,
|
e164: source,
|
||||||
uuid: sourceUuid,
|
uuid: sourceUuid,
|
||||||
});
|
});
|
||||||
ConversationController.get(localId, 'private').setProfileKey(
|
ConversationController.get(localId).setProfileKey(profileKey);
|
||||||
profileKey
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,15 +15,8 @@ let scheduleNext = null;
|
||||||
// do not support unidentified delivery.
|
// do not support unidentified delivery.
|
||||||
function refreshOurProfile() {
|
function refreshOurProfile() {
|
||||||
window.log.info('refreshOurProfile');
|
window.log.info('refreshOurProfile');
|
||||||
const ourNumber = textsecure.storage.user.getNumber();
|
const ourId = ConversationController.getOurConversationId();
|
||||||
const ourUuid = textsecure.storage.user.getUuid();
|
const conversation = ConversationController.get(ourId);
|
||||||
const ourId = ConversationController.ensureContactIds({
|
|
||||||
e164: ourNumber,
|
|
||||||
uuid: ourUuid,
|
|
||||||
});
|
|
||||||
const conversation = ConversationController.get(ourId, 'private');
|
|
||||||
conversation.updateUuid(ourUuid);
|
|
||||||
conversation.updateE164(ourNumber);
|
|
||||||
conversation.getProfiles();
|
conversation.getProfiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
const { bindActionCreators } = require('redux');
|
const { bindActionCreators } = require('redux');
|
||||||
const Backbone = require('../../ts/backbone');
|
const Backbone = require('../../ts/backbone');
|
||||||
const Crypto = require('../../ts/Crypto');
|
const Crypto = require('../../ts/Crypto');
|
||||||
|
const {
|
||||||
|
start: conversationControllerStart,
|
||||||
|
} = require('../../ts/ConversationController');
|
||||||
const Data = require('../../ts/sql/Client').default;
|
const Data = require('../../ts/sql/Client').default;
|
||||||
const Emojis = require('./emojis');
|
const Emojis = require('./emojis');
|
||||||
const EmojiLib = require('../../ts/components/emoji/lib');
|
const EmojiLib = require('../../ts/components/emoji/lib');
|
||||||
|
@ -357,6 +360,7 @@ exports.setup = (options = {}) => {
|
||||||
Backbone,
|
Backbone,
|
||||||
Components,
|
Components,
|
||||||
Crypto,
|
Crypto,
|
||||||
|
conversationControllerStart,
|
||||||
Data,
|
Data,
|
||||||
Emojis,
|
Emojis,
|
||||||
EmojiLib,
|
EmojiLib,
|
||||||
|
|
|
@ -28,9 +28,10 @@
|
||||||
|
|
||||||
const reactionsBySource = this.filter(re => {
|
const reactionsBySource = this.filter(re => {
|
||||||
const mcid = message.get('conversationId');
|
const mcid = message.get('conversationId');
|
||||||
const recid = ConversationController.getConversationId(
|
const recid = ConversationController.ensureContactIds({
|
||||||
re.get('targetAuthorE164') || re.get('targetAuthorUuid')
|
e164: re.get('targetAuthorE164'),
|
||||||
);
|
uuid: re.get('targetAuthorUuid'),
|
||||||
|
});
|
||||||
const mTime = message.get('sent_at');
|
const mTime = message.get('sent_at');
|
||||||
const rTime = re.get('targetTimestamp');
|
const rTime = re.get('targetTimestamp');
|
||||||
return mcid === recid && mTime === rTime;
|
return mcid === recid && mTime === rTime;
|
||||||
|
@ -47,9 +48,10 @@
|
||||||
async onReaction(reaction) {
|
async onReaction(reaction) {
|
||||||
try {
|
try {
|
||||||
const targetConversation = await ConversationController.getConversationForTargetMessage(
|
const targetConversation = await ConversationController.getConversationForTargetMessage(
|
||||||
// Do not use ensureContactIds here since maliciously malformed
|
ConversationController.ensureContactIds({
|
||||||
// reactions from clients could cause issues
|
e164: reaction.get('targetAuthorE164'),
|
||||||
reaction.get('targetAuthorE164') || reaction.get('targetAuthorUuid'),
|
uuid: reaction.get('targetAuthorUuid'),
|
||||||
|
}),
|
||||||
reaction.get('targetTimestamp')
|
reaction.get('targetTimestamp')
|
||||||
);
|
);
|
||||||
if (!targetConversation) {
|
if (!targetConversation) {
|
||||||
|
@ -85,10 +87,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const mcid = contact.get('id');
|
const mcid = contact.get('id');
|
||||||
const recid = ConversationController.getConversationId(
|
const recid = ConversationController.ensureContactIds({
|
||||||
reaction.get('targetAuthorE164') ||
|
e164: reaction.get('targetAuthorE164'),
|
||||||
reaction.get('targetAuthorUuid')
|
uuid: reaction.get('targetAuthorUuid'),
|
||||||
);
|
});
|
||||||
return mcid === recid;
|
return mcid === recid;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2058,7 +2058,7 @@
|
||||||
await contact.setApproved();
|
await contact.setApproved();
|
||||||
}
|
}
|
||||||
|
|
||||||
message.resend(contact.get('uuid') || contact.get('e164'));
|
message.resend(contact.getSendTarget());
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,13 @@
|
||||||
model: { window: options.window },
|
model: { window: options.window },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Whisper.events.on('refreshConversation', ({ oldId, newId }) => {
|
||||||
|
const convo = this.conversation_stack.lastConversation;
|
||||||
|
if (convo && convo.get('id') === oldId) {
|
||||||
|
this.conversation_stack.open(newId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!options.initialLoadComplete) {
|
if (!options.initialLoadComplete) {
|
||||||
this.appLoadingScreen = new Whisper.AppLoadingScreen();
|
this.appLoadingScreen = new Whisper.AppLoadingScreen();
|
||||||
this.appLoadingScreen.render();
|
this.appLoadingScreen.render();
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
<script type="text/javascript" src="../../js/models/blockedNumbers.js" data-cover></script>
|
<script type="text/javascript" src="../../js/models/blockedNumbers.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../../js/models/messages.js" data-cover></script>
|
<script type="text/javascript" src="../../js/models/messages.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../../js/models/conversations.js" data-cover></script>
|
<script type="text/javascript" src="../../js/models/conversations.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../../js/conversation_controller.js" data-cover></script>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="errors_test.js"></script>
|
<script type="text/javascript" src="errors_test.js"></script>
|
||||||
<script type="text/javascript" src="helpers_test.js"></script>
|
<script type="text/javascript" src="helpers_test.js"></script>
|
||||||
|
@ -51,6 +50,7 @@
|
||||||
<!-- Uncomment to start tests without code coverage enabled -->
|
<!-- Uncomment to start tests without code coverage enabled -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
mocha.run();
|
mocha.run();
|
||||||
|
window.Signal.conversationControllerStart();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -64,8 +64,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#b10f232fac62ba7f8775c9e086bb5558fe7d948b",
|
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#b10f232fac62ba7f8775c9e086bb5558fe7d948b",
|
||||||
"@sindresorhus/is": "0.8.0",
|
"@sindresorhus/is": "0.8.0",
|
||||||
"@types/node-fetch": "2.5.5",
|
|
||||||
"@types/websocket": "1.0.0",
|
|
||||||
"array-move": "2.1.0",
|
"array-move": "2.1.0",
|
||||||
"backbone": "1.3.3",
|
"backbone": "1.3.3",
|
||||||
"blob-util": "1.3.0",
|
"blob-util": "1.3.0",
|
||||||
|
@ -172,13 +170,14 @@
|
||||||
"@types/got": "9.4.1",
|
"@types/got": "9.4.1",
|
||||||
"@types/history": "4.7.2",
|
"@types/history": "4.7.2",
|
||||||
"@types/html-webpack-plugin": "3.2.1",
|
"@types/html-webpack-plugin": "3.2.1",
|
||||||
"@types/jquery": "3.3.29",
|
"@types/jquery": "3.5.0",
|
||||||
"@types/js-yaml": "3.12.0",
|
"@types/js-yaml": "3.12.0",
|
||||||
"@types/linkify-it": "2.1.0",
|
"@types/linkify-it": "2.1.0",
|
||||||
"@types/lodash": "4.14.106",
|
"@types/lodash": "4.14.106",
|
||||||
"@types/memoizee": "0.4.2",
|
"@types/memoizee": "0.4.2",
|
||||||
"@types/mkdirp": "0.5.2",
|
"@types/mkdirp": "0.5.2",
|
||||||
"@types/mocha": "5.0.0",
|
"@types/mocha": "5.0.0",
|
||||||
|
"@types/node-fetch": "2.5.5",
|
||||||
"@types/normalize-path": "3.0.0",
|
"@types/normalize-path": "3.0.0",
|
||||||
"@types/pify": "3.0.2",
|
"@types/pify": "3.0.2",
|
||||||
"@types/qs": "6.5.1",
|
"@types/qs": "6.5.1",
|
||||||
|
@ -197,9 +196,11 @@
|
||||||
"@types/storybook__addon-actions": "3.4.3",
|
"@types/storybook__addon-actions": "3.4.3",
|
||||||
"@types/storybook__addon-knobs": "5.0.3",
|
"@types/storybook__addon-knobs": "5.0.3",
|
||||||
"@types/storybook__react": "4.0.2",
|
"@types/storybook__react": "4.0.2",
|
||||||
|
"@types/underscore": "1.10.3",
|
||||||
"@types/uuid": "3.4.4",
|
"@types/uuid": "3.4.4",
|
||||||
"@types/webpack": "4.39.0",
|
"@types/webpack": "4.39.0",
|
||||||
"@types/webpack-dev-server": "3.1.7",
|
"@types/webpack-dev-server": "3.1.7",
|
||||||
|
"@types/websocket": "1.0.0",
|
||||||
"arraybuffer-loader": "1.0.3",
|
"arraybuffer-loader": "1.0.3",
|
||||||
"asar": "0.14.0",
|
"asar": "0.14.0",
|
||||||
"babel-core": "7.0.0-bridge.0",
|
"babel-core": "7.0.0-bridge.0",
|
||||||
|
|
|
@ -345,7 +345,6 @@
|
||||||
<script type="text/javascript" src="../js/models/messages.js" data-cover></script>
|
<script type="text/javascript" src="../js/models/messages.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../js/models/conversations.js" data-cover></script>
|
<script type="text/javascript" src="../js/models/conversations.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../js/models/blockedNumbers.js" data-cover></script>
|
<script type="text/javascript" src="../js/models/blockedNumbers.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../js/conversation_controller.js" data-cover></script>
|
|
||||||
<script type="text/javascript" src="../js/message_controller.js" data-cover></script>
|
<script type="text/javascript" src="../js/message_controller.js" data-cover></script>
|
||||||
<script type="text/javascript" src="../js/keychange_listener.js" data-cover></script>
|
<script type="text/javascript" src="../js/keychange_listener.js" data-cover></script>
|
||||||
<script type='text/javascript' src='../js/expiring_messages.js' data-cover></script>
|
<script type='text/javascript' src='../js/expiring_messages.js' data-cover></script>
|
||||||
|
@ -393,6 +392,7 @@
|
||||||
|
|
||||||
<!-- Uncomment to start tests without code coverage enabled -->
|
<!-- Uncomment to start tests without code coverage enabled -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
window.Signal.conversationControllerStart();
|
||||||
mocha.run();
|
mocha.run();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
675
ts/ConversationController.ts
Normal file
675
ts/ConversationController.ts
Normal file
|
@ -0,0 +1,675 @@
|
||||||
|
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
||||||
|
|
||||||
|
import { debounce, reduce, uniq, without } from 'lodash';
|
||||||
|
import dataInterface from './sql/Client';
|
||||||
|
import {
|
||||||
|
ConversationModelCollectionType,
|
||||||
|
ConversationModelType,
|
||||||
|
ConversationTypeType,
|
||||||
|
} from './model-types.d';
|
||||||
|
import { SendOptionsType } from './textsecure/SendMessage';
|
||||||
|
|
||||||
|
const {
|
||||||
|
getAllConversations,
|
||||||
|
getAllGroupsInvolvingId,
|
||||||
|
getMessagesBySentAt,
|
||||||
|
migrateConversationMessages,
|
||||||
|
removeConversation,
|
||||||
|
saveConversation,
|
||||||
|
updateConversation,
|
||||||
|
} = dataInterface;
|
||||||
|
|
||||||
|
// We have to run this in background.js, after all backbone models and collections on
|
||||||
|
// Whisper.* have been created. Once those are in typescript we can use more reasonable
|
||||||
|
// require statements for referencing these things, giving us more flexibility here.
|
||||||
|
export function start() {
|
||||||
|
const conversations = new window.Whisper.ConversationCollection();
|
||||||
|
|
||||||
|
// This class is entirely designed to keep the app title, badge and tray icon updated.
|
||||||
|
// In the future it could listen to redux changes and do its updates there.
|
||||||
|
const inboxCollection = new (window.Backbone.Collection.extend({
|
||||||
|
initialize() {
|
||||||
|
this.listenTo(conversations, 'add change:active_at', this.addActive);
|
||||||
|
this.listenTo(conversations, 'reset', () => this.reset([]));
|
||||||
|
|
||||||
|
this.on(
|
||||||
|
'add remove change:unreadCount',
|
||||||
|
debounce(this.updateUnreadCount.bind(this), 1000)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
addActive(model: ConversationModelType) {
|
||||||
|
if (model.get('active_at')) {
|
||||||
|
this.add(model);
|
||||||
|
} else {
|
||||||
|
this.remove(model);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateUnreadCount() {
|
||||||
|
const newUnreadCount = reduce(
|
||||||
|
this.map((m: ConversationModelType) => m.get('unreadCount')),
|
||||||
|
(item: number, memo: number) => (item || 0) + memo,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
window.storage.put('unreadCount', newUnreadCount);
|
||||||
|
|
||||||
|
if (newUnreadCount > 0) {
|
||||||
|
window.setBadgeCount(newUnreadCount);
|
||||||
|
window.document.title = `${window.getTitle()} (${newUnreadCount})`;
|
||||||
|
} else {
|
||||||
|
window.setBadgeCount(0);
|
||||||
|
window.document.title = window.getTitle();
|
||||||
|
}
|
||||||
|
window.updateTrayIcon(newUnreadCount);
|
||||||
|
},
|
||||||
|
}))();
|
||||||
|
|
||||||
|
window.getInboxCollection = () => inboxCollection;
|
||||||
|
window.getConversations = () => conversations;
|
||||||
|
window.ConversationController = new ConversationController(conversations);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConversationController {
|
||||||
|
_initialFetchComplete: boolean | undefined;
|
||||||
|
_initialPromise: Promise<void> = Promise.resolve();
|
||||||
|
_conversations: ConversationModelCollectionType;
|
||||||
|
|
||||||
|
constructor(conversations?: ConversationModelCollectionType) {
|
||||||
|
if (!conversations) {
|
||||||
|
throw new Error('ConversationController: need conversation collection!');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._conversations = conversations;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(id?: string | null): ConversationModelType | undefined {
|
||||||
|
if (!this._initialFetchComplete) {
|
||||||
|
throw new Error(
|
||||||
|
'ConversationController.get() needs complete initial fetch'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function takes null just fine. Backbone typings are too restrictive.
|
||||||
|
return this._conversations.get(id as string);
|
||||||
|
}
|
||||||
|
// Needed for some model setup which happens during the initial fetch() call below
|
||||||
|
getUnsafe(id: string) {
|
||||||
|
return this._conversations.get(id);
|
||||||
|
}
|
||||||
|
dangerouslyCreateAndAdd(attributes: Partial<ConversationModelType>) {
|
||||||
|
return this._conversations.add(attributes);
|
||||||
|
}
|
||||||
|
getOrCreate(
|
||||||
|
identifier: string,
|
||||||
|
type: ConversationTypeType,
|
||||||
|
additionalInitialProps = {}
|
||||||
|
) {
|
||||||
|
if (typeof identifier !== 'string') {
|
||||||
|
throw new TypeError("'id' must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type !== 'private' && type !== 'group') {
|
||||||
|
throw new TypeError(
|
||||||
|
`'type' must be 'private' or 'group'; got: '${type}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._initialFetchComplete) {
|
||||||
|
throw new Error(
|
||||||
|
'ConversationController.get() needs complete initial fetch'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let conversation = this._conversations.get(identifier);
|
||||||
|
if (conversation) {
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = window.getGuid();
|
||||||
|
|
||||||
|
if (type === 'group') {
|
||||||
|
conversation = this._conversations.add({
|
||||||
|
id,
|
||||||
|
uuid: null,
|
||||||
|
e164: null,
|
||||||
|
groupId: identifier,
|
||||||
|
type,
|
||||||
|
version: 2,
|
||||||
|
...additionalInitialProps,
|
||||||
|
});
|
||||||
|
} else if (window.isValidGuid(identifier)) {
|
||||||
|
conversation = this._conversations.add({
|
||||||
|
id,
|
||||||
|
uuid: identifier,
|
||||||
|
e164: null,
|
||||||
|
groupId: null,
|
||||||
|
type,
|
||||||
|
version: 2,
|
||||||
|
...additionalInitialProps,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
conversation = this._conversations.add({
|
||||||
|
id,
|
||||||
|
uuid: null,
|
||||||
|
e164: identifier,
|
||||||
|
groupId: null,
|
||||||
|
type,
|
||||||
|
version: 2,
|
||||||
|
...additionalInitialProps,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const create = async () => {
|
||||||
|
if (!conversation.isValid()) {
|
||||||
|
const validationError = conversation.validationError || {};
|
||||||
|
window.log.error(
|
||||||
|
'Contact is not valid. Not saving, but adding to collection:',
|
||||||
|
conversation.idForLogging(),
|
||||||
|
validationError.stack
|
||||||
|
);
|
||||||
|
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveConversation(conversation.attributes);
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'Conversation save failed! ',
|
||||||
|
identifier,
|
||||||
|
type,
|
||||||
|
'Error:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return conversation;
|
||||||
|
};
|
||||||
|
|
||||||
|
conversation.initialPromise = create();
|
||||||
|
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
|
async getOrCreateAndWait(
|
||||||
|
id: string,
|
||||||
|
type: ConversationTypeType,
|
||||||
|
additionalInitialProps = {}
|
||||||
|
) {
|
||||||
|
return this._initialPromise.then(async () => {
|
||||||
|
const conversation = this.getOrCreate(id, type, additionalInitialProps);
|
||||||
|
|
||||||
|
if (conversation) {
|
||||||
|
return conversation.initialPromise.then(() => conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(
|
||||||
|
new Error('getOrCreateAndWait: did not get conversation')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getConversationId(address: string) {
|
||||||
|
if (!address) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [id] = window.textsecure.utils.unencodeNumber(address);
|
||||||
|
const conv = this.get(id);
|
||||||
|
|
||||||
|
if (conv) {
|
||||||
|
return conv.get('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
getOurConversationId() {
|
||||||
|
const e164 = window.textsecure.storage.user.getNumber();
|
||||||
|
const uuid = window.textsecure.storage.user.getUuid();
|
||||||
|
return this.ensureContactIds({ e164, uuid, highTrust: true });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Given a UUID and/or an E164, resolves to a string representing the local
|
||||||
|
* database id of the given contact. It may create new contacts, and it may merge
|
||||||
|
* contacts.
|
||||||
|
*
|
||||||
|
* lowTrust = uuid/e164 pairing came from source like GroupV1 member list
|
||||||
|
* highTrust = uuid/e164 pairing came from source like CDS
|
||||||
|
*/
|
||||||
|
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
|
||||||
|
ensureContactIds({
|
||||||
|
e164,
|
||||||
|
uuid,
|
||||||
|
highTrust,
|
||||||
|
}: {
|
||||||
|
e164?: string;
|
||||||
|
uuid?: string;
|
||||||
|
highTrust?: boolean;
|
||||||
|
}) {
|
||||||
|
// Check for at least one parameter being provided. This is necessary
|
||||||
|
// because this path can be called on startup to resolve our own ID before
|
||||||
|
// our phone number or UUID are known. The existing behavior in these
|
||||||
|
// cases can handle a returned `undefined` id, so we do that.
|
||||||
|
const normalizedUuid = uuid ? uuid.toLowerCase() : undefined;
|
||||||
|
const identifier = normalizedUuid || e164;
|
||||||
|
|
||||||
|
if ((!e164 && !uuid) || !identifier) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const convoE164 = this.get(e164);
|
||||||
|
const convoUuid = this.get(normalizedUuid);
|
||||||
|
|
||||||
|
// 1. Handle no match at all
|
||||||
|
if (!convoE164 && !convoUuid) {
|
||||||
|
window.log.info(
|
||||||
|
'ensureContactIds: Creating new contact, no matches found'
|
||||||
|
);
|
||||||
|
const newConvo = this.getOrCreate(identifier, 'private');
|
||||||
|
if (highTrust && e164) {
|
||||||
|
newConvo.updateE164(e164);
|
||||||
|
}
|
||||||
|
if (normalizedUuid) {
|
||||||
|
newConvo.updateUuid(normalizedUuid);
|
||||||
|
}
|
||||||
|
if (highTrust && e164 && normalizedUuid) {
|
||||||
|
updateConversation(newConvo.attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newConvo.get('id');
|
||||||
|
|
||||||
|
// 2. Handle match on only E164
|
||||||
|
} else if (convoE164 && !convoUuid) {
|
||||||
|
const haveUuid = Boolean(normalizedUuid);
|
||||||
|
window.log.info(
|
||||||
|
`ensureContactIds: e164-only match found (have UUID: ${haveUuid})`
|
||||||
|
);
|
||||||
|
// If we are only searching based on e164 anyway, then return the first result
|
||||||
|
if (!normalizedUuid) {
|
||||||
|
return convoE164.get('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in the UUID for an e164-only contact
|
||||||
|
if (normalizedUuid && !convoE164.get('uuid')) {
|
||||||
|
if (highTrust) {
|
||||||
|
window.log.info('ensureContactIds: Adding UUID to e164-only match');
|
||||||
|
convoE164.updateUuid(normalizedUuid);
|
||||||
|
updateConversation(convoE164.attributes);
|
||||||
|
}
|
||||||
|
return convoE164.get('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info(
|
||||||
|
'ensureContactIds: e164 already had UUID, creating a new contact'
|
||||||
|
);
|
||||||
|
// If existing e164 match already has UUID, create a new contact...
|
||||||
|
const newConvo = this.getOrCreate(normalizedUuid, 'private');
|
||||||
|
|
||||||
|
if (highTrust) {
|
||||||
|
window.log.info(
|
||||||
|
'ensureContactIds: Moving e164 from old contact to new'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove the e164 from the old contact...
|
||||||
|
convoE164.set({ e164: undefined });
|
||||||
|
updateConversation(convoE164.attributes);
|
||||||
|
|
||||||
|
// ...and add it to the new one.
|
||||||
|
newConvo.updateE164(e164);
|
||||||
|
updateConversation(newConvo.attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newConvo.get('id');
|
||||||
|
|
||||||
|
// 3. Handle match on only UUID
|
||||||
|
} else if (!convoE164 && convoUuid) {
|
||||||
|
window.log.info(
|
||||||
|
`ensureContactIds: UUID-only match found (have e164: ${Boolean(e164)})`
|
||||||
|
);
|
||||||
|
if (e164 && highTrust) {
|
||||||
|
convoUuid.updateE164(e164);
|
||||||
|
updateConversation(convoUuid.attributes);
|
||||||
|
}
|
||||||
|
return convoUuid.get('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// For some reason, TypeScript doesn't believe that we can trust that these two values
|
||||||
|
// are truthy by this point. So we'll throw if we get there.
|
||||||
|
if (!convoE164 || !convoUuid) {
|
||||||
|
throw new Error('ensureContactIds: convoE164 or convoUuid are falsey!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, we know that we have a match for both e164 and uuid checks
|
||||||
|
|
||||||
|
if (convoE164 === convoUuid) {
|
||||||
|
return convoUuid.get('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (highTrust) {
|
||||||
|
// Conflict: If e164 match already has a UUID, we remove its e164.
|
||||||
|
if (convoE164.get('uuid') && convoE164.get('uuid') !== normalizedUuid) {
|
||||||
|
window.log.info(
|
||||||
|
'ensureContactIds: e164 match had different UUID than incoming pair, removing its e164.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove the e164 from the old contact...
|
||||||
|
convoE164.set({ e164: undefined });
|
||||||
|
updateConversation(convoE164.attributes);
|
||||||
|
|
||||||
|
// ...and add it to the new one.
|
||||||
|
convoUuid.updateE164(e164);
|
||||||
|
updateConversation(convoUuid.attributes);
|
||||||
|
|
||||||
|
return convoUuid.get('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.warn(
|
||||||
|
`ensureContactIds: Found a split contact - UUID ${normalizedUuid} and E164 ${e164}. Merging.`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Conflict: If e164 match has no UUID, we merge. We prefer the UUID match.
|
||||||
|
// Note: no await here, we want to keep this function synchronous
|
||||||
|
this.combineContacts(convoUuid, convoE164)
|
||||||
|
.then(() => {
|
||||||
|
// If the old conversation was currently displayed, we load the new one
|
||||||
|
window.Whisper.events.trigger('refreshConversation', {
|
||||||
|
newId: convoUuid.get('id'),
|
||||||
|
oldId: convoE164.get('id'),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
const errorText = error && error.stack ? error.stack : error;
|
||||||
|
window.log.warn(
|
||||||
|
`ensureContactIds error combining contacts: ${errorText}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return convoUuid.get('id');
|
||||||
|
}
|
||||||
|
async checkForConflicts() {
|
||||||
|
window.log.info('checkForConflicts: starting...');
|
||||||
|
const byUuid = Object.create(null);
|
||||||
|
const byE164 = Object.create(null);
|
||||||
|
|
||||||
|
// We iterate from the oldest conversations to the newest. This allows us, in a
|
||||||
|
// conflict case, to keep the one with activity the most recently.
|
||||||
|
const models = [...this._conversations.models.reverse()];
|
||||||
|
|
||||||
|
const max = models.length;
|
||||||
|
for (let i = 0; i < max; i += 1) {
|
||||||
|
const conversation = models[i];
|
||||||
|
const uuid = conversation.get('uuid');
|
||||||
|
const e164 = conversation.get('e164');
|
||||||
|
|
||||||
|
if (uuid) {
|
||||||
|
const existing = byUuid[uuid];
|
||||||
|
if (!existing) {
|
||||||
|
byUuid[uuid] = conversation;
|
||||||
|
} else {
|
||||||
|
window.log.warn(
|
||||||
|
`checkForConflicts: Found conflict with uuid ${uuid}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep the newer one if it has an e164, otherwise keep existing
|
||||||
|
if (conversation.get('e164')) {
|
||||||
|
// Keep new one
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await this.combineContacts(conversation, existing);
|
||||||
|
byUuid[uuid] = conversation;
|
||||||
|
} else {
|
||||||
|
// Keep existing - note that this applies if neither had an e164
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await this.combineContacts(existing, conversation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e164) {
|
||||||
|
const existing = byE164[e164];
|
||||||
|
if (!existing) {
|
||||||
|
byE164[e164] = conversation;
|
||||||
|
} else {
|
||||||
|
// If we have two contacts with the same e164 but different truthy UUIDs, then
|
||||||
|
// we'll delete the e164 on the older one
|
||||||
|
if (
|
||||||
|
conversation.get('uuid') &&
|
||||||
|
existing.get('uuid') &&
|
||||||
|
conversation.get('uuid') !== existing.get('uuid')
|
||||||
|
) {
|
||||||
|
window.log.warn(
|
||||||
|
`checkForConflicts: Found two matches on e164 ${e164} with different truthy UUIDs. Dropping e164 on older.`
|
||||||
|
);
|
||||||
|
|
||||||
|
existing.set({ e164: undefined });
|
||||||
|
updateConversation(existing.attributes);
|
||||||
|
|
||||||
|
byE164[e164] = conversation;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.warn(
|
||||||
|
`checkForConflicts: Found conflict with e164 ${e164}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep the newer one if it has a UUID, otherwise keep existing
|
||||||
|
if (conversation.get('uuid')) {
|
||||||
|
// Keep new one
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await this.combineContacts(conversation, existing);
|
||||||
|
byE164[e164] = conversation;
|
||||||
|
} else {
|
||||||
|
// Keep existing - note that this applies if neither had a UUID
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await this.combineContacts(existing, conversation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.info('checkForConflicts: complete!');
|
||||||
|
}
|
||||||
|
async combineContacts(
|
||||||
|
current: ConversationModelType,
|
||||||
|
obsolete: ConversationModelType
|
||||||
|
) {
|
||||||
|
const obsoleteId = obsolete.get('id');
|
||||||
|
const currentId = current.get('id');
|
||||||
|
window.log.warn('combineContacts: Combining two conversations', {
|
||||||
|
obsolete: obsoleteId,
|
||||||
|
current: currentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!current.get('profileKey') && obsolete.get('profileKey')) {
|
||||||
|
window.log.warn(
|
||||||
|
'combineContacts: Copying profile key from old to new contact'
|
||||||
|
);
|
||||||
|
|
||||||
|
await current.setProfileKey(obsolete.get('profileKey'));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.log.warn(
|
||||||
|
'combineContacts: Delete all sessions tied to old conversationId'
|
||||||
|
);
|
||||||
|
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
|
||||||
|
obsoleteId
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
deviceIds.map(async deviceId => {
|
||||||
|
await window.textsecure.storage.protocol.removeSession(
|
||||||
|
`${obsoleteId}.${deviceId}`
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
window.log.warn(
|
||||||
|
'combineContacts: Delete all identity information tied to old conversationId'
|
||||||
|
);
|
||||||
|
await window.textsecure.storage.protocol.removeIdentityKey(obsoleteId);
|
||||||
|
|
||||||
|
window.log.warn(
|
||||||
|
'combineContacts: Ensure that all V1 groups have new conversationId instead of old'
|
||||||
|
);
|
||||||
|
const groups = await this.getAllGroupsInvolvingId(obsoleteId);
|
||||||
|
groups.forEach(group => {
|
||||||
|
const members = group.get('members');
|
||||||
|
const withoutObsolete = without(members, obsoleteId);
|
||||||
|
const currentAdded = uniq([...withoutObsolete, currentId]);
|
||||||
|
|
||||||
|
group.set({
|
||||||
|
members: currentAdded,
|
||||||
|
});
|
||||||
|
updateConversation(group.attributes);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: we explicitly don't want to update V2 groups
|
||||||
|
|
||||||
|
window.log.warn(
|
||||||
|
'combineContacts: Delete the obsolete conversation from the database'
|
||||||
|
);
|
||||||
|
await removeConversation(obsoleteId, {
|
||||||
|
Conversation: window.Whisper.Conversation,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.log.warn('combineContacts: Update messages table');
|
||||||
|
await migrateConversationMessages(obsoleteId, currentId);
|
||||||
|
|
||||||
|
window.log.warn(
|
||||||
|
'combineContacts: Eliminate old conversation from ConversationController lookups'
|
||||||
|
);
|
||||||
|
this._conversations.remove(obsolete);
|
||||||
|
this.regenerateLookups();
|
||||||
|
|
||||||
|
window.log.warn('combineContacts: Complete!', {
|
||||||
|
obsolete: obsoleteId,
|
||||||
|
current: currentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
regenerateLookups() {
|
||||||
|
const models = [...this._conversations.models];
|
||||||
|
this.reset();
|
||||||
|
this._conversations.add(models);
|
||||||
|
|
||||||
|
// We force the initial fetch to be true
|
||||||
|
this._initialFetchComplete = true;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Given a groupId and optional additional initialization properties,
|
||||||
|
* ensures the existence of a group conversation and returns a string
|
||||||
|
* representing the local database ID of the group conversation.
|
||||||
|
*/
|
||||||
|
ensureGroup(groupId: string, additionalInitProps = {}) {
|
||||||
|
return this.getOrCreate(groupId, 'group', additionalInitProps).get('id');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Given certain metadata about a message (an identifier of who wrote the
|
||||||
|
* message and the sent_at timestamp of the message) returns the
|
||||||
|
* conversation the message belongs to OR null if a conversation isn't
|
||||||
|
* found.
|
||||||
|
*/
|
||||||
|
async getConversationForTargetMessage(
|
||||||
|
targetFromId: string,
|
||||||
|
targetTimestamp: number
|
||||||
|
) {
|
||||||
|
const messages = await getMessagesBySentAt(targetTimestamp, {
|
||||||
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
|
});
|
||||||
|
const targetMessage = messages.find(m => {
|
||||||
|
const contact = m.getContact();
|
||||||
|
|
||||||
|
if (!contact) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mcid = contact.get('id');
|
||||||
|
return mcid === targetFromId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (targetMessage) {
|
||||||
|
return targetMessage.getConversation();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
prepareForSend(
|
||||||
|
id: string,
|
||||||
|
options?: any
|
||||||
|
): {
|
||||||
|
wrap: (promise: Promise<any>) => Promise<void>;
|
||||||
|
sendOptions: SendOptionsType | undefined;
|
||||||
|
} {
|
||||||
|
// id is any valid conversation identifier
|
||||||
|
const conversation = this.get(id);
|
||||||
|
const sendOptions = conversation
|
||||||
|
? conversation.getSendOptions(options)
|
||||||
|
: undefined;
|
||||||
|
const wrap = conversation
|
||||||
|
? conversation.wrapSend.bind(conversation)
|
||||||
|
: async (promise: Promise<any>) => promise;
|
||||||
|
|
||||||
|
return { wrap, sendOptions };
|
||||||
|
}
|
||||||
|
async getAllGroupsInvolvingId(
|
||||||
|
conversationId: string
|
||||||
|
): Promise<Array<ConversationModelType>> {
|
||||||
|
const groups = await getAllGroupsInvolvingId(conversationId, {
|
||||||
|
ConversationCollection: window.Whisper.ConversationCollection,
|
||||||
|
});
|
||||||
|
return groups.map(group => this._conversations.add(group));
|
||||||
|
}
|
||||||
|
async loadPromise() {
|
||||||
|
return this._initialPromise;
|
||||||
|
}
|
||||||
|
reset() {
|
||||||
|
this._initialPromise = Promise.resolve();
|
||||||
|
this._initialFetchComplete = false;
|
||||||
|
this._conversations.reset([]);
|
||||||
|
}
|
||||||
|
async load() {
|
||||||
|
window.log.info('ConversationController: starting initial fetch');
|
||||||
|
|
||||||
|
if (this._conversations.length) {
|
||||||
|
throw new Error('ConversationController: Already loaded!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
const collection = await getAllConversations({
|
||||||
|
ConversationCollection: window.Whisper.ConversationCollection,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._conversations.add(collection.models);
|
||||||
|
|
||||||
|
this._initialFetchComplete = true;
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
this._conversations.map(async conversation => {
|
||||||
|
if (!conversation.get('lastMessage')) {
|
||||||
|
await conversation.updateLastMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case a too-large draft was saved to the database
|
||||||
|
const draft = conversation.get('draft');
|
||||||
|
if (draft && draft.length > MAX_MESSAGE_BODY_LENGTH) {
|
||||||
|
conversation.set({
|
||||||
|
draft: draft.slice(0, MAX_MESSAGE_BODY_LENGTH),
|
||||||
|
});
|
||||||
|
updateConversation(conversation.attributes);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
window.log.info('ConversationController: done with initial fetch');
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'ConversationController: initial fetch failed',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._initialPromise = load();
|
||||||
|
|
||||||
|
return this._initialPromise;
|
||||||
|
}
|
||||||
|
}
|
5
ts/model-types.d.ts
vendored
5
ts/model-types.d.ts
vendored
|
@ -1,7 +1,9 @@
|
||||||
import * as Backbone from 'backbone';
|
import * as Backbone from 'backbone';
|
||||||
|
|
||||||
import { ColorType, LocalizerType } from './types/Util';
|
import { ColorType, LocalizerType } from './types/Util';
|
||||||
import { SendOptionsType } from './textsecure/SendMessage';
|
|
||||||
import { ConversationType } from './state/ducks/conversations';
|
import { ConversationType } from './state/ducks/conversations';
|
||||||
|
import { CallingClass, CallHistoryDetailsType } from './services/calling';
|
||||||
|
import { SendOptionsType } from './textsecure/SendMessage';
|
||||||
import { SyncMessageClass } from './textsecure.d';
|
import { SyncMessageClass } from './textsecure.d';
|
||||||
|
|
||||||
interface ModelAttributesInterface {
|
interface ModelAttributesInterface {
|
||||||
|
@ -74,6 +76,7 @@ declare class ConversationModelType extends Backbone.Model<
|
||||||
cachedProps: ConversationType;
|
cachedProps: ConversationType;
|
||||||
initialPromise: Promise<any>;
|
initialPromise: Promise<any>;
|
||||||
|
|
||||||
|
addCallHistory(details: CallHistoryDetailsType): void;
|
||||||
applyMessageRequestResponse(
|
applyMessageRequestResponse(
|
||||||
response: number,
|
response: number,
|
||||||
options?: { fromSync: boolean }
|
options?: { fromSync: boolean }
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
CallDetailsType,
|
CallDetailsType,
|
||||||
} from '../state/ducks/calling';
|
} from '../state/ducks/calling';
|
||||||
import { CallingMessageClass, EnvelopeClass } from '../textsecure.d';
|
import { CallingMessageClass, EnvelopeClass } from '../textsecure.d';
|
||||||
import { ConversationType } from '../window.d';
|
import { ConversationModelType } from '../model-types.d';
|
||||||
import is from '@sindresorhus/is';
|
import is from '@sindresorhus/is';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -72,7 +72,7 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
async startOutgoingCall(
|
async startOutgoingCall(
|
||||||
conversation: ConversationType,
|
conversation: ConversationModelType,
|
||||||
isVideoCall: boolean
|
isVideoCall: boolean
|
||||||
) {
|
) {
|
||||||
if (!this.uxActions) {
|
if (!this.uxActions) {
|
||||||
|
@ -218,7 +218,14 @@ export class CallingClass {
|
||||||
message: CallingMessageClass
|
message: CallingMessageClass
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const conversation = window.ConversationController.get(remoteUserId);
|
const conversation = window.ConversationController.get(remoteUserId);
|
||||||
const sendOptions = conversation ? conversation.getSendOptions() : {};
|
const sendOptions = conversation
|
||||||
|
? conversation.getSendOptions()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (!window.textsecure.messaging) {
|
||||||
|
window.log.warn('handleOutgoingSignaling() returning false; offline');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.textsecure.messaging.sendCallingMessage(
|
await window.textsecure.messaging.sendCallingMessage(
|
||||||
|
@ -292,7 +299,7 @@ export class CallingClass {
|
||||||
this.addCallHistoryForAutoEndedIncomingCall(conversation, reason);
|
this.addCallHistoryForAutoEndedIncomingCall(conversation, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
private attachToCall(conversation: ConversationType, call: Call): void {
|
private attachToCall(conversation: ConversationModelType, call: Call): void {
|
||||||
const { uxActions } = this;
|
const { uxActions } = this;
|
||||||
if (!uxActions) {
|
if (!uxActions) {
|
||||||
return;
|
return;
|
||||||
|
@ -340,7 +347,7 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRemoteUserIdFromConversation(
|
private getRemoteUserIdFromConversation(
|
||||||
conversation: ConversationType
|
conversation: ConversationModelType
|
||||||
): UserId | undefined {
|
): UserId | undefined {
|
||||||
const recipients = conversation.getRecipients();
|
const recipients = conversation.getRecipients();
|
||||||
if (recipients.length !== 1) {
|
if (recipients.length !== 1) {
|
||||||
|
@ -366,8 +373,12 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getCallSettings(
|
private async getCallSettings(
|
||||||
conversation: ConversationType
|
conversation: ConversationModelType
|
||||||
): Promise<CallSettings> {
|
): Promise<CallSettings> {
|
||||||
|
if (!window.textsecure.messaging) {
|
||||||
|
throw new Error('getCallSettings: offline!');
|
||||||
|
}
|
||||||
|
|
||||||
const iceServerJson = await window.textsecure.messaging.server.getIceServers();
|
const iceServerJson = await window.textsecure.messaging.server.getIceServers();
|
||||||
|
|
||||||
const shouldRelayCalls = Boolean(await window.getAlwaysRelayCalls());
|
const shouldRelayCalls = Boolean(await window.getAlwaysRelayCalls());
|
||||||
|
@ -382,7 +393,7 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUxCallDetails(
|
private getUxCallDetails(
|
||||||
conversation: ConversationType,
|
conversation: ConversationModelType,
|
||||||
call: Call
|
call: Call
|
||||||
): CallDetailsType {
|
): CallDetailsType {
|
||||||
return {
|
return {
|
||||||
|
@ -398,7 +409,7 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
private addCallHistoryForEndedCall(
|
private addCallHistoryForEndedCall(
|
||||||
conversation: ConversationType,
|
conversation: ConversationModelType,
|
||||||
call: Call,
|
call: Call,
|
||||||
acceptedTime: number | undefined
|
acceptedTime: number | undefined
|
||||||
) {
|
) {
|
||||||
|
@ -429,7 +440,7 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
private addCallHistoryForFailedIncomingCall(
|
private addCallHistoryForFailedIncomingCall(
|
||||||
conversation: ConversationType,
|
conversation: ConversationModelType,
|
||||||
call: Call
|
call: Call
|
||||||
) {
|
) {
|
||||||
const callHistoryDetails: CallHistoryDetailsType = {
|
const callHistoryDetails: CallHistoryDetailsType = {
|
||||||
|
@ -444,7 +455,7 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
private addCallHistoryForAutoEndedIncomingCall(
|
private addCallHistoryForAutoEndedIncomingCall(
|
||||||
conversation: ConversationType,
|
conversation: ConversationModelType,
|
||||||
_reason: CallEndedReason
|
_reason: CallEndedReason
|
||||||
) {
|
) {
|
||||||
const callHistoryDetails: CallHistoryDetailsType = {
|
const callHistoryDetails: CallHistoryDetailsType = {
|
||||||
|
|
|
@ -19,12 +19,15 @@ import { arrayBufferToBase64, base64ToArrayBuffer } from '../Crypto';
|
||||||
import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message';
|
import { CURRENT_SCHEMA_VERSION } from '../../js/modules/types/message';
|
||||||
import { createBatcher } from '../util/batcher';
|
import { createBatcher } from '../util/batcher';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ConversationModelCollectionType,
|
||||||
|
ConversationModelType,
|
||||||
|
MessageModelCollectionType,
|
||||||
|
MessageModelType,
|
||||||
|
} from '../model-types.d';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AttachmentDownloadJobType,
|
AttachmentDownloadJobType,
|
||||||
BackboneConversationCollectionType,
|
|
||||||
BackboneConversationModelType,
|
|
||||||
BackboneMessageCollectionType,
|
|
||||||
BackboneMessageModelType,
|
|
||||||
ClientInterface,
|
ClientInterface,
|
||||||
ClientJobType,
|
ClientJobType,
|
||||||
ConversationType,
|
ConversationType,
|
||||||
|
@ -153,6 +156,7 @@ const dataInterface: ClientInterface = {
|
||||||
getOlderMessagesByConversation,
|
getOlderMessagesByConversation,
|
||||||
getNewerMessagesByConversation,
|
getNewerMessagesByConversation,
|
||||||
getMessageMetricsForConversation,
|
getMessageMetricsForConversation,
|
||||||
|
migrateConversationMessages,
|
||||||
|
|
||||||
getUnprocessedCount,
|
getUnprocessedCount,
|
||||||
getAllUnprocessed,
|
getAllUnprocessed,
|
||||||
|
@ -719,7 +723,7 @@ async function saveConversations(array: Array<ConversationType>) {
|
||||||
|
|
||||||
async function getConversationById(
|
async function getConversationById(
|
||||||
id: string,
|
id: string,
|
||||||
{ Conversation }: { Conversation: BackboneConversationModelType }
|
{ Conversation }: { Conversation: typeof ConversationModelType }
|
||||||
) {
|
) {
|
||||||
const data = await channels.getConversationById(id);
|
const data = await channels.getConversationById(id);
|
||||||
|
|
||||||
|
@ -749,7 +753,7 @@ async function updateConversations(array: Array<ConversationType>) {
|
||||||
|
|
||||||
async function removeConversation(
|
async function removeConversation(
|
||||||
id: string,
|
id: string,
|
||||||
{ Conversation }: { Conversation: BackboneConversationModelType }
|
{ Conversation }: { Conversation: typeof ConversationModelType }
|
||||||
) {
|
) {
|
||||||
const existing = await getConversationById(id, { Conversation });
|
const existing = await getConversationById(id, { Conversation });
|
||||||
|
|
||||||
|
@ -769,8 +773,8 @@ async function _removeConversations(ids: Array<string>) {
|
||||||
async function getAllConversations({
|
async function getAllConversations({
|
||||||
ConversationCollection,
|
ConversationCollection,
|
||||||
}: {
|
}: {
|
||||||
ConversationCollection: BackboneConversationCollectionType;
|
ConversationCollection: typeof ConversationModelCollectionType;
|
||||||
}) {
|
}): Promise<ConversationModelCollectionType> {
|
||||||
const conversations = await channels.getAllConversations();
|
const conversations = await channels.getAllConversations();
|
||||||
|
|
||||||
const collection = new ConversationCollection();
|
const collection = new ConversationCollection();
|
||||||
|
@ -788,7 +792,7 @@ async function getAllConversationIds() {
|
||||||
async function getAllPrivateConversations({
|
async function getAllPrivateConversations({
|
||||||
ConversationCollection,
|
ConversationCollection,
|
||||||
}: {
|
}: {
|
||||||
ConversationCollection: BackboneConversationCollectionType;
|
ConversationCollection: typeof ConversationModelCollectionType;
|
||||||
}) {
|
}) {
|
||||||
const conversations = await channels.getAllPrivateConversations();
|
const conversations = await channels.getAllPrivateConversations();
|
||||||
|
|
||||||
|
@ -803,7 +807,7 @@ async function getAllGroupsInvolvingId(
|
||||||
{
|
{
|
||||||
ConversationCollection,
|
ConversationCollection,
|
||||||
}: {
|
}: {
|
||||||
ConversationCollection: BackboneConversationCollectionType;
|
ConversationCollection: typeof ConversationModelCollectionType;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const conversations = await channels.getAllGroupsInvolvingId(id);
|
const conversations = await channels.getAllGroupsInvolvingId(id);
|
||||||
|
@ -861,7 +865,7 @@ async function saveMessage(
|
||||||
{
|
{
|
||||||
forceSave,
|
forceSave,
|
||||||
Message,
|
Message,
|
||||||
}: { forceSave?: boolean; Message: BackboneMessageModelType }
|
}: { forceSave?: boolean; Message: typeof MessageModelType }
|
||||||
) {
|
) {
|
||||||
const id = await channels.saveMessage(_cleanData(data), { forceSave });
|
const id = await channels.saveMessage(_cleanData(data), { forceSave });
|
||||||
Message.updateTimers();
|
Message.updateTimers();
|
||||||
|
@ -878,7 +882,7 @@ async function saveMessages(
|
||||||
|
|
||||||
async function removeMessage(
|
async function removeMessage(
|
||||||
id: string,
|
id: string,
|
||||||
{ Message }: { Message: BackboneMessageModelType }
|
{ Message }: { Message: typeof MessageModelType }
|
||||||
) {
|
) {
|
||||||
const message = await getMessageById(id, { Message });
|
const message = await getMessageById(id, { Message });
|
||||||
|
|
||||||
|
@ -897,7 +901,7 @@ async function _removeMessages(ids: Array<string>) {
|
||||||
|
|
||||||
async function getMessageById(
|
async function getMessageById(
|
||||||
id: string,
|
id: string,
|
||||||
{ Message }: { Message: BackboneMessageModelType }
|
{ Message }: { Message: typeof MessageModelType }
|
||||||
) {
|
) {
|
||||||
const message = await channels.getMessageById(id);
|
const message = await channels.getMessageById(id);
|
||||||
if (!message) {
|
if (!message) {
|
||||||
|
@ -911,7 +915,7 @@ async function getMessageById(
|
||||||
async function _getAllMessages({
|
async function _getAllMessages({
|
||||||
MessageCollection,
|
MessageCollection,
|
||||||
}: {
|
}: {
|
||||||
MessageCollection: BackboneMessageCollectionType;
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
}) {
|
}) {
|
||||||
const messages = await channels._getAllMessages();
|
const messages = await channels._getAllMessages();
|
||||||
|
|
||||||
|
@ -936,7 +940,7 @@ async function getMessageBySender(
|
||||||
sourceDevice: string;
|
sourceDevice: string;
|
||||||
sent_at: number;
|
sent_at: number;
|
||||||
},
|
},
|
||||||
{ Message }: { Message: BackboneMessageModelType }
|
{ Message }: { Message: typeof MessageModelType }
|
||||||
) {
|
) {
|
||||||
const messages = await channels.getMessageBySender({
|
const messages = await channels.getMessageBySender({
|
||||||
source,
|
source,
|
||||||
|
@ -953,7 +957,9 @@ async function getMessageBySender(
|
||||||
|
|
||||||
async function getUnreadByConversation(
|
async function getUnreadByConversation(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
|
{
|
||||||
|
MessageCollection,
|
||||||
|
}: { MessageCollection: typeof MessageModelCollectionType }
|
||||||
) {
|
) {
|
||||||
const messages = await channels.getUnreadByConversation(conversationId);
|
const messages = await channels.getUnreadByConversation(conversationId);
|
||||||
|
|
||||||
|
@ -975,7 +981,7 @@ async function getOlderMessagesByConversation(
|
||||||
limit?: number;
|
limit?: number;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
MessageCollection: BackboneMessageCollectionType;
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const messages = await channels.getOlderMessagesByConversation(
|
const messages = await channels.getOlderMessagesByConversation(
|
||||||
|
@ -998,7 +1004,7 @@ async function getNewerMessagesByConversation(
|
||||||
}: {
|
}: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
MessageCollection: BackboneMessageCollectionType;
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const messages = await channels.getNewerMessagesByConversation(
|
const messages = await channels.getNewerMessagesByConversation(
|
||||||
|
@ -1018,10 +1024,18 @@ async function getMessageMetricsForConversation(conversationId: string) {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
async function migrateConversationMessages(
|
||||||
|
obsoleteId: string,
|
||||||
|
currentId: string
|
||||||
|
) {
|
||||||
|
await channels.migrateConversationMessages(obsoleteId, currentId);
|
||||||
|
}
|
||||||
|
|
||||||
async function removeAllMessagesInConversation(
|
async function removeAllMessagesInConversation(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
|
{
|
||||||
|
MessageCollection,
|
||||||
|
}: { MessageCollection: typeof MessageModelCollectionType }
|
||||||
) {
|
) {
|
||||||
let messages;
|
let messages;
|
||||||
do {
|
do {
|
||||||
|
@ -1036,12 +1050,12 @@ async function removeAllMessagesInConversation(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ids = messages.map((message: BackboneMessageModelType) => message.id);
|
const ids = messages.map((message: MessageModelType) => message.id);
|
||||||
|
|
||||||
// Note: It's very important that these models are fully hydrated because
|
// Note: It's very important that these models are fully hydrated because
|
||||||
// we need to delete all associated on-disk files along with the database delete.
|
// we need to delete all associated on-disk files along with the database delete.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
messages.map((message: BackboneMessageModelType) => message.cleanup())
|
messages.map(async (message: MessageModelType) => message.cleanup())
|
||||||
);
|
);
|
||||||
|
|
||||||
await channels.removeMessage(ids);
|
await channels.removeMessage(ids);
|
||||||
|
@ -1050,7 +1064,9 @@ async function removeAllMessagesInConversation(
|
||||||
|
|
||||||
async function getMessagesBySentAt(
|
async function getMessagesBySentAt(
|
||||||
sentAt: number,
|
sentAt: number,
|
||||||
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
|
{
|
||||||
|
MessageCollection,
|
||||||
|
}: { MessageCollection: typeof MessageModelCollectionType }
|
||||||
) {
|
) {
|
||||||
const messages = await channels.getMessagesBySentAt(sentAt);
|
const messages = await channels.getMessagesBySentAt(sentAt);
|
||||||
|
|
||||||
|
@ -1060,7 +1076,7 @@ async function getMessagesBySentAt(
|
||||||
async function getExpiredMessages({
|
async function getExpiredMessages({
|
||||||
MessageCollection,
|
MessageCollection,
|
||||||
}: {
|
}: {
|
||||||
MessageCollection: BackboneMessageCollectionType;
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
}) {
|
}) {
|
||||||
const messages = await channels.getExpiredMessages();
|
const messages = await channels.getExpiredMessages();
|
||||||
|
|
||||||
|
@ -1070,7 +1086,7 @@ async function getExpiredMessages({
|
||||||
async function getOutgoingWithoutExpiresAt({
|
async function getOutgoingWithoutExpiresAt({
|
||||||
MessageCollection,
|
MessageCollection,
|
||||||
}: {
|
}: {
|
||||||
MessageCollection: BackboneMessageCollectionType;
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
}) {
|
}) {
|
||||||
const messages = await channels.getOutgoingWithoutExpiresAt();
|
const messages = await channels.getOutgoingWithoutExpiresAt();
|
||||||
|
|
||||||
|
@ -1080,7 +1096,7 @@ async function getOutgoingWithoutExpiresAt({
|
||||||
async function getNextExpiringMessage({
|
async function getNextExpiringMessage({
|
||||||
Message,
|
Message,
|
||||||
}: {
|
}: {
|
||||||
Message: BackboneMessageModelType;
|
Message: typeof MessageModelType;
|
||||||
}) {
|
}) {
|
||||||
const message = await channels.getNextExpiringMessage();
|
const message = await channels.getNextExpiringMessage();
|
||||||
|
|
||||||
|
@ -1094,7 +1110,7 @@ async function getNextExpiringMessage({
|
||||||
async function getNextTapToViewMessageToAgeOut({
|
async function getNextTapToViewMessageToAgeOut({
|
||||||
Message,
|
Message,
|
||||||
}: {
|
}: {
|
||||||
Message: BackboneMessageModelType;
|
Message: typeof MessageModelType;
|
||||||
}) {
|
}) {
|
||||||
const message = await channels.getNextTapToViewMessageToAgeOut();
|
const message = await channels.getNextTapToViewMessageToAgeOut();
|
||||||
if (!message) {
|
if (!message) {
|
||||||
|
@ -1106,7 +1122,7 @@ async function getNextTapToViewMessageToAgeOut({
|
||||||
async function getTapToViewMessagesNeedingErase({
|
async function getTapToViewMessagesNeedingErase({
|
||||||
MessageCollection,
|
MessageCollection,
|
||||||
}: {
|
}: {
|
||||||
MessageCollection: BackboneMessageCollectionType;
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
}) {
|
}) {
|
||||||
const messages = await channels.getTapToViewMessagesNeedingErase();
|
const messages = await channels.getTapToViewMessagesNeedingErase();
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,12 @@ export type StickerPackType = any;
|
||||||
export type StickerType = any;
|
export type StickerType = any;
|
||||||
export type UnprocessedType = any;
|
export type UnprocessedType = any;
|
||||||
|
|
||||||
export type BackboneConversationModelType = any;
|
import {
|
||||||
export type BackboneConversationCollectionType = any;
|
ConversationModelCollectionType,
|
||||||
export type BackboneMessageModelType = any;
|
ConversationModelType,
|
||||||
export type BackboneMessageCollectionType = any;
|
MessageModelCollectionType,
|
||||||
|
MessageModelType,
|
||||||
|
} from '../model-types.d';
|
||||||
|
|
||||||
export interface DataInterface {
|
export interface DataInterface {
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
|
@ -94,6 +96,10 @@ export interface DataInterface {
|
||||||
getMessageMetricsForConversation: (
|
getMessageMetricsForConversation: (
|
||||||
conversationId: string
|
conversationId: string
|
||||||
) => Promise<ConverationMetricsType>;
|
) => Promise<ConverationMetricsType>;
|
||||||
|
migrateConversationMessages: (
|
||||||
|
obsoleteId: string,
|
||||||
|
currentId: string
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
getUnprocessedCount: () => Promise<number>;
|
getUnprocessedCount: () => Promise<number>;
|
||||||
getAllUnprocessed: () => Promise<Array<UnprocessedType>>;
|
getAllUnprocessed: () => Promise<Array<UnprocessedType>>;
|
||||||
|
@ -242,33 +248,33 @@ export type ClientInterface = DataInterface & {
|
||||||
getAllConversations: ({
|
getAllConversations: ({
|
||||||
ConversationCollection,
|
ConversationCollection,
|
||||||
}: {
|
}: {
|
||||||
ConversationCollection: BackboneConversationCollectionType;
|
ConversationCollection: typeof ConversationModelCollectionType;
|
||||||
}) => Promise<Array<ConversationType>>;
|
}) => Promise<ConversationModelCollectionType>;
|
||||||
getAllGroupsInvolvingId: (
|
getAllGroupsInvolvingId: (
|
||||||
id: string,
|
id: string,
|
||||||
{
|
{
|
||||||
ConversationCollection,
|
ConversationCollection,
|
||||||
}: {
|
}: {
|
||||||
ConversationCollection: BackboneConversationCollectionType;
|
ConversationCollection: typeof ConversationModelCollectionType;
|
||||||
}
|
}
|
||||||
) => Promise<Array<ConversationType>>;
|
) => Promise<ConversationModelCollectionType>;
|
||||||
getAllPrivateConversations: ({
|
getAllPrivateConversations: ({
|
||||||
ConversationCollection,
|
ConversationCollection,
|
||||||
}: {
|
}: {
|
||||||
ConversationCollection: BackboneConversationCollectionType;
|
ConversationCollection: typeof ConversationModelCollectionType;
|
||||||
}) => Promise<Array<ConversationType>>;
|
}) => Promise<ConversationModelCollectionType>;
|
||||||
getConversationById: (
|
getConversationById: (
|
||||||
id: string,
|
id: string,
|
||||||
{ Conversation }: { Conversation: BackboneConversationModelType }
|
{ Conversation }: { Conversation: typeof ConversationModelType }
|
||||||
) => Promise<ConversationType>;
|
) => Promise<ConversationModelType>;
|
||||||
getExpiredMessages: ({
|
getExpiredMessages: ({
|
||||||
MessageCollection,
|
MessageCollection,
|
||||||
}: {
|
}: {
|
||||||
MessageCollection: BackboneMessageCollectionType;
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
}) => Promise<Array<MessageType>>;
|
}) => Promise<MessageModelCollectionType>;
|
||||||
getMessageById: (
|
getMessageById: (
|
||||||
id: string,
|
id: string,
|
||||||
{ Message }: { Message: BackboneMessageModelType }
|
{ Message }: { Message: typeof MessageModelType }
|
||||||
) => Promise<MessageType | undefined>;
|
) => Promise<MessageType | undefined>;
|
||||||
getMessageBySender: (
|
getMessageBySender: (
|
||||||
options: {
|
options: {
|
||||||
|
@ -277,63 +283,67 @@ export type ClientInterface = DataInterface & {
|
||||||
sourceDevice: string;
|
sourceDevice: string;
|
||||||
sent_at: number;
|
sent_at: number;
|
||||||
},
|
},
|
||||||
{ Message }: { Message: BackboneMessageModelType }
|
{ Message }: { Message: typeof MessageModelType }
|
||||||
) => Promise<Array<MessageType>>;
|
) => Promise<MessageModelType | null>;
|
||||||
getMessagesBySentAt: (
|
getMessagesBySentAt: (
|
||||||
sentAt: number,
|
sentAt: number,
|
||||||
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
|
{
|
||||||
) => Promise<Array<MessageType>>;
|
MessageCollection,
|
||||||
|
}: { MessageCollection: typeof MessageModelCollectionType }
|
||||||
|
) => Promise<MessageModelCollectionType>;
|
||||||
getOlderMessagesByConversation: (
|
getOlderMessagesByConversation: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
options: {
|
options: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
MessageCollection: BackboneMessageCollectionType;
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
}
|
}
|
||||||
) => Promise<Array<MessageTypeUnhydrated>>;
|
) => Promise<MessageModelCollectionType>;
|
||||||
getNewerMessagesByConversation: (
|
getNewerMessagesByConversation: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
options: {
|
options: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
MessageCollection: BackboneMessageCollectionType;
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
}
|
}
|
||||||
) => Promise<Array<MessageTypeUnhydrated>>;
|
) => Promise<MessageModelCollectionType>;
|
||||||
getNextExpiringMessage: ({
|
getNextExpiringMessage: ({
|
||||||
Message,
|
Message,
|
||||||
}: {
|
}: {
|
||||||
Message: BackboneMessageModelType;
|
Message: typeof MessageModelType;
|
||||||
}) => Promise<MessageType>;
|
}) => Promise<MessageModelType | null>;
|
||||||
getNextTapToViewMessageToAgeOut: ({
|
getNextTapToViewMessageToAgeOut: ({
|
||||||
Message,
|
Message,
|
||||||
}: {
|
}: {
|
||||||
Message: BackboneMessageModelType;
|
Message: typeof MessageModelType;
|
||||||
}) => Promise<MessageType>;
|
}) => Promise<MessageModelType | null>;
|
||||||
getOutgoingWithoutExpiresAt: ({
|
getOutgoingWithoutExpiresAt: ({
|
||||||
MessageCollection,
|
MessageCollection,
|
||||||
}: {
|
}: {
|
||||||
MessageCollection: BackboneMessageCollectionType;
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
}) => Promise<Array<MessageType>>;
|
}) => Promise<MessageModelCollectionType>;
|
||||||
getTapToViewMessagesNeedingErase: ({
|
getTapToViewMessagesNeedingErase: ({
|
||||||
MessageCollection,
|
MessageCollection,
|
||||||
}: {
|
}: {
|
||||||
MessageCollection: BackboneMessageCollectionType;
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
}) => Promise<Array<MessageType>>;
|
}) => Promise<MessageModelCollectionType>;
|
||||||
getUnreadByConversation: (
|
getUnreadByConversation: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
|
{
|
||||||
) => Promise<Array<MessageType>>;
|
MessageCollection,
|
||||||
|
}: { MessageCollection: typeof MessageModelCollectionType }
|
||||||
|
) => Promise<MessageModelCollectionType>;
|
||||||
removeConversation: (
|
removeConversation: (
|
||||||
id: string,
|
id: string,
|
||||||
{ Conversation }: { Conversation: BackboneConversationModelType }
|
{ Conversation }: { Conversation: typeof ConversationModelType }
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
removeMessage: (
|
removeMessage: (
|
||||||
id: string,
|
id: string,
|
||||||
{ Message }: { Message: BackboneMessageModelType }
|
{ Message }: { Message: typeof MessageModelType }
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
saveMessage: (
|
saveMessage: (
|
||||||
data: MessageType,
|
data: MessageType,
|
||||||
options: { forceSave?: boolean; Message: BackboneMessageModelType }
|
options: { forceSave?: boolean; Message: typeof MessageModelType }
|
||||||
) => Promise<number>;
|
) => Promise<number>;
|
||||||
updateConversation: (data: ConversationType) => void;
|
updateConversation: (data: ConversationType) => void;
|
||||||
|
|
||||||
|
@ -342,15 +352,17 @@ export type ClientInterface = DataInterface & {
|
||||||
_getAllMessages: ({
|
_getAllMessages: ({
|
||||||
MessageCollection,
|
MessageCollection,
|
||||||
}: {
|
}: {
|
||||||
MessageCollection: BackboneMessageCollectionType;
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
}) => Promise<Array<MessageType>>;
|
}) => Promise<MessageModelCollectionType>;
|
||||||
|
|
||||||
// Client-side only
|
// Client-side only
|
||||||
|
|
||||||
shutdown: () => Promise<void>;
|
shutdown: () => Promise<void>;
|
||||||
removeAllMessagesInConversation: (
|
removeAllMessagesInConversation: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
|
{
|
||||||
|
MessageCollection,
|
||||||
|
}: { MessageCollection: typeof MessageModelCollectionType }
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
removeOtherData: () => Promise<void>;
|
removeOtherData: () => Promise<void>;
|
||||||
cleanupOrphanedAttachments: () => Promise<void>;
|
cleanupOrphanedAttachments: () => Promise<void>;
|
||||||
|
|
|
@ -131,6 +131,7 @@ const dataInterface: ServerInterface = {
|
||||||
getOlderMessagesByConversation,
|
getOlderMessagesByConversation,
|
||||||
getNewerMessagesByConversation,
|
getNewerMessagesByConversation,
|
||||||
getMessageMetricsForConversation,
|
getMessageMetricsForConversation,
|
||||||
|
migrateConversationMessages,
|
||||||
|
|
||||||
getUnprocessedCount,
|
getUnprocessedCount,
|
||||||
getAllUnprocessed,
|
getAllUnprocessed,
|
||||||
|
@ -2797,6 +2798,25 @@ async function getMessageMetricsForConversation(conversationId: string) {
|
||||||
}
|
}
|
||||||
getMessageMetricsForConversation.needsSerial = true;
|
getMessageMetricsForConversation.needsSerial = true;
|
||||||
|
|
||||||
|
async function migrateConversationMessages(
|
||||||
|
obsoleteId: string,
|
||||||
|
currentId: string
|
||||||
|
) {
|
||||||
|
const db = getInstance();
|
||||||
|
|
||||||
|
await db.run(
|
||||||
|
`UPDATE messages SET
|
||||||
|
conversationId = $currentId,
|
||||||
|
json = json_set(json, '$.conversationId', $currentId)
|
||||||
|
WHERE conversationId = $obsoleteId;`,
|
||||||
|
{
|
||||||
|
$obsoleteId: obsoleteId,
|
||||||
|
$currentId: currentId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
migrateConversationMessages.needsSerial = true;
|
||||||
|
|
||||||
async function getMessagesBySentAt(sentAt: number) {
|
async function getMessagesBySentAt(sentAt: number) {
|
||||||
const db = getInstance();
|
const db = getInstance();
|
||||||
const rows = await db.all(
|
const rows = await db.all(
|
||||||
|
|
29
ts/textsecure.d.ts
vendored
29
ts/textsecure.d.ts
vendored
|
@ -10,6 +10,8 @@ import EventTarget from './textsecure/EventTarget';
|
||||||
import { ByteBufferClass } from './window.d';
|
import { ByteBufferClass } from './window.d';
|
||||||
import { SendOptionsType } from './textsecure/SendMessage';
|
import { SendOptionsType } from './textsecure/SendMessage';
|
||||||
import { WebAPIType } from './textsecure/WebAPI';
|
import { WebAPIType } from './textsecure/WebAPI';
|
||||||
|
import utils from './textsecure/Helpers';
|
||||||
|
import SendMessage from './textsecure/SendMessage';
|
||||||
|
|
||||||
type AttachmentType = any;
|
type AttachmentType = any;
|
||||||
|
|
||||||
|
@ -79,31 +81,9 @@ export type TextSecureType = {
|
||||||
attachment: AttachmentPointerClass
|
attachment: AttachmentPointerClass
|
||||||
) => Promise<DownloadAttachmentType>;
|
) => Promise<DownloadAttachmentType>;
|
||||||
};
|
};
|
||||||
messaging: {
|
messaging?: SendMessage;
|
||||||
getStorageCredentials: () => Promise<StorageServiceCredentials>;
|
|
||||||
getStorageManifest: (
|
|
||||||
options: StorageServiceCallOptionsType
|
|
||||||
) => Promise<ArrayBuffer>;
|
|
||||||
getStorageRecords: (
|
|
||||||
data: ArrayBuffer,
|
|
||||||
options: StorageServiceCallOptionsType
|
|
||||||
) => Promise<ArrayBuffer>;
|
|
||||||
sendStickerPackSync: (
|
|
||||||
operations: Array<{
|
|
||||||
packId: string;
|
|
||||||
packKey: string;
|
|
||||||
installed: boolean;
|
|
||||||
}>,
|
|
||||||
options: Object
|
|
||||||
) => Promise<void>;
|
|
||||||
sendCallingMessage: (
|
|
||||||
recipientId: string,
|
|
||||||
callingMessage: CallingMessageClass,
|
|
||||||
sendOptions: SendOptionsType
|
|
||||||
) => Promise<void>;
|
|
||||||
server: WebAPIType;
|
|
||||||
};
|
|
||||||
protobuf: ProtobufCollectionType;
|
protobuf: ProtobufCollectionType;
|
||||||
|
utils: typeof utils;
|
||||||
|
|
||||||
EventTarget: typeof EventTarget;
|
EventTarget: typeof EventTarget;
|
||||||
MessageReceiver: typeof MessageReceiver;
|
MessageReceiver: typeof MessageReceiver;
|
||||||
|
@ -150,6 +130,7 @@ export type StorageProtocolType = StorageType & {
|
||||||
verifiedStatus: number,
|
verifiedStatus: number,
|
||||||
publicKey: ArrayBuffer
|
publicKey: ArrayBuffer
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
|
removeIdentityKey: (identifier: string) => Promise<void>;
|
||||||
saveIdentityWithAttributes: (
|
saveIdentityWithAttributes: (
|
||||||
number: string,
|
number: string,
|
||||||
options: IdentityKeyRecord
|
options: IdentityKeyRecord
|
||||||
|
|
|
@ -696,6 +696,7 @@ export default class AccountManager extends EventTarget {
|
||||||
const conversationId = window.ConversationController.ensureContactIds({
|
const conversationId = window.ConversationController.ensureContactIds({
|
||||||
e164: number,
|
e164: number,
|
||||||
uuid,
|
uuid,
|
||||||
|
highTrust: true,
|
||||||
});
|
});
|
||||||
if (!conversationId) {
|
if (!conversationId) {
|
||||||
throw new Error('registrationDone: no conversationId!');
|
throw new Error('registrationDone: no conversationId!');
|
||||||
|
|
|
@ -1058,6 +1058,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
destination,
|
destination,
|
||||||
|
destinationUuid,
|
||||||
timestamp,
|
timestamp,
|
||||||
message: msg,
|
message: msg,
|
||||||
expirationStartTimestamp,
|
expirationStartTimestamp,
|
||||||
|
@ -1083,12 +1084,13 @@ class MessageReceiverInner extends EventTarget {
|
||||||
msg.flags &&
|
msg.flags &&
|
||||||
msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION
|
msg.flags & window.textsecure.protobuf.DataMessage.Flags.END_SESSION
|
||||||
) {
|
) {
|
||||||
if (!destination) {
|
const identifier = destination || destinationUuid;
|
||||||
|
if (!identifier) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'MessageReceiver.handleSentMessage: Cannot end session with falsey destination'
|
'MessageReceiver.handleSentMessage: Cannot end session with falsey destination'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
p = this.handleEndSession(destination);
|
p = this.handleEndSession(identifier);
|
||||||
}
|
}
|
||||||
return p.then(async () =>
|
return p.then(async () =>
|
||||||
this.processDecrypted(envelope, msg).then(message => {
|
this.processDecrypted(envelope, msg).then(message => {
|
||||||
|
@ -1120,6 +1122,7 @@ class MessageReceiverInner extends EventTarget {
|
||||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||||
ev.data = {
|
ev.data = {
|
||||||
destination,
|
destination,
|
||||||
|
destinationUuid,
|
||||||
timestamp: timestamp.toNumber(),
|
timestamp: timestamp.toNumber(),
|
||||||
serverTimestamp: envelope.serverTimestamp,
|
serverTimestamp: envelope.serverTimestamp,
|
||||||
device: envelope.sourceDevice,
|
device: envelope.sourceDevice,
|
||||||
|
@ -1303,7 +1306,8 @@ class MessageReceiverInner extends EventTarget {
|
||||||
ev.timestamp = envelope.timestamp.toNumber();
|
ev.timestamp = envelope.timestamp.toNumber();
|
||||||
ev.read = {
|
ev.read = {
|
||||||
timestamp: receiptMessage.timestamp[i].toNumber(),
|
timestamp: receiptMessage.timestamp[i].toNumber(),
|
||||||
reader: envelope.source || envelope.sourceUuid,
|
source: envelope.source,
|
||||||
|
sourceUuid: envelope.sourceUuid,
|
||||||
};
|
};
|
||||||
results.push(this.dispatchAndWait(ev));
|
results.push(this.dispatchAndWait(ev));
|
||||||
}
|
}
|
||||||
|
|
|
@ -925,7 +925,7 @@ export default class MessageSender {
|
||||||
async sendCallingMessage(
|
async sendCallingMessage(
|
||||||
recipientId: string,
|
recipientId: string,
|
||||||
callingMessage: CallingMessageClass,
|
callingMessage: CallingMessageClass,
|
||||||
sendOptions: SendOptionsType
|
sendOptions?: SendOptionsType
|
||||||
) {
|
) {
|
||||||
const recipients = [recipientId];
|
const recipients = [recipientId];
|
||||||
const finalTimestamp = Date.now();
|
const finalTimestamp = Date.now();
|
||||||
|
@ -1001,7 +1001,11 @@ export default class MessageSender {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
async syncReadMessages(
|
async syncReadMessages(
|
||||||
reads: Array<{ sender: string; timestamp: number }>,
|
reads: Array<{
|
||||||
|
senderUuid?: string;
|
||||||
|
senderE164?: string;
|
||||||
|
timestamp: number;
|
||||||
|
}>,
|
||||||
options?: SendOptionsType
|
options?: SendOptionsType
|
||||||
) {
|
) {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
|
@ -1013,7 +1017,8 @@ export default class MessageSender {
|
||||||
for (let i = 0; i < reads.length; i += 1) {
|
for (let i = 0; i < reads.length; i += 1) {
|
||||||
const read = new window.textsecure.protobuf.SyncMessage.Read();
|
const read = new window.textsecure.protobuf.SyncMessage.Read();
|
||||||
read.timestamp = reads[i].timestamp;
|
read.timestamp = reads[i].timestamp;
|
||||||
read.sender = reads[i].sender;
|
read.sender = reads[i].senderE164;
|
||||||
|
read.senderUuid = reads[i].senderUuid;
|
||||||
|
|
||||||
syncMessage.read.push(read);
|
syncMessage.read.push(read);
|
||||||
}
|
}
|
||||||
|
@ -1352,20 +1357,20 @@ export default class MessageSender {
|
||||||
proto.flags = window.textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
proto.flags = window.textsecure.protobuf.DataMessage.Flags.END_SESSION;
|
||||||
proto.timestamp = timestamp;
|
proto.timestamp = timestamp;
|
||||||
|
|
||||||
const identifier = e164 || uuid;
|
const identifier = uuid || e164;
|
||||||
|
|
||||||
const logError = (prefix: string) => (error: Error) => {
|
const logError = (prefix: string) => (error: Error) => {
|
||||||
window.log.error(prefix, error && error.stack ? error.stack : error);
|
window.log.error(prefix, error && error.stack ? error.stack : error);
|
||||||
throw error;
|
throw error;
|
||||||
};
|
};
|
||||||
const deleteAllSessions = async (targetNumber: string) =>
|
const deleteAllSessions = async (targetIdentifier: string) =>
|
||||||
window.textsecure.storage.protocol
|
window.textsecure.storage.protocol
|
||||||
.getDeviceIds(targetNumber)
|
.getDeviceIds(targetIdentifier)
|
||||||
.then(async deviceIds =>
|
.then(async deviceIds =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
deviceIds.map(async deviceId => {
|
deviceIds.map(async deviceId => {
|
||||||
const address = new window.libsignal.SignalProtocolAddress(
|
const address = new window.libsignal.SignalProtocolAddress(
|
||||||
targetNumber,
|
targetIdentifier,
|
||||||
deviceId
|
deviceId
|
||||||
);
|
);
|
||||||
window.log.info('deleting sessions for', address.toString());
|
window.log.info('deleting sessions for', address.toString());
|
||||||
|
@ -1401,7 +1406,7 @@ export default class MessageSender {
|
||||||
const myNumber = window.textsecure.storage.user.getNumber();
|
const myNumber = window.textsecure.storage.user.getNumber();
|
||||||
const myUuid = window.textsecure.storage.user.getUuid();
|
const myUuid = window.textsecure.storage.user.getUuid();
|
||||||
// We already sent the reset session to our other devices in the code above!
|
// We already sent the reset session to our other devices in the code above!
|
||||||
if (e164 === myNumber || uuid === myUuid) {
|
if ((e164 && e164 === myNumber) || (uuid && uuid === myUuid)) {
|
||||||
return sendToContactPromise;
|
return sendToContactPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { Agent } from 'https';
|
||||||
import is from '@sindresorhus/is';
|
import is from '@sindresorhus/is';
|
||||||
import { redactPackId } from '../../js/modules/stickers';
|
import { redactPackId } from '../../js/modules/stickers';
|
||||||
import { getRandomValue } from '../Crypto';
|
import { getRandomValue } from '../Crypto';
|
||||||
|
import MessageSender from './SendMessage';
|
||||||
|
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import { v4 as getGuid } from 'uuid';
|
import { v4 as getGuid } from 'uuid';
|
||||||
|
@ -13,7 +14,6 @@ import { v4 as getGuid } from 'uuid';
|
||||||
import {
|
import {
|
||||||
StorageServiceCallOptionsType,
|
StorageServiceCallOptionsType,
|
||||||
StorageServiceCredentials,
|
StorageServiceCredentials,
|
||||||
TextSecureType,
|
|
||||||
} from '../textsecure.d';
|
} from '../textsecure.d';
|
||||||
|
|
||||||
// tslint:disable no-bitwise
|
// tslint:disable no-bitwise
|
||||||
|
@ -589,9 +589,9 @@ export type WebAPIType = {
|
||||||
getSenderCertificate: (withUuid?: boolean) => Promise<any>;
|
getSenderCertificate: (withUuid?: boolean) => Promise<any>;
|
||||||
getSticker: (packId: string, stickerId: string) => Promise<any>;
|
getSticker: (packId: string, stickerId: string) => Promise<any>;
|
||||||
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
|
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
|
||||||
getStorageCredentials: TextSecureType['messaging']['getStorageCredentials'];
|
getStorageCredentials: MessageSender['getStorageCredentials'];
|
||||||
getStorageManifest: TextSecureType['messaging']['getStorageManifest'];
|
getStorageManifest: MessageSender['getStorageManifest'];
|
||||||
getStorageRecords: TextSecureType['messaging']['getStorageRecords'];
|
getStorageRecords: MessageSender['getStorageRecords'];
|
||||||
makeProxiedRequest: (
|
makeProxiedRequest: (
|
||||||
targetUrl: string,
|
targetUrl: string,
|
||||||
options?: ProxiedRequestOptionsType
|
options?: ProxiedRequestOptionsType
|
||||||
|
|
|
@ -160,22 +160,6 @@
|
||||||
"updated": "2018-09-19T21:59:32.770Z",
|
"updated": "2018-09-19T21:59:32.770Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "jQuery-load(",
|
|
||||||
"path": "js/conversation_controller.js",
|
|
||||||
"line": " async load() {",
|
|
||||||
"lineNumber": 306,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-06-19T18:29:40.067Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-load(",
|
|
||||||
"path": "js/conversation_controller.js",
|
|
||||||
"line": " this._initialPromise = load();",
|
|
||||||
"lineNumber": 348,
|
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-06-19T18:29:40.067Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/debug_log_start.js",
|
"path": "js/debug_log_start.js",
|
||||||
|
@ -223,7 +207,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/models/conversations.js",
|
"path": "js/models/conversations.js",
|
||||||
"line": " await wrap(",
|
"line": " await wrap(",
|
||||||
"lineNumber": 652,
|
"lineNumber": 663,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-06-09T20:26:46.515Z"
|
"updated": "2020-06-09T20:26:46.515Z"
|
||||||
},
|
},
|
||||||
|
@ -504,7 +488,7 @@
|
||||||
"rule": "jQuery-prependTo(",
|
"rule": "jQuery-prependTo(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.appLoadingScreen.$el.prependTo(this.el);",
|
"line": " this.appLoadingScreen.$el.prependTo(this.el);",
|
||||||
"lineNumber": 90,
|
"lineNumber": 97,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-10-21T22:30:15.622Z",
|
"updated": "2019-10-21T22:30:15.622Z",
|
||||||
"reasonDetail": "Known DOM elements"
|
"reasonDetail": "Known DOM elements"
|
||||||
|
@ -513,7 +497,7 @@
|
||||||
"rule": "jQuery-appendTo(",
|
"rule": "jQuery-appendTo(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " toast.$el.appendTo(this.$el);",
|
"line": " toast.$el.appendTo(this.$el);",
|
||||||
"lineNumber": 99,
|
"lineNumber": 106,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-28T17:42:35.329Z",
|
"updated": "2020-05-28T17:42:35.329Z",
|
||||||
"reasonDetail": "Known DOM elements"
|
"reasonDetail": "Known DOM elements"
|
||||||
|
@ -522,7 +506,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);",
|
"line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);",
|
||||||
"lineNumber": 121,
|
"lineNumber": 128,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-28T17:42:35.329Z",
|
"updated": "2020-05-28T17:42:35.329Z",
|
||||||
"reasonDetail": "<optional>"
|
"reasonDetail": "<optional>"
|
||||||
|
@ -531,7 +515,7 @@
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);",
|
"line": " this.$('.call-manager-placeholder').append(this.callManagerView.el);",
|
||||||
"lineNumber": 121,
|
"lineNumber": 128,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-28T17:42:35.329Z",
|
"updated": "2020-05-28T17:42:35.329Z",
|
||||||
"reasonDetail": "<optional>"
|
"reasonDetail": "<optional>"
|
||||||
|
@ -540,7 +524,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||||
"lineNumber": 132,
|
"lineNumber": 139,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-28T17:42:35.329Z",
|
"updated": "2020-05-28T17:42:35.329Z",
|
||||||
"reasonDetail": "Known DOM elements"
|
"reasonDetail": "Known DOM elements"
|
||||||
|
@ -549,7 +533,7 @@
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||||
"lineNumber": 132,
|
"lineNumber": 139,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-28T17:42:35.329Z",
|
"updated": "2020-05-28T17:42:35.329Z",
|
||||||
"reasonDetail": "Known DOM elements"
|
"reasonDetail": "Known DOM elements"
|
||||||
|
@ -558,7 +542,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
|
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
|
||||||
"lineNumber": 183,
|
"lineNumber": 190,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-28T17:42:35.329Z",
|
"updated": "2020-05-28T17:42:35.329Z",
|
||||||
"reasonDetail": "Known DOM elements"
|
"reasonDetail": "Known DOM elements"
|
||||||
|
@ -567,7 +551,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('#header, .gutter').addClass('inactive');",
|
"line": " this.$('#header, .gutter').addClass('inactive');",
|
||||||
"lineNumber": 187,
|
"lineNumber": 194,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-28T17:42:35.329Z",
|
"updated": "2020-05-28T17:42:35.329Z",
|
||||||
"reasonDetail": "Hardcoded selector"
|
"reasonDetail": "Hardcoded selector"
|
||||||
|
@ -576,7 +560,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.conversation-stack').addClass('inactive');",
|
"line": " this.$('.conversation-stack').addClass('inactive');",
|
||||||
"lineNumber": 191,
|
"lineNumber": 198,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-28T17:42:35.329Z",
|
"updated": "2020-05-28T17:42:35.329Z",
|
||||||
"reasonDetail": "Hardcoded selector"
|
"reasonDetail": "Hardcoded selector"
|
||||||
|
@ -585,7 +569,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.conversation:first .menu').trigger('close');",
|
"line": " this.$('.conversation:first .menu').trigger('close');",
|
||||||
"lineNumber": 193,
|
"lineNumber": 200,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-28T17:42:35.329Z",
|
"updated": "2020-05-28T17:42:35.329Z",
|
||||||
"reasonDetail": "Hardcoded selector"
|
"reasonDetail": "Hardcoded selector"
|
||||||
|
@ -594,7 +578,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
||||||
"lineNumber": 213,
|
"lineNumber": 220,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-29T18:29:18.234Z",
|
"updated": "2020-05-29T18:29:18.234Z",
|
||||||
"reasonDetail": "Known DOM elements"
|
"reasonDetail": "Known DOM elements"
|
||||||
|
@ -603,7 +587,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
||||||
"lineNumber": 216,
|
"lineNumber": 223,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-05-29T18:29:18.234Z",
|
"updated": "2020-05-29T18:29:18.234Z",
|
||||||
"reasonDetail": "Hardcoded selector"
|
"reasonDetail": "Hardcoded selector"
|
||||||
|
|
|
@ -60,6 +60,8 @@ const excludedFiles = [
|
||||||
'^ts/Crypto.ts',
|
'^ts/Crypto.ts',
|
||||||
'^ts/textsecure/MessageReceiver.js',
|
'^ts/textsecure/MessageReceiver.js',
|
||||||
'^ts/textsecure/MessageReceiver.ts',
|
'^ts/textsecure/MessageReceiver.ts',
|
||||||
|
'^ts/ConversationController.js',
|
||||||
|
'^ts/ConversationController.ts',
|
||||||
|
|
||||||
// Generated files
|
// Generated files
|
||||||
'^js/components.js',
|
'^js/components.js',
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
ManifestRecordClass,
|
ManifestRecordClass,
|
||||||
StorageItemClass,
|
StorageItemClass,
|
||||||
} from '../textsecure.d';
|
} from '../textsecure.d';
|
||||||
import { ConversationType } from '../window.d';
|
import { ConversationModelType } from '../model-types.d';
|
||||||
|
|
||||||
function fromRecordVerified(verified: number): number {
|
function fromRecordVerified(verified: number): number {
|
||||||
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
|
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
|
||||||
|
@ -35,6 +35,11 @@ function fromRecordVerified(verified: number): number {
|
||||||
|
|
||||||
async function fetchManifest(manifestVersion: string) {
|
async function fetchManifest(manifestVersion: string) {
|
||||||
window.log.info('storageService.fetchManifest');
|
window.log.info('storageService.fetchManifest');
|
||||||
|
|
||||||
|
if (!window.textsecure.messaging) {
|
||||||
|
throw new Error('fetchManifest: We are offline!');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const credentials = await window.textsecure.messaging.getStorageCredentials();
|
const credentials = await window.textsecure.messaging.getStorageCredentials();
|
||||||
window.storage.put('storageCredentials', credentials);
|
window.storage.put('storageCredentials', credentials);
|
||||||
|
@ -286,6 +291,10 @@ async function processManifest(
|
||||||
const storageKeyBase64 = window.storage.get('storageKey');
|
const storageKeyBase64 = window.storage.get('storageKey');
|
||||||
const storageKey = base64ToArrayBuffer(storageKeyBase64);
|
const storageKey = base64ToArrayBuffer(storageKeyBase64);
|
||||||
|
|
||||||
|
if (!window.textsecure.messaging) {
|
||||||
|
throw new Error('processManifest: We are offline!');
|
||||||
|
}
|
||||||
|
|
||||||
const remoteKeysTypeMap = new Map();
|
const remoteKeysTypeMap = new Map();
|
||||||
manifest.keys.forEach(key => {
|
manifest.keys.forEach(key => {
|
||||||
remoteKeysTypeMap.set(
|
remoteKeysTypeMap.set(
|
||||||
|
@ -296,7 +305,7 @@ async function processManifest(
|
||||||
|
|
||||||
const localKeys = window
|
const localKeys = window
|
||||||
.getConversations()
|
.getConversations()
|
||||||
.map((conversation: ConversationType) => conversation.get('storageID'))
|
.map((conversation: ConversationModelType) => conversation.get('storageID'))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
window.log.info(
|
window.log.info(
|
||||||
`storageService.processManifest localKeys.length ${localKeys.length}`
|
`storageService.processManifest localKeys.length ${localKeys.length}`
|
||||||
|
|
98
ts/window.d.ts
vendored
98
ts/window.d.ts
vendored
|
@ -1,6 +1,14 @@
|
||||||
// Captures the globals put in place by preload.js, background.js and others
|
// Captures the globals put in place by preload.js, background.js and others
|
||||||
|
|
||||||
|
import * as Backbone from 'backbone';
|
||||||
|
import * as Underscore from 'underscore';
|
||||||
import { Ref } from 'react';
|
import { Ref } from 'react';
|
||||||
|
import {
|
||||||
|
ConversationModelCollectionType,
|
||||||
|
ConversationModelType,
|
||||||
|
MessageModelCollectionType,
|
||||||
|
MessageModelType,
|
||||||
|
} from './model-types.d';
|
||||||
import {
|
import {
|
||||||
LibSignalType,
|
LibSignalType,
|
||||||
SignalProtocolAddressClass,
|
SignalProtocolAddressClass,
|
||||||
|
@ -11,6 +19,7 @@ import { WebAPIConnectType } from './textsecure/WebAPI';
|
||||||
import { CallingClass, CallHistoryDetailsType } from './services/calling';
|
import { CallingClass, CallHistoryDetailsType } from './services/calling';
|
||||||
import * as Crypto from './Crypto';
|
import * as Crypto from './Crypto';
|
||||||
import { ColorType, LocalizerType } from './types/Util';
|
import { ColorType, LocalizerType } from './types/Util';
|
||||||
|
import { ConversationController } from './ConversationController';
|
||||||
import { SendOptionsType } from './textsecure/SendMessage';
|
import { SendOptionsType } from './textsecure/SendMessage';
|
||||||
import Data from './sql/Client';
|
import Data from './sql/Client';
|
||||||
|
|
||||||
|
@ -19,18 +28,22 @@ type TaskResultType = any;
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
dcodeIO: DCodeIOType;
|
dcodeIO: DCodeIOType;
|
||||||
getConversations: () => ConversationControllerType;
|
|
||||||
getExpiration: () => string;
|
|
||||||
getEnvironment: () => string;
|
|
||||||
getSocketStatus: () => number;
|
|
||||||
getAlwaysRelayCalls: () => Promise<boolean>;
|
getAlwaysRelayCalls: () => Promise<boolean>;
|
||||||
getIncomingCallNotification: () => Promise<boolean>;
|
|
||||||
getCallRingtoneNotification: () => Promise<boolean>;
|
getCallRingtoneNotification: () => Promise<boolean>;
|
||||||
getCallSystemNotification: () => Promise<boolean>;
|
getCallSystemNotification: () => Promise<boolean>;
|
||||||
getMediaPermissions: () => Promise<boolean>;
|
getConversations: () => ConversationModelCollectionType;
|
||||||
|
getEnvironment: () => string;
|
||||||
|
getExpiration: () => string;
|
||||||
|
getGuid: () => string;
|
||||||
|
getInboxCollection: () => ConversationModelCollectionType;
|
||||||
|
getIncomingCallNotification: () => Promise<boolean>;
|
||||||
getMediaCameraPermissions: () => Promise<boolean>;
|
getMediaCameraPermissions: () => Promise<boolean>;
|
||||||
|
getMediaPermissions: () => Promise<boolean>;
|
||||||
|
getSocketStatus: () => number;
|
||||||
|
getTitle: () => string;
|
||||||
showCallingPermissionsPopup: (forCamera: boolean) => Promise<void>;
|
showCallingPermissionsPopup: (forCamera: boolean) => Promise<void>;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
isValidGuid: (maybeGuid: string) => boolean;
|
||||||
libphonenumber: {
|
libphonenumber: {
|
||||||
util: {
|
util: {
|
||||||
getRegionCodeForNumber: (number: string) => string;
|
getRegionCodeForNumber: (number: string) => string;
|
||||||
|
@ -46,6 +59,7 @@ declare global {
|
||||||
platform: string;
|
platform: string;
|
||||||
restart: () => void;
|
restart: () => void;
|
||||||
showWindow: () => void;
|
showWindow: () => void;
|
||||||
|
setBadgeCount: (count: number) => void;
|
||||||
storage: {
|
storage: {
|
||||||
put: (key: string, value: any) => void;
|
put: (key: string, value: any) => void;
|
||||||
remove: (key: string) => void;
|
remove: (key: string) => void;
|
||||||
|
@ -55,7 +69,9 @@ declare global {
|
||||||
removeBlockedNumber: (number: string) => void;
|
removeBlockedNumber: (number: string) => void;
|
||||||
};
|
};
|
||||||
textsecure: TextSecureType;
|
textsecure: TextSecureType;
|
||||||
|
updateTrayIcon: (count: number) => void;
|
||||||
|
|
||||||
|
Backbone: typeof Backbone;
|
||||||
Signal: {
|
Signal: {
|
||||||
Crypto: typeof Crypto;
|
Crypto: typeof Crypto;
|
||||||
Data: typeof Data;
|
Data: typeof Data;
|
||||||
|
@ -69,7 +85,7 @@ declare global {
|
||||||
calling: CallingClass;
|
calling: CallingClass;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
ConversationController: ConversationControllerType;
|
ConversationController: ConversationController;
|
||||||
WebAPI: WebAPIConnectType;
|
WebAPI: WebAPIConnectType;
|
||||||
Whisper: WhisperType;
|
Whisper: WhisperType;
|
||||||
|
|
||||||
|
@ -82,68 +98,6 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConversationAttributes = {
|
|
||||||
e164?: string | null;
|
|
||||||
isArchived?: boolean;
|
|
||||||
profileFamilyName?: string | null;
|
|
||||||
profileKey?: string | null;
|
|
||||||
profileName?: string | null;
|
|
||||||
profileSharing?: boolean;
|
|
||||||
name?: string;
|
|
||||||
storageID?: string;
|
|
||||||
uuid?: string | null;
|
|
||||||
verified?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ConversationType = {
|
|
||||||
attributes: ConversationAttributes;
|
|
||||||
fromRecordVerified: (
|
|
||||||
verified: ContactRecordIdentityState
|
|
||||||
) => ContactRecordIdentityState;
|
|
||||||
set: (props: Partial<ConversationAttributes>) => void;
|
|
||||||
updateE164: (e164?: string) => void;
|
|
||||||
updateUuid: (uuid?: string) => void;
|
|
||||||
id: string;
|
|
||||||
get: (key: string) => any;
|
|
||||||
getAvatarPath(): string | undefined;
|
|
||||||
getColor(): ColorType | undefined;
|
|
||||||
getName(): string | undefined;
|
|
||||||
getNumber(): string;
|
|
||||||
getProfiles(): Promise<Array<Promise<void>>>;
|
|
||||||
getProfileName(): string | undefined;
|
|
||||||
getRecipients: () => Array<string>;
|
|
||||||
getSendOptions(): SendOptionsType;
|
|
||||||
getTitle(): string;
|
|
||||||
isVerified(): boolean;
|
|
||||||
safeGetVerified(): Promise<number>;
|
|
||||||
getIsAddedByContact(): boolean;
|
|
||||||
addCallHistory(details: CallHistoryDetailsType): void;
|
|
||||||
toggleVerified(): Promise<TaskResultType>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ConversationControllerType = {
|
|
||||||
getOrCreateAndWait: (
|
|
||||||
identifier: string,
|
|
||||||
type: 'private' | 'group'
|
|
||||||
) => Promise<ConversationType>;
|
|
||||||
getOrCreate: (
|
|
||||||
identifier: string,
|
|
||||||
type: 'private' | 'group'
|
|
||||||
) => ConversationType;
|
|
||||||
getConversationId: (identifier: string) => string | null;
|
|
||||||
ensureContactIds: (o: { e164?: string; uuid?: string }) => string;
|
|
||||||
getOurConversationId: () => string | null;
|
|
||||||
prepareForSend: (
|
|
||||||
id: string,
|
|
||||||
options: Object
|
|
||||||
) => {
|
|
||||||
wrap: (promise: Promise<any>) => Promise<void>;
|
|
||||||
sendOptions: Object;
|
|
||||||
};
|
|
||||||
get: (identifier: string) => null | ConversationType;
|
|
||||||
map: (mapFn: (conversation: ConversationType) => any) => any;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DCodeIOType = {
|
export type DCodeIOType = {
|
||||||
ByteBuffer: typeof ByteBufferClass;
|
ByteBuffer: typeof ByteBufferClass;
|
||||||
Long: {
|
Long: {
|
||||||
|
@ -212,7 +166,7 @@ export type LoggerType = (...args: Array<any>) => void;
|
||||||
|
|
||||||
export type WhisperType = {
|
export type WhisperType = {
|
||||||
events: {
|
events: {
|
||||||
trigger: (name: string, param1: any, param2: any) => void;
|
trigger: (name: string, param1: any, param2?: any) => void;
|
||||||
};
|
};
|
||||||
Database: {
|
Database: {
|
||||||
open: () => Promise<IDBDatabase>;
|
open: () => Promise<IDBDatabase>;
|
||||||
|
@ -222,4 +176,8 @@ export type WhisperType = {
|
||||||
reject: Function
|
reject: Function
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
ConversationCollection: typeof ConversationModelCollectionType;
|
||||||
|
Conversation: typeof ConversationModelType;
|
||||||
|
MessageCollection: typeof MessageModelCollectionType;
|
||||||
|
Message: typeof MessageModelType;
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
// Preferred by Prettier:
|
// Preferred by Prettier:
|
||||||
"arrow-parens": [true, "ban-single-arg-parens"],
|
"arrow-parens": [true, "ban-single-arg-parens"],
|
||||||
|
|
||||||
|
// Breaks when we use .extend() to create a Backbone subclass
|
||||||
|
"no-invalid-this": false,
|
||||||
|
|
||||||
"import-spacing": false,
|
"import-spacing": false,
|
||||||
"indent": [true, "spaces", 2],
|
"indent": [true, "spaces", 2],
|
||||||
"interface-name": [true, "never-prefix"],
|
"interface-name": [true, "never-prefix"],
|
||||||
|
|
14
yarn.lock
14
yarn.lock
|
@ -2232,20 +2232,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/jquery@*":
|
"@types/jquery@*", "@types/jquery@3.5.0":
|
||||||
version "3.5.0"
|
version "3.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.0.tgz#ccb7dfd317d02d4227dd3803c75297d0c10dad68"
|
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.0.tgz#ccb7dfd317d02d4227dd3803c75297d0c10dad68"
|
||||||
integrity sha512-C7qQUjpMWDUNYQRTXsP5nbYYwCwwgy84yPgoTT7fPN69NH92wLeCtFaMsWeolJD1AF/6uQw3pYt62rzv83sMmw==
|
integrity sha512-C7qQUjpMWDUNYQRTXsP5nbYYwCwwgy84yPgoTT7fPN69NH92wLeCtFaMsWeolJD1AF/6uQw3pYt62rzv83sMmw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/sizzle" "*"
|
"@types/sizzle" "*"
|
||||||
|
|
||||||
"@types/jquery@3.3.29":
|
|
||||||
version "3.3.29"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.29.tgz#680a2219ce3c9250483722fccf5570d1e2d08abd"
|
|
||||||
integrity sha512-FhJvBninYD36v3k6c+bVk1DSZwh7B5Dpb/Pyk3HKVsiohn0nhbefZZ+3JXbWQhFyt0MxSl2jRDdGQPHeOHFXrQ==
|
|
||||||
dependencies:
|
|
||||||
"@types/sizzle" "*"
|
|
||||||
|
|
||||||
"@types/js-yaml@3.12.0":
|
"@types/js-yaml@3.12.0":
|
||||||
version "3.12.0"
|
version "3.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.0.tgz#3494ce97358e2675e24e97a747ec23478eeaf8b6"
|
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.0.tgz#3494ce97358e2675e24e97a747ec23478eeaf8b6"
|
||||||
|
@ -2523,6 +2516,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.14.tgz#a2a831c72a12deddaef26028d16a5aa48aadbee0"
|
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.14.tgz#a2a831c72a12deddaef26028d16a5aa48aadbee0"
|
||||||
integrity sha512-VE20ZYf38nmOU1lU0wpQBWcGPlskfKK8uU8AN1UIz5PjxT2YM7HTF0iUA85iGJnbQ3tZweqIfQqmLgLMtP27YQ==
|
integrity sha512-VE20ZYf38nmOU1lU0wpQBWcGPlskfKK8uU8AN1UIz5PjxT2YM7HTF0iUA85iGJnbQ3tZweqIfQqmLgLMtP27YQ==
|
||||||
|
|
||||||
|
"@types/underscore@1.10.3":
|
||||||
|
version "1.10.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.3.tgz#927ff2b2e516444587fc80fb7845bb5c1806aad2"
|
||||||
|
integrity sha512-WgNbx0H2QO4ccIk2R1aWkteETuPxSa9OYKXoYujBgc0R4u+d2PWsb9MPpP77H+xqwbCXT+wuEBQ/6fl6s4C0OA==
|
||||||
|
|
||||||
"@types/uuid@3.4.4":
|
"@types/uuid@3.4.4":
|
||||||
version "3.4.4"
|
version "3.4.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5"
|
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5"
|
||||||
|
|
Loading…
Reference in a new issue