Message Requests
This commit is contained in:
parent
4d4b7a26a5
commit
83574eb067
60 changed files with 2566 additions and 216 deletions
|
@ -1479,6 +1479,34 @@
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.Signal.RemoteConfig.initRemoteConfig();
|
||||
|
||||
// Maybe refresh remote configuration when we become active
|
||||
window.registerForActive(async () => {
|
||||
await window.Signal.RemoteConfig.maybeRefreshRemoteConfig();
|
||||
});
|
||||
|
||||
// Listen for changes to the `desktop.messageRequests` remote configuration flag
|
||||
const removeMessageRequestListener = window.Signal.RemoteConfig.onChange(
|
||||
'desktop.messageRequests',
|
||||
({ enabled }) => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conversations = window.getConversations();
|
||||
conversations.forEach(conversation => {
|
||||
conversation.set({
|
||||
messageCountBeforeMessageRequests:
|
||||
conversation.get('messageCount') || 0,
|
||||
});
|
||||
window.Signal.Data.updateConversation(conversation.attributes);
|
||||
});
|
||||
|
||||
removeMessageRequestListener();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
window.getSyncRequest = () =>
|
||||
|
@ -1629,6 +1657,8 @@
|
|||
addQueuedEventListener('typing', onTyping);
|
||||
addQueuedEventListener('sticker-pack', onStickerPack);
|
||||
addQueuedEventListener('viewSync', onViewSync);
|
||||
addQueuedEventListener('messageRequestResponse', onMessageRequestResponse);
|
||||
addQueuedEventListener('profileKeyUpdate', onProfileKeyUpdate);
|
||||
|
||||
window.Signal.AttachmentDownloads.start({
|
||||
getMessageReceiver: () => messageReceiver,
|
||||
|
@ -2258,7 +2288,10 @@
|
|||
|
||||
const result = ConversationController.getOrCreate(
|
||||
messageDescriptor.id,
|
||||
messageDescriptor.type
|
||||
messageDescriptor.type,
|
||||
messageDescriptor.type === 'group'
|
||||
? { addedBy: message.getContact().get('id') }
|
||||
: undefined
|
||||
);
|
||||
|
||||
if (messageDescriptor.type === 'private') {
|
||||
|
@ -2306,6 +2339,41 @@
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async function onProfileKeyUpdate({ data, confirm }) {
|
||||
const conversation = ConversationController.get(
|
||||
data.source || data.sourceUuid
|
||||
);
|
||||
|
||||
if (!conversation) {
|
||||
window.log.error(
|
||||
'onProfileKeyUpdate: could not find conversation',
|
||||
data.source,
|
||||
data.sourceUuid
|
||||
);
|
||||
confirm();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.profileKey) {
|
||||
window.log.error(
|
||||
'onProfileKeyUpdate: missing profileKey',
|
||||
data.profileKey
|
||||
);
|
||||
confirm();
|
||||
return;
|
||||
}
|
||||
|
||||
window.log.info(
|
||||
'onProfileKeyUpdate: updating profileKey',
|
||||
data.source,
|
||||
data.sourceUuid
|
||||
);
|
||||
|
||||
await conversation.setProfileKey(data.profileKey);
|
||||
|
||||
confirm();
|
||||
}
|
||||
|
||||
async function handleMessageSentProfileUpdate({
|
||||
data,
|
||||
confirm,
|
||||
|
@ -2318,7 +2386,7 @@
|
|||
type
|
||||
);
|
||||
|
||||
conversation.set({ profileSharing: true });
|
||||
conversation.enableProfileSharing();
|
||||
window.Signal.Data.updateConversation(conversation.attributes);
|
||||
|
||||
// Then we update our own profileKey if it's different from what we have
|
||||
|
@ -2635,6 +2703,25 @@
|
|||
Whisper.ViewSyncs.onSync(sync);
|
||||
}
|
||||
|
||||
async function onMessageRequestResponse(ev) {
|
||||
ev.confirm();
|
||||
|
||||
const { threadE164, threadUuid, groupId, messageRequestResponseType } = ev;
|
||||
|
||||
const args = {
|
||||
threadE164,
|
||||
threadUuid,
|
||||
groupId,
|
||||
type: messageRequestResponseType,
|
||||
};
|
||||
|
||||
window.log.info('message request response', args);
|
||||
|
||||
const sync = Whisper.MessageRequests.add(args);
|
||||
|
||||
Whisper.MessageRequests.onResponse(sync);
|
||||
}
|
||||
|
||||
function onReadReceipt(ev) {
|
||||
const readAt = ev.timestamp;
|
||||
const { timestamp } = ev.read;
|
||||
|
|
91
js/message_requests.js
Normal file
91
js/message_requests.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
/* global
|
||||
Backbone,
|
||||
Whisper,
|
||||
ConversationController,
|
||||
*/
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
Whisper.MessageRequests = new (Backbone.Collection.extend({
|
||||
forConversation(conversation) {
|
||||
if (conversation.get('e164')) {
|
||||
const syncByE164 = this.findWhere({
|
||||
e164: conversation.get('e164'),
|
||||
});
|
||||
if (syncByE164) {
|
||||
window.log.info(
|
||||
`Found early message request response for E164 ${conversation.get(
|
||||
'e164'
|
||||
)}`
|
||||
);
|
||||
this.remove(syncByE164);
|
||||
return syncByE164;
|
||||
}
|
||||
}
|
||||
|
||||
if (conversation.get('uuid')) {
|
||||
const syncByUuid = this.findWhere({
|
||||
uuid: conversation.get('uuid'),
|
||||
});
|
||||
if (syncByUuid) {
|
||||
window.log.info(
|
||||
`Found early message request response for UUID ${conversation.get(
|
||||
'uuid'
|
||||
)}`
|
||||
);
|
||||
this.remove(syncByUuid);
|
||||
return syncByUuid;
|
||||
}
|
||||
}
|
||||
|
||||
if (conversation.get('groupId')) {
|
||||
const syncByGroupId = this.findWhere({
|
||||
uuid: conversation.get('groupId'),
|
||||
});
|
||||
if (syncByGroupId) {
|
||||
window.log.info(
|
||||
`Found early message request response for GROUP ID ${conversation.get(
|
||||
'groupId'
|
||||
)}`
|
||||
);
|
||||
this.remove(syncByGroupId);
|
||||
return syncByGroupId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
async onResponse(sync) {
|
||||
try {
|
||||
const threadE164 = sync.get('threadE164');
|
||||
const threadUuid = sync.get('threadUuid');
|
||||
const groupId = sync.get('groupId');
|
||||
const identifier = threadE164 || threadUuid || groupId;
|
||||
const conversation = ConversationController.get(identifier);
|
||||
|
||||
if (!conversation) {
|
||||
window.log(
|
||||
`Received message request response for unknown conversation: ${identifier}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
conversation.applyMessageRequestResponse(sync.get('type'), {
|
||||
fromSync: true,
|
||||
});
|
||||
|
||||
this.remove(sync);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'MessageRequests.onResponse error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
},
|
||||
}))();
|
||||
})();
|
|
@ -1,4 +1,4 @@
|
|||
/* global storage, _ */
|
||||
/* global storage, _, ConversationController */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
|
@ -79,4 +79,48 @@
|
|||
window.log.info(`removing group(${groupId} from blocked list`);
|
||||
storage.put(BLOCKED_GROUPS_ID, _.without(groupIds, groupId));
|
||||
};
|
||||
|
||||
/**
|
||||
* Optimistically adds a conversation to our local block list.
|
||||
* @param {string} id
|
||||
*/
|
||||
storage.blockIdentifier = id => {
|
||||
const conv = ConversationController.get(id);
|
||||
if (conv) {
|
||||
const uuid = conv.get('uuid');
|
||||
if (uuid) {
|
||||
storage.addBlockedUuid(uuid);
|
||||
}
|
||||
const e164 = conv.get('e164');
|
||||
if (e164) {
|
||||
storage.addBlockedNumber(e164);
|
||||
}
|
||||
const groupId = conv.get('groupId');
|
||||
if (groupId) {
|
||||
storage.addBlockedGroup(groupId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Optimistically removes a conversation from our local block list.
|
||||
* @param {string} id
|
||||
*/
|
||||
storage.unblockIdentifier = id => {
|
||||
const conv = ConversationController.get(id);
|
||||
if (conv) {
|
||||
const uuid = conv.get('uuid');
|
||||
if (uuid) {
|
||||
storage.removeBlockedUuid(uuid);
|
||||
}
|
||||
const e164 = conv.get('e164');
|
||||
if (e164) {
|
||||
storage.removeBlockedNumber(e164);
|
||||
}
|
||||
const groupId = conv.get('groupId');
|
||||
if (groupId) {
|
||||
storage.removeBlockedGroup(groupId);
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
libsignal,
|
||||
storage,
|
||||
textsecure,
|
||||
Whisper
|
||||
Whisper,
|
||||
Signal
|
||||
*/
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
@ -60,6 +61,8 @@
|
|||
return {
|
||||
unreadCount: 0,
|
||||
verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT,
|
||||
messageCount: 0,
|
||||
sentMessageCount: 0,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -97,6 +100,8 @@
|
|||
this.ourNumber = textsecure.storage.user.getNumber();
|
||||
this.ourUuid = textsecure.storage.user.getUuid();
|
||||
this.verifiedEnum = textsecure.storage.protocol.VerifiedStatus;
|
||||
this.messageRequestEnum =
|
||||
textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
// This may be overridden by ConversationController.getOrCreate, and signify
|
||||
// our first save to the database. Or first fetch from the database.
|
||||
|
@ -164,6 +169,52 @@
|
|||
);
|
||||
},
|
||||
|
||||
isBlocked() {
|
||||
const uuid = this.get('uuid');
|
||||
if (uuid) {
|
||||
return window.storage.isUuidBlocked(uuid);
|
||||
}
|
||||
|
||||
const e164 = this.get('e164');
|
||||
if (e164) {
|
||||
return window.storage.isBlocked(e164);
|
||||
}
|
||||
|
||||
const groupId = this.get('groupId');
|
||||
if (groupId) {
|
||||
return window.storage.isGroupBlocked(groupId);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
unblock() {
|
||||
const uuid = this.get('uuid');
|
||||
if (uuid) {
|
||||
return window.storage.removeBlockedUuid(uuid);
|
||||
}
|
||||
|
||||
const e164 = this.get('e164');
|
||||
if (e164) {
|
||||
return window.storage.removeBlockedNumber(e164);
|
||||
}
|
||||
|
||||
const groupId = this.get('groupId');
|
||||
if (groupId) {
|
||||
return window.storage.removeBlockedGroup(groupId);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
enableProfileSharing() {
|
||||
this.set({ profileSharing: true });
|
||||
},
|
||||
|
||||
disableProfileSharing() {
|
||||
this.set({ profileSharing: false });
|
||||
},
|
||||
|
||||
hasDraft() {
|
||||
const draftAttachments = this.get('draftAttachments') || [];
|
||||
return (
|
||||
|
@ -320,6 +371,10 @@
|
|||
this.messageCollection.remove(id);
|
||||
existing.trigger('expired');
|
||||
existing.cleanup();
|
||||
|
||||
// An expired message only counts as decrementing the message count, not
|
||||
// the sent message count
|
||||
this.decrementMessageCount();
|
||||
};
|
||||
|
||||
// If a fetch is in progress, then we need to wait until that's complete to
|
||||
|
@ -390,11 +445,15 @@
|
|||
const shouldShowDraft =
|
||||
this.hasDraft() && draftTimestamp && draftTimestamp >= timestamp;
|
||||
const inboxPosition = this.get('inbox_position');
|
||||
const messageRequestsEnabled = Signal.RemoteConfig.isEnabled(
|
||||
'desktop.messageRequests'
|
||||
);
|
||||
|
||||
const result = {
|
||||
id: this.id,
|
||||
|
||||
isArchived: this.get('isArchived'),
|
||||
isBlocked: this.isBlocked(),
|
||||
activeAt: this.get('active_at'),
|
||||
avatarPath: this.getAvatarPath(),
|
||||
color,
|
||||
|
@ -414,11 +473,17 @@
|
|||
draftText,
|
||||
|
||||
phoneNumber: this.getNumber(),
|
||||
membersCount: this.isPrivate()
|
||||
? undefined
|
||||
: (this.get('members') || []).length,
|
||||
lastMessage: {
|
||||
status: this.get('lastMessageStatus'),
|
||||
text: this.get('lastMessage'),
|
||||
deletedForEveryone: this.get('lastMessageDeletedForEveryone'),
|
||||
},
|
||||
|
||||
acceptedMessageRequest: this.getAccepted(),
|
||||
messageRequestsEnabled,
|
||||
};
|
||||
|
||||
return result;
|
||||
|
@ -449,6 +514,143 @@
|
|||
}
|
||||
},
|
||||
|
||||
incrementMessageCount() {
|
||||
this.set({
|
||||
messageCount: (this.get('messageCount') || 0) + 1,
|
||||
});
|
||||
window.Signal.Data.updateConversation(this.attributes);
|
||||
},
|
||||
|
||||
decrementMessageCount() {
|
||||
this.set({
|
||||
messageCount: Math.max((this.get('messageCount') || 0) - 1, 0),
|
||||
});
|
||||
window.Signal.Data.updateConversation(this.attributes);
|
||||
},
|
||||
|
||||
incrementSentMessageCount() {
|
||||
this.set({
|
||||
messageCount: (this.get('messageCount') || 0) + 1,
|
||||
sentMessageCount: (this.get('sentMessageCount') || 0) + 1,
|
||||
});
|
||||
window.Signal.Data.updateConversation(this.attributes);
|
||||
},
|
||||
|
||||
decrementSentMessageCount() {
|
||||
this.set({
|
||||
messageCount: Math.max((this.get('messageCount') || 0) - 1, 0),
|
||||
sentMessageCount: Math.max((this.get('sentMessageCount') || 0) - 1, 0),
|
||||
});
|
||||
window.Signal.Data.updateConversation(this.attributes);
|
||||
},
|
||||
|
||||
/**
|
||||
* This function is called when a message request is accepted in order to
|
||||
* handle sending read receipts and download any pending attachments.
|
||||
*/
|
||||
async handleReadAndDownloadAttachments() {
|
||||
let messages;
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
messages = await window.Signal.Data.getOlderMessagesByConversation(
|
||||
this.get('id'),
|
||||
{
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
limit: 100,
|
||||
receivedAt: messages
|
||||
? messages.first().get('received_at')
|
||||
: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
if (!messages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const readMessages = messages.filter(
|
||||
m => !m.hasErrors() && m.isIncoming()
|
||||
);
|
||||
const receiptSpecs = readMessages.map(m => ({
|
||||
sender: m.get('source') || m.get('sourceUuid'),
|
||||
timestamp: m.get('sent_at'),
|
||||
hasErrors: m.hasErrors(),
|
||||
}));
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.sendReadReceiptsFor(receiptSpecs);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(readMessages.map(m => m.queueAttachmentDownloads()));
|
||||
} while (messages.length > 0);
|
||||
},
|
||||
|
||||
async applyMessageRequestResponse(response, { fromSync = false } = {}) {
|
||||
// Apply message request response locally
|
||||
this.set({
|
||||
messageRequestResponseType: response,
|
||||
});
|
||||
window.Signal.Data.updateConversation(this.attributes);
|
||||
|
||||
if (response === this.messageRequestEnum.ACCEPT) {
|
||||
this.unblock();
|
||||
this.enableProfileSharing();
|
||||
this.sendProfileKeyUpdate();
|
||||
if (!fromSync) {
|
||||
// Locally accepted
|
||||
await this.handleReadAndDownloadAttachments();
|
||||
}
|
||||
} else if (response === this.messageRequestEnum.BLOCK) {
|
||||
// Block locally, other devices should block upon receiving the sync message
|
||||
window.storage.blockIdentifier(this.get('id'));
|
||||
this.disableProfileSharing();
|
||||
} else if (response === this.messageRequestEnum.DELETE) {
|
||||
// Delete messages locally, other devices should delete upon receiving
|
||||
// the sync message
|
||||
this.destroyMessages();
|
||||
this.disableProfileSharing();
|
||||
this.updateLastMessage();
|
||||
if (!fromSync) {
|
||||
this.trigger('unload', 'deleted from message request');
|
||||
}
|
||||
} else if (response === this.messageRequestEnum.BLOCK_AND_DELETE) {
|
||||
// Delete messages locally, other devices should delete upon receiving
|
||||
// the sync message
|
||||
this.destroyMessages();
|
||||
this.disableProfileSharing();
|
||||
this.updateLastMessage();
|
||||
// Block locally, other devices should block upon receiving the sync message
|
||||
window.storage.blockIdentifier(this.get('id'));
|
||||
// Leave group if this was a local action
|
||||
if (!fromSync) {
|
||||
this.leaveGroup();
|
||||
this.trigger('unload', 'blocked and deleted from message request');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async syncMessageRequestResponse(response) {
|
||||
// Let this run, no await
|
||||
this.applyMessageRequestResponse(response);
|
||||
|
||||
const { ourNumber, ourUuid } = this;
|
||||
const { wrap, sendOptions } = ConversationController.prepareForSend(
|
||||
ourNumber || ourUuid,
|
||||
{
|
||||
syncMessage: true,
|
||||
}
|
||||
);
|
||||
|
||||
await wrap(
|
||||
textsecure.messaging.syncMessageRequestResponse(
|
||||
{
|
||||
threadE164: this.get('e164'),
|
||||
threadUuid: this.get('uuid'),
|
||||
groupId: this.get('groupId'),
|
||||
type: response,
|
||||
},
|
||||
sendOptions
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
onMessageError() {
|
||||
this.updateVerified();
|
||||
},
|
||||
|
@ -687,6 +889,52 @@
|
|||
);
|
||||
});
|
||||
},
|
||||
|
||||
getSentMessageCount() {
|
||||
return this.get('sentMessageCount') || 0;
|
||||
},
|
||||
|
||||
getMessageRequestResponseType() {
|
||||
return this.get('messageRequestResponseType') || 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if this conversation should be considered "accepted" in terms
|
||||
* of message requests
|
||||
*/
|
||||
getAccepted() {
|
||||
const messageRequestsEnabled = Signal.RemoteConfig.isEnabled(
|
||||
'desktop.messageRequests'
|
||||
);
|
||||
|
||||
if (!messageRequestsEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.isMe()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this.getMessageRequestResponseType() === this.messageRequestEnum.ACCEPT
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const fromContact = this.getIsAddedByContact();
|
||||
const hasSentMessages = this.getSentMessageCount() > 0;
|
||||
const hasMessagesBeforeMessageRequests =
|
||||
(this.get('messageCountBeforeMessageRequests') || 0) > 0;
|
||||
const hasNoMessages = (this.get('messageCount') || 0) === 0;
|
||||
|
||||
return (
|
||||
fromContact ||
|
||||
hasSentMessages ||
|
||||
hasMessagesBeforeMessageRequests ||
|
||||
hasNoMessages
|
||||
);
|
||||
},
|
||||
|
||||
onMemberVerifiedChange() {
|
||||
// If the verified state of a member changes, our aggregate state changes.
|
||||
// We trigger both events to replicate the behavior of Backbone.Model.set()
|
||||
|
@ -1159,6 +1407,33 @@
|
|||
});
|
||||
},
|
||||
|
||||
async sendProfileKeyUpdate() {
|
||||
const id = this.get('id');
|
||||
const recipients = this.isPrivate()
|
||||
? [this.get('uuid') || this.get('e164')]
|
||||
: this.getRecipients();
|
||||
if (!this.get('profileSharing')) {
|
||||
window.log.error(
|
||||
'Attempted to send profileKeyUpdate to conversation without profileSharing enabled',
|
||||
id,
|
||||
recipients
|
||||
);
|
||||
return;
|
||||
}
|
||||
window.log.info(
|
||||
'Sending profileKeyUpdate to conversation',
|
||||
id,
|
||||
recipients
|
||||
);
|
||||
const profileKey = storage.get('profileKey');
|
||||
await textsecure.messaging.sendProfileKeyUpdate(
|
||||
profileKey,
|
||||
recipients,
|
||||
this.getSendOptions(),
|
||||
this.get('groupId')
|
||||
);
|
||||
},
|
||||
|
||||
sendMessage(body, attachments, quote, preview, sticker) {
|
||||
this.clearTypingTimers();
|
||||
|
||||
|
@ -1226,6 +1501,7 @@
|
|||
draft: null,
|
||||
draftTimestamp: null,
|
||||
});
|
||||
this.incrementSentMessageCount();
|
||||
window.Signal.Data.updateConversation(this.attributes);
|
||||
|
||||
// We're offline!
|
||||
|
@ -1487,6 +1763,32 @@
|
|||
};
|
||||
},
|
||||
|
||||
getIsContact() {
|
||||
if (this.isPrivate()) {
|
||||
return Boolean(this.get('name'));
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
getIsAddedByContact() {
|
||||
if (this.isPrivate()) {
|
||||
return this.getIsContact();
|
||||
}
|
||||
|
||||
const addedBy = this.get('addedBy');
|
||||
if (!addedBy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const conv = ConversationController.get(addedBy);
|
||||
if (!conv) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return conv.getIsContact();
|
||||
},
|
||||
|
||||
async updateLastMessage() {
|
||||
if (!this.id) {
|
||||
return;
|
||||
|
@ -1793,7 +2095,7 @@
|
|||
async leaveGroup() {
|
||||
const now = Date.now();
|
||||
if (this.get('type') === 'group') {
|
||||
const groupNumbers = this.getRecipients();
|
||||
const groupIdentifiers = this.getRecipients();
|
||||
this.set({ left: true });
|
||||
window.Signal.Data.updateConversation(this.attributes);
|
||||
|
||||
|
@ -1816,7 +2118,7 @@
|
|||
const options = this.getSendOptions();
|
||||
message.send(
|
||||
this.wrapSend(
|
||||
textsecure.messaging.leaveGroup(this.id, groupNumbers, options)
|
||||
textsecure.messaging.leaveGroup(this.id, groupIdentifiers, options)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1845,11 +2147,10 @@
|
|||
// Note that this will update the message in the database
|
||||
await m.markRead(options.readAt);
|
||||
|
||||
const errors = m.get('errors');
|
||||
return {
|
||||
sender: m.get('source'),
|
||||
sender: m.get('source') || m.get('sourceUuid'),
|
||||
timestamp: m.get('sent_at'),
|
||||
hasErrors: Boolean(errors && errors.length),
|
||||
hasErrors: m.hasErrors(),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
@ -1870,7 +2171,7 @@
|
|||
read = read.filter(item => !item.hasErrors);
|
||||
|
||||
if (read.length && options.sendReadReceipts) {
|
||||
window.log.info(`Sending ${read.length} read receipts`);
|
||||
window.log.info(`Sending ${read.length} read syncs`);
|
||||
// Because syncReadMessages sends to our other devices, and sendReadReceipts goes
|
||||
// to a contact, we need accessKeys for both.
|
||||
const { sendOptions } = ConversationController.prepareForSend(
|
||||
|
@ -1880,25 +2181,31 @@
|
|||
await this.wrapSend(
|
||||
textsecure.messaging.syncReadMessages(read, sendOptions)
|
||||
);
|
||||
await this.sendReadReceiptsFor(read);
|
||||
}
|
||||
},
|
||||
|
||||
if (storage.get('read-receipt-setting')) {
|
||||
const convoSendOptions = this.getSendOptions();
|
||||
async sendReadReceiptsFor(items) {
|
||||
// Only send read receipts for accepted conversations
|
||||
if (storage.get('read-receipt-setting') && this.getAccepted()) {
|
||||
window.log.info(`Sending ${items.length} read receipts`);
|
||||
const convoSendOptions = this.getSendOptions();
|
||||
const receiptsBySender = _.groupBy(items, 'sender');
|
||||
|
||||
await Promise.all(
|
||||
_.map(_.groupBy(read, 'sender'), async (receipts, identifier) => {
|
||||
const timestamps = _.map(receipts, 'timestamp');
|
||||
const c = ConversationController.get(identifier);
|
||||
await this.wrapSend(
|
||||
textsecure.messaging.sendReadReceipts(
|
||||
c.get('e164'),
|
||||
c.get('uuid'),
|
||||
timestamps,
|
||||
convoSendOptions
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
await Promise.all(
|
||||
_.map(receiptsBySender, async (receipts, identifier) => {
|
||||
const timestamps = _.map(receipts, 'timestamp');
|
||||
const c = ConversationController.get(identifier);
|
||||
await this.wrapSend(
|
||||
textsecure.messaging.sendReadReceipts(
|
||||
c.get('e164'),
|
||||
c.get('uuid'),
|
||||
timestamps,
|
||||
convoSendOptions
|
||||
)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -543,6 +543,9 @@
|
|||
|
||||
const conversation = this.getConversation();
|
||||
const isGroup = conversation && !conversation.isPrivate();
|
||||
const conversationAccepted = Boolean(
|
||||
conversation && conversation.getAccepted()
|
||||
);
|
||||
const sticker = this.get('sticker');
|
||||
|
||||
const isTapToView = this.isTapToView();
|
||||
|
@ -577,6 +580,7 @@
|
|||
textPending: this.get('bodyPending'),
|
||||
id: this.id,
|
||||
conversationId: this.get('conversationId'),
|
||||
conversationAccepted,
|
||||
isSticker: Boolean(sticker),
|
||||
direction: this.isIncoming() ? 'incoming' : 'outgoing',
|
||||
timestamp: this.get('sent_at'),
|
||||
|
@ -1145,6 +1149,28 @@
|
|||
});
|
||||
}
|
||||
},
|
||||
isEmpty() {
|
||||
const body = this.get('body');
|
||||
const hasAttachment = (this.get('attachments') || []).length > 0;
|
||||
const quote = this.get('quote');
|
||||
const hasContact = (this.get('contact') || []).length > 0;
|
||||
const sticker = this.get('sticker');
|
||||
const hasPreview = (this.get('preview') || []).length > 0;
|
||||
const groupUpdate = this.get('group_update');
|
||||
const expirationTimerUpdate = this.get('expirationTimerUpdate');
|
||||
|
||||
const notEmpty = Boolean(
|
||||
body ||
|
||||
hasAttachment ||
|
||||
quote ||
|
||||
hasContact ||
|
||||
sticker ||
|
||||
hasPreview ||
|
||||
groupUpdate ||
|
||||
expirationTimerUpdate
|
||||
);
|
||||
return !notEmpty;
|
||||
},
|
||||
unload() {
|
||||
if (this.quotedMessage) {
|
||||
this.quotedMessage = null;
|
||||
|
@ -1454,26 +1480,32 @@
|
|||
);
|
||||
},
|
||||
canReply() {
|
||||
const isAccepted = this.getConversation().getAccepted();
|
||||
const errors = this.get('errors');
|
||||
const isOutgoing = this.get('type') === 'outgoing';
|
||||
const numDelivered = this.get('delivered');
|
||||
|
||||
// Case 1: We cannot reply if this message is deleted for everyone
|
||||
// Case 1: We cannot reply if we have accepted the message request
|
||||
if (!isAccepted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 2: We cannot reply if this message is deleted for everyone
|
||||
if (this.get('deletedForEveryone')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 2: We can reply if this is outgoing and delievered to at least one recipient
|
||||
// Case 3: We can reply if this is outgoing and delievered to at least one recipient
|
||||
if (isOutgoing && numDelivered > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Case 3: We can reply if there are no errors
|
||||
// Case 4: We can reply if there are no errors
|
||||
if (!errors || (errors && errors.length === 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise we cannot reply
|
||||
// Case 5: default
|
||||
return false;
|
||||
},
|
||||
|
||||
|
@ -2171,10 +2203,12 @@
|
|||
}
|
||||
|
||||
// Send delivery receipts, but only for incoming sealed sender messages
|
||||
// and not for messages from unaccepted conversations
|
||||
if (
|
||||
type === 'incoming' &&
|
||||
this.get('unidentifiedDeliveryReceived') &&
|
||||
!this.hasErrors()
|
||||
!this.hasErrors() &&
|
||||
conversation.getAccepted()
|
||||
) {
|
||||
// Note: We both queue and batch because we want to wait until we are done
|
||||
// processing incoming messages to start sending outgoing delivery receipts.
|
||||
|
@ -2344,6 +2378,7 @@
|
|||
if (conversation.get('left')) {
|
||||
window.log.warn('re-added to a left group');
|
||||
attributes.left = false;
|
||||
conversation.set({ addedBy: message.getContact().get('id') });
|
||||
}
|
||||
} else if (dataMessage.group.type === GROUP_TYPES.QUIT) {
|
||||
const sender = ConversationController.get(source || sourceUuid);
|
||||
|
@ -2549,6 +2584,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Drop empty messages. This needs to happen after the initial
|
||||
// message.set call to make sure all possible properties are set
|
||||
// before we determine that a message is empty.
|
||||
if (message.isEmpty()) {
|
||||
window.log.info(
|
||||
`Dropping empty datamessage ${message.idForLogging()} in conversation ${conversation.idForLogging()}`
|
||||
);
|
||||
confirm();
|
||||
return;
|
||||
}
|
||||
|
||||
const conversationTimestamp = conversation.get('timestamp');
|
||||
if (
|
||||
!conversationTimestamp ||
|
||||
|
@ -2561,9 +2607,14 @@
|
|||
}
|
||||
|
||||
MessageController.register(message.id, message);
|
||||
conversation.incrementMessageCount();
|
||||
window.Signal.Data.updateConversation(conversation.attributes);
|
||||
|
||||
await message.queueAttachmentDownloads();
|
||||
// Only queue attachments for downloads if this is an outgoing message
|
||||
// or we've accepted the conversation
|
||||
if (this.getConversation().getAccepted() || message.isOutgoing()) {
|
||||
await message.queueAttachmentDownloads();
|
||||
}
|
||||
|
||||
// Does this message have any pending, previously-received associated reactions?
|
||||
const reactions = Whisper.Reactions.forMessage(message);
|
||||
|
@ -2589,6 +2640,11 @@
|
|||
await conversation.notify(message);
|
||||
}
|
||||
|
||||
// Increment the sent message count if this is an outgoing message
|
||||
if (type === 'outgoing') {
|
||||
conversation.incrementSentMessageCount();
|
||||
}
|
||||
|
||||
Whisper.events.trigger('incrementProgress');
|
||||
confirm();
|
||||
} catch (error) {
|
||||
|
|
|
@ -11,6 +11,7 @@ const Notifications = require('../../ts/notifications');
|
|||
const OS = require('../../ts/OS');
|
||||
const Stickers = require('./stickers');
|
||||
const Settings = require('./settings');
|
||||
const RemoteConfig = require('../../ts/RemoteConfig');
|
||||
const Util = require('../../ts/util');
|
||||
const Metadata = require('./metadata/SecretSessionCipher');
|
||||
const RefreshSenderCertificate = require('./refresh_sender_certificate');
|
||||
|
@ -350,6 +351,7 @@ exports.setup = (options = {}) => {
|
|||
Notifications,
|
||||
OS,
|
||||
RefreshSenderCertificate,
|
||||
RemoteConfig,
|
||||
Settings,
|
||||
Services,
|
||||
State,
|
||||
|
|
|
@ -367,6 +367,7 @@
|
|||
color: this.model.getColor(),
|
||||
avatarPath: this.model.getAvatarPath(),
|
||||
|
||||
isAccepted: this.model.getAccepted(),
|
||||
isVerified: this.model.isVerified(),
|
||||
isMe: this.model.isMe(),
|
||||
isGroup: !this.model.isPrivate(),
|
||||
|
@ -447,6 +448,9 @@
|
|||
</div>
|
||||
`)[0];
|
||||
|
||||
const messageRequestEnum =
|
||||
window.textsecure.protobuf.SyncMessage.MessageRequestResponse.Type;
|
||||
|
||||
const props = {
|
||||
id: this.model.id,
|
||||
compositionApi,
|
||||
|
@ -462,6 +466,26 @@
|
|||
clearQuotedMessage: () => this.setQuoteMessage(null),
|
||||
micCellEl,
|
||||
attachmentListEl,
|
||||
onAccept: this.model.syncMessageRequestResponse.bind(
|
||||
this.model,
|
||||
messageRequestEnum.ACCEPT
|
||||
),
|
||||
onBlock: this.model.syncMessageRequestResponse.bind(
|
||||
this.model,
|
||||
messageRequestEnum.BLOCK
|
||||
),
|
||||
onUnblock: this.model.syncMessageRequestResponse.bind(
|
||||
this.model,
|
||||
messageRequestEnum.ACCEPT
|
||||
),
|
||||
onDelete: this.model.syncMessageRequestResponse.bind(
|
||||
this.model,
|
||||
messageRequestEnum.DELETE
|
||||
),
|
||||
onBlockAndDelete: this.model.syncMessageRequestResponse.bind(
|
||||
this.model,
|
||||
messageRequestEnum.BLOCK_AND_DELETE
|
||||
),
|
||||
};
|
||||
|
||||
this.compositionAreaView = new Whisper.ReactWrapperView({
|
||||
|
@ -1223,7 +1247,13 @@
|
|||
}
|
||||
|
||||
return {
|
||||
..._.pick(attachment, ['contentType', 'fileName', 'size', 'caption']),
|
||||
..._.pick(attachment, [
|
||||
'contentType',
|
||||
'fileName',
|
||||
'size',
|
||||
'caption',
|
||||
'blurHash',
|
||||
]),
|
||||
data,
|
||||
};
|
||||
},
|
||||
|
@ -1433,6 +1463,7 @@
|
|||
},
|
||||
|
||||
async handleImageAttachment(file) {
|
||||
const blurHash = await window.imageToBlurHash(file);
|
||||
if (MIME.isJPEG(file.type)) {
|
||||
const rotatedDataUrl = await window.autoOrientImage(file);
|
||||
const rotatedBlob = window.dataURLToBlobSync(rotatedDataUrl);
|
||||
|
@ -1454,6 +1485,7 @@
|
|||
contentType,
|
||||
data,
|
||||
size: data.byteLength,
|
||||
blurHash,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1470,6 +1502,7 @@
|
|||
contentType,
|
||||
data,
|
||||
size: data.byteLength,
|
||||
blurHash,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -2168,6 +2201,11 @@
|
|||
});
|
||||
message.trigger('unload');
|
||||
this.model.messageCollection.remove(message.id);
|
||||
if (message.isOutgoing()) {
|
||||
this.model.decrementSentMessageCount();
|
||||
} else {
|
||||
this.model.decrementMessageCount();
|
||||
}
|
||||
this.resetPanel();
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue