Message Requests

This commit is contained in:
Ken Powers 2020-05-27 17:37:06 -04:00 committed by Scott Nonnenberg
parent 4d4b7a26a5
commit 83574eb067
60 changed files with 2566 additions and 216 deletions

View file

@ -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
View 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
);
}
},
}))();
})();

View file

@ -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);
}
}
};
})();

View file

@ -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
)
);
})
);
}
},

View file

@ -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) {

View file

@ -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,

View file

@ -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();
},
});