Move message.getPropsForBubble and friends to selectors
This commit is contained in:
parent
03a187097f
commit
68f1023946
73 changed files with 3394 additions and 2576 deletions
|
@ -337,19 +337,10 @@
|
||||||
<script type='text/javascript' src='libtextsecure/protobufs.js'></script>
|
<script type='text/javascript' src='libtextsecure/protobufs.js'></script>
|
||||||
|
|
||||||
<script type='text/javascript' src='js/notifications.js'></script>
|
<script type='text/javascript' src='js/notifications.js'></script>
|
||||||
<script type='text/javascript' src='js/delivery_receipts.js'></script>
|
|
||||||
<script type='text/javascript' src='js/read_receipts.js'></script>
|
|
||||||
<script type='text/javascript' src='js/read_syncs.js'></script>
|
|
||||||
<script type='text/javascript' src='js/view_syncs.js'></script>
|
|
||||||
<script type='text/javascript' src='js/message_requests.js'></script>
|
|
||||||
<script type='text/javascript' src='js/reactions.js'></script>
|
|
||||||
<script type='text/javascript' src='js/deletes.js'></script>
|
|
||||||
<script type='text/javascript' src='js/libphonenumber-util.js'></script>
|
<script type='text/javascript' src='js/libphonenumber-util.js'></script>
|
||||||
<script type='text/javascript' src='js/expiring_messages.js'></script>
|
<script type='text/javascript' src='js/expiring_messages.js'></script>
|
||||||
<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/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>
|
||||||
<script type='text/javascript' src='js/views/list_view.js'></script>
|
<script type='text/javascript' src='js/views/list_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/contact_list_view.js'></script>
|
<script type='text/javascript' src='js/views/contact_list_view.js'></script>
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
// Copyright 2016-2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
/* global
|
|
||||||
Backbone,
|
|
||||||
Whisper,
|
|
||||||
ConversationController,
|
|
||||||
MessageController,
|
|
||||||
_
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function () {
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
|
|
||||||
Whisper.DeliveryReceipts = new (Backbone.Collection.extend({
|
|
||||||
forMessage(conversation, message) {
|
|
||||||
let recipients;
|
|
||||||
if (conversation.isPrivate()) {
|
|
||||||
recipients = [conversation.id];
|
|
||||||
} else {
|
|
||||||
recipients = conversation.getMemberIds();
|
|
||||||
}
|
|
||||||
const receipts = this.filter(
|
|
||||||
receipt =>
|
|
||||||
receipt.get('timestamp') === message.get('sent_at') &&
|
|
||||||
recipients.indexOf(receipt.get('deliveredTo')) > -1
|
|
||||||
);
|
|
||||||
this.remove(receipts);
|
|
||||||
return receipts;
|
|
||||||
},
|
|
||||||
async getTargetMessage(sourceId, messages) {
|
|
||||||
if (messages.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const message = messages.find(
|
|
||||||
item => !item.isIncoming() && sourceId === item.get('conversationId')
|
|
||||||
);
|
|
||||||
if (message) {
|
|
||||||
return MessageController.register(message.id, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const groups = await window.Signal.Data.getAllGroupsInvolvingId(
|
|
||||||
sourceId,
|
|
||||||
{
|
|
||||||
ConversationCollection: Whisper.ConversationCollection,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const ids = groups.pluck('id');
|
|
||||||
ids.push(sourceId);
|
|
||||||
|
|
||||||
const target = messages.find(
|
|
||||||
item =>
|
|
||||||
!item.isIncoming() && _.contains(ids, item.get('conversationId'))
|
|
||||||
);
|
|
||||||
if (!target) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MessageController.register(target.id, target);
|
|
||||||
},
|
|
||||||
async onReceipt(receipt) {
|
|
||||||
try {
|
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
|
||||||
receipt.get('timestamp'),
|
|
||||||
{
|
|
||||||
MessageCollection: Whisper.MessageCollection,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const message = await this.getTargetMessage(
|
|
||||||
receipt.get('deliveredTo'),
|
|
||||||
messages
|
|
||||||
);
|
|
||||||
if (!message) {
|
|
||||||
window.log.info(
|
|
||||||
'No message for delivery receipt',
|
|
||||||
receipt.get('deliveredTo'),
|
|
||||||
receipt.get('timestamp')
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deliveries = message.get('delivered') || 0;
|
|
||||||
const deliveredTo = message.get('delivered_to') || [];
|
|
||||||
const expirationStartTimestamp = message.get(
|
|
||||||
'expirationStartTimestamp'
|
|
||||||
);
|
|
||||||
message.set({
|
|
||||||
delivered_to: _.union(deliveredTo, [receipt.get('deliveredTo')]),
|
|
||||||
delivered: deliveries + 1,
|
|
||||||
expirationStartTimestamp: expirationStartTimestamp || Date.now(),
|
|
||||||
sent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
window.Signal.Util.queueUpdateMessage(message.attributes);
|
|
||||||
|
|
||||||
// notify frontend listeners
|
|
||||||
const conversation = ConversationController.get(
|
|
||||||
message.get('conversationId')
|
|
||||||
);
|
|
||||||
const updateLeftPane = conversation
|
|
||||||
? conversation.debouncedUpdateLastMessage
|
|
||||||
: undefined;
|
|
||||||
if (updateLeftPane) {
|
|
||||||
updateLeftPane();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.remove(receipt);
|
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
'DeliveryReceipts.onReceipt error:',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}))();
|
|
||||||
})();
|
|
|
@ -1,118 +0,0 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
/* global
|
|
||||||
Backbone,
|
|
||||||
Whisper,
|
|
||||||
ConversationController,
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function () {
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
Whisper.MessageRequests = new (Backbone.Collection.extend({
|
|
||||||
forConversation(conversation) {
|
|
||||||
if (conversation.get('e164')) {
|
|
||||||
const syncByE164 = this.findWhere({
|
|
||||||
threadE164: conversation.get('e164'),
|
|
||||||
});
|
|
||||||
if (syncByE164) {
|
|
||||||
window.log.info(
|
|
||||||
`Found early message request response for E164 ${conversation.idForLogging()}`
|
|
||||||
);
|
|
||||||
this.remove(syncByE164);
|
|
||||||
return syncByE164;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conversation.get('uuid')) {
|
|
||||||
const syncByUuid = this.findWhere({
|
|
||||||
threadUuid: conversation.get('uuid'),
|
|
||||||
});
|
|
||||||
if (syncByUuid) {
|
|
||||||
window.log.info(
|
|
||||||
`Found early message request response for UUID ${conversation.idForLogging()}`
|
|
||||||
);
|
|
||||||
this.remove(syncByUuid);
|
|
||||||
return syncByUuid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// V1 Group
|
|
||||||
if (conversation.get('groupId')) {
|
|
||||||
const syncByGroupId = this.findWhere({
|
|
||||||
groupId: conversation.get('groupId'),
|
|
||||||
});
|
|
||||||
if (syncByGroupId) {
|
|
||||||
window.log.info(
|
|
||||||
`Found early message request response for group v1 ID ${conversation.idForLogging()}`
|
|
||||||
);
|
|
||||||
this.remove(syncByGroupId);
|
|
||||||
return syncByGroupId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// V2 group
|
|
||||||
if (conversation.get('groupId')) {
|
|
||||||
const syncByGroupId = this.findWhere({
|
|
||||||
groupV2Id: conversation.get('groupId'),
|
|
||||||
});
|
|
||||||
if (syncByGroupId) {
|
|
||||||
window.log.info(
|
|
||||||
`Found early message request response for group v2 ID ${conversation.idForLogging()}`
|
|
||||||
);
|
|
||||||
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 groupV2Id = sync.get('groupV2Id');
|
|
||||||
|
|
||||||
let conversation;
|
|
||||||
|
|
||||||
// We multiplex between GV1/GV2 groups here, but we don't kick off migrations
|
|
||||||
if (groupV2Id) {
|
|
||||||
conversation = ConversationController.get(groupV2Id);
|
|
||||||
}
|
|
||||||
if (!conversation && groupId) {
|
|
||||||
conversation = ConversationController.get(groupId);
|
|
||||||
}
|
|
||||||
if (!conversation && (threadE164 || threadUuid)) {
|
|
||||||
conversation = ConversationController.get(
|
|
||||||
ConversationController.ensureContactIds({
|
|
||||||
e164: threadE164,
|
|
||||||
uuid: threadUuid,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!conversation) {
|
|
||||||
window.log(
|
|
||||||
`Received message request response for unknown conversation: groupv2(${groupV2Id}) group(${groupId}) ${threadUuid} ${threadE164}`
|
|
||||||
);
|
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}))();
|
|
||||||
})();
|
|
|
@ -22,7 +22,6 @@ const Settings = require('./settings');
|
||||||
const RemoteConfig = require('../../ts/RemoteConfig');
|
const RemoteConfig = require('../../ts/RemoteConfig');
|
||||||
const Util = require('../../ts/util');
|
const Util = require('../../ts/util');
|
||||||
const LinkPreviews = require('./link_previews');
|
const LinkPreviews = require('./link_previews');
|
||||||
const AttachmentDownloads = require('./attachment_downloads');
|
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
const {
|
const {
|
||||||
|
@ -443,7 +442,6 @@ exports.setup = (options = {}) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
AttachmentDownloads,
|
|
||||||
Backbone,
|
Backbone,
|
||||||
Components,
|
Components,
|
||||||
Crypto,
|
Crypto,
|
||||||
|
|
|
@ -1,122 +0,0 @@
|
||||||
// Copyright 2016-2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
/* global
|
|
||||||
Whisper,
|
|
||||||
Backbone,
|
|
||||||
_,
|
|
||||||
ConversationController,
|
|
||||||
MessageController,
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function () {
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
Whisper.ReadReceipts = new (Backbone.Collection.extend({
|
|
||||||
forMessage(conversation, message) {
|
|
||||||
if (!message.isOutgoing()) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
let ids = [];
|
|
||||||
if (conversation.isPrivate()) {
|
|
||||||
ids = [conversation.id];
|
|
||||||
} else {
|
|
||||||
ids = conversation.getMemberIds();
|
|
||||||
}
|
|
||||||
const receipts = this.filter(
|
|
||||||
receipt =>
|
|
||||||
receipt.get('timestamp') === message.get('sent_at') &&
|
|
||||||
_.contains(ids, receipt.get('reader'))
|
|
||||||
);
|
|
||||||
if (receipts.length) {
|
|
||||||
window.log.info('Found early read receipts for message');
|
|
||||||
this.remove(receipts);
|
|
||||||
}
|
|
||||||
return receipts;
|
|
||||||
},
|
|
||||||
async getTargetMessage(reader, messages) {
|
|
||||||
if (messages.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const message = messages.find(
|
|
||||||
item => item.isOutgoing() && reader === item.get('conversationId')
|
|
||||||
);
|
|
||||||
if (message) {
|
|
||||||
return MessageController.register(message.id, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const groups = await window.Signal.Data.getAllGroupsInvolvingId(reader, {
|
|
||||||
ConversationCollection: Whisper.ConversationCollection,
|
|
||||||
});
|
|
||||||
const ids = groups.pluck('id');
|
|
||||||
ids.push(reader);
|
|
||||||
|
|
||||||
const target = messages.find(
|
|
||||||
item => item.isOutgoing() && _.contains(ids, item.get('conversationId'))
|
|
||||||
);
|
|
||||||
if (!target) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MessageController.register(target.id, target);
|
|
||||||
},
|
|
||||||
async onReceipt(receipt) {
|
|
||||||
try {
|
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
|
||||||
receipt.get('timestamp'),
|
|
||||||
{
|
|
||||||
MessageCollection: Whisper.MessageCollection,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const message = await this.getTargetMessage(
|
|
||||||
receipt.get('reader'),
|
|
||||||
messages
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
window.log.info(
|
|
||||||
'No message for read receipt',
|
|
||||||
receipt.get('reader'),
|
|
||||||
receipt.get('timestamp')
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const readBy = message.get('read_by') || [];
|
|
||||||
const expirationStartTimestamp = message.get(
|
|
||||||
'expirationStartTimestamp'
|
|
||||||
);
|
|
||||||
|
|
||||||
readBy.push(receipt.get('reader'));
|
|
||||||
message.set({
|
|
||||||
read_by: readBy,
|
|
||||||
expirationStartTimestamp: expirationStartTimestamp || Date.now(),
|
|
||||||
sent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
window.Signal.Util.queueUpdateMessage(message.attributes);
|
|
||||||
|
|
||||||
// notify frontend listeners
|
|
||||||
const conversation = ConversationController.get(
|
|
||||||
message.get('conversationId')
|
|
||||||
);
|
|
||||||
const updateLeftPane = conversation
|
|
||||||
? conversation.debouncedUpdateLastMessage
|
|
||||||
: undefined;
|
|
||||||
if (updateLeftPane) {
|
|
||||||
updateLeftPane();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.remove(receipt);
|
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
'ReadReceipts.onReceipt error:',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}))();
|
|
||||||
})();
|
|
140
js/read_syncs.js
140
js/read_syncs.js
|
@ -1,140 +0,0 @@
|
||||||
// Copyright 2017-2021 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
/* global
|
|
||||||
Backbone,
|
|
||||||
Whisper,
|
|
||||||
MessageController
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function () {
|
|
||||||
async function maybeItIsAReactionReadSync(receipt) {
|
|
||||||
const readReaction = await window.Signal.Data.markReactionAsRead(
|
|
||||||
receipt.get('senderUuid'),
|
|
||||||
Number(receipt.get('timestamp'))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!readReaction) {
|
|
||||||
window.log.info(
|
|
||||||
'Nothing found for read sync',
|
|
||||||
receipt.get('senderId'),
|
|
||||||
receipt.get('sender'),
|
|
||||||
receipt.get('senderUuid'),
|
|
||||||
receipt.get('timestamp')
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Whisper.Notifications.removeBy({
|
|
||||||
conversationId: readReaction.conversationId,
|
|
||||||
emoji: readReaction.emoji,
|
|
||||||
targetAuthorUuid: readReaction.targetAuthorUuid,
|
|
||||||
targetTimestamp: readReaction.targetTimestamp,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
Whisper.ReadSyncs = new (Backbone.Collection.extend({
|
|
||||||
forMessage(message) {
|
|
||||||
const senderId = window.ConversationController.ensureContactIds({
|
|
||||||
e164: message.get('source'),
|
|
||||||
uuid: message.get('sourceUuid'),
|
|
||||||
});
|
|
||||||
const receipt = this.findWhere({
|
|
||||||
senderId,
|
|
||||||
timestamp: message.get('sent_at'),
|
|
||||||
});
|
|
||||||
if (receipt) {
|
|
||||||
window.log.info('Found early read sync for message');
|
|
||||||
this.remove(receipt);
|
|
||||||
return receipt;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
async onReceipt(receipt) {
|
|
||||||
try {
|
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
|
||||||
receipt.get('timestamp'),
|
|
||||||
{
|
|
||||||
MessageCollection: Whisper.MessageCollection,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const found = messages.find(item => {
|
|
||||||
const senderId = window.ConversationController.ensureContactIds({
|
|
||||||
e164: item.get('source'),
|
|
||||||
uuid: item.get('sourceUuid'),
|
|
||||||
});
|
|
||||||
|
|
||||||
return item.isIncoming() && senderId === receipt.get('senderId');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
await maybeItIsAReactionReadSync(receipt);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Whisper.Notifications.removeBy({ messageId: found.id });
|
|
||||||
|
|
||||||
const message = MessageController.register(found.id, found);
|
|
||||||
const readAt = receipt.get('read_at');
|
|
||||||
|
|
||||||
// If message is unread, we mark it read. Otherwise, we update the expiration
|
|
||||||
// timer to the time specified by the read sync if it's earlier than
|
|
||||||
// the previous read time.
|
|
||||||
if (message.isUnread()) {
|
|
||||||
// TODO DESKTOP-1509: use MessageUpdater.markRead once this is TS
|
|
||||||
message.markRead(readAt, { skipSave: true });
|
|
||||||
|
|
||||||
const updateConversation = () => {
|
|
||||||
// onReadMessage may result in messages older than this one being
|
|
||||||
// marked read. We want those messages to have the same expire timer
|
|
||||||
// start time as this one, so we pass the readAt value through.
|
|
||||||
const conversation = message.getConversation();
|
|
||||||
if (conversation) {
|
|
||||||
conversation.onReadMessage(message, readAt);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (window.startupProcessingQueue) {
|
|
||||||
const conversation = message.getConversation();
|
|
||||||
if (conversation) {
|
|
||||||
window.startupProcessingQueue.add(
|
|
||||||
conversation.get('id'),
|
|
||||||
updateConversation
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateConversation();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const now = Date.now();
|
|
||||||
const existingTimestamp = message.get('expirationStartTimestamp');
|
|
||||||
const expirationStartTimestamp = Math.min(
|
|
||||||
now,
|
|
||||||
Math.min(existingTimestamp || now, readAt || now)
|
|
||||||
);
|
|
||||||
message.set({ expirationStartTimestamp });
|
|
||||||
|
|
||||||
const conversation = message.getConversation();
|
|
||||||
if (conversation) {
|
|
||||||
conversation.trigger('expiration-change', message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.Signal.Util.queueUpdateMessage(message.attributes);
|
|
||||||
|
|
||||||
this.remove(receipt);
|
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
'ReadSyncs.onReceipt error:',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}))();
|
|
||||||
})();
|
|
|
@ -1,89 +0,0 @@
|
||||||
// Copyright 2019-2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
/* global
|
|
||||||
Backbone,
|
|
||||||
Whisper,
|
|
||||||
MessageController
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function () {
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
Whisper.ViewSyncs = new (Backbone.Collection.extend({
|
|
||||||
forMessage(message) {
|
|
||||||
const syncBySourceUuid = this.findWhere({
|
|
||||||
sourceUuid: message.get('sourceUuid'),
|
|
||||||
timestamp: message.get('sent_at'),
|
|
||||||
});
|
|
||||||
if (syncBySourceUuid) {
|
|
||||||
window.log.info('Found early view sync for message');
|
|
||||||
this.remove(syncBySourceUuid);
|
|
||||||
return syncBySourceUuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncBySource = this.findWhere({
|
|
||||||
source: message.get('source'),
|
|
||||||
timestamp: message.get('sent_at'),
|
|
||||||
});
|
|
||||||
if (syncBySource) {
|
|
||||||
window.log.info('Found early view sync for message');
|
|
||||||
this.remove(syncBySource);
|
|
||||||
return syncBySource;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
async onSync(sync) {
|
|
||||||
try {
|
|
||||||
const messages = await window.Signal.Data.getMessagesBySentAt(
|
|
||||||
sync.get('timestamp'),
|
|
||||||
{
|
|
||||||
MessageCollection: Whisper.MessageCollection,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const found = messages.find(item => {
|
|
||||||
const itemSourceUuid = item.get('sourceUuid');
|
|
||||||
const syncSourceUuid = sync.get('sourceUuid');
|
|
||||||
const itemSource = item.get('source');
|
|
||||||
const syncSource = sync.get('source');
|
|
||||||
|
|
||||||
return (
|
|
||||||
(itemSourceUuid &&
|
|
||||||
syncSourceUuid &&
|
|
||||||
itemSourceUuid === syncSourceUuid) ||
|
|
||||||
(itemSource && syncSource && itemSource === syncSource)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const syncSource = sync.get('source');
|
|
||||||
const syncSourceUuid = sync.get('sourceUuid');
|
|
||||||
const syncTimestamp = sync.get('timestamp');
|
|
||||||
const wasMessageFound = Boolean(found);
|
|
||||||
window.log.info('Receive view sync:', {
|
|
||||||
syncSource,
|
|
||||||
syncSourceUuid,
|
|
||||||
syncTimestamp,
|
|
||||||
wasMessageFound,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = MessageController.register(found.id, found);
|
|
||||||
await message.markViewed({ fromSync: true });
|
|
||||||
|
|
||||||
this.remove(sync);
|
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
'ViewSyncs.onSync error:',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}))();
|
|
||||||
})();
|
|
|
@ -489,7 +489,6 @@ try {
|
||||||
window.dataURLToBlobSync = require('blueimp-canvas-to-blob');
|
window.dataURLToBlobSync = require('blueimp-canvas-to-blob');
|
||||||
window.imageToBlurHash = imageToBlurHash;
|
window.imageToBlurHash = imageToBlurHash;
|
||||||
window.emojiData = require('emoji-datasource');
|
window.emojiData = require('emoji-datasource');
|
||||||
window.filesize = require('filesize');
|
|
||||||
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
|
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
|
||||||
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
|
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
|
||||||
window.loadImage = require('blueimp-load-image');
|
window.loadImage = require('blueimp-load-image');
|
||||||
|
|
|
@ -347,7 +347,6 @@
|
||||||
<script type="text/javascript" src="../libtextsecure/protobufs.js"></script>
|
<script type="text/javascript" src="../libtextsecure/protobufs.js"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="../js/libphonenumber-util.js"></script>
|
<script type="text/javascript" src="../js/libphonenumber-util.js"></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>
|
||||||
<script type='text/javascript' src='../js/expiring_tap_to_view_messages.js' data-cover></script>
|
<script type='text/javascript' src='../js/expiring_tap_to_view_messages.js' data-cover></script>
|
||||||
|
|
|
@ -715,7 +715,7 @@ export class ConversationController {
|
||||||
async getConversationForTargetMessage(
|
async getConversationForTargetMessage(
|
||||||
targetFromId: string,
|
targetFromId: string,
|
||||||
targetTimestamp: number
|
targetTimestamp: number
|
||||||
): Promise<boolean | ConversationModel | null | undefined> {
|
): Promise<ConversationModel | null | undefined> {
|
||||||
const messages = await getMessagesBySentAt(targetTimestamp, {
|
const messages = await getMessagesBySentAt(targetTimestamp, {
|
||||||
MessageCollection: window.Whisper.MessageCollection,
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
});
|
});
|
||||||
|
|
|
@ -43,7 +43,16 @@ import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
|
||||||
import { getSendOptions } from './util/getSendOptions';
|
import { getSendOptions } from './util/getSendOptions';
|
||||||
import { BackOff } from './util/BackOff';
|
import { BackOff } from './util/BackOff';
|
||||||
import { AppViewType } from './state/ducks/app';
|
import { AppViewType } from './state/ducks/app';
|
||||||
|
import { hasErrors, isIncoming } from './state/selectors/message';
|
||||||
import { actionCreators } from './state/actions';
|
import { actionCreators } from './state/actions';
|
||||||
|
import { Deletes } from './messageModifiers/Deletes';
|
||||||
|
import { DeliveryReceipts } from './messageModifiers/DeliveryReceipts';
|
||||||
|
import { MessageRequests } from './messageModifiers/MessageRequests';
|
||||||
|
import { Reactions } from './messageModifiers/Reactions';
|
||||||
|
import { ReadReceipts } from './messageModifiers/ReadReceipts';
|
||||||
|
import { ReadSyncs } from './messageModifiers/ReadSyncs';
|
||||||
|
import { ViewSyncs } from './messageModifiers/ViewSyncs';
|
||||||
|
import * as AttachmentDownloads from './messageModifiers/AttachmentDownloads';
|
||||||
|
|
||||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||||
|
|
||||||
|
@ -337,13 +346,15 @@ export async function startApp(): Promise<void> {
|
||||||
PASSWORD
|
PASSWORD
|
||||||
);
|
);
|
||||||
accountManager.addEventListener('registration', () => {
|
accountManager.addEventListener('registration', () => {
|
||||||
|
const ourDeviceId = window.textsecure.storage.user.getDeviceId();
|
||||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||||
const ourUuid = window.textsecure.storage.user.getUuid();
|
const ourUuid = window.textsecure.storage.user.getUuid();
|
||||||
const user = {
|
const user = {
|
||||||
regionCode: window.storage.get('regionCode'),
|
ourConversationId: window.ConversationController.getOurConversationId(),
|
||||||
|
ourDeviceId,
|
||||||
ourNumber,
|
ourNumber,
|
||||||
ourUuid,
|
ourUuid,
|
||||||
ourConversationId: window.ConversationController.getOurConversationId(),
|
regionCode: window.storage.get('regionCode'),
|
||||||
};
|
};
|
||||||
window.Whisper.events.trigger('userChanged', user);
|
window.Whisper.events.trigger('userChanged', user);
|
||||||
|
|
||||||
|
@ -548,7 +559,7 @@ export async function startApp(): Promise<void> {
|
||||||
shutdown: async () => {
|
shutdown: async () => {
|
||||||
window.log.info('background/shutdown');
|
window.log.info('background/shutdown');
|
||||||
// Stop background processing
|
// Stop background processing
|
||||||
window.Signal.AttachmentDownloads.stop();
|
AttachmentDownloads.stop();
|
||||||
if (idleDetector) {
|
if (idleDetector) {
|
||||||
idleDetector.stop();
|
idleDetector.stop();
|
||||||
}
|
}
|
||||||
|
@ -957,6 +968,7 @@ export async function startApp(): Promise<void> {
|
||||||
// Binding these actions to our redux store and exposing them allows us to update
|
// Binding these actions to our redux store and exposing them allows us to update
|
||||||
// redux when things change in the backbone world.
|
// redux when things change in the backbone world.
|
||||||
window.reduxActions = {
|
window.reduxActions = {
|
||||||
|
accounts: bindActionCreators(actionCreators.accounts, store.dispatch),
|
||||||
app: bindActionCreators(actionCreators.app, store.dispatch),
|
app: bindActionCreators(actionCreators.app, store.dispatch),
|
||||||
audioPlayer: bindActionCreators(
|
audioPlayer: bindActionCreators(
|
||||||
actionCreators.audioPlayer,
|
actionCreators.audioPlayer,
|
||||||
|
@ -1659,7 +1671,7 @@ export async function startApp(): Promise<void> {
|
||||||
const delivered = message.get('delivered');
|
const delivered = message.get('delivered');
|
||||||
const sentAt = message.get('sent_at');
|
const sentAt = message.get('sent_at');
|
||||||
|
|
||||||
if (message.hasErrors()) {
|
if (hasErrors(message.attributes)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1887,7 +1899,7 @@ export async function startApp(): Promise<void> {
|
||||||
// Clear timer, since we're only called when the timer is expired
|
// Clear timer, since we're only called when the timer is expired
|
||||||
disconnectTimer = null;
|
disconnectTimer = null;
|
||||||
|
|
||||||
window.Signal.AttachmentDownloads.stop();
|
AttachmentDownloads.stop();
|
||||||
if (messageReceiver) {
|
if (messageReceiver) {
|
||||||
await messageReceiver.close();
|
await messageReceiver.close();
|
||||||
}
|
}
|
||||||
|
@ -2063,7 +2075,7 @@ export async function startApp(): Promise<void> {
|
||||||
addQueuedEventListener('fetchLatest', onFetchLatestSync);
|
addQueuedEventListener('fetchLatest', onFetchLatestSync);
|
||||||
addQueuedEventListener('keys', onKeysSync);
|
addQueuedEventListener('keys', onKeysSync);
|
||||||
|
|
||||||
window.Signal.AttachmentDownloads.start({
|
AttachmentDownloads.start({
|
||||||
getMessageReceiver: () => messageReceiver,
|
getMessageReceiver: () => messageReceiver,
|
||||||
logger: window.log,
|
logger: window.log,
|
||||||
});
|
});
|
||||||
|
@ -2859,7 +2871,10 @@ export async function startApp(): Promise<void> {
|
||||||
|
|
||||||
const message = initIncomingMessage(data, messageDescriptor);
|
const message = initIncomingMessage(data, messageDescriptor);
|
||||||
|
|
||||||
if (message.isIncoming() && message.get('unidentifiedDeliveryReceived')) {
|
if (
|
||||||
|
isIncoming(message.attributes) &&
|
||||||
|
message.get('unidentifiedDeliveryReceived')
|
||||||
|
) {
|
||||||
const sender = message.getContact();
|
const sender = message.getContact();
|
||||||
|
|
||||||
if (!sender) {
|
if (!sender) {
|
||||||
|
@ -2890,7 +2905,7 @@ export async function startApp(): Promise<void> {
|
||||||
'Queuing incoming reaction for',
|
'Queuing incoming reaction for',
|
||||||
reaction.targetTimestamp
|
reaction.targetTimestamp
|
||||||
);
|
);
|
||||||
const reactionModel = window.Whisper.Reactions.add({
|
const reactionModel = Reactions.getSingleton().add({
|
||||||
emoji: reaction.emoji,
|
emoji: reaction.emoji,
|
||||||
remove: reaction.remove,
|
remove: reaction.remove,
|
||||||
targetAuthorUuid: reaction.targetAuthorUuid,
|
targetAuthorUuid: reaction.targetAuthorUuid,
|
||||||
|
@ -2902,7 +2917,7 @@ export async function startApp(): Promise<void> {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
// Note: We do not wait for completion here
|
// Note: We do not wait for completion here
|
||||||
window.Whisper.Reactions.onReaction(reactionModel);
|
Reactions.getSingleton().onReaction(reactionModel);
|
||||||
confirm();
|
confirm();
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -2910,7 +2925,7 @@ export async function startApp(): Promise<void> {
|
||||||
if (data.message.delete) {
|
if (data.message.delete) {
|
||||||
const { delete: del } = data.message;
|
const { delete: del } = data.message;
|
||||||
window.log.info('Queuing incoming DOE for', del.targetSentTimestamp);
|
window.log.info('Queuing incoming DOE for', del.targetSentTimestamp);
|
||||||
const deleteModel = window.Whisper.Deletes.add({
|
const deleteModel = Deletes.getSingleton().add({
|
||||||
targetSentTimestamp: del.targetSentTimestamp,
|
targetSentTimestamp: del.targetSentTimestamp,
|
||||||
serverTimestamp: data.serverTimestamp,
|
serverTimestamp: data.serverTimestamp,
|
||||||
fromId: window.ConversationController.ensureContactIds({
|
fromId: window.ConversationController.ensureContactIds({
|
||||||
|
@ -2919,7 +2934,7 @@ export async function startApp(): Promise<void> {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
// Note: We do not wait for completion here
|
// Note: We do not wait for completion here
|
||||||
window.Whisper.Deletes.onDelete(deleteModel);
|
Deletes.getSingleton().onDelete(deleteModel);
|
||||||
confirm();
|
confirm();
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -3184,7 +3199,7 @@ export async function startApp(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.log.info('Queuing sent reaction for', reaction.targetTimestamp);
|
window.log.info('Queuing sent reaction for', reaction.targetTimestamp);
|
||||||
const reactionModel = window.Whisper.Reactions.add({
|
const reactionModel = Reactions.getSingleton().add({
|
||||||
emoji: reaction.emoji,
|
emoji: reaction.emoji,
|
||||||
remove: reaction.remove,
|
remove: reaction.remove,
|
||||||
targetAuthorUuid: reaction.targetAuthorUuid,
|
targetAuthorUuid: reaction.targetAuthorUuid,
|
||||||
|
@ -3194,7 +3209,7 @@ export async function startApp(): Promise<void> {
|
||||||
fromSync: true,
|
fromSync: true,
|
||||||
});
|
});
|
||||||
// Note: We do not wait for completion here
|
// Note: We do not wait for completion here
|
||||||
window.Whisper.Reactions.onReaction(reactionModel);
|
Reactions.getSingleton().onReaction(reactionModel);
|
||||||
|
|
||||||
event.confirm();
|
event.confirm();
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
@ -3203,13 +3218,13 @@ export async function startApp(): Promise<void> {
|
||||||
if (data.message.delete) {
|
if (data.message.delete) {
|
||||||
const { delete: del } = data.message;
|
const { delete: del } = data.message;
|
||||||
window.log.info('Queuing sent DOE for', del.targetSentTimestamp);
|
window.log.info('Queuing sent DOE for', del.targetSentTimestamp);
|
||||||
const deleteModel = window.Whisper.Deletes.add({
|
const deleteModel = Deletes.getSingleton().add({
|
||||||
targetSentTimestamp: del.targetSentTimestamp,
|
targetSentTimestamp: del.targetSentTimestamp,
|
||||||
serverTimestamp: del.serverTimestamp,
|
serverTimestamp: del.serverTimestamp,
|
||||||
fromId: window.ConversationController.getOurConversationId(),
|
fromId: window.ConversationController.getOurConversationId(),
|
||||||
});
|
});
|
||||||
// Note: We do not wait for completion here
|
// Note: We do not wait for completion here
|
||||||
window.Whisper.Deletes.onDelete(deleteModel);
|
Deletes.getSingleton().onDelete(deleteModel);
|
||||||
confirm();
|
confirm();
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -3299,11 +3314,13 @@ export async function startApp(): Promise<void> {
|
||||||
window.Signal.Util.Registration.remove();
|
window.Signal.Util.Registration.remove();
|
||||||
|
|
||||||
const NUMBER_ID_KEY = 'number_id';
|
const NUMBER_ID_KEY = 'number_id';
|
||||||
|
const UUID_ID_KEY = 'uuid_id';
|
||||||
const VERSION_KEY = 'version';
|
const VERSION_KEY = 'version';
|
||||||
const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex';
|
const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex';
|
||||||
const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete';
|
const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete';
|
||||||
|
|
||||||
const previousNumberId = window.textsecure.storage.get(NUMBER_ID_KEY);
|
const previousNumberId = window.textsecure.storage.get(NUMBER_ID_KEY);
|
||||||
|
const previousUuidId = window.textsecure.storage.get(UUID_ID_KEY);
|
||||||
const lastProcessedIndex = window.textsecure.storage.get(
|
const lastProcessedIndex = window.textsecure.storage.get(
|
||||||
LAST_PROCESSED_INDEX_KEY
|
LAST_PROCESSED_INDEX_KEY
|
||||||
);
|
);
|
||||||
|
@ -3327,6 +3344,9 @@ export async function startApp(): Promise<void> {
|
||||||
if (previousNumberId !== undefined) {
|
if (previousNumberId !== undefined) {
|
||||||
await window.textsecure.storage.put(NUMBER_ID_KEY, previousNumberId);
|
await window.textsecure.storage.put(NUMBER_ID_KEY, previousNumberId);
|
||||||
}
|
}
|
||||||
|
if (previousUuidId !== undefined) {
|
||||||
|
await window.textsecure.storage.put(UUID_ID_KEY, previousUuidId);
|
||||||
|
}
|
||||||
|
|
||||||
// These two are important to ensure we don't rip through every message
|
// These two are important to ensure we don't rip through every message
|
||||||
// in the database attempting to upgrade it after starting up again.
|
// in the database attempting to upgrade it after starting up again.
|
||||||
|
@ -3782,13 +3802,13 @@ export async function startApp(): Promise<void> {
|
||||||
const { source, sourceUuid, timestamp } = ev;
|
const { source, sourceUuid, timestamp } = ev;
|
||||||
window.log.info(`view sync ${source} ${timestamp}`);
|
window.log.info(`view sync ${source} ${timestamp}`);
|
||||||
|
|
||||||
const sync = window.Whisper.ViewSyncs.add({
|
const sync = ViewSyncs.getSingleton().add({
|
||||||
source,
|
source,
|
||||||
sourceUuid,
|
sourceUuid,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
window.Whisper.ViewSyncs.onSync(sync);
|
ViewSyncs.getSingleton().onSync(sync);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onFetchLatestSync(ev: WhatIsThis) {
|
async function onFetchLatestSync(ev: WhatIsThis) {
|
||||||
|
@ -3855,7 +3875,7 @@ export async function startApp(): Promise<void> {
|
||||||
messageRequestResponseType,
|
messageRequestResponseType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sync = window.Whisper.MessageRequests.add({
|
const sync = MessageRequests.getSingleton().add({
|
||||||
threadE164,
|
threadE164,
|
||||||
threadUuid,
|
threadUuid,
|
||||||
groupId,
|
groupId,
|
||||||
|
@ -3863,7 +3883,7 @@ export async function startApp(): Promise<void> {
|
||||||
type: messageRequestResponseType,
|
type: messageRequestResponseType,
|
||||||
});
|
});
|
||||||
|
|
||||||
window.Whisper.MessageRequests.onResponse(sync);
|
MessageRequests.getSingleton().onResponse(sync);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReadReceipt(ev: WhatIsThis) {
|
function onReadReceipt(ev: WhatIsThis) {
|
||||||
|
@ -3890,14 +3910,14 @@ export async function startApp(): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const receipt = window.Whisper.ReadReceipts.add({
|
const receipt = ReadReceipts.getSingleton().add({
|
||||||
reader,
|
reader,
|
||||||
timestamp,
|
timestamp,
|
||||||
read_at: readAt,
|
readAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Note: We do not wait for completion here
|
// Note: We do not wait for completion here
|
||||||
window.Whisper.ReadReceipts.onReceipt(receipt);
|
ReadReceipts.getSingleton().onReceipt(receipt);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReadSync(ev: WhatIsThis) {
|
function onReadSync(ev: WhatIsThis) {
|
||||||
|
@ -3918,19 +3938,19 @@ export async function startApp(): Promise<void> {
|
||||||
timestamp
|
timestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
const receipt = window.Whisper.ReadSyncs.add({
|
const receipt = ReadSyncs.getSingleton().add({
|
||||||
senderId,
|
senderId,
|
||||||
sender,
|
sender,
|
||||||
senderUuid,
|
senderUuid,
|
||||||
timestamp,
|
timestamp,
|
||||||
read_at: readAt,
|
readAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
receipt.on('remove', ev.confirm);
|
receipt.on('remove', ev.confirm);
|
||||||
|
|
||||||
// Note: Here we wait, because we want read states to be in the database
|
// Note: Here we wait, because we want read states to be in the database
|
||||||
// before we move on.
|
// before we move on.
|
||||||
return window.Whisper.ReadSyncs.onReceipt(receipt);
|
return ReadSyncs.getSingleton().onReceipt(receipt);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onVerified(ev: WhatIsThis) {
|
async function onVerified(ev: WhatIsThis) {
|
||||||
|
@ -4037,13 +4057,13 @@ export async function startApp(): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const receipt = window.Whisper.DeliveryReceipts.add({
|
const receipt = DeliveryReceipts.getSingleton().add({
|
||||||
timestamp,
|
timestamp,
|
||||||
deliveredTo,
|
deliveredTo,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Note: We don't wait for completion here
|
// Note: We don't wait for completion here
|
||||||
window.Whisper.DeliveryReceipts.onReceipt(receipt);
|
DeliveryReceipts.getSingleton().onReceipt(receipt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -109,6 +109,7 @@ story.add('media attachments', () => {
|
||||||
width: 112,
|
width: 112,
|
||||||
url: '/fixtures/kitten-4-112-112.jpg',
|
url: '/fixtures/kitten-4-112-112.jpg',
|
||||||
contentType: IMAGE_JPEG,
|
contentType: IMAGE_JPEG,
|
||||||
|
path: 'originalPath',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
|
@ -60,6 +60,7 @@ story.add('Multiple Visual Attachments', () => {
|
||||||
width: 112,
|
width: 112,
|
||||||
url: '/fixtures/kitten-4-112-112.jpg',
|
url: '/fixtures/kitten-4-112-112.jpg',
|
||||||
contentType: IMAGE_JPEG,
|
contentType: IMAGE_JPEG,
|
||||||
|
path: 'originalpath',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -100,6 +101,7 @@ story.add('Multiple with Non-Visual Types', () => {
|
||||||
width: 112,
|
width: 112,
|
||||||
url: '/fixtures/kitten-4-112-112.jpg',
|
url: '/fixtures/kitten-4-112-112.jpg',
|
||||||
contentType: IMAGE_JPEG,
|
contentType: IMAGE_JPEG,
|
||||||
|
path: 'originalpath',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -77,7 +77,7 @@ export const AttachmentList = ({
|
||||||
<Image
|
<Image
|
||||||
key={key}
|
key={key}
|
||||||
alt={i18n('stagedImageAttachment', [
|
alt={i18n('stagedImageAttachment', [
|
||||||
url || attachment.fileName,
|
attachment.fileName || url || index.toString(),
|
||||||
])}
|
])}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
attachment={attachment}
|
attachment={attachment}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { ContactDetail, Props } from './ContactDetail';
|
||||||
import { AddressType, ContactFormType } from '../../types/Contact';
|
import { AddressType, ContactFormType } from '../../types/Contact';
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
import { IMAGE_GIF } from '../../types/MIME';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -73,6 +74,7 @@ const fullContact = {
|
||||||
avatar: {
|
avatar: {
|
||||||
avatar: {
|
avatar: {
|
||||||
path: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
|
path: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
|
||||||
|
contentType: IMAGE_GIF,
|
||||||
},
|
},
|
||||||
isProfile: true,
|
isProfile: true,
|
||||||
},
|
},
|
||||||
|
@ -208,6 +210,7 @@ story.add('Loading Avatar', () => {
|
||||||
contact: {
|
contact: {
|
||||||
avatar: {
|
avatar: {
|
||||||
avatar: {
|
avatar: {
|
||||||
|
contentType: IMAGE_GIF,
|
||||||
pending: true,
|
pending: true,
|
||||||
},
|
},
|
||||||
isProfile: true,
|
isProfile: true,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { EmbeddedContact, Props } from './EmbeddedContact';
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import { ContactFormType } from '../../types/Contact';
|
import { ContactFormType } from '../../types/Contact';
|
||||||
|
import { IMAGE_GIF } from '../../types/MIME';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ const fullContact = {
|
||||||
avatar: {
|
avatar: {
|
||||||
avatar: {
|
avatar: {
|
||||||
path: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
|
path: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
|
||||||
|
contentType: IMAGE_GIF,
|
||||||
},
|
},
|
||||||
isProfile: true,
|
isProfile: true,
|
||||||
},
|
},
|
||||||
|
@ -134,6 +136,7 @@ story.add('Loading Avatar', () => {
|
||||||
avatar: {
|
avatar: {
|
||||||
avatar: {
|
avatar: {
|
||||||
pending: true,
|
pending: true,
|
||||||
|
contentType: IMAGE_GIF,
|
||||||
},
|
},
|
||||||
isProfile: true,
|
isProfile: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,61 +23,55 @@ export type Props = {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class EmbeddedContact extends React.Component<Props> {
|
export const EmbeddedContact: React.FC<Props> = (props: Props) => {
|
||||||
public render(): JSX.Element {
|
const {
|
||||||
const {
|
contact,
|
||||||
contact,
|
i18n,
|
||||||
i18n,
|
isIncoming,
|
||||||
isIncoming,
|
onClick,
|
||||||
onClick,
|
tabIndex,
|
||||||
tabIndex,
|
withContentAbove,
|
||||||
withContentAbove,
|
withContentBelow,
|
||||||
withContentBelow,
|
} = props;
|
||||||
} = this.props;
|
const module = 'embedded-contact';
|
||||||
const module = 'embedded-contact';
|
const direction = isIncoming ? 'incoming' : 'outgoing';
|
||||||
const direction = isIncoming ? 'incoming' : 'outgoing';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-embedded-contact',
|
'module-embedded-contact',
|
||||||
`module-embedded-contact--${direction}`,
|
`module-embedded-contact--${direction}`,
|
||||||
withContentAbove
|
withContentAbove ? 'module-embedded-contact--with-content-above' : null,
|
||||||
? 'module-embedded-contact--with-content-above'
|
withContentBelow ? 'module-embedded-contact--with-content-below' : null
|
||||||
: null,
|
)}
|
||||||
withContentBelow
|
onKeyDown={(event: React.KeyboardEvent) => {
|
||||||
? 'module-embedded-contact--with-content-below'
|
if (event.key !== 'Enter' && event.key !== 'Space') {
|
||||||
: null
|
return;
|
||||||
)}
|
}
|
||||||
onKeyDown={(event: React.KeyboardEvent) => {
|
|
||||||
if (event.key !== 'Enter' && event.key !== 'Space') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onClick={(event: React.MouseEvent) => {
|
onClick={(event: React.MouseEvent) => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
>
|
>
|
||||||
{renderAvatar({ contact, i18n, size: 52, direction })}
|
{renderAvatar({ contact, i18n, size: 52, direction })}
|
||||||
<div className="module-embedded-contact__text-container">
|
<div className="module-embedded-contact__text-container">
|
||||||
{renderName({ contact, isIncoming, module })}
|
{renderName({ contact, isIncoming, module })}
|
||||||
{renderContactShorthand({ contact, isIncoming, module })}
|
{renderContactShorthand({ contact, isIncoming, module })}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
@ -260,6 +260,7 @@ story.add('Mixed Content Types', () => {
|
||||||
width: 112,
|
width: 112,
|
||||||
url: '/fixtures/kitten-4-112-112.jpg',
|
url: '/fixtures/kitten-4-112-112.jpg',
|
||||||
contentType: IMAGE_JPEG,
|
contentType: IMAGE_JPEG,
|
||||||
|
path: 'originalpath',
|
||||||
},
|
},
|
||||||
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
|
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
|
||||||
width: 112,
|
width: 112,
|
||||||
|
|
|
@ -76,6 +76,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
canReply: true,
|
canReply: true,
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
canDeleteForEveryone: overrideProps.canDeleteForEveryone || false,
|
canDeleteForEveryone: overrideProps.canDeleteForEveryone || false,
|
||||||
|
checkForAccount: action('checkForAccount'),
|
||||||
clearSelectedMessage: action('clearSelectedMessage'),
|
clearSelectedMessage: action('clearSelectedMessage'),
|
||||||
collapseMetadata: overrideProps.collapseMetadata,
|
collapseMetadata: overrideProps.collapseMetadata,
|
||||||
conversationColor:
|
conversationColor:
|
||||||
|
@ -90,6 +91,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
disableScroll: overrideProps.disableScroll,
|
disableScroll: overrideProps.disableScroll,
|
||||||
direction: overrideProps.direction || 'incoming',
|
direction: overrideProps.direction || 'incoming',
|
||||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||||
|
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||||
downloadAttachment: action('downloadAttachment'),
|
downloadAttachment: action('downloadAttachment'),
|
||||||
expirationLength:
|
expirationLength:
|
||||||
number('expirationLength', overrideProps.expirationLength || 0) ||
|
number('expirationLength', overrideProps.expirationLength || 0) ||
|
||||||
|
@ -114,6 +116,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
isTapToViewExpired: overrideProps.isTapToViewExpired,
|
isTapToViewExpired: overrideProps.isTapToViewExpired,
|
||||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||||
|
onHeightChange: action('onHeightChange'),
|
||||||
openConversation: action('openConversation'),
|
openConversation: action('openConversation'),
|
||||||
openLink: action('openLink'),
|
openLink: action('openLink'),
|
||||||
previews: overrideProps.previews || [],
|
previews: overrideProps.previews || [],
|
||||||
|
|
|
@ -8,7 +8,11 @@ import { drop, groupBy, orderBy, take } from 'lodash';
|
||||||
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
||||||
import { Manager, Popper, Reference } from 'react-popper';
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
|
|
||||||
import { ConversationType } from '../../state/ducks/conversations';
|
import {
|
||||||
|
ConversationType,
|
||||||
|
ConversationTypeType,
|
||||||
|
InteractionModeType,
|
||||||
|
} from '../../state/ducks/conversations';
|
||||||
import { Avatar } from '../Avatar';
|
import { Avatar } from '../Avatar';
|
||||||
import { Spinner } from '../Spinner';
|
import { Spinner } from '../Spinner';
|
||||||
import { MessageBody } from './MessageBody';
|
import { MessageBody } from './MessageBody';
|
||||||
|
@ -80,15 +84,9 @@ export const MessageStatuses = [
|
||||||
] as const;
|
] as const;
|
||||||
export type MessageStatusType = typeof MessageStatuses[number];
|
export type MessageStatusType = typeof MessageStatuses[number];
|
||||||
|
|
||||||
export const InteractionModes = ['mouse', 'keyboard'] as const;
|
|
||||||
export type InteractionModeType = typeof InteractionModes[number];
|
|
||||||
|
|
||||||
export const Directions = ['incoming', 'outgoing'] as const;
|
export const Directions = ['incoming', 'outgoing'] as const;
|
||||||
export type DirectionType = typeof Directions[number];
|
export type DirectionType = typeof Directions[number];
|
||||||
|
|
||||||
export const ConversationTypes = ['direct', 'group'] as const;
|
|
||||||
export type ConversationTypesType = typeof ConversationTypes[number];
|
|
||||||
|
|
||||||
export type AudioAttachmentProps = {
|
export type AudioAttachmentProps = {
|
||||||
id: string;
|
id: string;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -133,7 +131,7 @@ export type PropsData = {
|
||||||
| 'unblurredAvatarPath'
|
| 'unblurredAvatarPath'
|
||||||
>;
|
>;
|
||||||
reducedMotion?: boolean;
|
reducedMotion?: boolean;
|
||||||
conversationType: ConversationTypesType;
|
conversationType: ConversationTypeType;
|
||||||
attachments?: Array<AttachmentType>;
|
attachments?: Array<AttachmentType>;
|
||||||
quote?: {
|
quote?: {
|
||||||
conversationColor: ConversationColorType;
|
conversationColor: ConversationColorType;
|
||||||
|
@ -185,6 +183,9 @@ export type PropsHousekeeping = {
|
||||||
|
|
||||||
export type PropsActions = {
|
export type PropsActions = {
|
||||||
clearSelectedMessage: () => unknown;
|
clearSelectedMessage: () => unknown;
|
||||||
|
doubleCheckMissingQuoteReference: (messageId: string) => unknown;
|
||||||
|
onHeightChange: () => unknown;
|
||||||
|
checkForAccount: (identifier: string) => unknown;
|
||||||
|
|
||||||
reactToMessage: (
|
reactToMessage: (
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -405,18 +406,21 @@ export class Message extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { expirationLength } = this.props;
|
const { expirationLength } = this.props;
|
||||||
if (!expirationLength) {
|
if (expirationLength) {
|
||||||
return;
|
const increment = getIncrement(expirationLength);
|
||||||
|
const checkFrequency = Math.max(EXPIRATION_CHECK_MINIMUM, increment);
|
||||||
|
|
||||||
|
this.checkExpired();
|
||||||
|
|
||||||
|
this.expirationCheckInterval = setInterval(() => {
|
||||||
|
this.checkExpired();
|
||||||
|
}, checkFrequency);
|
||||||
}
|
}
|
||||||
|
|
||||||
const increment = getIncrement(expirationLength);
|
const { contact, checkForAccount } = this.props;
|
||||||
const checkFrequency = Math.max(EXPIRATION_CHECK_MINIMUM, increment);
|
if (contact && contact.firstNumber && !contact.isNumberOnSignal) {
|
||||||
|
checkForAccount(contact.firstNumber);
|
||||||
this.checkExpired();
|
}
|
||||||
|
|
||||||
this.expirationCheckInterval = setInterval(() => {
|
|
||||||
this.checkExpired();
|
|
||||||
}, checkFrequency);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
|
@ -448,12 +452,31 @@ export class Message extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.checkExpired();
|
this.checkExpired();
|
||||||
|
this.checkForHeightChange(prevProps);
|
||||||
|
|
||||||
if (canDeleteForEveryone !== prevProps.canDeleteForEveryone) {
|
if (canDeleteForEveryone !== prevProps.canDeleteForEveryone) {
|
||||||
this.startDeleteForEveryoneTimer();
|
this.startDeleteForEveryoneTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public checkForHeightChange(prevProps: Props): void {
|
||||||
|
const { contact, onHeightChange } = this.props;
|
||||||
|
const willRenderSendMessageButton = Boolean(
|
||||||
|
contact && contact.firstNumber && contact.isNumberOnSignal
|
||||||
|
);
|
||||||
|
|
||||||
|
const { contact: previousContact } = prevProps;
|
||||||
|
const previouslyRenderedSendMessageButton = Boolean(
|
||||||
|
previousContact &&
|
||||||
|
previousContact.firstNumber &&
|
||||||
|
previousContact.isNumberOnSignal
|
||||||
|
);
|
||||||
|
|
||||||
|
if (willRenderSendMessageButton !== previouslyRenderedSendMessageButton) {
|
||||||
|
onHeightChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public startSelectedTimer(): void {
|
public startSelectedTimer(): void {
|
||||||
const { clearSelectedMessage, interactionMode } = this.props;
|
const { clearSelectedMessage, interactionMode } = this.props;
|
||||||
const { isSelected } = this.state;
|
const { isSelected } = this.state;
|
||||||
|
@ -1064,7 +1087,9 @@ export class Message extends React.Component<Props, State> {
|
||||||
customColor,
|
customColor,
|
||||||
direction,
|
direction,
|
||||||
disableScroll,
|
disableScroll,
|
||||||
|
doubleCheckMissingQuoteReference,
|
||||||
i18n,
|
i18n,
|
||||||
|
id,
|
||||||
quote,
|
quote,
|
||||||
scrollToQuotedMessage,
|
scrollToQuotedMessage,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -1104,6 +1129,9 @@ export class Message extends React.Component<Props, State> {
|
||||||
referencedMessageNotFound={referencedMessageNotFound}
|
referencedMessageNotFound={referencedMessageNotFound}
|
||||||
isFromMe={quote.isFromMe}
|
isFromMe={quote.isFromMe}
|
||||||
withContentAbove={withContentAbove}
|
withContentAbove={withContentAbove}
|
||||||
|
doubleCheckMissingQuoteReference={() =>
|
||||||
|
doubleCheckMissingQuoteReference(id)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1127,7 +1155,9 @@ export class Message extends React.Component<Props, State> {
|
||||||
conversationType === 'group' && direction === 'incoming';
|
conversationType === 'group' && direction === 'incoming';
|
||||||
const withContentBelow = withCaption || !collapseMetadata;
|
const withContentBelow = withCaption || !collapseMetadata;
|
||||||
|
|
||||||
const otherContent = (contact && contact.signalAccount) || withCaption;
|
const otherContent =
|
||||||
|
(contact && contact.firstNumber && contact.isNumberOnSignal) ||
|
||||||
|
withCaption;
|
||||||
const tabIndex = otherContent ? 0 : -1;
|
const tabIndex = otherContent ? 0 : -1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1136,7 +1166,7 @@ export class Message extends React.Component<Props, State> {
|
||||||
isIncoming={direction === 'incoming'}
|
isIncoming={direction === 'incoming'}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showContactDetail({ contact, signalAccount: contact.signalAccount });
|
showContactDetail({ contact, signalAccount: contact.firstNumber });
|
||||||
}}
|
}}
|
||||||
withContentAbove={withContentAbove}
|
withContentAbove={withContentAbove}
|
||||||
withContentBelow={withContentBelow}
|
withContentBelow={withContentBelow}
|
||||||
|
@ -1147,18 +1177,18 @@ export class Message extends React.Component<Props, State> {
|
||||||
|
|
||||||
public renderSendMessageButton(): JSX.Element | null {
|
public renderSendMessageButton(): JSX.Element | null {
|
||||||
const { contact, openConversation, i18n } = this.props;
|
const { contact, openConversation, i18n } = this.props;
|
||||||
if (!contact || !contact.signalAccount) {
|
if (!contact) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { firstNumber, isNumberOnSignal } = contact;
|
||||||
|
if (!firstNumber || !isNumberOnSignal) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => openConversation(firstNumber)}
|
||||||
if (contact.signalAccount) {
|
|
||||||
openConversation(contact.signalAccount);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="module-message__send-message-button"
|
className="module-message__send-message-button"
|
||||||
>
|
>
|
||||||
{i18n('sendMessageToContact')}
|
{i18n('sendMessageToContact')}
|
||||||
|
@ -2181,15 +2211,15 @@ export class Message extends React.Component<Props, State> {
|
||||||
this.audioButtonRef.current.click();
|
this.audioButtonRef.current.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contact && contact.signalAccount) {
|
if (contact && contact.firstNumber && contact.isNumberOnSignal) {
|
||||||
openConversation(contact.signalAccount);
|
openConversation(contact.firstNumber);
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contact) {
|
if (contact) {
|
||||||
showContactDetail({ contact, signalAccount: contact.signalAccount });
|
showContactDetail({ contact, signalAccount: contact.firstNumber });
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
|
@ -61,26 +61,32 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
sendAnyway: action('onSendAnyway'),
|
sendAnyway: action('onSendAnyway'),
|
||||||
showSafetyNumber: action('onShowSafetyNumber'),
|
showSafetyNumber: action('onShowSafetyNumber'),
|
||||||
|
|
||||||
clearSelectedMessage: () => null,
|
checkForAccount: action('checkForAccount'),
|
||||||
|
clearSelectedMessage: action('clearSelectedMessage'),
|
||||||
deleteMessage: action('deleteMessage'),
|
deleteMessage: action('deleteMessage'),
|
||||||
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
||||||
displayTapToViewMessage: () => null,
|
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||||
downloadAttachment: () => null,
|
downloadAttachment: action('downloadAttachment'),
|
||||||
|
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||||
openConversation: () => null,
|
openConversation: action('openConversation'),
|
||||||
openLink: () => null,
|
openLink: action('openLink'),
|
||||||
reactToMessage: () => null,
|
reactToMessage: action('reactToMessage'),
|
||||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||||
renderEmojiPicker: () => <div />,
|
renderEmojiPicker: () => <div />,
|
||||||
replyToMessage: () => null,
|
replyToMessage: action('replyToMessage'),
|
||||||
retrySend: () => null,
|
retrySend: action('retrySend'),
|
||||||
showContactDetail: () => null,
|
showContactDetail: action('showContactDetail'),
|
||||||
showContactModal: () => null,
|
showContactModal: action('showContactModal'),
|
||||||
showExpiredIncomingTapToViewToast: () => null,
|
showExpiredIncomingTapToViewToast: action(
|
||||||
showExpiredOutgoingTapToViewToast: () => null,
|
'showExpiredIncomingTapToViewToast'
|
||||||
showForwardMessageModal: () => null,
|
),
|
||||||
showVisualAttachment: () => null,
|
showExpiredOutgoingTapToViewToast: action(
|
||||||
|
'showExpiredOutgoingTapToViewToast'
|
||||||
|
),
|
||||||
|
showForwardMessageModal: action('showForwardMessageModal'),
|
||||||
|
showVisualAttachment: action('showVisualAttachment'),
|
||||||
});
|
});
|
||||||
|
|
||||||
story.add('Delivered Incoming', () => {
|
story.add('Delivered Incoming', () => {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { noop } from 'lodash';
|
||||||
|
|
||||||
import { GlobalAudioProvider } from '../GlobalAudioContext';
|
import { GlobalAudioProvider } from '../GlobalAudioContext';
|
||||||
import { Avatar } from '../Avatar';
|
import { Avatar } from '../Avatar';
|
||||||
|
@ -55,11 +56,13 @@ export type Props = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
} & Pick<
|
} & Pick<
|
||||||
MessagePropsType,
|
MessagePropsType,
|
||||||
|
| 'checkForAccount'
|
||||||
| 'clearSelectedMessage'
|
| 'clearSelectedMessage'
|
||||||
| 'deleteMessage'
|
| 'deleteMessage'
|
||||||
| 'deleteMessageForEveryone'
|
| 'deleteMessageForEveryone'
|
||||||
| 'displayTapToViewMessage'
|
| 'displayTapToViewMessage'
|
||||||
| 'downloadAttachment'
|
| 'downloadAttachment'
|
||||||
|
| 'doubleCheckMissingQuoteReference'
|
||||||
| 'interactionMode'
|
| 'interactionMode'
|
||||||
| 'kickOffAttachmentDownload'
|
| 'kickOffAttachmentDownload'
|
||||||
| 'markAttachmentAsCorrupted'
|
| 'markAttachmentAsCorrupted'
|
||||||
|
@ -233,12 +236,14 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
receivedAt,
|
receivedAt,
|
||||||
sentAt,
|
sentAt,
|
||||||
|
|
||||||
|
checkForAccount,
|
||||||
clearSelectedMessage,
|
clearSelectedMessage,
|
||||||
contactNameColor,
|
contactNameColor,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
deleteMessageForEveryone,
|
deleteMessageForEveryone,
|
||||||
displayTapToViewMessage,
|
displayTapToViewMessage,
|
||||||
downloadAttachment,
|
downloadAttachment,
|
||||||
|
doubleCheckMissingQuoteReference,
|
||||||
i18n,
|
i18n,
|
||||||
interactionMode,
|
interactionMode,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
|
@ -265,6 +270,7 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
<GlobalAudioProvider conversationId={message.conversationId}>
|
<GlobalAudioProvider conversationId={message.conversationId}>
|
||||||
<Message
|
<Message
|
||||||
{...message}
|
{...message}
|
||||||
|
checkForAccount={checkForAccount}
|
||||||
clearSelectedMessage={clearSelectedMessage}
|
clearSelectedMessage={clearSelectedMessage}
|
||||||
contactNameColor={contactNameColor}
|
contactNameColor={contactNameColor}
|
||||||
deleteMessage={deleteMessage}
|
deleteMessage={deleteMessage}
|
||||||
|
@ -273,10 +279,14 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
disableScroll
|
disableScroll
|
||||||
displayTapToViewMessage={displayTapToViewMessage}
|
displayTapToViewMessage={displayTapToViewMessage}
|
||||||
downloadAttachment={downloadAttachment}
|
downloadAttachment={downloadAttachment}
|
||||||
|
doubleCheckMissingQuoteReference={
|
||||||
|
doubleCheckMissingQuoteReference
|
||||||
|
}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
interactionMode={interactionMode}
|
interactionMode={interactionMode}
|
||||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||||
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
||||||
|
onHeightChange={noop}
|
||||||
openConversation={openConversation}
|
openConversation={openConversation}
|
||||||
openLink={openLink}
|
openLink={openLink}
|
||||||
reactToMessage={reactToMessage}
|
reactToMessage={reactToMessage}
|
||||||
|
|
|
@ -35,39 +35,48 @@ const defaultMessageProps: MessagesProps = {
|
||||||
canReply: true,
|
canReply: true,
|
||||||
canDeleteForEveryone: true,
|
canDeleteForEveryone: true,
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
clearSelectedMessage: () => null,
|
checkForAccount: action('checkForAccount'),
|
||||||
|
clearSelectedMessage: action('default--clearSelectedMessage'),
|
||||||
conversationColor: 'crimson',
|
conversationColor: 'crimson',
|
||||||
conversationId: 'conversationId',
|
conversationId: 'conversationId',
|
||||||
conversationType: 'direct', // override
|
conversationType: 'direct', // override
|
||||||
deleteMessage: () => null,
|
deleteMessage: action('default--deleteMessage'),
|
||||||
deleteMessageForEveryone: () => null,
|
deleteMessageForEveryone: action('default--deleteMessageForEveryone'),
|
||||||
direction: 'incoming',
|
direction: 'incoming',
|
||||||
displayTapToViewMessage: () => null,
|
displayTapToViewMessage: action('default--displayTapToViewMessage'),
|
||||||
downloadAttachment: () => null,
|
downloadAttachment: action('default--downloadAttachment'),
|
||||||
|
doubleCheckMissingQuoteReference: action(
|
||||||
|
'default--doubleCheckMissingQuoteReference'
|
||||||
|
),
|
||||||
i18n,
|
i18n,
|
||||||
id: 'messageId',
|
id: 'messageId',
|
||||||
interactionMode: 'keyboard',
|
interactionMode: 'keyboard',
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
isMessageRequestAccepted: true,
|
isMessageRequestAccepted: true,
|
||||||
kickOffAttachmentDownload: () => null,
|
kickOffAttachmentDownload: action('default--kickOffAttachmentDownload'),
|
||||||
markAttachmentAsCorrupted: () => null,
|
markAttachmentAsCorrupted: action('default--markAttachmentAsCorrupted'),
|
||||||
openConversation: () => null,
|
onHeightChange: action('onHeightChange'),
|
||||||
openLink: () => null,
|
openConversation: action('default--openConversation'),
|
||||||
|
openLink: action('default--openLink'),
|
||||||
previews: [],
|
previews: [],
|
||||||
reactToMessage: () => null,
|
reactToMessage: action('default--reactToMessage'),
|
||||||
renderEmojiPicker: () => <div />,
|
renderEmojiPicker: () => <div />,
|
||||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||||
replyToMessage: () => null,
|
replyToMessage: action('default--replyToMessage'),
|
||||||
retrySend: () => null,
|
retrySend: action('default--retrySend'),
|
||||||
scrollToQuotedMessage: () => null,
|
scrollToQuotedMessage: action('default--scrollToQuotedMessage'),
|
||||||
selectMessage: () => null,
|
selectMessage: action('default--selectMessage'),
|
||||||
showContactDetail: () => null,
|
showContactDetail: action('default--showContactDetail'),
|
||||||
showContactModal: () => null,
|
showContactModal: action('default--showContactModal'),
|
||||||
showExpiredIncomingTapToViewToast: () => null,
|
showExpiredIncomingTapToViewToast: action(
|
||||||
showExpiredOutgoingTapToViewToast: () => null,
|
'showExpiredIncomingTapToViewToast'
|
||||||
showForwardMessageModal: () => null,
|
),
|
||||||
showMessageDetail: () => null,
|
showExpiredOutgoingTapToViewToast: action(
|
||||||
showVisualAttachment: () => null,
|
'showExpiredOutgoingTapToViewToast'
|
||||||
|
),
|
||||||
|
showForwardMessageModal: action('default--showForwardMessageModal'),
|
||||||
|
showMessageDetail: action('default--showMessageDetail'),
|
||||||
|
showVisualAttachment: action('default--showVisualAttachment'),
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
text: 'This is really interesting.',
|
text: 'This is really interesting.',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
@ -125,6 +134,9 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
),
|
),
|
||||||
authorTitle: text('authorTitle', overrideProps.authorTitle || ''),
|
authorTitle: text('authorTitle', overrideProps.authorTitle || ''),
|
||||||
conversationColor: overrideProps.conversationColor || 'forest',
|
conversationColor: overrideProps.conversationColor || 'forest',
|
||||||
|
doubleCheckMissingQuoteReference:
|
||||||
|
overrideProps.doubleCheckMissingQuoteReference ||
|
||||||
|
action('doubleCheckMissingQuoteReference'),
|
||||||
i18n,
|
i18n,
|
||||||
isFromMe: boolean('isFromMe', overrideProps.isFromMe || false),
|
isFromMe: boolean('isFromMe', overrideProps.isFromMe || false),
|
||||||
isIncoming: boolean('isIncoming', overrideProps.isIncoming || false),
|
isIncoming: boolean('isIncoming', overrideProps.isIncoming || false),
|
||||||
|
|
|
@ -33,6 +33,7 @@ export type Props = {
|
||||||
rawAttachment?: QuotedAttachmentType;
|
rawAttachment?: QuotedAttachmentType;
|
||||||
isViewOnce: boolean;
|
isViewOnce: boolean;
|
||||||
referencedMessageNotFound: boolean;
|
referencedMessageNotFound: boolean;
|
||||||
|
doubleCheckMissingQuoteReference: () => unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
@ -41,7 +42,7 @@ type State = {
|
||||||
|
|
||||||
export type QuotedAttachmentType = {
|
export type QuotedAttachmentType = {
|
||||||
contentType: MIME.MIMEType;
|
contentType: MIME.MIMEType;
|
||||||
fileName: string;
|
fileName?: string;
|
||||||
/** Not included in protobuf */
|
/** Not included in protobuf */
|
||||||
isVoiceMessage: boolean;
|
isVoiceMessage: boolean;
|
||||||
thumbnail?: Attachment;
|
thumbnail?: Attachment;
|
||||||
|
@ -125,6 +126,17 @@ export class Quote extends React.Component<Props, State> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
const {
|
||||||
|
doubleCheckMissingQuoteReference,
|
||||||
|
referencedMessageNotFound,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (referencedMessageNotFound) {
|
||||||
|
doubleCheckMissingQuoteReference();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public handleKeyDown = (
|
public handleKeyDown = (
|
||||||
event: React.KeyboardEvent<HTMLButtonElement>
|
event: React.KeyboardEvent<HTMLButtonElement>
|
||||||
): void => {
|
): void => {
|
||||||
|
|
|
@ -298,6 +298,7 @@ const actions = () => ({
|
||||||
acknowledgeGroupMemberNameCollisions: action(
|
acknowledgeGroupMemberNameCollisions: action(
|
||||||
'acknowledgeGroupMemberNameCollisions'
|
'acknowledgeGroupMemberNameCollisions'
|
||||||
),
|
),
|
||||||
|
checkForAccount: action('checkForAccount'),
|
||||||
clearChangedMessages: action('clearChangedMessages'),
|
clearChangedMessages: action('clearChangedMessages'),
|
||||||
clearInvitedConversationsForNewlyCreatedGroup: action(
|
clearInvitedConversationsForNewlyCreatedGroup: action(
|
||||||
'clearInvitedConversationsForNewlyCreatedGroup'
|
'clearInvitedConversationsForNewlyCreatedGroup'
|
||||||
|
@ -327,7 +328,9 @@ const actions = () => ({
|
||||||
showVisualAttachment: action('showVisualAttachment'),
|
showVisualAttachment: action('showVisualAttachment'),
|
||||||
downloadAttachment: action('downloadAttachment'),
|
downloadAttachment: action('downloadAttachment'),
|
||||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||||
|
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||||
|
|
||||||
|
onHeightChange: action('onHeightChange'),
|
||||||
openLink: action('openLink'),
|
openLink: action('openLink'),
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
showExpiredIncomingTapToViewToast: action(
|
showExpiredIncomingTapToViewToast: action(
|
||||||
|
|
|
@ -104,6 +104,7 @@ type PropsHousekeepingType = {
|
||||||
renderItem: (
|
renderItem: (
|
||||||
id: string,
|
id: string,
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
|
onHeightChange: (messageId: string) => unknown,
|
||||||
actions: Record<string, unknown>
|
actions: Record<string, unknown>
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
renderLastSeenIndicator: (id: string) => JSX.Element;
|
renderLastSeenIndicator: (id: string) => JSX.Element;
|
||||||
|
@ -367,6 +368,22 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
this.resize(0);
|
this.resize(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public resizeMessage = (messageId: string): void => {
|
||||||
|
const { items } = this.props;
|
||||||
|
|
||||||
|
if (!items || !items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = items.findIndex(item => item === messageId);
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = this.fromItemIndexToRow(index);
|
||||||
|
this.resize(row);
|
||||||
|
};
|
||||||
|
|
||||||
public onScroll = (data: OnScrollParamsType): void => {
|
public onScroll = (data: OnScrollParamsType): void => {
|
||||||
// Ignore scroll events generated as react-virtualized recursively scrolls and
|
// Ignore scroll events generated as react-virtualized recursively scrolls and
|
||||||
// re-measures to get us where we want to go.
|
// re-measures to get us where we want to go.
|
||||||
|
@ -711,7 +728,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
||||||
style={styleWithWidth}
|
style={styleWithWidth}
|
||||||
role="row"
|
role="row"
|
||||||
>
|
>
|
||||||
{renderItem(messageId, id, this.props)}
|
{renderItem(messageId, id, this.resizeMessage, this.props)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ const getDefaultProps = () => ({
|
||||||
interactionMode: 'keyboard' as const,
|
interactionMode: 'keyboard' as const,
|
||||||
selectMessage: action('selectMessage'),
|
selectMessage: action('selectMessage'),
|
||||||
reactToMessage: action('reactToMessage'),
|
reactToMessage: action('reactToMessage'),
|
||||||
|
checkForAccount: action('checkForAccount'),
|
||||||
clearSelectedMessage: action('clearSelectedMessage'),
|
clearSelectedMessage: action('clearSelectedMessage'),
|
||||||
contactSupport: action('contactSupport'),
|
contactSupport: action('contactSupport'),
|
||||||
replyToMessage: action('replyToMessage'),
|
replyToMessage: action('replyToMessage'),
|
||||||
|
@ -63,12 +64,14 @@ const getDefaultProps = () => ({
|
||||||
showVisualAttachment: action('showVisualAttachment'),
|
showVisualAttachment: action('showVisualAttachment'),
|
||||||
downloadAttachment: action('downloadAttachment'),
|
downloadAttachment: action('downloadAttachment'),
|
||||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||||
|
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||||
showExpiredIncomingTapToViewToast: action(
|
showExpiredIncomingTapToViewToast: action(
|
||||||
'showExpiredIncomingTapToViewToast'
|
'showExpiredIncomingTapToViewToast'
|
||||||
),
|
),
|
||||||
showExpiredOutgoingTapToViewToast: action(
|
showExpiredOutgoingTapToViewToast: action(
|
||||||
'showExpiredIncomingTapToViewToast'
|
'showExpiredIncomingTapToViewToast'
|
||||||
),
|
),
|
||||||
|
onHeightChange: action('onHeightChange'),
|
||||||
openLink: action('openLink'),
|
openLink: action('openLink'),
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
downloadNewVersion: action('downloadNewVersion'),
|
downloadNewVersion: action('downloadNewVersion'),
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { LocalizerType, ThemeType } from '../../types/Util';
|
import { LocalizerType, ThemeType } from '../../types/Util';
|
||||||
|
|
||||||
|
import { InteractionModeType } from '../../state/ducks/conversations';
|
||||||
import {
|
import {
|
||||||
Message,
|
Message,
|
||||||
InteractionModeType,
|
|
||||||
Props as AllMessageProps,
|
Props as AllMessageProps,
|
||||||
PropsActions as MessageActionsType,
|
PropsActions as MessageActionsType,
|
||||||
PropsData as MessageProps,
|
PropsData as MessageProps,
|
||||||
|
|
|
@ -1,16 +1,22 @@
|
||||||
// Copyright 2019-2020 Signal Messenger, LLC
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
/* global
|
import { isFunction, isNumber, omit } from 'lodash';
|
||||||
Whisper,
|
import { v4 as getGuid } from 'uuid';
|
||||||
Signal,
|
|
||||||
setTimeout,
|
import dataInterface from '../sql/Client';
|
||||||
clearTimeout,
|
import { downloadAttachment } from '../util/downloadAttachment';
|
||||||
MessageController
|
import { stringFromBytes } from '../Crypto';
|
||||||
*/
|
import MessageReceiver from '../textsecure/MessageReceiver';
|
||||||
|
import {
|
||||||
|
AttachmentDownloadJobType,
|
||||||
|
AttachmentDownloadJobTypeType,
|
||||||
|
} from '../sql/Interface';
|
||||||
|
|
||||||
|
import { MessageModel } from '../models/messages';
|
||||||
|
import { AttachmentType } from '../types/Attachment';
|
||||||
|
import { LoggerType } from '../window.d';
|
||||||
|
|
||||||
const { isFunction, isNumber, omit } = require('lodash');
|
|
||||||
const getGuid = require('uuid/v4');
|
|
||||||
const {
|
const {
|
||||||
getMessageById,
|
getMessageById,
|
||||||
getNextAttachmentDownloadJobs,
|
getNextAttachmentDownloadJobs,
|
||||||
|
@ -19,15 +25,7 @@ const {
|
||||||
saveAttachmentDownloadJob,
|
saveAttachmentDownloadJob,
|
||||||
saveMessage,
|
saveMessage,
|
||||||
setAttachmentDownloadJobPending,
|
setAttachmentDownloadJobPending,
|
||||||
} = require('../../ts/sql/Client').default;
|
} = dataInterface;
|
||||||
const { downloadAttachment } = require('../../ts/util/downloadAttachment');
|
|
||||||
const { stringFromBytes } = require('../../ts/Crypto');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
start,
|
|
||||||
stop,
|
|
||||||
addJob,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MAX_ATTACHMENT_JOB_PARALLELISM = 3;
|
const MAX_ATTACHMENT_JOB_PARALLELISM = 3;
|
||||||
|
|
||||||
|
@ -36,19 +34,27 @@ const MINUTE = 60 * SECOND;
|
||||||
const HOUR = 60 * MINUTE;
|
const HOUR = 60 * MINUTE;
|
||||||
const TICK_INTERVAL = MINUTE;
|
const TICK_INTERVAL = MINUTE;
|
||||||
|
|
||||||
const RETRY_BACKOFF = {
|
const RETRY_BACKOFF: Record<number, number> = {
|
||||||
1: 30 * SECOND,
|
1: 30 * SECOND,
|
||||||
2: 30 * MINUTE,
|
2: 30 * MINUTE,
|
||||||
3: 6 * HOUR,
|
3: 6 * HOUR,
|
||||||
};
|
};
|
||||||
|
|
||||||
let enabled = false;
|
let enabled = false;
|
||||||
let timeout;
|
let timeout: NodeJS.Timeout | null;
|
||||||
let getMessageReceiver;
|
let getMessageReceiver: () => MessageReceiver | undefined;
|
||||||
let logger;
|
let logger: LoggerType;
|
||||||
const _activeAttachmentDownloadJobs = {};
|
const _activeAttachmentDownloadJobs: Record<
|
||||||
|
string,
|
||||||
|
Promise<void> | undefined
|
||||||
|
> = {};
|
||||||
|
|
||||||
async function start(options = {}) {
|
type StartOptionsType = {
|
||||||
|
getMessageReceiver: () => MessageReceiver | undefined;
|
||||||
|
logger: LoggerType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function start(options: StartOptionsType): Promise<void> {
|
||||||
({ getMessageReceiver, logger } = options);
|
({ getMessageReceiver, logger } = options);
|
||||||
if (!isFunction(getMessageReceiver)) {
|
if (!isFunction(getMessageReceiver)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -66,7 +72,7 @@ async function start(options = {}) {
|
||||||
_tick();
|
_tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stop() {
|
export async function stop(): Promise<void> {
|
||||||
// If `.start()` wasn't called - the `logger` is `undefined`
|
// If `.start()` wasn't called - the `logger` is `undefined`
|
||||||
if (logger) {
|
if (logger) {
|
||||||
logger.info('attachment_downloads/stop: disabling');
|
logger.info('attachment_downloads/stop: disabling');
|
||||||
|
@ -78,7 +84,10 @@ async function stop() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addJob(attachment, job = {}) {
|
export async function addJob(
|
||||||
|
attachment: AttachmentType,
|
||||||
|
job: { messageId: string; type: AttachmentDownloadJobTypeType; index: number }
|
||||||
|
): Promise<AttachmentType> {
|
||||||
if (!attachment) {
|
if (!attachment) {
|
||||||
throw new Error('attachments_download/addJob: attachment is required');
|
throw new Error('attachments_download/addJob: attachment is required');
|
||||||
}
|
}
|
||||||
|
@ -96,7 +105,7 @@ async function addJob(attachment, job = {}) {
|
||||||
|
|
||||||
const id = getGuid();
|
const id = getGuid();
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const toSave = {
|
const toSave: AttachmentDownloadJobType = {
|
||||||
...job,
|
...job,
|
||||||
id,
|
id,
|
||||||
attachment,
|
attachment,
|
||||||
|
@ -116,7 +125,7 @@ async function addJob(attachment, job = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _tick() {
|
async function _tick(): Promise<void> {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = null;
|
timeout = null;
|
||||||
|
@ -126,7 +135,7 @@ async function _tick() {
|
||||||
timeout = setTimeout(_tick, TICK_INTERVAL);
|
timeout = setTimeout(_tick, TICK_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _maybeStartJob() {
|
async function _maybeStartJob(): Promise<void> {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
logger.info('attachment_downloads/_maybeStartJob: not enabled, returning');
|
logger.info('attachment_downloads/_maybeStartJob: not enabled, returning');
|
||||||
return;
|
return;
|
||||||
|
@ -178,8 +187,13 @@ async function _maybeStartJob() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _runJob(job) {
|
async function _runJob(job?: AttachmentDownloadJobType): Promise<void> {
|
||||||
const { id, messageId, attachment, type, index, attempts } = job || {};
|
if (!job) {
|
||||||
|
window.log.warn('_runJob: Job was missing!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, messageId, attachment, type, index, attempts } = job;
|
||||||
let message;
|
let message;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -192,16 +206,16 @@ async function _runJob(job) {
|
||||||
logger.info(`attachment_downloads/_runJob for job id ${id}`);
|
logger.info(`attachment_downloads/_runJob for job id ${id}`);
|
||||||
|
|
||||||
const found =
|
const found =
|
||||||
MessageController.getById(messageId) ||
|
window.MessageController.getById(messageId) ||
|
||||||
(await getMessageById(messageId, {
|
(await getMessageById(messageId, {
|
||||||
Message: Whisper.Message,
|
Message: window.Whisper.Message,
|
||||||
}));
|
}));
|
||||||
if (!found) {
|
if (!found) {
|
||||||
logger.error('_runJob: Source message not found, deleting job');
|
logger.error('_runJob: Source message not found, deleting job');
|
||||||
await _finishJob(null, id);
|
await _finishJob(null, id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
message = MessageController.register(found.id, found);
|
message = window.MessageController.register(found.id, found);
|
||||||
|
|
||||||
const pending = true;
|
const pending = true;
|
||||||
await setAttachmentDownloadJobPending(id, pending);
|
await setAttachmentDownloadJobPending(id, pending);
|
||||||
|
@ -231,7 +245,7 @@ async function _runJob(job) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const upgradedAttachment = await Signal.Migrations.processNewAttachment(
|
const upgradedAttachment = await window.Signal.Migrations.processNewAttachment(
|
||||||
downloaded
|
downloaded
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -239,11 +253,12 @@ async function _runJob(job) {
|
||||||
|
|
||||||
await _finishJob(message, id);
|
await _finishJob(message, id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const logId = message ? message.idForLogging() : id || '<no id>';
|
||||||
const currentAttempt = (attempts || 0) + 1;
|
const currentAttempt = (attempts || 0) + 1;
|
||||||
|
|
||||||
if (currentAttempt >= 3) {
|
if (currentAttempt >= 3) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`_runJob: ${currentAttempt} failed attempts, marking attachment ${id} from message ${message.idForLogging()} as permament error:`,
|
`_runJob: ${currentAttempt} failed attempts, marking attachment ${id} from message ${logId} as permament error:`,
|
||||||
error && error.stack ? error.stack : error
|
error && error.stack ? error.stack : error
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -258,7 +273,7 @@ async function _runJob(job) {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(
|
logger.error(
|
||||||
`_runJob: Failed to download attachment type ${type} for message ${message.idForLogging()}, attempt ${currentAttempt}:`,
|
`_runJob: Failed to download attachment type ${type} for message ${logId}, attempt ${currentAttempt}:`,
|
||||||
error && error.stack ? error.stack : error
|
error && error.stack ? error.stack : error
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -266,7 +281,8 @@ async function _runJob(job) {
|
||||||
...job,
|
...job,
|
||||||
pending: 0,
|
pending: 0,
|
||||||
attempts: currentAttempt,
|
attempts: currentAttempt,
|
||||||
timestamp: Date.now() + RETRY_BACKOFF[currentAttempt],
|
timestamp:
|
||||||
|
Date.now() + (RETRY_BACKOFF[currentAttempt] || RETRY_BACKOFF[3]),
|
||||||
};
|
};
|
||||||
|
|
||||||
await saveAttachmentDownloadJob(failedJob);
|
await saveAttachmentDownloadJob(failedJob);
|
||||||
|
@ -275,11 +291,14 @@ async function _runJob(job) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _finishJob(message, id) {
|
async function _finishJob(
|
||||||
|
message: MessageModel | null | undefined,
|
||||||
|
id: string
|
||||||
|
): Promise<void> {
|
||||||
if (message) {
|
if (message) {
|
||||||
logger.info(`attachment_downloads/_finishJob for job id: ${id}`);
|
logger.info(`attachment_downloads/_finishJob for job id: ${id}`);
|
||||||
await saveMessage(message.attributes, {
|
await saveMessage(message.attributes, {
|
||||||
Message: Whisper.Message,
|
Message: window.Whisper.Message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,18 +307,22 @@ async function _finishJob(message, id) {
|
||||||
_maybeStartJob();
|
_maybeStartJob();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveJobCount() {
|
function getActiveJobCount(): number {
|
||||||
return Object.keys(_activeAttachmentDownloadJobs).length;
|
return Object.keys(_activeAttachmentDownloadJobs).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _markAttachmentAsError(attachment) {
|
function _markAttachmentAsError(attachment: AttachmentType): AttachmentType {
|
||||||
return {
|
return {
|
||||||
...omit(attachment, ['key', 'digest', 'id']),
|
...omit(attachment, ['key', 'digest', 'id']),
|
||||||
error: true,
|
error: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _addAttachmentToMessage(message, attachment, { type, index }) {
|
async function _addAttachmentToMessage(
|
||||||
|
message: MessageModel | null | undefined,
|
||||||
|
attachment: AttachmentType,
|
||||||
|
{ type, index }: { type: AttachmentDownloadJobTypeType; index: number }
|
||||||
|
): Promise<void> {
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -308,13 +331,17 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
|
||||||
|
|
||||||
if (type === 'long-message') {
|
if (type === 'long-message') {
|
||||||
try {
|
try {
|
||||||
const { data } = await Signal.Migrations.loadAttachmentData(attachment);
|
const { data } = await window.Signal.Migrations.loadAttachmentData(
|
||||||
|
attachment
|
||||||
|
);
|
||||||
message.set({
|
message.set({
|
||||||
body: attachment.isError ? message.get('body') : stringFromBytes(data),
|
body: attachment.error ? message.get('body') : stringFromBytes(data),
|
||||||
bodyPending: false,
|
bodyPending: false,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
Signal.Migrations.deleteAttachmentData(attachment.path);
|
if (attachment.path) {
|
||||||
|
window.Signal.Migrations.deleteAttachmentData(attachment.path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -326,7 +353,7 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
|
||||||
`_addAttachmentToMessage: attachments didn't exist or ${index} was too large`
|
`_addAttachmentToMessage: attachments didn't exist or ${index} was too large`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_checkOldAttachment(attachments, index, attachment, logPrefix);
|
_checkOldAttachment(attachments, index.toString(), logPrefix);
|
||||||
|
|
||||||
const newAttachments = [...attachments];
|
const newAttachments = [...attachments];
|
||||||
newAttachments[index] = attachment;
|
newAttachments[index] = attachment;
|
||||||
|
@ -348,7 +375,7 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
|
||||||
throw new Error(`_addAttachmentToMessage: preview ${index} was falsey`);
|
throw new Error(`_addAttachmentToMessage: preview ${index} was falsey`);
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkOldAttachment(item, 'image', attachment, logPrefix);
|
_checkOldAttachment(item, 'image', logPrefix);
|
||||||
|
|
||||||
const newPreview = [...preview];
|
const newPreview = [...preview];
|
||||||
newPreview[index] = {
|
newPreview[index] = {
|
||||||
|
@ -370,13 +397,13 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
|
||||||
}
|
}
|
||||||
const item = contact[index];
|
const item = contact[index];
|
||||||
if (item && item.avatar && item.avatar.avatar) {
|
if (item && item.avatar && item.avatar.avatar) {
|
||||||
_checkOldAttachment(item.avatar, 'avatar', attachment, logPrefix);
|
_checkOldAttachment(item.avatar, 'avatar', logPrefix);
|
||||||
|
|
||||||
const newContact = [...contact];
|
const newContact = [...contact];
|
||||||
newContact[index] = {
|
newContact[index] = {
|
||||||
...contact[index],
|
...item,
|
||||||
avatar: {
|
avatar: {
|
||||||
...contact[index].avatar,
|
...item.avatar,
|
||||||
avatar: attachment,
|
avatar: attachment,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -410,7 +437,7 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkOldAttachment(item, 'thumbnail', attachment, logPrefix);
|
_checkOldAttachment(item, 'thumbnail', logPrefix);
|
||||||
|
|
||||||
const newAttachments = [...attachments];
|
const newAttachments = [...attachments];
|
||||||
newAttachments[index] = {
|
newAttachments[index] = {
|
||||||
|
@ -448,7 +475,12 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _checkOldAttachment(object, key, newAttachment, logPrefix) {
|
function _checkOldAttachment(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
object: any,
|
||||||
|
key: string,
|
||||||
|
logPrefix: string
|
||||||
|
): void {
|
||||||
const oldAttachment = object[key];
|
const oldAttachment = object[key];
|
||||||
if (oldAttachment && oldAttachment.path) {
|
if (oldAttachment && oldAttachment.path) {
|
||||||
logger.error(
|
logger.error(
|
105
ts/messageModifiers/Deletes.ts
Normal file
105
ts/messageModifiers/Deletes.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
|
||||||
|
import { Collection, Model } from 'backbone';
|
||||||
|
import { MessageModel } from '../models/messages';
|
||||||
|
|
||||||
|
type DeleteAttributesType = {
|
||||||
|
targetSentTimestamp: number;
|
||||||
|
serverTimestamp: number;
|
||||||
|
fromId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class DeleteModel extends Model<DeleteAttributesType> {}
|
||||||
|
|
||||||
|
let singleton: Deletes | undefined;
|
||||||
|
|
||||||
|
export class Deletes extends Collection<DeleteModel> {
|
||||||
|
static getSingleton(): Deletes {
|
||||||
|
if (!singleton) {
|
||||||
|
singleton = new Deletes();
|
||||||
|
}
|
||||||
|
|
||||||
|
return singleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
forMessage(message: MessageModel): Array<DeleteModel> {
|
||||||
|
const matchingDeletes = this.filter(item => {
|
||||||
|
return (
|
||||||
|
item.get('targetSentTimestamp') === message.get('sent_at') &&
|
||||||
|
item.get('fromId') === message.getContactId()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchingDeletes.length > 0) {
|
||||||
|
window.log.info('Found early DOE for message');
|
||||||
|
this.remove(matchingDeletes);
|
||||||
|
return matchingDeletes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async onDelete(del: DeleteModel): Promise<void> {
|
||||||
|
try {
|
||||||
|
// The conversation the deleted message was in; we have to find it in the database
|
||||||
|
// to to figure that out.
|
||||||
|
const targetConversation = await window.ConversationController.getConversationForTargetMessage(
|
||||||
|
del.get('fromId'),
|
||||||
|
del.get('targetSentTimestamp')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!targetConversation) {
|
||||||
|
window.log.info(
|
||||||
|
'No target conversation for DOE',
|
||||||
|
del.get('fromId'),
|
||||||
|
del.get('targetSentTimestamp')
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not await, since this can deadlock the queue
|
||||||
|
targetConversation.queueJob('Deletes.onDelete', async () => {
|
||||||
|
window.log.info('Handling DOE for', del.get('targetSentTimestamp'));
|
||||||
|
|
||||||
|
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||||
|
del.get('targetSentTimestamp'),
|
||||||
|
{
|
||||||
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const targetMessage = messages.find(
|
||||||
|
m => del.get('fromId') === m.getContactId()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!targetMessage) {
|
||||||
|
window.log.info(
|
||||||
|
'No message for DOE',
|
||||||
|
del.get('fromId'),
|
||||||
|
del.get('targetSentTimestamp')
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = window.MessageController.register(
|
||||||
|
targetMessage.id,
|
||||||
|
targetMessage
|
||||||
|
);
|
||||||
|
|
||||||
|
await window.Signal.Util.deleteForEveryone(message, del);
|
||||||
|
|
||||||
|
this.remove(del);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'Deletes.onDelete error:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
137
ts/messageModifiers/DeliveryReceipts.ts
Normal file
137
ts/messageModifiers/DeliveryReceipts.ts
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
// Copyright 2016-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
|
||||||
|
import { union } from 'lodash';
|
||||||
|
import { Collection, Model } from 'backbone';
|
||||||
|
|
||||||
|
import { ConversationModel } from '../models/conversations';
|
||||||
|
import { MessageModel } from '../models/messages';
|
||||||
|
import { MessageModelCollectionType } from '../model-types.d';
|
||||||
|
import { isIncoming } from '../state/selectors/message';
|
||||||
|
|
||||||
|
type DeliveryReceiptAttributesType = {
|
||||||
|
timestamp: number;
|
||||||
|
deliveredTo: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeliveryReceiptModel extends Model<DeliveryReceiptAttributesType> {}
|
||||||
|
|
||||||
|
let singleton: DeliveryReceipts | undefined;
|
||||||
|
|
||||||
|
async function getTargetMessage(
|
||||||
|
sourceId: string,
|
||||||
|
messages: MessageModelCollectionType
|
||||||
|
): Promise<MessageModel | null> {
|
||||||
|
if (messages.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const message = messages.find(
|
||||||
|
item =>
|
||||||
|
!isIncoming(item.attributes) && sourceId === item.get('conversationId')
|
||||||
|
);
|
||||||
|
if (message) {
|
||||||
|
return window.MessageController.register(message.id, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = await window.Signal.Data.getAllGroupsInvolvingId(sourceId, {
|
||||||
|
ConversationCollection: window.Whisper.ConversationCollection,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ids = groups.pluck('id');
|
||||||
|
ids.push(sourceId);
|
||||||
|
|
||||||
|
const target = messages.find(
|
||||||
|
item =>
|
||||||
|
!isIncoming(item.attributes) && ids.includes(item.get('conversationId'))
|
||||||
|
);
|
||||||
|
if (!target) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.MessageController.register(target.id, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeliveryReceipts extends Collection<DeliveryReceiptModel> {
|
||||||
|
static getSingleton(): DeliveryReceipts {
|
||||||
|
if (!singleton) {
|
||||||
|
singleton = new DeliveryReceipts();
|
||||||
|
}
|
||||||
|
|
||||||
|
return singleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
forMessage(
|
||||||
|
conversation: ConversationModel,
|
||||||
|
message: MessageModel
|
||||||
|
): Array<DeliveryReceiptModel> {
|
||||||
|
let recipients: Array<string>;
|
||||||
|
if (conversation.isPrivate()) {
|
||||||
|
recipients = [conversation.id];
|
||||||
|
} else {
|
||||||
|
recipients = conversation.getMemberIds();
|
||||||
|
}
|
||||||
|
const receipts = this.filter(
|
||||||
|
receipt =>
|
||||||
|
receipt.get('timestamp') === message.get('sent_at') &&
|
||||||
|
recipients.indexOf(receipt.get('deliveredTo')) > -1
|
||||||
|
);
|
||||||
|
this.remove(receipts);
|
||||||
|
return receipts;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onReceipt(receipt: DeliveryReceiptModel): Promise<void> {
|
||||||
|
try {
|
||||||
|
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||||
|
receipt.get('timestamp'),
|
||||||
|
{
|
||||||
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = await getTargetMessage(
|
||||||
|
receipt.get('deliveredTo'),
|
||||||
|
messages
|
||||||
|
);
|
||||||
|
if (!message) {
|
||||||
|
window.log.info(
|
||||||
|
'No message for delivery receipt',
|
||||||
|
receipt.get('deliveredTo'),
|
||||||
|
receipt.get('timestamp')
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deliveries = message.get('delivered') || 0;
|
||||||
|
const deliveredTo = message.get('delivered_to') || [];
|
||||||
|
const expirationStartTimestamp = message.get('expirationStartTimestamp');
|
||||||
|
message.set({
|
||||||
|
delivered_to: union(deliveredTo, [receipt.get('deliveredTo')]),
|
||||||
|
delivered: deliveries + 1,
|
||||||
|
expirationStartTimestamp: expirationStartTimestamp || Date.now(),
|
||||||
|
sent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.Signal.Util.queueUpdateMessage(message.attributes);
|
||||||
|
|
||||||
|
// notify frontend listeners
|
||||||
|
const conversation = window.ConversationController.get(
|
||||||
|
message.get('conversationId')
|
||||||
|
);
|
||||||
|
const updateLeftPane = conversation
|
||||||
|
? conversation.debouncedUpdateLastMessage
|
||||||
|
: undefined;
|
||||||
|
if (updateLeftPane) {
|
||||||
|
updateLeftPane();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.remove(receipt);
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'DeliveryReceipts.onReceipt error:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
ts/messageModifiers/MessageRequests.ts
Normal file
132
ts/messageModifiers/MessageRequests.ts
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
|
||||||
|
import { Collection, Model } from 'backbone';
|
||||||
|
import { ConversationModel } from '../models/conversations';
|
||||||
|
|
||||||
|
type MessageRequestAttributesType = {
|
||||||
|
threadE164?: string;
|
||||||
|
threadUuid?: string;
|
||||||
|
groupId?: string;
|
||||||
|
groupV2Id?: string;
|
||||||
|
type: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MessageRequestModel extends Model<MessageRequestAttributesType> {}
|
||||||
|
|
||||||
|
let singleton: MessageRequests | undefined;
|
||||||
|
|
||||||
|
export class MessageRequests extends Collection<MessageRequestModel> {
|
||||||
|
static getSingleton(): MessageRequests {
|
||||||
|
if (!singleton) {
|
||||||
|
singleton = new MessageRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
return singleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
forConversation(conversation: ConversationModel): MessageRequestModel | null {
|
||||||
|
if (conversation.get('e164')) {
|
||||||
|
const syncByE164 = this.findWhere({
|
||||||
|
threadE164: conversation.get('e164'),
|
||||||
|
});
|
||||||
|
if (syncByE164) {
|
||||||
|
window.log.info(
|
||||||
|
`Found early message request response for E164 ${conversation.idForLogging()}`
|
||||||
|
);
|
||||||
|
this.remove(syncByE164);
|
||||||
|
return syncByE164;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversation.get('uuid')) {
|
||||||
|
const syncByUuid = this.findWhere({
|
||||||
|
threadUuid: conversation.get('uuid'),
|
||||||
|
});
|
||||||
|
if (syncByUuid) {
|
||||||
|
window.log.info(
|
||||||
|
`Found early message request response for UUID ${conversation.idForLogging()}`
|
||||||
|
);
|
||||||
|
this.remove(syncByUuid);
|
||||||
|
return syncByUuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// V1 Group
|
||||||
|
if (conversation.get('groupId')) {
|
||||||
|
const syncByGroupId = this.findWhere({
|
||||||
|
groupId: conversation.get('groupId'),
|
||||||
|
});
|
||||||
|
if (syncByGroupId) {
|
||||||
|
window.log.info(
|
||||||
|
`Found early message request response for group v1 ID ${conversation.idForLogging()}`
|
||||||
|
);
|
||||||
|
this.remove(syncByGroupId);
|
||||||
|
return syncByGroupId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// V2 group
|
||||||
|
if (conversation.get('groupId')) {
|
||||||
|
const syncByGroupId = this.findWhere({
|
||||||
|
groupV2Id: conversation.get('groupId'),
|
||||||
|
});
|
||||||
|
if (syncByGroupId) {
|
||||||
|
window.log.info(
|
||||||
|
`Found early message request response for group v2 ID ${conversation.idForLogging()}`
|
||||||
|
);
|
||||||
|
this.remove(syncByGroupId);
|
||||||
|
return syncByGroupId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onResponse(sync: MessageRequestModel): Promise<void> {
|
||||||
|
try {
|
||||||
|
const threadE164 = sync.get('threadE164');
|
||||||
|
const threadUuid = sync.get('threadUuid');
|
||||||
|
const groupId = sync.get('groupId');
|
||||||
|
const groupV2Id = sync.get('groupV2Id');
|
||||||
|
|
||||||
|
let conversation;
|
||||||
|
|
||||||
|
// We multiplex between GV1/GV2 groups here, but we don't kick off migrations
|
||||||
|
if (groupV2Id) {
|
||||||
|
conversation = window.ConversationController.get(groupV2Id);
|
||||||
|
}
|
||||||
|
if (!conversation && groupId) {
|
||||||
|
conversation = window.ConversationController.get(groupId);
|
||||||
|
}
|
||||||
|
if (!conversation && (threadE164 || threadUuid)) {
|
||||||
|
conversation = window.ConversationController.get(
|
||||||
|
window.ConversationController.ensureContactIds({
|
||||||
|
e164: threadE164,
|
||||||
|
uuid: threadUuid,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
window.log.warn(
|
||||||
|
`Received message request response for unknown conversation: groupv2(${groupV2Id}) group(${groupId}) ${threadUuid} ${threadE164}`
|
||||||
|
);
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
167
ts/messageModifiers/Reactions.ts
Normal file
167
ts/messageModifiers/Reactions.ts
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
|
||||||
|
import { Collection, Model } from 'backbone';
|
||||||
|
import { MessageModel } from '../models/messages';
|
||||||
|
import { isOutgoing } from '../state/selectors/message';
|
||||||
|
|
||||||
|
type ReactionsAttributesType = {
|
||||||
|
emoji: string;
|
||||||
|
remove: boolean;
|
||||||
|
targetAuthorUuid: string;
|
||||||
|
targetTimestamp: number;
|
||||||
|
timestamp: number;
|
||||||
|
fromId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ReactionModel extends Model<ReactionsAttributesType> {}
|
||||||
|
|
||||||
|
let singleton: Reactions | undefined;
|
||||||
|
|
||||||
|
export class Reactions extends Collection {
|
||||||
|
static getSingleton(): Reactions {
|
||||||
|
if (!singleton) {
|
||||||
|
singleton = new Reactions();
|
||||||
|
}
|
||||||
|
|
||||||
|
return singleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
forMessage(message: MessageModel): Array<ReactionModel> {
|
||||||
|
if (isOutgoing(message.attributes)) {
|
||||||
|
const outgoingReactions = this.filter(
|
||||||
|
item => item.get('targetTimestamp') === message.get('sent_at')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (outgoingReactions.length > 0) {
|
||||||
|
window.log.info('Found early reaction for outgoing message');
|
||||||
|
this.remove(outgoingReactions);
|
||||||
|
return outgoingReactions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderId = message.getContactId();
|
||||||
|
const sentAt = message.get('sent_at');
|
||||||
|
const reactionsBySource = this.filter(re => {
|
||||||
|
const targetSenderId = window.ConversationController.ensureContactIds({
|
||||||
|
uuid: re.get('targetAuthorUuid'),
|
||||||
|
});
|
||||||
|
const targetTimestamp = re.get('targetTimestamp');
|
||||||
|
return targetSenderId === senderId && targetTimestamp === sentAt;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reactionsBySource.length > 0) {
|
||||||
|
window.log.info('Found early reaction for message');
|
||||||
|
this.remove(reactionsBySource);
|
||||||
|
return reactionsBySource;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async onReaction(
|
||||||
|
reaction: ReactionModel
|
||||||
|
): Promise<ReactionModel | undefined> {
|
||||||
|
try {
|
||||||
|
// The conversation the target message was in; we have to find it in the database
|
||||||
|
// to to figure that out.
|
||||||
|
const targetConversationId = window.ConversationController.ensureContactIds(
|
||||||
|
{
|
||||||
|
uuid: reaction.get('targetAuthorUuid'),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!targetConversationId) {
|
||||||
|
throw new Error(
|
||||||
|
'onReaction: No conversationId returned from ensureContactIds!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetConversation = await window.ConversationController.getConversationForTargetMessage(
|
||||||
|
targetConversationId,
|
||||||
|
reaction.get('targetTimestamp')
|
||||||
|
);
|
||||||
|
if (!targetConversation) {
|
||||||
|
window.log.info(
|
||||||
|
'No target conversation for reaction',
|
||||||
|
reaction.get('targetAuthorUuid'),
|
||||||
|
reaction.get('targetTimestamp')
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// awaiting is safe since `onReaction` is never called from inside the queue
|
||||||
|
return await targetConversation.queueJob(
|
||||||
|
'Reactions.onReaction',
|
||||||
|
async () => {
|
||||||
|
window.log.info(
|
||||||
|
'Handling reaction for',
|
||||||
|
reaction.get('targetTimestamp')
|
||||||
|
);
|
||||||
|
|
||||||
|
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||||
|
reaction.get('targetTimestamp'),
|
||||||
|
{
|
||||||
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Message is fetched inside the conversation queue so we have the
|
||||||
|
// most recent data
|
||||||
|
const targetMessage = messages.find(m => {
|
||||||
|
const contact = m.getContact();
|
||||||
|
|
||||||
|
if (!contact) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mcid = contact.get('id');
|
||||||
|
const recid = window.ConversationController.ensureContactIds({
|
||||||
|
uuid: reaction.get('targetAuthorUuid'),
|
||||||
|
});
|
||||||
|
return mcid === recid;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetMessage) {
|
||||||
|
window.log.info(
|
||||||
|
'No message for reaction',
|
||||||
|
reaction.get('targetAuthorUuid'),
|
||||||
|
reaction.get('targetTimestamp')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Since we haven't received the message for which we are removing a
|
||||||
|
// reaction, we can just remove those pending reactions
|
||||||
|
if (reaction.get('remove')) {
|
||||||
|
this.remove(reaction);
|
||||||
|
const oldReaction = this.where({
|
||||||
|
targetAuthorUuid: reaction.get('targetAuthorUuid'),
|
||||||
|
targetTimestamp: reaction.get('targetTimestamp'),
|
||||||
|
emoji: reaction.get('emoji'),
|
||||||
|
});
|
||||||
|
oldReaction.forEach(r => this.remove(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = window.MessageController.register(
|
||||||
|
targetMessage.id,
|
||||||
|
targetMessage
|
||||||
|
);
|
||||||
|
|
||||||
|
const oldReaction = await message.handleReaction(reaction);
|
||||||
|
|
||||||
|
this.remove(reaction);
|
||||||
|
|
||||||
|
return oldReaction;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'Reactions.onReaction error:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
139
ts/messageModifiers/ReadReceipts.ts
Normal file
139
ts/messageModifiers/ReadReceipts.ts
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
// Copyright 2016-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
|
||||||
|
import { Collection, Model } from 'backbone';
|
||||||
|
|
||||||
|
import { ConversationModel } from '../models/conversations';
|
||||||
|
import { MessageModel } from '../models/messages';
|
||||||
|
import { MessageModelCollectionType } from '../model-types.d';
|
||||||
|
import { isOutgoing } from '../state/selectors/message';
|
||||||
|
|
||||||
|
type ReadReceiptAttributesType = {
|
||||||
|
reader: string;
|
||||||
|
timestamp: number;
|
||||||
|
readAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ReadReceiptModel extends Model<ReadReceiptAttributesType> {}
|
||||||
|
|
||||||
|
let singleton: ReadReceipts | undefined;
|
||||||
|
|
||||||
|
async function getTargetMessage(
|
||||||
|
reader: string,
|
||||||
|
messages: MessageModelCollectionType
|
||||||
|
): Promise<MessageModel | null> {
|
||||||
|
if (messages.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const message = messages.find(
|
||||||
|
item => isOutgoing(item.attributes) && reader === item.get('conversationId')
|
||||||
|
);
|
||||||
|
if (message) {
|
||||||
|
return window.MessageController.register(message.id, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = await window.Signal.Data.getAllGroupsInvolvingId(reader, {
|
||||||
|
ConversationCollection: window.Whisper.ConversationCollection,
|
||||||
|
});
|
||||||
|
const ids = groups.pluck('id');
|
||||||
|
ids.push(reader);
|
||||||
|
|
||||||
|
const target = messages.find(
|
||||||
|
item =>
|
||||||
|
isOutgoing(item.attributes) && ids.includes(item.get('conversationId'))
|
||||||
|
);
|
||||||
|
if (!target) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.MessageController.register(target.id, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReadReceipts extends Collection<ReadReceiptModel> {
|
||||||
|
static getSingleton(): ReadReceipts {
|
||||||
|
if (!singleton) {
|
||||||
|
singleton = new ReadReceipts();
|
||||||
|
}
|
||||||
|
|
||||||
|
return singleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
forMessage(
|
||||||
|
conversation: ConversationModel,
|
||||||
|
message: MessageModel
|
||||||
|
): Array<ReadReceiptModel> {
|
||||||
|
if (!isOutgoing(message.attributes)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let ids: Array<string>;
|
||||||
|
if (conversation.isPrivate()) {
|
||||||
|
ids = [conversation.id];
|
||||||
|
} else {
|
||||||
|
ids = conversation.getMemberIds();
|
||||||
|
}
|
||||||
|
const receipts = this.filter(
|
||||||
|
receipt =>
|
||||||
|
receipt.get('timestamp') === message.get('sent_at') &&
|
||||||
|
ids.includes(receipt.get('reader'))
|
||||||
|
);
|
||||||
|
if (receipts.length) {
|
||||||
|
window.log.info('Found early read receipts for message');
|
||||||
|
this.remove(receipts);
|
||||||
|
}
|
||||||
|
return receipts;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onReceipt(receipt: ReadReceiptModel): Promise<void> {
|
||||||
|
try {
|
||||||
|
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||||
|
receipt.get('timestamp'),
|
||||||
|
{
|
||||||
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = await getTargetMessage(receipt.get('reader'), messages);
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
window.log.info(
|
||||||
|
'No message for read receipt',
|
||||||
|
receipt.get('reader'),
|
||||||
|
receipt.get('timestamp')
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readBy = message.get('read_by') || [];
|
||||||
|
const expirationStartTimestamp = message.get('expirationStartTimestamp');
|
||||||
|
|
||||||
|
readBy.push(receipt.get('reader'));
|
||||||
|
message.set({
|
||||||
|
read_by: readBy,
|
||||||
|
expirationStartTimestamp: expirationStartTimestamp || Date.now(),
|
||||||
|
sent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.Signal.Util.queueUpdateMessage(message.attributes);
|
||||||
|
|
||||||
|
// notify frontend listeners
|
||||||
|
const conversation = window.ConversationController.get(
|
||||||
|
message.get('conversationId')
|
||||||
|
);
|
||||||
|
const updateLeftPane = conversation
|
||||||
|
? conversation.debouncedUpdateLastMessage
|
||||||
|
: undefined;
|
||||||
|
if (updateLeftPane) {
|
||||||
|
updateLeftPane();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.remove(receipt);
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'ReadReceipts.onReceipt error:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
162
ts/messageModifiers/ReadSyncs.ts
Normal file
162
ts/messageModifiers/ReadSyncs.ts
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
// Copyright 2017-2020 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
|
||||||
|
import { Collection, Model } from 'backbone';
|
||||||
|
|
||||||
|
import { MessageModel } from '../models/messages';
|
||||||
|
import { isIncoming } from '../state/selectors/message';
|
||||||
|
|
||||||
|
type ReadSyncAttributesType = {
|
||||||
|
senderId: string;
|
||||||
|
sender: string;
|
||||||
|
senderUuid: string;
|
||||||
|
timestamp: number;
|
||||||
|
readAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ReadSyncModel extends Model<ReadSyncAttributesType> {}
|
||||||
|
|
||||||
|
let singleton: ReadSyncs | undefined;
|
||||||
|
|
||||||
|
async function maybeItIsAReactionReadSync(
|
||||||
|
receipt: ReadSyncModel
|
||||||
|
): Promise<void> {
|
||||||
|
const readReaction = await window.Signal.Data.markReactionAsRead(
|
||||||
|
receipt.get('senderUuid'),
|
||||||
|
Number(receipt.get('timestamp'))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!readReaction) {
|
||||||
|
window.log.info(
|
||||||
|
'Nothing found for read sync',
|
||||||
|
receipt.get('senderId'),
|
||||||
|
receipt.get('sender'),
|
||||||
|
receipt.get('senderUuid'),
|
||||||
|
receipt.get('timestamp')
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Whisper.Notifications.removeBy({
|
||||||
|
conversationId: readReaction.conversationId,
|
||||||
|
emoji: readReaction.emoji,
|
||||||
|
targetAuthorUuid: readReaction.targetAuthorUuid,
|
||||||
|
targetTimestamp: readReaction.targetTimestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReadSyncs extends Collection {
|
||||||
|
static getSingleton(): ReadSyncs {
|
||||||
|
if (!singleton) {
|
||||||
|
singleton = new ReadSyncs();
|
||||||
|
}
|
||||||
|
|
||||||
|
return singleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
forMessage(message: MessageModel): ReadSyncModel | null {
|
||||||
|
const senderId = window.ConversationController.ensureContactIds({
|
||||||
|
e164: message.get('source'),
|
||||||
|
uuid: message.get('sourceUuid'),
|
||||||
|
});
|
||||||
|
const receipt = this.find(item => {
|
||||||
|
return (
|
||||||
|
item.get('senderId') === senderId &&
|
||||||
|
item.get('timestamp') === message.get('sent_at')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (receipt) {
|
||||||
|
window.log.info('Found early read sync for message');
|
||||||
|
this.remove(receipt);
|
||||||
|
return receipt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onReceipt(receipt: ReadSyncModel): Promise<void> {
|
||||||
|
try {
|
||||||
|
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||||
|
receipt.get('timestamp'),
|
||||||
|
{
|
||||||
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const found = messages.find(item => {
|
||||||
|
const senderId = window.ConversationController.ensureContactIds({
|
||||||
|
e164: item.get('source'),
|
||||||
|
uuid: item.get('sourceUuid'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
isIncoming(item.attributes) && senderId === receipt.get('senderId')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
await maybeItIsAReactionReadSync(receipt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Whisper.Notifications.removeBy({ messageId: found.id });
|
||||||
|
|
||||||
|
const message = window.MessageController.register(found.id, found);
|
||||||
|
const readAt = receipt.get('readAt');
|
||||||
|
|
||||||
|
// If message is unread, we mark it read. Otherwise, we update the expiration
|
||||||
|
// timer to the time specified by the read sync if it's earlier than
|
||||||
|
// the previous read time.
|
||||||
|
if (message.isUnread()) {
|
||||||
|
// TODO DESKTOP-1509: use MessageUpdater.markRead once this is TS
|
||||||
|
message.markRead(readAt, { skipSave: true });
|
||||||
|
|
||||||
|
const updateConversation = () => {
|
||||||
|
// onReadMessage may result in messages older than this one being
|
||||||
|
// marked read. We want those messages to have the same expire timer
|
||||||
|
// start time as this one, so we pass the readAt value through.
|
||||||
|
const conversation = message.getConversation();
|
||||||
|
if (conversation) {
|
||||||
|
conversation.onReadMessage(message, readAt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (window.startupProcessingQueue) {
|
||||||
|
const conversation = message.getConversation();
|
||||||
|
if (conversation) {
|
||||||
|
window.startupProcessingQueue.add(
|
||||||
|
conversation.get('id'),
|
||||||
|
updateConversation
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateConversation();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const now = Date.now();
|
||||||
|
const existingTimestamp = message.get('expirationStartTimestamp');
|
||||||
|
const expirationStartTimestamp = Math.min(
|
||||||
|
now,
|
||||||
|
Math.min(existingTimestamp || now, readAt || now)
|
||||||
|
);
|
||||||
|
message.set({ expirationStartTimestamp });
|
||||||
|
|
||||||
|
const conversation = message.getConversation();
|
||||||
|
if (conversation) {
|
||||||
|
conversation.trigger('expiration-change', message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Signal.Util.queueUpdateMessage(message.attributes);
|
||||||
|
|
||||||
|
this.remove(receipt);
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'ReadSyncs.onReceipt error:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
105
ts/messageModifiers/ViewSyncs.ts
Normal file
105
ts/messageModifiers/ViewSyncs.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
|
||||||
|
import { Collection, Model } from 'backbone';
|
||||||
|
import { MessageModel } from '../models/messages';
|
||||||
|
|
||||||
|
type ViewSyncAttributesType = {
|
||||||
|
source?: string;
|
||||||
|
sourceUuid: string;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ViewSyncModel extends Model<ViewSyncAttributesType> {}
|
||||||
|
|
||||||
|
let singleton: ViewSyncs | undefined;
|
||||||
|
|
||||||
|
export class ViewSyncs extends Collection<ViewSyncModel> {
|
||||||
|
static getSingleton(): ViewSyncs {
|
||||||
|
if (!singleton) {
|
||||||
|
singleton = new ViewSyncs();
|
||||||
|
}
|
||||||
|
|
||||||
|
return singleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
forMessage(message: MessageModel): ViewSyncModel | null {
|
||||||
|
const syncBySourceUuid = this.find(item => {
|
||||||
|
return (
|
||||||
|
item.get('sourceUuid') === message.get('sourceUuid') &&
|
||||||
|
item.get('timestamp') === message.get('sent_at')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (syncBySourceUuid) {
|
||||||
|
window.log.info('Found early view sync for message');
|
||||||
|
this.remove(syncBySourceUuid);
|
||||||
|
return syncBySourceUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncBySource = this.find(item => {
|
||||||
|
return (
|
||||||
|
item.get('source') === message.get('source') &&
|
||||||
|
item.get('timestamp') === message.get('sent_at')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (syncBySource) {
|
||||||
|
window.log.info('Found early view sync for message');
|
||||||
|
this.remove(syncBySource);
|
||||||
|
return syncBySource;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSync(sync: ViewSyncModel): Promise<void> {
|
||||||
|
try {
|
||||||
|
const messages = await window.Signal.Data.getMessagesBySentAt(
|
||||||
|
sync.get('timestamp'),
|
||||||
|
{
|
||||||
|
MessageCollection: window.Whisper.MessageCollection,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const found = messages.find(item => {
|
||||||
|
const itemSourceUuid = item.get('sourceUuid');
|
||||||
|
const syncSourceUuid = sync.get('sourceUuid');
|
||||||
|
const itemSource = item.get('source');
|
||||||
|
const syncSource = sync.get('source');
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
|
(itemSourceUuid &&
|
||||||
|
syncSourceUuid &&
|
||||||
|
itemSourceUuid === syncSourceUuid) ||
|
||||||
|
(itemSource && syncSource && itemSource === syncSource)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncSource = sync.get('source');
|
||||||
|
const syncSourceUuid = sync.get('sourceUuid');
|
||||||
|
const syncTimestamp = sync.get('timestamp');
|
||||||
|
const wasMessageFound = Boolean(found);
|
||||||
|
window.log.info('Receive view sync:', {
|
||||||
|
syncSource,
|
||||||
|
syncSourceUuid,
|
||||||
|
syncTimestamp,
|
||||||
|
wasMessageFound,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = window.MessageController.register(found.id, found);
|
||||||
|
await message.markViewed({ fromSync: true });
|
||||||
|
|
||||||
|
this.remove(sync);
|
||||||
|
} catch (error) {
|
||||||
|
window.log.error(
|
||||||
|
'ViewSyncs.onSync error:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
ts/model-types.d.ts
vendored
121
ts/model-types.d.ts
vendored
|
@ -7,11 +7,6 @@ import { GroupV2ChangeType } from './groups';
|
||||||
import { LocalizerType, BodyRangeType, BodyRangesType } from './types/Util';
|
import { LocalizerType, BodyRangeType, BodyRangesType } from './types/Util';
|
||||||
import { CallHistoryDetailsFromDiskType } from './types/Calling';
|
import { CallHistoryDetailsFromDiskType } from './types/Calling';
|
||||||
import { CustomColorType } from './types/Colors';
|
import { CustomColorType } from './types/Colors';
|
||||||
import {
|
|
||||||
ConversationType,
|
|
||||||
MessageType,
|
|
||||||
LastMessageStatus,
|
|
||||||
} from './state/ducks/conversations';
|
|
||||||
import { DeviceType } from './textsecure/Types';
|
import { DeviceType } from './textsecure/Types';
|
||||||
import { SendOptionsType } from './textsecure/SendMessage';
|
import { SendOptionsType } from './textsecure/SendMessage';
|
||||||
import { SendMessageChallengeData } from './textsecure/Errors';
|
import { SendMessageChallengeData } from './textsecure/Errors';
|
||||||
|
@ -27,19 +22,19 @@ import { ProfileNameChangeType } from './util/getStringForProfileChange';
|
||||||
import { CapabilitiesType } from './textsecure/WebAPI';
|
import { CapabilitiesType } from './textsecure/WebAPI';
|
||||||
import { GroupNameCollisionsWithIdsByTitle } from './util/groupMemberNameCollisions';
|
import { GroupNameCollisionsWithIdsByTitle } from './util/groupMemberNameCollisions';
|
||||||
import { ConversationColorType } from './types/Colors';
|
import { ConversationColorType } from './types/Colors';
|
||||||
|
import { AttachmentType, ThumbnailType } from './types/Attachment';
|
||||||
|
import { ContactType } from './types/Contact';
|
||||||
|
|
||||||
export type WhatIsThis = any;
|
export type WhatIsThis = any;
|
||||||
|
|
||||||
type DeletesAttributesType = {
|
export type LastMessageStatus =
|
||||||
fromId: string;
|
| 'paused'
|
||||||
serverTimestamp: number;
|
| 'error'
|
||||||
targetSentTimestamp: number;
|
| 'partial-sent'
|
||||||
};
|
| 'sending'
|
||||||
|
| 'sent'
|
||||||
export declare class DeletesModelType extends Backbone.Model<DeletesAttributesType> {
|
| 'delivered'
|
||||||
forMessage(message: MessageModel): Array<DeletesModelType>;
|
| 'read';
|
||||||
onDelete(doe: DeletesAttributesType): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TaskResultType = any;
|
type TaskResultType = any;
|
||||||
|
|
||||||
|
@ -77,38 +72,38 @@ export type RetryOptions = Readonly<{
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type MessageAttributesType = {
|
export type MessageAttributesType = {
|
||||||
bodyPending: boolean;
|
bodyPending?: boolean;
|
||||||
bodyRanges: BodyRangesType;
|
bodyRanges?: BodyRangesType;
|
||||||
callHistoryDetails: CallHistoryDetailsFromDiskType;
|
callHistoryDetails?: CallHistoryDetailsFromDiskType;
|
||||||
changedId: string;
|
changedId?: string;
|
||||||
dataMessage: ArrayBuffer | null;
|
dataMessage?: ArrayBuffer | null;
|
||||||
decrypted_at: number;
|
decrypted_at?: number;
|
||||||
deletedForEveryone: boolean;
|
deletedForEveryone?: boolean;
|
||||||
deletedForEveryoneTimestamp?: number;
|
deletedForEveryoneTimestamp?: number;
|
||||||
delivered: number;
|
delivered?: number;
|
||||||
delivered_to: Array<string | null>;
|
delivered_to?: Array<string | null>;
|
||||||
errors?: Array<CustomError>;
|
errors?: Array<CustomError>;
|
||||||
expirationStartTimestamp: number | null;
|
expirationStartTimestamp?: number | null;
|
||||||
expireTimer: number;
|
expireTimer?: number;
|
||||||
groupMigration?: GroupMigrationType;
|
groupMigration?: GroupMigrationType;
|
||||||
group_update: {
|
group_update?: {
|
||||||
avatarUpdated: boolean;
|
avatarUpdated: boolean;
|
||||||
joined: Array<string>;
|
joined: Array<string>;
|
||||||
left: string | 'You';
|
left: string | 'You';
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
hasAttachments: boolean;
|
hasAttachments?: boolean;
|
||||||
hasFileAttachments: boolean;
|
hasFileAttachments?: boolean;
|
||||||
hasVisualMediaAttachments: boolean;
|
hasVisualMediaAttachments?: boolean;
|
||||||
isErased: boolean;
|
isErased?: boolean;
|
||||||
isTapToViewInvalid: boolean;
|
isTapToViewInvalid?: boolean;
|
||||||
isViewOnce: boolean;
|
isViewOnce?: boolean;
|
||||||
key_changed: string;
|
key_changed?: string;
|
||||||
local: boolean;
|
local?: boolean;
|
||||||
logger: unknown;
|
logger?: unknown;
|
||||||
message: unknown;
|
message?: unknown;
|
||||||
messageTimer: unknown;
|
messageTimer?: unknown;
|
||||||
profileChange: ProfileNameChangeType;
|
profileChange?: ProfileNameChangeType;
|
||||||
quote?: QuotedMessageType;
|
quote?: QuotedMessageType;
|
||||||
reactions?: Array<{
|
reactions?: Array<{
|
||||||
emoji: string;
|
emoji: string;
|
||||||
|
@ -117,20 +112,19 @@ export type MessageAttributesType = {
|
||||||
targetTimestamp: number;
|
targetTimestamp: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}>;
|
}>;
|
||||||
read_by: Array<string | null>;
|
read_by?: Array<string | null>;
|
||||||
requiredProtocolVersion: number;
|
requiredProtocolVersion?: number;
|
||||||
retryOptions?: RetryOptions;
|
retryOptions?: RetryOptions;
|
||||||
sent: boolean;
|
sent?: boolean;
|
||||||
sourceDevice: string | number;
|
sourceDevice?: string | number;
|
||||||
snippet: unknown;
|
supportedVersionAtReceive?: unknown;
|
||||||
supportedVersionAtReceive: unknown;
|
synced?: boolean;
|
||||||
synced: boolean;
|
unidentifiedDeliveryReceived?: boolean;
|
||||||
unidentifiedDeliveryReceived: boolean;
|
verified?: boolean;
|
||||||
verified: boolean;
|
verifiedChanged?: string;
|
||||||
verifiedChanged: string;
|
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
type?:
|
type:
|
||||||
| 'call-history'
|
| 'call-history'
|
||||||
| 'chat-session-refreshed'
|
| 'chat-session-refreshed'
|
||||||
| 'delivery-issue'
|
| 'delivery-issue'
|
||||||
|
@ -145,17 +139,22 @@ export type MessageAttributesType = {
|
||||||
| 'timer-notification'
|
| 'timer-notification'
|
||||||
| 'universal-timer-notification'
|
| 'universal-timer-notification'
|
||||||
| 'verified-change';
|
| 'verified-change';
|
||||||
body: string;
|
body?: string;
|
||||||
attachments: Array<WhatIsThis>;
|
attachments?: Array<AttachmentType>;
|
||||||
preview: Array<WhatIsThis>;
|
preview?: Array<WhatIsThis>;
|
||||||
sticker: WhatIsThis;
|
sticker?: {
|
||||||
|
packId: string;
|
||||||
|
stickerId: number;
|
||||||
|
packKey: string;
|
||||||
|
data?: AttachmentType;
|
||||||
|
};
|
||||||
sent_at: number;
|
sent_at: number;
|
||||||
sent_to: Array<string>;
|
sent_to?: Array<string>;
|
||||||
unidentifiedDeliveries: Array<string>;
|
unidentifiedDeliveries?: Array<string>;
|
||||||
contact: Array<WhatIsThis>;
|
contact?: Array<ContactType>;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
recipients: Array<string>;
|
recipients?: Array<string>;
|
||||||
reaction: WhatIsThis;
|
reaction?: WhatIsThis;
|
||||||
destination?: WhatIsThis;
|
destination?: WhatIsThis;
|
||||||
destinationUuid?: string;
|
destinationUuid?: string;
|
||||||
|
|
||||||
|
@ -174,7 +173,7 @@ export type MessageAttributesType = {
|
||||||
// More of a legacy feature, needed as we were updating the schema of messages in the
|
// More of a legacy feature, needed as we were updating the schema of messages in the
|
||||||
// background, when we were still in IndexedDB, before attachments had gone to disk
|
// background, when we were still in IndexedDB, before attachments had gone to disk
|
||||||
// We set this so that the idle message upgrade process doesn't pick this message up
|
// We set this so that the idle message upgrade process doesn't pick this message up
|
||||||
schemaVersion: number;
|
schemaVersion?: number;
|
||||||
// This should always be set for new messages, but older messages may not have them. We
|
// This should always be set for new messages, but older messages may not have them. We
|
||||||
// may not have these for outbound messages, either, as we have not needed them.
|
// may not have these for outbound messages, either, as we have not needed them.
|
||||||
serverGuid?: string;
|
serverGuid?: string;
|
||||||
|
@ -182,7 +181,7 @@ export type MessageAttributesType = {
|
||||||
source?: string;
|
source?: string;
|
||||||
sourceUuid?: string;
|
sourceUuid?: string;
|
||||||
|
|
||||||
unread: boolean;
|
unread?: boolean;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|
||||||
// Backwards-compatibility with prerelease data schema
|
// Backwards-compatibility with prerelease data schema
|
||||||
|
|
|
@ -62,6 +62,14 @@ import {
|
||||||
isMe,
|
isMe,
|
||||||
} from '../util/whatTypeOfConversation';
|
} from '../util/whatTypeOfConversation';
|
||||||
import { deprecated } from '../util/deprecated';
|
import { deprecated } from '../util/deprecated';
|
||||||
|
import {
|
||||||
|
hasErrors,
|
||||||
|
isIncoming,
|
||||||
|
isTapToView,
|
||||||
|
getMessagePropStatus,
|
||||||
|
} from '../state/selectors/message';
|
||||||
|
import { Deletes } from '../messageModifiers/Deletes';
|
||||||
|
import { Reactions } from '../messageModifiers/Reactions';
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
@ -1238,7 +1246,7 @@ export class ConversationModel extends window.Backbone
|
||||||
const isNewMessage = true;
|
const isNewMessage = true;
|
||||||
messagesAdded(
|
messagesAdded(
|
||||||
this.id,
|
this.id,
|
||||||
[message.getReduxData()],
|
[{ ...message.attributes }],
|
||||||
isNewMessage,
|
isNewMessage,
|
||||||
window.isActive()
|
window.isActive()
|
||||||
);
|
);
|
||||||
|
@ -1560,7 +1568,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
const readMessages = messages.filter(
|
const readMessages = messages.filter(
|
||||||
m => !m.hasErrors() && m.isIncoming()
|
m => !hasErrors(m.attributes) && isIncoming(m.attributes)
|
||||||
);
|
);
|
||||||
const receiptSpecs = readMessages.map(m => ({
|
const receiptSpecs = readMessages.map(m => ({
|
||||||
senderE164: m.get('source'),
|
senderE164: m.get('source'),
|
||||||
|
@ -1570,7 +1578,7 @@ export class ConversationModel extends window.Backbone
|
||||||
uuid: m.get('sourceUuid'),
|
uuid: m.get('sourceUuid'),
|
||||||
}),
|
}),
|
||||||
timestamp: m.get('sent_at'),
|
timestamp: m.get('sent_at'),
|
||||||
hasErrors: m.hasErrors(),
|
hasErrors: hasErrors(m.attributes),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (isLocalAction) {
|
if (isLocalAction) {
|
||||||
|
@ -2324,27 +2332,6 @@ export class ConversationModel extends window.Backbone
|
||||||
return this.get('messageRequestResponseType') || 0;
|
return this.get('messageRequestResponseType') || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
isMissingRequiredProfileSharing(): boolean {
|
|
||||||
const mandatoryProfileSharingEnabled = window.Signal.RemoteConfig.isEnabled(
|
|
||||||
'desktop.mandatoryProfileSharing'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!mandatoryProfileSharingEnabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasNoMessages = (this.get('messageCount') || 0) === 0;
|
|
||||||
if (hasNoMessages) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isGroupV1(this.attributes) && !isDirectConversation(this.attributes)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !this.get('profileSharing');
|
|
||||||
}
|
|
||||||
|
|
||||||
getAboutText(): string | undefined {
|
getAboutText(): string | undefined {
|
||||||
if (!this.get('about')) {
|
if (!this.get('about')) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -3006,9 +2993,9 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
async getQuoteAttachment(
|
async getQuoteAttachment(
|
||||||
attachments: Array<WhatIsThis>,
|
attachments?: Array<WhatIsThis>,
|
||||||
preview: Array<WhatIsThis>,
|
preview?: Array<WhatIsThis>,
|
||||||
sticker: WhatIsThis
|
sticker?: WhatIsThis
|
||||||
): Promise<WhatIsThis> {
|
): Promise<WhatIsThis> {
|
||||||
if (attachments && attachments.length) {
|
if (attachments && attachments.length) {
|
||||||
const validAttachments = filter(
|
const validAttachments = filter(
|
||||||
|
@ -3104,8 +3091,8 @@ export class ConversationModel extends window.Backbone
|
||||||
bodyRanges: quotedMessage.get('bodyRanges'),
|
bodyRanges: quotedMessage.get('bodyRanges'),
|
||||||
id: quotedMessage.get('sent_at'),
|
id: quotedMessage.get('sent_at'),
|
||||||
text: body || embeddedContactName,
|
text: body || embeddedContactName,
|
||||||
isViewOnce: quotedMessage.isTapToView(),
|
isViewOnce: isTapToView(quotedMessage.attributes),
|
||||||
attachments: quotedMessage.isTapToView()
|
attachments: isTapToView(quotedMessage.attributes)
|
||||||
? [{ contentType: 'image/jpeg', fileName: null }]
|
? [{ contentType: 'image/jpeg', fileName: null }]
|
||||||
: await this.getQuoteAttachment(attachments, preview, sticker),
|
: await this.getQuoteAttachment(attachments, preview, sticker),
|
||||||
};
|
};
|
||||||
|
@ -3166,7 +3153,7 @@ export class ConversationModel extends window.Backbone
|
||||||
throw new Error('Cannot send DOE for a message older than three hours');
|
throw new Error('Cannot send DOE for a message older than three hours');
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteModel = window.Whisper.Deletes.add({
|
const deleteModel = Deletes.getSingleton().add({
|
||||||
targetSentTimestamp: targetTimestamp,
|
targetSentTimestamp: targetTimestamp,
|
||||||
fromId: window.ConversationController.getOurConversationId(),
|
fromId: window.ConversationController.getOurConversationId(),
|
||||||
});
|
});
|
||||||
|
@ -3264,7 +3251,7 @@ export class ConversationModel extends window.Backbone
|
||||||
// send error.
|
// send error.
|
||||||
throw new Error('No successful delivery for delete for everyone');
|
throw new Error('No successful delivery for delete for everyone');
|
||||||
}
|
}
|
||||||
window.Whisper.Deletes.onDelete(deleteModel);
|
Deletes.getSingleton().onDelete(deleteModel);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
@ -3289,7 +3276,7 @@ export class ConversationModel extends window.Backbone
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const outgoingReaction = { ...reaction, ...target };
|
const outgoingReaction = { ...reaction, ...target };
|
||||||
|
|
||||||
const reactionModel = window.Whisper.Reactions.add({
|
const reactionModel = Reactions.getSingleton().add({
|
||||||
...outgoingReaction,
|
...outgoingReaction,
|
||||||
fromId: window.ConversationController.getOurConversationId(),
|
fromId: window.ConversationController.getOurConversationId(),
|
||||||
timestamp,
|
timestamp,
|
||||||
|
@ -3297,7 +3284,7 @@ export class ConversationModel extends window.Backbone
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply reaction optimistically
|
// Apply reaction optimistically
|
||||||
const oldReaction = await window.Whisper.Reactions.onReaction(
|
const oldReaction = await Reactions.getSingleton().onReaction(
|
||||||
reactionModel
|
reactionModel
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3367,7 +3354,7 @@ export class ConversationModel extends window.Backbone
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
const result = await message.sendSyncMessageOnly(dataMessage);
|
const result = await message.sendSyncMessageOnly(dataMessage);
|
||||||
window.Whisper.Reactions.onReaction(reactionModel);
|
Reactions.getSingleton().onReaction(reactionModel);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3426,7 +3413,7 @@ export class ConversationModel extends window.Backbone
|
||||||
let reverseReaction: ReactionModelType;
|
let reverseReaction: ReactionModelType;
|
||||||
if (oldReaction) {
|
if (oldReaction) {
|
||||||
// Either restore old reaction
|
// Either restore old reaction
|
||||||
reverseReaction = window.Whisper.Reactions.add({
|
reverseReaction = Reactions.getSingleton().add({
|
||||||
...oldReaction,
|
...oldReaction,
|
||||||
fromId: window.ConversationController.getOurConversationId(),
|
fromId: window.ConversationController.getOurConversationId(),
|
||||||
timestamp,
|
timestamp,
|
||||||
|
@ -3437,7 +3424,7 @@ export class ConversationModel extends window.Backbone
|
||||||
reverseReaction.set('remove', !reverseReaction.get('remove'));
|
reverseReaction.set('remove', !reverseReaction.get('remove'));
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Whisper.Reactions.onReaction(reverseReaction);
|
Reactions.getSingleton().onReaction(reverseReaction);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3762,7 +3749,12 @@ export class ConversationModel extends window.Backbone
|
||||||
lastMessage:
|
lastMessage:
|
||||||
(previewMessage ? previewMessage.getNotificationText() : '') || '',
|
(previewMessage ? previewMessage.getNotificationText() : '') || '',
|
||||||
lastMessageStatus:
|
lastMessageStatus:
|
||||||
(previewMessage ? previewMessage.getMessagePropStatus() : null) || null,
|
(previewMessage
|
||||||
|
? getMessagePropStatus(
|
||||||
|
previewMessage.attributes,
|
||||||
|
window.storage.get('read-receipt-setting', false)
|
||||||
|
)
|
||||||
|
: null) || null,
|
||||||
timestamp,
|
timestamp,
|
||||||
lastMessageDeletedForEveryone: previewMessage
|
lastMessageDeletedForEveryone: previewMessage
|
||||||
? previewMessage.get('deletedForEveryone')
|
? previewMessage.get('deletedForEveryone')
|
||||||
|
@ -5024,7 +5016,7 @@ export class ConversationModel extends window.Backbone
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message.isIncoming() && !reaction) {
|
if (!isIncoming(message.attributes) && !reaction) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -37,8 +37,8 @@ export function getExpiresAt(
|
||||||
'expireTimer' | 'expirationStartTimestamp'
|
'expireTimer' | 'expirationStartTimestamp'
|
||||||
>
|
>
|
||||||
): number | undefined {
|
): number | undefined {
|
||||||
const expireTimerMs = messageAttrs.expireTimer * 1000;
|
const { expireTimer, expirationStartTimestamp } = messageAttrs;
|
||||||
return messageAttrs.expirationStartTimestamp
|
return expirationStartTimestamp && expireTimer
|
||||||
? messageAttrs.expirationStartTimestamp + expireTimerMs
|
? expirationStartTimestamp + expireTimer * 1000
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,6 @@
|
||||||
// Copyright 2019-2020 Signal Messenger, LLC
|
// Copyright 2019-2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// Matching Whisper.Message API
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
|
||||||
export function getBubbleProps(attributes: any): any {
|
|
||||||
const model = new window.Whisper.Message(attributes);
|
|
||||||
|
|
||||||
return model.getPropsForBubble();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function showSettings(): void {
|
export function showSettings(): void {
|
||||||
window.showSettings();
|
window.showSettings();
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,25 @@ import { StoredJob } from '../jobs/types';
|
||||||
import { ReactionType } from '../types/Reactions';
|
import { ReactionType } from '../types/Reactions';
|
||||||
import { ConversationColorType, CustomColorType } from '../types/Colors';
|
import { ConversationColorType, CustomColorType } from '../types/Colors';
|
||||||
import { StorageAccessType } from '../types/Storage.d';
|
import { StorageAccessType } from '../types/Storage.d';
|
||||||
|
import { AttachmentType } from '../types/Attachment';
|
||||||
|
|
||||||
|
export type AttachmentDownloadJobTypeType =
|
||||||
|
| 'long-message'
|
||||||
|
| 'attachment'
|
||||||
|
| 'preview'
|
||||||
|
| 'contact'
|
||||||
|
| 'quote'
|
||||||
|
| 'sticker';
|
||||||
|
|
||||||
export type AttachmentDownloadJobType = {
|
export type AttachmentDownloadJobType = {
|
||||||
id: string;
|
attachment: AttachmentType;
|
||||||
timestamp: number;
|
|
||||||
pending: number;
|
|
||||||
attempts: number;
|
attempts: number;
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
messageId: string;
|
||||||
|
pending: number;
|
||||||
|
timestamp: number;
|
||||||
|
type: AttachmentDownloadJobTypeType;
|
||||||
};
|
};
|
||||||
export type MessageMetricsType = {
|
export type MessageMetricsType = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -3061,7 +3061,7 @@ function saveMessageSync(
|
||||||
isErased: isErased ? 1 : 0,
|
isErased: isErased ? 1 : 0,
|
||||||
isViewOnce: isViewOnce ? 1 : 0,
|
isViewOnce: isViewOnce ? 1 : 0,
|
||||||
received_at: received_at || null,
|
received_at: received_at || null,
|
||||||
schemaVersion,
|
schemaVersion: schemaVersion || 0,
|
||||||
serverGuid: serverGuid || null,
|
serverGuid: serverGuid || null,
|
||||||
sent_at: sent_at || null,
|
sent_at: sent_at || null,
|
||||||
source: source || null,
|
source: source || null,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright 2019-2020 Signal Messenger, LLC
|
// Copyright 2019-2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { actions as accounts } from './ducks/accounts';
|
||||||
import { actions as app } from './ducks/app';
|
import { actions as app } from './ducks/app';
|
||||||
import { actions as audioPlayer } from './ducks/audioPlayer';
|
import { actions as audioPlayer } from './ducks/audioPlayer';
|
||||||
import { actions as calling } from './ducks/calling';
|
import { actions as calling } from './ducks/calling';
|
||||||
|
@ -19,6 +20,7 @@ import { actions as user } from './ducks/user';
|
||||||
import { ReduxActions } from './types';
|
import { ReduxActions } from './types';
|
||||||
|
|
||||||
export const actionCreators: ReduxActions = {
|
export const actionCreators: ReduxActions = {
|
||||||
|
accounts,
|
||||||
app,
|
app,
|
||||||
audioPlayer,
|
audioPlayer,
|
||||||
calling,
|
calling,
|
||||||
|
@ -37,6 +39,7 @@ export const actionCreators: ReduxActions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapDispatchToProps = {
|
export const mapDispatchToProps = {
|
||||||
|
...accounts,
|
||||||
...app,
|
...app,
|
||||||
...audioPlayer,
|
...audioPlayer,
|
||||||
...calling,
|
...calling,
|
||||||
|
|
99
ts/state/ducks/accounts.ts
Normal file
99
ts/state/ducks/accounts.ts
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
import { StateType as RootStateType } from '../reducer';
|
||||||
|
|
||||||
|
import { NoopActionType } from './noop';
|
||||||
|
|
||||||
|
// State
|
||||||
|
|
||||||
|
export type AccountsStateType = {
|
||||||
|
accounts: Record<string, boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
type AccountUpdateActionType = {
|
||||||
|
type: 'accounts/UPDATE';
|
||||||
|
payload: {
|
||||||
|
identifier: string;
|
||||||
|
hasAccount: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AccountsActionType = AccountUpdateActionType;
|
||||||
|
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
checkForAccount,
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkForAccount(
|
||||||
|
identifier: string
|
||||||
|
): ThunkAction<
|
||||||
|
void,
|
||||||
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
AccountUpdateActionType | NoopActionType
|
||||||
|
> {
|
||||||
|
return async dispatch => {
|
||||||
|
if (!window.textsecure.messaging) {
|
||||||
|
dispatch({
|
||||||
|
type: 'NOOP',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasAccount = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.textsecure.messaging.getProfile(identifier);
|
||||||
|
hasAccount = true;
|
||||||
|
} catch (_error) {
|
||||||
|
// Doing nothing with this failed fetch
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'accounts/UPDATE',
|
||||||
|
payload: {
|
||||||
|
identifier,
|
||||||
|
hasAccount,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reducer
|
||||||
|
|
||||||
|
export function getEmptyState(): AccountsStateType {
|
||||||
|
return {
|
||||||
|
accounts: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reducer(
|
||||||
|
state: Readonly<AccountsStateType> = getEmptyState(),
|
||||||
|
action: Readonly<AccountsActionType>
|
||||||
|
): AccountsStateType {
|
||||||
|
if (!state) {
|
||||||
|
return getEmptyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === 'accounts/UPDATE') {
|
||||||
|
const { payload } = action;
|
||||||
|
const { identifier, hasAccount } = payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
accounts: {
|
||||||
|
...state.accounts,
|
||||||
|
[identifier]: hasAccount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ import { calling } from '../../services/calling';
|
||||||
import { getOwn } from '../../util/getOwn';
|
import { getOwn } from '../../util/getOwn';
|
||||||
import { assert } from '../../util/assert';
|
import { assert } from '../../util/assert';
|
||||||
import { trigger } from '../../shims/events';
|
import { trigger } from '../../shims/events';
|
||||||
import { AttachmentType } from '../../types/Attachment';
|
|
||||||
import {
|
import {
|
||||||
AvatarColorType,
|
AvatarColorType,
|
||||||
ConversationColorType,
|
ConversationColorType,
|
||||||
|
@ -29,9 +29,13 @@ import {
|
||||||
DefaultConversationColorType,
|
DefaultConversationColorType,
|
||||||
DEFAULT_CONVERSATION_COLOR,
|
DEFAULT_CONVERSATION_COLOR,
|
||||||
} from '../../types/Colors';
|
} from '../../types/Colors';
|
||||||
import { ConversationAttributesType } from '../../model-types.d';
|
import {
|
||||||
|
LastMessageStatus,
|
||||||
|
ConversationAttributesType,
|
||||||
|
MessageAttributesType,
|
||||||
|
} from '../../model-types.d';
|
||||||
import { BodyRangeType } from '../../types/Util';
|
import { BodyRangeType } from '../../types/Util';
|
||||||
import { CallMode, CallHistoryDetailsFromDiskType } from '../../types/Calling';
|
import { CallMode } from '../../types/Calling';
|
||||||
import { MediaItemType } from '../../components/LightboxGallery';
|
import { MediaItemType } from '../../components/LightboxGallery';
|
||||||
import {
|
import {
|
||||||
getGroupSizeRecommendedLimit,
|
getGroupSizeRecommendedLimit,
|
||||||
|
@ -41,6 +45,8 @@ import { toggleSelectedContactForGroupAddition } from '../../groups/toggleSelect
|
||||||
import { GroupNameCollisionsWithIdsByTitle } from '../../util/groupMemberNameCollisions';
|
import { GroupNameCollisionsWithIdsByTitle } from '../../util/groupMemberNameCollisions';
|
||||||
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||||
|
|
||||||
|
import { NoopActionType } from './noop';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
export type DBConversationType = {
|
export type DBConversationType = {
|
||||||
|
@ -50,16 +56,15 @@ export type DBConversationType = {
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LastMessageStatus =
|
export const InteractionModes = ['mouse', 'keyboard'] as const;
|
||||||
| 'paused'
|
export type InteractionModeType = typeof InteractionModes[number];
|
||||||
| 'error'
|
|
||||||
| 'partial-sent'
|
|
||||||
| 'sending'
|
|
||||||
| 'sent'
|
|
||||||
| 'delivered'
|
|
||||||
| 'read';
|
|
||||||
|
|
||||||
export type ConversationTypeType = 'direct' | 'group';
|
export type MessageType = MessageAttributesType & {
|
||||||
|
interactionType?: InteractionModeType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConversationTypes = ['direct', 'group'] as const;
|
||||||
|
export type ConversationTypeType = typeof ConversationTypes[number];
|
||||||
|
|
||||||
export type ConversationType = {
|
export type ConversationType = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -159,52 +164,6 @@ export type CustomError = Error & {
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
number?: string;
|
number?: string;
|
||||||
};
|
};
|
||||||
export type MessageType = {
|
|
||||||
id: string;
|
|
||||||
conversationId: string;
|
|
||||||
source?: string;
|
|
||||||
sourceUuid?: string;
|
|
||||||
type?:
|
|
||||||
| 'call-history'
|
|
||||||
| 'chat-session-refreshed'
|
|
||||||
| 'delivery-issue'
|
|
||||||
| 'group'
|
|
||||||
| 'group-v1-migration'
|
|
||||||
| 'group-v2-change'
|
|
||||||
| 'incoming'
|
|
||||||
| 'keychange'
|
|
||||||
| 'message-history-unsynced'
|
|
||||||
| 'outgoing'
|
|
||||||
| 'profile-change'
|
|
||||||
| 'timer-notification'
|
|
||||||
| 'universal-timer-notification'
|
|
||||||
| 'verified-change';
|
|
||||||
quote?: { author?: string; authorUuid?: string };
|
|
||||||
received_at: number;
|
|
||||||
sent_at?: number;
|
|
||||||
hasSignalAccount?: boolean;
|
|
||||||
bodyPending?: boolean;
|
|
||||||
attachments: Array<AttachmentType>;
|
|
||||||
sticker: {
|
|
||||||
data?: {
|
|
||||||
pending?: boolean;
|
|
||||||
blurHash?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
unread: boolean;
|
|
||||||
reactions?: Array<{
|
|
||||||
emoji: string;
|
|
||||||
timestamp: number;
|
|
||||||
}>;
|
|
||||||
deletedForEveryone?: boolean;
|
|
||||||
|
|
||||||
errors?: Array<CustomError>;
|
|
||||||
group_update?: unknown;
|
|
||||||
callHistoryDetails?: CallHistoryDetailsFromDiskType;
|
|
||||||
|
|
||||||
// No need to go beyond this; unused at this stage, since this goes into
|
|
||||||
// a reducer still in plain JavaScript and comes out well-formed
|
|
||||||
};
|
|
||||||
|
|
||||||
type MessagePointerType = {
|
type MessagePointerType = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -219,7 +178,7 @@ type MessageMetricsType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageLookupType = {
|
export type MessageLookupType = {
|
||||||
[key: string]: MessageType;
|
[key: string]: MessageAttributesType;
|
||||||
};
|
};
|
||||||
export type ConversationMessageType = {
|
export type ConversationMessageType = {
|
||||||
heightChangeMessageIds: Array<string>;
|
heightChangeMessageIds: Array<string>;
|
||||||
|
@ -460,7 +419,7 @@ export type MessageChangedActionType = {
|
||||||
payload: {
|
payload: {
|
||||||
id: string;
|
id: string;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
data: MessageType;
|
data: MessageAttributesType;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type MessageDeletedActionType = {
|
export type MessageDeletedActionType = {
|
||||||
|
@ -481,7 +440,7 @@ export type MessagesAddedActionType = {
|
||||||
type: 'MESSAGES_ADDED';
|
type: 'MESSAGES_ADDED';
|
||||||
payload: {
|
payload: {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
messages: Array<MessageType>;
|
messages: Array<MessageAttributesType>;
|
||||||
isNewMessage: boolean;
|
isNewMessage: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
};
|
};
|
||||||
|
@ -503,7 +462,7 @@ export type MessagesResetActionType = {
|
||||||
type: 'MESSAGES_RESET';
|
type: 'MESSAGES_RESET';
|
||||||
payload: {
|
payload: {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
messages: Array<MessageType>;
|
messages: Array<MessageAttributesType>;
|
||||||
metrics: MessageMetricsType;
|
metrics: MessageMetricsType;
|
||||||
scrollToMessageId?: string;
|
scrollToMessageId?: string;
|
||||||
// The set of provided messages should be trusted, even if it conflicts with metrics,
|
// The set of provided messages should be trusted, even if it conflicts with metrics,
|
||||||
|
@ -701,6 +660,7 @@ export const actions = {
|
||||||
conversationUnloaded,
|
conversationUnloaded,
|
||||||
colorSelected,
|
colorSelected,
|
||||||
createGroup,
|
createGroup,
|
||||||
|
doubleCheckMissingQuoteReference,
|
||||||
messageChanged,
|
messageChanged,
|
||||||
messageDeleted,
|
messageDeleted,
|
||||||
messagesAdded,
|
messagesAdded,
|
||||||
|
@ -990,7 +950,7 @@ function selectMessage(
|
||||||
function messageChanged(
|
function messageChanged(
|
||||||
id: string,
|
id: string,
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
data: MessageType
|
data: MessageAttributesType
|
||||||
): MessageChangedActionType {
|
): MessageChangedActionType {
|
||||||
return {
|
return {
|
||||||
type: 'MESSAGE_CHANGED',
|
type: 'MESSAGE_CHANGED',
|
||||||
|
@ -1027,7 +987,7 @@ function messageSizeChanged(
|
||||||
}
|
}
|
||||||
function messagesAdded(
|
function messagesAdded(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
messages: Array<MessageType>,
|
messages: Array<MessageAttributesType>,
|
||||||
isNewMessage: boolean,
|
isNewMessage: boolean,
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
): MessagesAddedActionType {
|
): MessagesAddedActionType {
|
||||||
|
@ -1082,7 +1042,7 @@ function reviewMessageRequestNameCollision(
|
||||||
|
|
||||||
function messagesReset(
|
function messagesReset(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
messages: Array<MessageType>,
|
messages: Array<MessageAttributesType>,
|
||||||
metrics: MessageMetricsType,
|
metrics: MessageMetricsType,
|
||||||
scrollToMessageId?: string,
|
scrollToMessageId?: string,
|
||||||
unboundedFetch?: boolean
|
unboundedFetch?: boolean
|
||||||
|
@ -1345,6 +1305,18 @@ function showArchivedConversations(): ShowArchivedConversationsActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function doubleCheckMissingQuoteReference(messageId: string): NoopActionType {
|
||||||
|
const message = window.MessageController.getById(messageId);
|
||||||
|
if (message) {
|
||||||
|
message.doubleCheckMissingQuoteReference();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'NOOP',
|
||||||
|
payload: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Reducer
|
// Reducer
|
||||||
|
|
||||||
export function getEmptyState(): ConversationsStateType {
|
export function getEmptyState(): ConversationsStateType {
|
||||||
|
@ -1363,8 +1335,8 @@ export function getEmptyState(): ConversationsStateType {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasMessageHeightChanged(
|
function hasMessageHeightChanged(
|
||||||
message: MessageType,
|
message: MessageAttributesType,
|
||||||
previous: MessageType
|
previous: MessageAttributesType
|
||||||
): boolean {
|
): boolean {
|
||||||
const messageAttachments = message.attachments || [];
|
const messageAttachments = message.attachments || [];
|
||||||
const previousAttachments = previous.attachments || [];
|
const previousAttachments = previous.attachments || [];
|
||||||
|
@ -1410,13 +1382,6 @@ function hasMessageHeightChanged(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const signalAccountChanged =
|
|
||||||
Boolean(message.hasSignalAccount || previous.hasSignalAccount) &&
|
|
||||||
message.hasSignalAccount !== previous.hasSignalAccount;
|
|
||||||
if (signalAccountChanged) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentReactions = message.reactions || [];
|
const currentReactions = message.reactions || [];
|
||||||
const lastReactions = previous.reactions || [];
|
const lastReactions = previous.reactions || [];
|
||||||
const reactionsChanged =
|
const reactionsChanged =
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
} from '../../sql/Interface';
|
} from '../../sql/Interface';
|
||||||
import dataInterface from '../../sql/Client';
|
import dataInterface from '../../sql/Client';
|
||||||
import { makeLookup } from '../../util/makeLookup';
|
import { makeLookup } from '../../util/makeLookup';
|
||||||
import { BodyRangesType } from '../../types/Util';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ConversationUnloadedActionType,
|
ConversationUnloadedActionType,
|
||||||
|
@ -32,9 +31,7 @@ const {
|
||||||
// State
|
// State
|
||||||
|
|
||||||
export type MessageSearchResultType = MessageType & {
|
export type MessageSearchResultType = MessageType & {
|
||||||
snippet: string;
|
snippet?: string;
|
||||||
body: string;
|
|
||||||
bodyRanges: BodyRangesType;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageSearchResultLookupType = {
|
export type MessageSearchResultLookupType = {
|
||||||
|
|
|
@ -13,6 +13,7 @@ export type UserStateType = {
|
||||||
stickersPath: string;
|
stickersPath: string;
|
||||||
tempPath: string;
|
tempPath: string;
|
||||||
ourConversationId: string;
|
ourConversationId: string;
|
||||||
|
ourDeviceId: number;
|
||||||
ourUuid: string;
|
ourUuid: string;
|
||||||
ourNumber: string;
|
ourNumber: string;
|
||||||
platform: string;
|
platform: string;
|
||||||
|
@ -28,6 +29,7 @@ type UserChangedActionType = {
|
||||||
type: 'USER_CHANGED';
|
type: 'USER_CHANGED';
|
||||||
payload: {
|
payload: {
|
||||||
ourConversationId?: string;
|
ourConversationId?: string;
|
||||||
|
ourDeviceId?: number;
|
||||||
ourUuid?: string;
|
ourUuid?: string;
|
||||||
ourNumber?: string;
|
ourNumber?: string;
|
||||||
regionCode?: string;
|
regionCode?: string;
|
||||||
|
@ -48,6 +50,7 @@ export const actions = {
|
||||||
function userChanged(attributes: {
|
function userChanged(attributes: {
|
||||||
interactionMode?: 'mouse' | 'keyboard';
|
interactionMode?: 'mouse' | 'keyboard';
|
||||||
ourConversationId?: string;
|
ourConversationId?: string;
|
||||||
|
ourDeviceId?: number;
|
||||||
ourNumber?: string;
|
ourNumber?: string;
|
||||||
ourUuid?: string;
|
ourUuid?: string;
|
||||||
regionCode?: string;
|
regionCode?: string;
|
||||||
|
@ -76,6 +79,7 @@ export function getEmptyState(): UserStateType {
|
||||||
stickersPath: 'missing',
|
stickersPath: 'missing',
|
||||||
tempPath: 'missing',
|
tempPath: 'missing',
|
||||||
ourConversationId: 'missing',
|
ourConversationId: 'missing',
|
||||||
|
ourDeviceId: 0,
|
||||||
ourUuid: 'missing',
|
ourUuid: 'missing',
|
||||||
ourNumber: 'missing',
|
ourNumber: 'missing',
|
||||||
regionCode: 'missing',
|
regionCode: 'missing',
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
|
|
||||||
|
import { reducer as accounts } from './ducks/accounts';
|
||||||
import { reducer as app } from './ducks/app';
|
import { reducer as app } from './ducks/app';
|
||||||
import { reducer as audioPlayer } from './ducks/audioPlayer';
|
import { reducer as audioPlayer } from './ducks/audioPlayer';
|
||||||
import { reducer as calling } from './ducks/calling';
|
import { reducer as calling } from './ducks/calling';
|
||||||
|
@ -20,6 +21,7 @@ import { reducer as updates } from './ducks/updates';
|
||||||
import { reducer as user } from './ducks/user';
|
import { reducer as user } from './ducks/user';
|
||||||
|
|
||||||
export const reducer = combineReducers({
|
export const reducer = combineReducers({
|
||||||
|
accounts,
|
||||||
app,
|
app,
|
||||||
audioPlayer,
|
audioPlayer,
|
||||||
calling,
|
calling,
|
||||||
|
|
24
ts/state/selectors/accounts.ts
Normal file
24
ts/state/selectors/accounts.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
import { AccountsStateType } from '../ducks/accounts';
|
||||||
|
|
||||||
|
export const getAccounts = (state: StateType): AccountsStateType =>
|
||||||
|
state.accounts;
|
||||||
|
|
||||||
|
export type AccountSelectorType = (identifier?: string) => boolean;
|
||||||
|
export const getAccountSelector = createSelector(
|
||||||
|
getAccounts,
|
||||||
|
(accounts: AccountsStateType): AccountSelectorType => {
|
||||||
|
return (identifier?: string) => {
|
||||||
|
if (!identifier) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts.accounts[identifier] || false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
|
@ -8,16 +8,18 @@ import {
|
||||||
CallingStateType,
|
CallingStateType,
|
||||||
CallsByConversationType,
|
CallsByConversationType,
|
||||||
DirectCallStateType,
|
DirectCallStateType,
|
||||||
getActiveCall,
|
GroupCallStateType,
|
||||||
} from '../ducks/calling';
|
} from '../ducks/calling';
|
||||||
import { CallMode, CallState } from '../../types/Calling';
|
import { CallMode, CallState } from '../../types/Calling';
|
||||||
import { getOwn } from '../../util/getOwn';
|
import { getOwn } from '../../util/getOwn';
|
||||||
|
|
||||||
|
export type CallStateType = DirectCallStateType | GroupCallStateType;
|
||||||
|
|
||||||
const getCalling = (state: StateType): CallingStateType => state.calling;
|
const getCalling = (state: StateType): CallingStateType => state.calling;
|
||||||
|
|
||||||
export const isInCall = createSelector(
|
export const getActiveCallState = createSelector(
|
||||||
getCalling,
|
getCalling,
|
||||||
(state: CallingStateType): boolean => Boolean(getActiveCall(state))
|
(state: CallingStateType) => state.activeCallState
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getCallsByConversation = createSelector(
|
export const getCallsByConversation = createSelector(
|
||||||
|
@ -26,10 +28,31 @@ export const getCallsByConversation = createSelector(
|
||||||
state.callsByConversation
|
state.callsByConversation
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type CallSelectorType = (
|
||||||
|
conversationId: string
|
||||||
|
) => CallStateType | undefined;
|
||||||
export const getCallSelector = createSelector(
|
export const getCallSelector = createSelector(
|
||||||
getCallsByConversation,
|
getCallsByConversation,
|
||||||
(callsByConversation: CallsByConversationType) => (conversationId: string) =>
|
(callsByConversation: CallsByConversationType): CallSelectorType => (
|
||||||
getOwn(callsByConversation, conversationId)
|
conversationId: string
|
||||||
|
) => getOwn(callsByConversation, conversationId)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getActiveCall = createSelector(
|
||||||
|
getActiveCallState,
|
||||||
|
getCallSelector,
|
||||||
|
(activeCallState, callSelector): undefined | CallStateType => {
|
||||||
|
if (activeCallState && activeCallState.conversationId) {
|
||||||
|
return callSelector(activeCallState.conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const isInCall = createSelector(
|
||||||
|
getActiveCall,
|
||||||
|
(call: CallStateType | undefined): boolean => Boolean(call)
|
||||||
);
|
);
|
||||||
|
|
||||||
// In theory, there could be multiple incoming calls. In practice, neither RingRTC nor the
|
// In theory, there could be multiple incoming calls. In practice, neither RingRTC nor the
|
||||||
|
|
|
@ -14,31 +14,35 @@ import {
|
||||||
ConversationType,
|
ConversationType,
|
||||||
MessageLookupType,
|
MessageLookupType,
|
||||||
MessagesByConversationType,
|
MessagesByConversationType,
|
||||||
MessageType,
|
|
||||||
OneTimeModalState,
|
OneTimeModalState,
|
||||||
PreJoinConversationType,
|
PreJoinConversationType,
|
||||||
} from '../ducks/conversations';
|
} from '../ducks/conversations';
|
||||||
import { getOwn } from '../../util/getOwn';
|
import { getOwn } from '../../util/getOwn';
|
||||||
import { deconstructLookup } from '../../util/deconstructLookup';
|
import { deconstructLookup } from '../../util/deconstructLookup';
|
||||||
import type { CallsByConversationType } from '../ducks/calling';
|
|
||||||
import { getCallsByConversation } from './calling';
|
|
||||||
import { getBubbleProps } from '../../shims/Whisper';
|
|
||||||
import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
|
import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
|
||||||
import { TimelineItemType } from '../../components/conversation/TimelineItem';
|
import { TimelineItemType } from '../../components/conversation/TimelineItem';
|
||||||
import { assert } from '../../util/assert';
|
import { assert } from '../../util/assert';
|
||||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
||||||
import { filterAndSortConversationsByTitle } from '../../util/filterAndSortConversations';
|
import { filterAndSortConversationsByTitle } from '../../util/filterAndSortConversations';
|
||||||
import { ContactNameColors, ContactNameColorType } from '../../types/Colors';
|
import { ContactNameColors, ContactNameColorType } from '../../types/Colors';
|
||||||
|
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getInteractionMode,
|
|
||||||
getIntl,
|
getIntl,
|
||||||
getRegionCode,
|
getRegionCode,
|
||||||
getUserConversationId,
|
getUserConversationId,
|
||||||
getUserNumber,
|
getUserNumber,
|
||||||
|
getUserUuid,
|
||||||
} from './user';
|
} from './user';
|
||||||
import { getPinnedConversationIds } from './items';
|
import { getPinnedConversationIds, getReadReceiptSetting } from './items';
|
||||||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
import { getPropsForBubble } from './message';
|
||||||
|
import {
|
||||||
|
CallSelectorType,
|
||||||
|
CallStateType,
|
||||||
|
getActiveCall,
|
||||||
|
getCallSelector,
|
||||||
|
} from './calling';
|
||||||
|
import { getAccountSelector, AccountSelectorType } from './accounts';
|
||||||
|
|
||||||
let placeholderContact: ConversationType;
|
let placeholderContact: ConversationType;
|
||||||
export const getPlaceholderContact = (): ConversationType => {
|
export const getPlaceholderContact = (): ConversationType => {
|
||||||
|
@ -640,66 +644,15 @@ export const getConversationByIdSelector = createSelector(
|
||||||
getOwn(conversationLookup, id)
|
getOwn(conversationLookup, id)
|
||||||
);
|
);
|
||||||
|
|
||||||
// For now we use a shim, as selector logic is still happening in the Backbone Model.
|
|
||||||
// What needs to happen to pull that selector logic here?
|
|
||||||
// 1) translate ~500 lines of selector logic into TypeScript
|
|
||||||
// 2) other places still rely on that prop-gen code - need to put these under Roots:
|
|
||||||
// - quote compose
|
|
||||||
// - message details
|
|
||||||
export function _messageSelector(
|
|
||||||
message: MessageType,
|
|
||||||
_ourNumber: string,
|
|
||||||
_regionCode: string,
|
|
||||||
interactionMode: 'mouse' | 'keyboard',
|
|
||||||
_getConversationById: GetConversationByIdType,
|
|
||||||
_callsByConversation: CallsByConversationType,
|
|
||||||
selectedMessageId?: string,
|
|
||||||
selectedMessageCounter?: number
|
|
||||||
): TimelineItemType {
|
|
||||||
// Note: We don't use all of those parameters here, but the shim we call does.
|
|
||||||
// We want to call this function again if any of those parameters change.
|
|
||||||
const props = getBubbleProps(message);
|
|
||||||
|
|
||||||
if (selectedMessageId === message.id) {
|
|
||||||
return {
|
|
||||||
...props,
|
|
||||||
data: {
|
|
||||||
...props.data,
|
|
||||||
interactionMode,
|
|
||||||
isSelected: true,
|
|
||||||
isSelectedCounter: selectedMessageCounter,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...props,
|
|
||||||
data: {
|
|
||||||
...props.data,
|
|
||||||
interactionMode,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// A little optimization to reset our selector cache whenever high-level application data
|
// A little optimization to reset our selector cache whenever high-level application data
|
||||||
// changes: regionCode and userNumber.
|
// changes: regionCode and userNumber.
|
||||||
type CachedMessageSelectorType = (
|
|
||||||
message: MessageType,
|
|
||||||
ourNumber: string,
|
|
||||||
regionCode: string,
|
|
||||||
interactionMode: 'mouse' | 'keyboard',
|
|
||||||
getConversationById: GetConversationByIdType,
|
|
||||||
callsByConversation: CallsByConversationType,
|
|
||||||
selectedMessageId?: string,
|
|
||||||
selectedMessageCounter?: number
|
|
||||||
) => TimelineItemType;
|
|
||||||
export const getCachedSelectorForMessage = createSelector(
|
export const getCachedSelectorForMessage = createSelector(
|
||||||
getRegionCode,
|
getRegionCode,
|
||||||
getUserNumber,
|
getUserNumber,
|
||||||
(): CachedMessageSelectorType => {
|
(): typeof getPropsForBubble => {
|
||||||
// Note: memoizee will check all parameters provided, and only run our selector
|
// Note: memoizee will check all parameters provided, and only run our selector
|
||||||
// if any of them have changed.
|
// if any of them have changed.
|
||||||
return memoizee(_messageSelector, { max: 2000 });
|
return memoizee(getPropsForBubble, { max: 2000 });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -710,18 +663,26 @@ export const getMessageSelector = createSelector(
|
||||||
getSelectedMessage,
|
getSelectedMessage,
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
getRegionCode,
|
getRegionCode,
|
||||||
|
getReadReceiptSetting,
|
||||||
getUserNumber,
|
getUserNumber,
|
||||||
getInteractionMode,
|
getUserUuid,
|
||||||
getCallsByConversation,
|
getUserConversationId,
|
||||||
|
getCallSelector,
|
||||||
|
getActiveCall,
|
||||||
|
getAccountSelector,
|
||||||
(
|
(
|
||||||
messageSelector: CachedMessageSelectorType,
|
messageSelector: typeof getPropsForBubble,
|
||||||
messageLookup: MessageLookupType,
|
messageLookup: MessageLookupType,
|
||||||
selectedMessage: SelectedMessageType | undefined,
|
selectedMessage: SelectedMessageType | undefined,
|
||||||
conversationSelector: GetConversationByIdType,
|
conversationSelector: GetConversationByIdType,
|
||||||
regionCode: string,
|
regionCode: string,
|
||||||
|
readReceiptSetting: boolean,
|
||||||
ourNumber: string,
|
ourNumber: string,
|
||||||
interactionMode: 'keyboard' | 'mouse',
|
ourUuid: string,
|
||||||
callsByConversation: CallsByConversationType
|
ourConversationId: string,
|
||||||
|
callSelector: CallSelectorType,
|
||||||
|
activeCall: undefined | CallStateType,
|
||||||
|
accountSelector: AccountSelectorType
|
||||||
): GetMessageByIdType => {
|
): GetMessageByIdType => {
|
||||||
return (id: string) => {
|
return (id: string) => {
|
||||||
const message = messageLookup[id];
|
const message = messageLookup[id];
|
||||||
|
@ -731,13 +692,17 @@ export const getMessageSelector = createSelector(
|
||||||
|
|
||||||
return messageSelector(
|
return messageSelector(
|
||||||
message,
|
message,
|
||||||
ourNumber,
|
|
||||||
regionCode,
|
|
||||||
interactionMode,
|
|
||||||
conversationSelector,
|
conversationSelector,
|
||||||
callsByConversation,
|
ourConversationId,
|
||||||
|
ourNumber,
|
||||||
|
ourUuid,
|
||||||
|
regionCode,
|
||||||
|
readReceiptSetting,
|
||||||
selectedMessage ? selectedMessage.id : undefined,
|
selectedMessage ? selectedMessage.id : undefined,
|
||||||
selectedMessage ? selectedMessage.counter : undefined
|
selectedMessage ? selectedMessage.counter : undefined,
|
||||||
|
callSelector,
|
||||||
|
activeCall,
|
||||||
|
accountSelector
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -911,3 +876,14 @@ export const getConversationsWithCustomColorSelector = createSelector(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export function isMissingRequiredProfileSharing(
|
||||||
|
conversation: ConversationType
|
||||||
|
): boolean {
|
||||||
|
return Boolean(
|
||||||
|
!conversation.profileSharing &&
|
||||||
|
window.Signal.RemoteConfig.isEnabled('desktop.mandatoryProfileSharing') &&
|
||||||
|
conversation.messageCount &&
|
||||||
|
conversation.messageCount > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,11 @@ export const getUserAgent = createSelector(
|
||||||
(state: ItemsStateType): string => state.userAgent as string
|
(state: ItemsStateType): string => state.userAgent as string
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getReadReceiptSetting = createSelector(
|
||||||
|
getItems,
|
||||||
|
(state: ItemsStateType): boolean => Boolean(state['read-receipt-setting'])
|
||||||
|
);
|
||||||
|
|
||||||
export const getPinnedConversationIds = createSelector(
|
export const getPinnedConversationIds = createSelector(
|
||||||
getItems,
|
getItems,
|
||||||
(state: ItemsStateType): Array<string> =>
|
(state: ItemsStateType): Array<string> =>
|
||||||
|
|
1203
ts/state/selectors/message.ts
Normal file
1203
ts/state/selectors/message.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -143,7 +143,7 @@ export const getCachedSelectorForMessageSearchResult = createSelector(
|
||||||
id: message.id,
|
id: message.id,
|
||||||
conversationId: message.conversationId,
|
conversationId: message.conversationId,
|
||||||
sentAt: message.sent_at,
|
sentAt: message.sent_at,
|
||||||
snippet: message.snippet,
|
snippet: message.snippet || '',
|
||||||
bodyRanges: bodyRanges.map((bodyRange: BodyRangeType) => {
|
bodyRanges: bodyRanges.map((bodyRange: BodyRangeType) => {
|
||||||
const conversation = conversationSelector(bodyRange.mentionUuid);
|
const conversation = conversationSelector(bodyRange.mentionUuid);
|
||||||
|
|
||||||
|
@ -152,7 +152,7 @@ export const getCachedSelectorForMessageSearchResult = createSelector(
|
||||||
replacementText: conversation.title,
|
replacementText: conversation.title,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
body: message.body,
|
body: message.body || '',
|
||||||
|
|
||||||
isSelected: Boolean(
|
isSelected: Boolean(
|
||||||
selectedMessageId && message.id === selectedMessageId
|
selectedMessageId && message.id === selectedMessageId
|
||||||
|
|
|
@ -15,6 +15,11 @@ export const getUserNumber = createSelector(
|
||||||
(state: UserStateType): string => state.ourNumber
|
(state: UserStateType): string => state.ourNumber
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getUserDeviceId = createSelector(
|
||||||
|
getUser,
|
||||||
|
(state: UserStateType): number => state.ourDeviceId
|
||||||
|
);
|
||||||
|
|
||||||
export const getRegionCode = createSelector(
|
export const getRegionCode = createSelector(
|
||||||
getUser,
|
getUser,
|
||||||
(state: UserStateType): string => state.regionCode
|
(state: UserStateType): string => state.regionCode
|
||||||
|
|
|
@ -10,7 +10,10 @@ import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
|
||||||
|
|
||||||
import { selectRecentEmojis } from '../selectors/emojis';
|
import { selectRecentEmojis } from '../selectors/emojis';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { getConversationSelector } from '../selectors/conversations';
|
import {
|
||||||
|
getConversationSelector,
|
||||||
|
isMissingRequiredProfileSharing,
|
||||||
|
} from '../selectors/conversations';
|
||||||
import {
|
import {
|
||||||
getBlessedStickerPacks,
|
getBlessedStickerPacks,
|
||||||
getInstalledStickerPacks,
|
getInstalledStickerPacks,
|
||||||
|
@ -75,13 +78,8 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
conversationType: conversation.type,
|
conversationType: conversation.type,
|
||||||
isSMSOnly: Boolean(isConversationSMSOnly(conversation)),
|
isSMSOnly: Boolean(isConversationSMSOnly(conversation)),
|
||||||
isFetchingUUID: conversation.isFetchingUUID,
|
isFetchingUUID: conversation.isFetchingUUID,
|
||||||
isMissingMandatoryProfileSharing: Boolean(
|
isMissingMandatoryProfileSharing: isMissingRequiredProfileSharing(
|
||||||
!conversation.profileSharing &&
|
conversation
|
||||||
window.Signal.RemoteConfig.isEnabled(
|
|
||||||
'desktop.mandatoryProfileSharing'
|
|
||||||
) &&
|
|
||||||
conversation.messageCount &&
|
|
||||||
conversation.messageCount > 0
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,10 @@ import {
|
||||||
ConversationHeader,
|
ConversationHeader,
|
||||||
OutgoingCallButtonStyle,
|
OutgoingCallButtonStyle,
|
||||||
} from '../../components/conversation/ConversationHeader';
|
} from '../../components/conversation/ConversationHeader';
|
||||||
import { getConversationSelector } from '../selectors/conversations';
|
import {
|
||||||
|
getConversationSelector,
|
||||||
|
isMissingRequiredProfileSharing,
|
||||||
|
} from '../selectors/conversations';
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
import { CallMode } from '../../types/Calling';
|
import { CallMode } from '../../types/Calling';
|
||||||
import {
|
import {
|
||||||
|
@ -111,13 +114,8 @@ const mapStateToProps = (state: StateType, ownProps: OwnProps) => {
|
||||||
'unblurredAvatarPath',
|
'unblurredAvatarPath',
|
||||||
]),
|
]),
|
||||||
conversationTitle: state.conversations.selectedConversationTitle,
|
conversationTitle: state.conversations.selectedConversationTitle,
|
||||||
isMissingMandatoryProfileSharing: Boolean(
|
isMissingMandatoryProfileSharing: isMissingRequiredProfileSharing(
|
||||||
!conversation.profileSharing &&
|
conversation
|
||||||
window.Signal.RemoteConfig.isEnabled(
|
|
||||||
'desktop.mandatoryProfileSharing'
|
|
||||||
) &&
|
|
||||||
conversation.messageCount &&
|
|
||||||
conversation.messageCount > 0
|
|
||||||
),
|
),
|
||||||
isSMSOnly: isConversationSMSOnly(conversation),
|
isSMSOnly: isConversationSMSOnly(conversation),
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
|
|
|
@ -33,10 +33,12 @@ export type OwnProps = {
|
||||||
} & Pick<
|
} & Pick<
|
||||||
MessageDetailProps,
|
MessageDetailProps,
|
||||||
| 'clearSelectedMessage'
|
| 'clearSelectedMessage'
|
||||||
|
| 'checkForAccount'
|
||||||
| 'deleteMessage'
|
| 'deleteMessage'
|
||||||
| 'deleteMessageForEveryone'
|
| 'deleteMessageForEveryone'
|
||||||
| 'displayTapToViewMessage'
|
| 'displayTapToViewMessage'
|
||||||
| 'downloadAttachment'
|
| 'downloadAttachment'
|
||||||
|
| 'doubleCheckMissingQuoteReference'
|
||||||
| 'kickOffAttachmentDownload'
|
| 'kickOffAttachmentDownload'
|
||||||
| 'markAttachmentAsCorrupted'
|
| 'markAttachmentAsCorrupted'
|
||||||
| 'openConversation'
|
| 'openConversation'
|
||||||
|
@ -66,11 +68,13 @@ const mapStateToProps = (
|
||||||
sendAnyway,
|
sendAnyway,
|
||||||
showSafetyNumber,
|
showSafetyNumber,
|
||||||
|
|
||||||
|
checkForAccount,
|
||||||
clearSelectedMessage,
|
clearSelectedMessage,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
deleteMessageForEveryone,
|
deleteMessageForEveryone,
|
||||||
displayTapToViewMessage,
|
displayTapToViewMessage,
|
||||||
downloadAttachment,
|
downloadAttachment,
|
||||||
|
doubleCheckMissingQuoteReference,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
markAttachmentAsCorrupted,
|
markAttachmentAsCorrupted,
|
||||||
openConversation,
|
openConversation,
|
||||||
|
@ -108,11 +112,13 @@ const mapStateToProps = (
|
||||||
sendAnyway,
|
sendAnyway,
|
||||||
showSafetyNumber,
|
showSafetyNumber,
|
||||||
|
|
||||||
|
checkForAccount,
|
||||||
clearSelectedMessage,
|
clearSelectedMessage,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
deleteMessageForEveryone,
|
deleteMessageForEveryone,
|
||||||
displayTapToViewMessage,
|
displayTapToViewMessage,
|
||||||
downloadAttachment,
|
downloadAttachment,
|
||||||
|
doubleCheckMissingQuoteReference,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
markAttachmentAsCorrupted,
|
markAttachmentAsCorrupted,
|
||||||
openConversation,
|
openConversation,
|
||||||
|
|
|
@ -59,6 +59,7 @@ type ExternalProps = {
|
||||||
function renderItem(
|
function renderItem(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
|
onHeightChange: (messageId: string) => unknown,
|
||||||
actionProps: Record<string, unknown>
|
actionProps: Record<string, unknown>
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
@ -66,6 +67,7 @@ function renderItem(
|
||||||
{...actionProps}
|
{...actionProps}
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
id={messageId}
|
id={messageId}
|
||||||
|
onHeightChange={() => onHeightChange(messageId)}
|
||||||
renderEmojiPicker={renderEmojiPicker}
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
renderAudioAttachment={renderAudioAttachment}
|
renderAudioAttachment={renderAudioAttachment}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { actions as accounts } from './ducks/accounts';
|
||||||
import { actions as app } from './ducks/app';
|
import { actions as app } from './ducks/app';
|
||||||
import { actions as audioPlayer } from './ducks/audioPlayer';
|
import { actions as audioPlayer } from './ducks/audioPlayer';
|
||||||
import { actions as calling } from './ducks/calling';
|
import { actions as calling } from './ducks/calling';
|
||||||
|
@ -18,6 +19,7 @@ import { actions as updates } from './ducks/updates';
|
||||||
import { actions as user } from './ducks/user';
|
import { actions as user } from './ducks/user';
|
||||||
|
|
||||||
export type ReduxActions = {
|
export type ReduxActions = {
|
||||||
|
accounts: typeof accounts;
|
||||||
app: typeof app;
|
app: typeof app;
|
||||||
audioPlayer: typeof audioPlayer;
|
audioPlayer: typeof audioPlayer;
|
||||||
calling: typeof calling;
|
calling: typeof calling;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ConversationType,
|
ConversationType,
|
||||||
|
@ -24,20 +25,35 @@ import { getDefaultConversation } from '../../helpers/getDefaultConversation';
|
||||||
import { StateType, reducer as rootReducer } from '../../../state/reducer';
|
import { StateType, reducer as rootReducer } from '../../../state/reducer';
|
||||||
|
|
||||||
describe('both/state/selectors/search', () => {
|
describe('both/state/selectors/search', () => {
|
||||||
|
const NOW = 1_000_000;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let clock: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
clock = sinon.useFakeTimers({
|
||||||
|
now: NOW,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
const getEmptyRootState = (): StateType => {
|
const getEmptyRootState = (): StateType => {
|
||||||
return rootReducer(undefined, noopAction());
|
return rootReducer(undefined, noopAction());
|
||||||
};
|
};
|
||||||
|
|
||||||
function getDefaultMessage(id: string): MessageType {
|
function getDefaultMessage(id: string): MessageType {
|
||||||
return {
|
return {
|
||||||
id,
|
attachments: [],
|
||||||
conversationId: 'conversationId',
|
conversationId: 'conversationId',
|
||||||
|
id,
|
||||||
|
received_at: NOW,
|
||||||
|
sent_at: NOW,
|
||||||
source: 'source',
|
source: 'source',
|
||||||
sourceUuid: 'sourceUuid',
|
sourceUuid: 'sourceUuid',
|
||||||
|
timestamp: NOW,
|
||||||
type: 'incoming' as const,
|
type: 'incoming' as const,
|
||||||
received_at: Date.now(),
|
|
||||||
attachments: [],
|
|
||||||
sticker: {},
|
|
||||||
unread: false,
|
unread: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -126,7 +142,7 @@ describe('both/state/selectors/search', () => {
|
||||||
|
|
||||||
id: searchId,
|
id: searchId,
|
||||||
conversationId: toId,
|
conversationId: toId,
|
||||||
sentAt: undefined,
|
sentAt: NOW,
|
||||||
snippet: 'snippet',
|
snippet: 'snippet',
|
||||||
body: 'snippet',
|
body: 'snippet',
|
||||||
bodyRanges: [],
|
bodyRanges: [],
|
||||||
|
@ -227,7 +243,7 @@ describe('both/state/selectors/search', () => {
|
||||||
|
|
||||||
id: searchId,
|
id: searchId,
|
||||||
conversationId: toId,
|
conversationId: toId,
|
||||||
sentAt: undefined,
|
sentAt: NOW,
|
||||||
snippet: 'snippet',
|
snippet: 'snippet',
|
||||||
body: 'snippet',
|
body: 'snippet',
|
||||||
bodyRanges: [],
|
bodyRanges: [],
|
||||||
|
|
|
@ -5,6 +5,12 @@ import { assert } from 'chai';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
import {
|
||||||
|
isEndSession,
|
||||||
|
isGroupUpdate,
|
||||||
|
isIncoming,
|
||||||
|
isOutgoing,
|
||||||
|
} from '../../state/selectors/message';
|
||||||
|
|
||||||
describe('Message', () => {
|
describe('Message', () => {
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
@ -122,9 +128,9 @@ describe('Message', () => {
|
||||||
it('checks if is incoming message', () => {
|
it('checks if is incoming message', () => {
|
||||||
const messages = new window.Whisper.MessageCollection();
|
const messages = new window.Whisper.MessageCollection();
|
||||||
let message = messages.add(attributes);
|
let message = messages.add(attributes);
|
||||||
assert.notOk(message.isIncoming());
|
assert.notOk(isIncoming(message.attributes));
|
||||||
message = messages.add({ type: 'incoming' });
|
message = messages.add({ type: 'incoming' });
|
||||||
assert.ok(message.isIncoming());
|
assert.ok(isIncoming(message.attributes));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -132,9 +138,9 @@ describe('Message', () => {
|
||||||
it('checks if is outgoing message', () => {
|
it('checks if is outgoing message', () => {
|
||||||
const messages = new window.Whisper.MessageCollection();
|
const messages = new window.Whisper.MessageCollection();
|
||||||
let message = messages.add(attributes);
|
let message = messages.add(attributes);
|
||||||
assert.ok(message.isOutgoing());
|
assert.ok(isOutgoing(message.attributes));
|
||||||
message = messages.add({ type: 'incoming' });
|
message = messages.add({ type: 'incoming' });
|
||||||
assert.notOk(message.isOutgoing());
|
assert.notOk(isOutgoing(message.attributes));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -142,10 +148,10 @@ describe('Message', () => {
|
||||||
it('checks if is group update', () => {
|
it('checks if is group update', () => {
|
||||||
const messages = new window.Whisper.MessageCollection();
|
const messages = new window.Whisper.MessageCollection();
|
||||||
let message = messages.add(attributes);
|
let message = messages.add(attributes);
|
||||||
assert.notOk(message.isGroupUpdate());
|
assert.notOk(isGroupUpdate(message.attributes));
|
||||||
|
|
||||||
message = messages.add({ group_update: true });
|
message = messages.add({ group_update: true });
|
||||||
assert.ok(message.isGroupUpdate());
|
assert.ok(isGroupUpdate(message.attributes));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -553,10 +559,10 @@ describe('Message', () => {
|
||||||
it('checks if it is end of the session', () => {
|
it('checks if it is end of the session', () => {
|
||||||
const messages = new window.Whisper.MessageCollection();
|
const messages = new window.Whisper.MessageCollection();
|
||||||
let message = messages.add(attributes);
|
let message = messages.add(attributes);
|
||||||
assert.notOk(message.isEndSession());
|
assert.notOk(isEndSession(message.attributes));
|
||||||
|
|
||||||
message = messages.add({ type: 'incoming', source, flags: true });
|
message = messages.add({ type: 'incoming', source, flags: true });
|
||||||
assert.ok(message.isEndSession());
|
assert.ok(isEndSession(message.attributes));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -292,6 +292,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
|
|
||||||
describe('reducer', () => {
|
describe('reducer', () => {
|
||||||
const time = Date.now();
|
const time = Date.now();
|
||||||
|
const previousTime = time - 1;
|
||||||
const conversationId = 'conversation-guid-1';
|
const conversationId = 'conversation-guid-1';
|
||||||
const messageId = 'message-guid-1';
|
const messageId = 'message-guid-1';
|
||||||
const messageIdTwo = 'message-guid-2';
|
const messageIdTwo = 'message-guid-2';
|
||||||
|
@ -299,14 +300,15 @@ describe('both/state/ducks/conversations', () => {
|
||||||
|
|
||||||
function getDefaultMessage(id: string): MessageType {
|
function getDefaultMessage(id: string): MessageType {
|
||||||
return {
|
return {
|
||||||
id,
|
attachments: [],
|
||||||
conversationId: 'conversationId',
|
conversationId: 'conversationId',
|
||||||
|
id,
|
||||||
|
received_at: previousTime,
|
||||||
|
sent_at: previousTime,
|
||||||
source: 'source',
|
source: 'source',
|
||||||
sourceUuid: 'sourceUuid',
|
sourceUuid: 'sourceUuid',
|
||||||
|
timestamp: previousTime,
|
||||||
type: 'incoming' as const,
|
type: 'incoming' as const,
|
||||||
received_at: Date.now(),
|
|
||||||
attachments: [],
|
|
||||||
sticker: {},
|
|
||||||
unread: false,
|
unread: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -953,6 +955,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
[messageId]: {
|
[messageId]: {
|
||||||
...getDefaultMessage(messageId),
|
...getDefaultMessage(messageId),
|
||||||
received_at: time,
|
received_at: time,
|
||||||
|
sent_at: time,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
messagesByConversation: {
|
messagesByConversation: {
|
||||||
|
@ -972,6 +975,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
[messageId]: {
|
[messageId]: {
|
||||||
...getDefaultMessage(messageId),
|
...getDefaultMessage(messageId),
|
||||||
received_at: time,
|
received_at: time,
|
||||||
|
sent_at: time,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
messagesByConversation: {
|
messagesByConversation: {
|
||||||
|
@ -983,6 +987,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
newest: {
|
newest: {
|
||||||
id: messageId,
|
id: messageId,
|
||||||
received_at: time,
|
received_at: time,
|
||||||
|
sent_at: time,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1060,6 +1065,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
[messageId]: {
|
[messageId]: {
|
||||||
...getDefaultMessage(messageId),
|
...getDefaultMessage(messageId),
|
||||||
received_at: time,
|
received_at: time,
|
||||||
|
sent_at: time,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
messagesByConversation: {
|
messagesByConversation: {
|
||||||
|
@ -1079,6 +1085,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
[messageId]: {
|
[messageId]: {
|
||||||
...getDefaultMessage(messageId),
|
...getDefaultMessage(messageId),
|
||||||
received_at: time,
|
received_at: time,
|
||||||
|
sent_at: time,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
messagesByConversation: {
|
messagesByConversation: {
|
||||||
|
@ -1090,6 +1097,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
oldest: {
|
oldest: {
|
||||||
id: messageId,
|
id: messageId,
|
||||||
received_at: time,
|
received_at: time,
|
||||||
|
sent_at: time,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import { IMAGE_GIF } from '../../types/MIME';
|
||||||
import { contactSelector, getName } from '../../types/Contact';
|
import { contactSelector, getName } from '../../types/Contact';
|
||||||
|
|
||||||
describe('Contact', () => {
|
describe('Contact', () => {
|
||||||
|
@ -66,7 +67,8 @@ describe('Contact', () => {
|
||||||
});
|
});
|
||||||
describe('contactSelector', () => {
|
describe('contactSelector', () => {
|
||||||
const regionCode = '1';
|
const regionCode = '1';
|
||||||
const signalAccount = '+1202555000';
|
const firstNumber = '+1202555000';
|
||||||
|
const isNumberOnSignal = false;
|
||||||
const getAbsoluteAttachmentPath = (path: string) => `absolute:${path}`;
|
const getAbsoluteAttachmentPath = (path: string) => `absolute:${path}`;
|
||||||
|
|
||||||
it('eliminates avatar if it has had an attachment download error', () => {
|
it('eliminates avatar if it has had an attachment download error', () => {
|
||||||
|
@ -81,6 +83,7 @@ describe('Contact', () => {
|
||||||
isProfile: true,
|
isProfile: true,
|
||||||
avatar: {
|
avatar: {
|
||||||
error: true,
|
error: true,
|
||||||
|
contentType: IMAGE_GIF,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -92,12 +95,14 @@ describe('Contact', () => {
|
||||||
},
|
},
|
||||||
organization: 'Somewhere, Inc.',
|
organization: 'Somewhere, Inc.',
|
||||||
avatar: undefined,
|
avatar: undefined,
|
||||||
signalAccount,
|
firstNumber,
|
||||||
|
isNumberOnSignal,
|
||||||
number: undefined,
|
number: undefined,
|
||||||
};
|
};
|
||||||
const actual = contactSelector(contact, {
|
const actual = contactSelector(contact, {
|
||||||
regionCode,
|
regionCode,
|
||||||
signalAccount,
|
firstNumber,
|
||||||
|
isNumberOnSignal,
|
||||||
getAbsoluteAttachmentPath,
|
getAbsoluteAttachmentPath,
|
||||||
});
|
});
|
||||||
assert.deepEqual(actual, expected);
|
assert.deepEqual(actual, expected);
|
||||||
|
@ -115,6 +120,7 @@ describe('Contact', () => {
|
||||||
isProfile: true,
|
isProfile: true,
|
||||||
avatar: {
|
avatar: {
|
||||||
pending: true,
|
pending: true,
|
||||||
|
contentType: IMAGE_GIF,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -130,14 +136,17 @@ describe('Contact', () => {
|
||||||
avatar: {
|
avatar: {
|
||||||
pending: true,
|
pending: true,
|
||||||
path: undefined,
|
path: undefined,
|
||||||
|
contentType: IMAGE_GIF,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
signalAccount,
|
firstNumber,
|
||||||
|
isNumberOnSignal,
|
||||||
number: undefined,
|
number: undefined,
|
||||||
};
|
};
|
||||||
const actual = contactSelector(contact, {
|
const actual = contactSelector(contact, {
|
||||||
regionCode,
|
regionCode,
|
||||||
signalAccount,
|
firstNumber,
|
||||||
|
isNumberOnSignal,
|
||||||
getAbsoluteAttachmentPath,
|
getAbsoluteAttachmentPath,
|
||||||
});
|
});
|
||||||
assert.deepEqual(actual, expected);
|
assert.deepEqual(actual, expected);
|
||||||
|
@ -155,6 +164,7 @@ describe('Contact', () => {
|
||||||
isProfile: true,
|
isProfile: true,
|
||||||
avatar: {
|
avatar: {
|
||||||
path: 'somewhere',
|
path: 'somewhere',
|
||||||
|
contentType: IMAGE_GIF,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -169,14 +179,17 @@ describe('Contact', () => {
|
||||||
isProfile: true,
|
isProfile: true,
|
||||||
avatar: {
|
avatar: {
|
||||||
path: 'absolute:somewhere',
|
path: 'absolute:somewhere',
|
||||||
|
contentType: IMAGE_GIF,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
signalAccount,
|
firstNumber,
|
||||||
|
isNumberOnSignal: true,
|
||||||
number: undefined,
|
number: undefined,
|
||||||
};
|
};
|
||||||
const actual = contactSelector(contact, {
|
const actual = contactSelector(contact, {
|
||||||
regionCode,
|
regionCode,
|
||||||
signalAccount,
|
firstNumber,
|
||||||
|
isNumberOnSignal: true,
|
||||||
getAbsoluteAttachmentPath,
|
getAbsoluteAttachmentPath,
|
||||||
});
|
});
|
||||||
assert.deepEqual(actual, expected);
|
assert.deepEqual(actual, expected);
|
||||||
|
|
|
@ -2489,10 +2489,14 @@ class MessageReceiverInner extends EventTarget {
|
||||||
async downloadAttachment(
|
async downloadAttachment(
|
||||||
attachment: AttachmentPointerClass
|
attachment: AttachmentPointerClass
|
||||||
): Promise<DownloadAttachmentType> {
|
): Promise<DownloadAttachmentType> {
|
||||||
const encrypted = await this.server.getAttachment(
|
const cdnId = attachment.cdnId || attachment.cdnKey;
|
||||||
attachment.cdnId || attachment.cdnKey,
|
const { cdnNumber } = attachment;
|
||||||
attachment.cdnNumber || 0
|
|
||||||
);
|
if (!cdnId) {
|
||||||
|
throw new Error('downloadAttachment: Attachment was missing cdnId!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const encrypted = await this.server.getAttachment(cdnId, cdnNumber);
|
||||||
const { key, digest, size } = attachment;
|
const { key, digest, size } = attachment;
|
||||||
|
|
||||||
if (!digest) {
|
if (!digest) {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
compact,
|
compact,
|
||||||
Dictionary,
|
Dictionary,
|
||||||
escapeRegExp,
|
escapeRegExp,
|
||||||
|
isNumber,
|
||||||
mapValues,
|
mapValues,
|
||||||
zipObject,
|
zipObject,
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
|
@ -933,7 +934,7 @@ export type WebAPIType = {
|
||||||
group: GroupClass,
|
group: GroupClass,
|
||||||
options: GroupCredentialsType
|
options: GroupCredentialsType
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
getAttachment: (cdnKey: string, cdnNumber: number) => Promise<any>;
|
getAttachment: (cdnKey: string, cdnNumber?: number) => Promise<any>;
|
||||||
getAvatar: (path: string) => Promise<any>;
|
getAvatar: (path: string) => Promise<any>;
|
||||||
getDevices: () => Promise<any>;
|
getDevices: () => Promise<any>;
|
||||||
getGroup: (options: GroupCredentialsType) => Promise<GroupClass>;
|
getGroup: (options: GroupCredentialsType) => Promise<GroupClass>;
|
||||||
|
@ -1976,8 +1977,10 @@ export function initialize({
|
||||||
return packId;
|
return packId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAttachment(cdnKey: string, cdnNumber: number) {
|
async function getAttachment(cdnKey: string, cdnNumber?: number) {
|
||||||
const cdnUrl = cdnUrlObject[cdnNumber] || cdnUrlObject['0'];
|
const cdnUrl = isNumber(cdnNumber)
|
||||||
|
? cdnUrlObject[cdnNumber] || cdnUrlObject['0']
|
||||||
|
: cdnUrlObject['0'];
|
||||||
// This is going to the CDN, not the service, so we use _outerAjax
|
// This is going to the CDN, not the service, so we use _outerAjax
|
||||||
return _outerAjax(`${cdnUrl}/attachments/${cdnKey}`, {
|
return _outerAjax(`${cdnUrl}/attachments/${cdnKey}`, {
|
||||||
certificateAuthority,
|
certificateAuthority,
|
||||||
|
|
|
@ -21,10 +21,11 @@ const MIN_HEIGHT = 50;
|
||||||
// Used for display
|
// Used for display
|
||||||
|
|
||||||
export type AttachmentType = {
|
export type AttachmentType = {
|
||||||
|
error?: boolean;
|
||||||
blurHash?: string;
|
blurHash?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
contentType: MIME.MIMEType;
|
contentType: MIME.MIMEType;
|
||||||
fileName: string;
|
fileName?: string;
|
||||||
/** Not included in protobuf, needs to be pulled from flags */
|
/** Not included in protobuf, needs to be pulled from flags */
|
||||||
isVoiceMessage?: boolean;
|
isVoiceMessage?: boolean;
|
||||||
/** For messages not already on disk, this will be a data url */
|
/** For messages not already on disk, this will be a data url */
|
||||||
|
@ -40,16 +41,25 @@ export type AttachmentType = {
|
||||||
width: number;
|
width: number;
|
||||||
url: string;
|
url: string;
|
||||||
contentType: MIME.MIMEType;
|
contentType: MIME.MIMEType;
|
||||||
};
|
|
||||||
flags?: number;
|
|
||||||
thumbnail?: {
|
|
||||||
height: number;
|
|
||||||
width: number;
|
|
||||||
url: string;
|
|
||||||
contentType: MIME.MIMEType;
|
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
flags?: number;
|
||||||
|
thumbnail?: ThumbnailType;
|
||||||
isCorrupted?: boolean;
|
isCorrupted?: boolean;
|
||||||
|
downloadJobId?: string;
|
||||||
|
cdnNumber?: number;
|
||||||
|
cdnId?: string;
|
||||||
|
cdnKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ThumbnailType = {
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
url: string;
|
||||||
|
contentType: MIME.MIMEType;
|
||||||
|
path: string;
|
||||||
|
// Only used when quote needed to make an in-memory thumbnail
|
||||||
|
objectUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// UI-focused functions
|
// UI-focused functions
|
||||||
|
@ -58,7 +68,7 @@ export function getExtensionForDisplay({
|
||||||
fileName,
|
fileName,
|
||||||
contentType,
|
contentType,
|
||||||
}: {
|
}: {
|
||||||
fileName: string;
|
fileName?: string;
|
||||||
contentType: MIME.MIMEType;
|
contentType: MIME.MIMEType;
|
||||||
}): string | undefined {
|
}): string | undefined {
|
||||||
if (fileName && fileName.indexOf('.') >= 0) {
|
if (fileName && fileName.indexOf('.') >= 0) {
|
||||||
|
@ -354,7 +364,9 @@ export const isFile = (attachment: Attachment): boolean => {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isVoiceMessage = (attachment: Attachment): boolean => {
|
export const isVoiceMessage = (
|
||||||
|
attachment: Attachment | AttachmentType
|
||||||
|
): boolean => {
|
||||||
const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE;
|
const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE;
|
||||||
const hasFlag =
|
const hasFlag =
|
||||||
// eslint-disable-next-line no-bitwise
|
// eslint-disable-next-line no-bitwise
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { format as formatPhoneNumber } from './PhoneNumber';
|
import { format as formatPhoneNumber } from './PhoneNumber';
|
||||||
|
import { AttachmentType } from './Attachment';
|
||||||
|
|
||||||
export type ContactType = {
|
export type ContactType = {
|
||||||
name?: Name;
|
name?: Name;
|
||||||
|
@ -10,7 +11,10 @@ export type ContactType = {
|
||||||
address?: Array<PostalAddress>;
|
address?: Array<PostalAddress>;
|
||||||
avatar?: Avatar;
|
avatar?: Avatar;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
signalAccount?: string;
|
|
||||||
|
// Populated by selector
|
||||||
|
firstNumber?: string;
|
||||||
|
isNumberOnSignal?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Name = {
|
type Name = {
|
||||||
|
@ -60,25 +64,25 @@ export type PostalAddress = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type Avatar = {
|
type Avatar = {
|
||||||
avatar: Attachment;
|
avatar: AttachmentType;
|
||||||
isProfile: boolean;
|
isProfile: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Attachment = {
|
|
||||||
path?: string;
|
|
||||||
error?: boolean;
|
|
||||||
pending?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function contactSelector(
|
export function contactSelector(
|
||||||
contact: ContactType,
|
contact: ContactType,
|
||||||
options: {
|
options: {
|
||||||
regionCode: string;
|
regionCode: string;
|
||||||
signalAccount?: string;
|
firstNumber?: string;
|
||||||
|
isNumberOnSignal?: boolean;
|
||||||
getAbsoluteAttachmentPath: (path: string) => string;
|
getAbsoluteAttachmentPath: (path: string) => string;
|
||||||
}
|
}
|
||||||
): ContactType {
|
): ContactType {
|
||||||
const { getAbsoluteAttachmentPath, signalAccount, regionCode } = options;
|
const {
|
||||||
|
getAbsoluteAttachmentPath,
|
||||||
|
firstNumber,
|
||||||
|
isNumberOnSignal,
|
||||||
|
regionCode,
|
||||||
|
} = options;
|
||||||
|
|
||||||
let { avatar } = contact;
|
let { avatar } = contact;
|
||||||
if (avatar && avatar.avatar) {
|
if (avatar && avatar.avatar) {
|
||||||
|
@ -99,7 +103,8 @@ export function contactSelector(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...contact,
|
...contact,
|
||||||
signalAccount,
|
firstNumber,
|
||||||
|
isNumberOnSignal,
|
||||||
avatar,
|
avatar,
|
||||||
number:
|
number:
|
||||||
contact.number &&
|
contact.number &&
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { DeletesModelType } from '../model-types.d';
|
import { DeleteModel } from '../messageModifiers/Deletes';
|
||||||
import { MessageModel } from '../models/messages';
|
import { MessageModel } from '../models/messages';
|
||||||
|
|
||||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
export async function deleteForEveryone(
|
export async function deleteForEveryone(
|
||||||
message: MessageModel,
|
message: MessageModel,
|
||||||
doe: DeletesModelType,
|
doe: DeleteModel,
|
||||||
shouldPersist = true
|
shouldPersist = true
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Make sure the server timestamps for the DOE and the matching message
|
// Make sure the server timestamps for the DOE and the matching message
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { ConversationAttributesType } from '../model-types.d';
|
import { ConversationAttributesType } from '../model-types.d';
|
||||||
import { handleMessageSend } from './handleMessageSend';
|
import { handleMessageSend } from './handleMessageSend';
|
||||||
import { sendReadReceiptsFor } from './sendReadReceiptsFor';
|
import { sendReadReceiptsFor } from './sendReadReceiptsFor';
|
||||||
|
import { hasErrors } from '../state/selectors/message';
|
||||||
|
|
||||||
export async function markConversationRead(
|
export async function markConversationRead(
|
||||||
conversationAttrs: ConversationAttributesType,
|
conversationAttrs: ConversationAttributesType,
|
||||||
|
@ -74,7 +75,7 @@ export async function markConversationRead(
|
||||||
uuid: messageSyncData.sourceUuid,
|
uuid: messageSyncData.sourceUuid,
|
||||||
}),
|
}),
|
||||||
timestamp: messageSyncData.sent_at,
|
timestamp: messageSyncData.sent_at,
|
||||||
hasErrors: message ? message.hasErrors() : false,
|
hasErrors: message ? hasErrors(message.attributes) : false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,11 @@ import { ConversationModel } from '../models/conversations';
|
||||||
import {
|
import {
|
||||||
GroupV2PendingMemberType,
|
GroupV2PendingMemberType,
|
||||||
MessageModelCollectionType,
|
MessageModelCollectionType,
|
||||||
|
MessageAttributesType,
|
||||||
} from '../model-types.d';
|
} from '../model-types.d';
|
||||||
import { LinkPreviewType } from '../types/message/LinkPreviews';
|
import { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||||
import { MediaItemType } from '../components/LightboxGallery';
|
import { MediaItemType } from '../components/LightboxGallery';
|
||||||
import { MessageModel } from '../models/messages';
|
import { MessageModel } from '../models/messages';
|
||||||
import { MessageType } from '../state/ducks/conversations';
|
|
||||||
import { assert } from '../util/assert';
|
import { assert } from '../util/assert';
|
||||||
import { maybeParseUrl } from '../util/url';
|
import { maybeParseUrl } from '../util/url';
|
||||||
import { addReportSpamJob } from '../jobs/helpers/addReportSpamJob';
|
import { addReportSpamJob } from '../jobs/helpers/addReportSpamJob';
|
||||||
|
@ -24,6 +24,13 @@ import {
|
||||||
isGroupV2,
|
isGroupV2,
|
||||||
isMe,
|
isMe,
|
||||||
} from '../util/whatTypeOfConversation';
|
} from '../util/whatTypeOfConversation';
|
||||||
|
import {
|
||||||
|
canReply,
|
||||||
|
getAttachmentsForMessage,
|
||||||
|
getPropsForQuote,
|
||||||
|
isOutgoing,
|
||||||
|
isTapToView,
|
||||||
|
} from '../state/selectors/message';
|
||||||
|
|
||||||
type GetLinkPreviewImageResult = {
|
type GetLinkPreviewImageResult = {
|
||||||
data: ArrayBuffer;
|
data: ArrayBuffer;
|
||||||
|
@ -910,9 +917,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
const isNewMessage = false;
|
const isNewMessage = false;
|
||||||
messagesAdded(
|
messagesAdded(
|
||||||
id,
|
id,
|
||||||
cleaned.map((messageModel: MessageModel) =>
|
cleaned.map((messageModel: MessageModel) => ({
|
||||||
messageModel.getReduxData()
|
...messageModel.attributes,
|
||||||
),
|
})),
|
||||||
isNewMessage,
|
isNewMessage,
|
||||||
window.isActive()
|
window.isActive()
|
||||||
);
|
);
|
||||||
|
@ -965,9 +972,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
const isNewMessage = false;
|
const isNewMessage = false;
|
||||||
messagesAdded(
|
messagesAdded(
|
||||||
id,
|
id,
|
||||||
cleaned.map((messageModel: MessageModel) =>
|
cleaned.map((messageModel: MessageModel) => ({
|
||||||
messageModel.getReduxData()
|
...messageModel.attributes,
|
||||||
),
|
})),
|
||||||
isNewMessage,
|
isNewMessage,
|
||||||
window.isActive()
|
window.isActive()
|
||||||
);
|
);
|
||||||
|
@ -1210,9 +1217,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
|
|
||||||
messagesReset(
|
messagesReset(
|
||||||
conversationId,
|
conversationId,
|
||||||
cleaned.map((messageModel: MessageModel) =>
|
cleaned.map((messageModel: MessageModel) => ({
|
||||||
messageModel.getReduxData()
|
...messageModel.attributes,
|
||||||
),
|
})),
|
||||||
metrics,
|
metrics,
|
||||||
scrollToMessageId
|
scrollToMessageId
|
||||||
);
|
);
|
||||||
|
@ -1294,9 +1301,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
const unboundedFetch = true;
|
const unboundedFetch = true;
|
||||||
messagesReset(
|
messagesReset(
|
||||||
conversationId,
|
conversationId,
|
||||||
cleaned.map((messageModel: MessageModel) =>
|
cleaned.map((messageModel: MessageModel) => ({
|
||||||
messageModel.getReduxData()
|
...messageModel.attributes,
|
||||||
),
|
})),
|
||||||
metrics,
|
metrics,
|
||||||
scrollToMessageId,
|
scrollToMessageId,
|
||||||
unboundedFetch
|
unboundedFetch
|
||||||
|
@ -2327,7 +2334,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
throw new Error(`showForwardMessageModal: Message ${messageId} missing!`);
|
throw new Error(`showForwardMessageModal: Message ${messageId} missing!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachments = message.getAttachmentsForMessage();
|
const attachments = getAttachmentsForMessage(message.attributes);
|
||||||
this.forwardMessageModal = new Whisper.ReactWrapperView({
|
this.forwardMessageModal = new Whisper.ReactWrapperView({
|
||||||
JSX: window.Signal.State.Roots.createForwardMessageModal(
|
JSX: window.Signal.State.Roots.createForwardMessageModal(
|
||||||
window.reduxStore,
|
window.reduxStore,
|
||||||
|
@ -2557,7 +2564,10 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
const message = rawMedia[i];
|
const message = rawMedia[i];
|
||||||
const { schemaVersion } = message;
|
const { schemaVersion } = message;
|
||||||
|
|
||||||
if (schemaVersion < Message.VERSION_NEEDED_FOR_DISPLAY) {
|
if (
|
||||||
|
schemaVersion &&
|
||||||
|
schemaVersion < Message.VERSION_NEEDED_FOR_DISPLAY
|
||||||
|
) {
|
||||||
// Yep, we really do want to wait for each of these
|
// Yep, we really do want to wait for each of these
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
rawMedia[i] = await upgradeMessageSchema(message);
|
rawMedia[i] = await upgradeMessageSchema(message);
|
||||||
|
@ -2871,7 +2881,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
throw new Error(`displayTapToViewMessage: Message ${messageId} missing!`);
|
throw new Error(`displayTapToViewMessage: Message ${messageId} missing!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message.isTapToView()) {
|
if (!isTapToView(message.attributes)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`displayTapToViewMessage: Message ${message.idForLogging()} is not a tap to view message`
|
`displayTapToViewMessage: Message ${message.idForLogging()} is not a tap to view message`
|
||||||
);
|
);
|
||||||
|
@ -2883,7 +2893,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstAttachment = message.get('attachments')[0];
|
const firstAttachment = (message.get('attachments') || [])[0];
|
||||||
if (!firstAttachment || !firstAttachment.path) {
|
if (!firstAttachment || !firstAttachment.path) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`displayTapToViewMessage: Message ${message.idForLogging()} had no first attachment with path`
|
`displayTapToViewMessage: Message ${message.idForLogging()} had no first attachment with path`
|
||||||
|
@ -2955,7 +2965,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
Message: Whisper.Message,
|
Message: Whisper.Message,
|
||||||
});
|
});
|
||||||
message.cleanup();
|
message.cleanup();
|
||||||
if (message.isOutgoing()) {
|
if (isOutgoing(message.attributes)) {
|
||||||
this.model.decrementSentMessageCount();
|
this.model.decrementSentMessageCount();
|
||||||
} else {
|
} else {
|
||||||
this.model.decrementMessageCount();
|
this.model.decrementMessageCount();
|
||||||
|
@ -3531,7 +3541,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
async loadRecentMediaItems(limit: number): Promise<void> {
|
async loadRecentMediaItems(limit: number): Promise<void> {
|
||||||
const { model }: { model: ConversationModel } = this;
|
const { model }: { model: ConversationModel } = this;
|
||||||
|
|
||||||
const messages: Array<MessageType> = await window.Signal.Data.getMessagesWithVisualMediaAttachments(
|
const messages: Array<MessageAttributesType> = await window.Signal.Data.getMessagesWithVisualMediaAttachments(
|
||||||
model.id,
|
model.id,
|
||||||
{
|
{
|
||||||
limit,
|
limit,
|
||||||
|
@ -3543,7 +3553,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
.reduce(
|
.reduce(
|
||||||
(acc, message) => [
|
(acc, message) => [
|
||||||
...acc,
|
...acc,
|
||||||
...message.attachments.map(
|
...(message.attachments || []).map(
|
||||||
(attachment: AttachmentType, index: number): MediaItemType => {
|
(attachment: AttachmentType, index: number): MediaItemType => {
|
||||||
const { thumbnail } = attachment;
|
const { thumbnail } = attachment;
|
||||||
|
|
||||||
|
@ -3792,7 +3802,12 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (message && !message.canReply()) {
|
if (
|
||||||
|
message &&
|
||||||
|
!canReply(message.attributes, (id?: string) =>
|
||||||
|
message.findAndFormatContact(id)
|
||||||
|
)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3855,7 +3870,11 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
message.quotedMessage = this.quotedMessage;
|
message.quotedMessage = this.quotedMessage;
|
||||||
this.quoteHolder = message;
|
this.quoteHolder = message;
|
||||||
|
|
||||||
const props = message.getPropsForQuote();
|
const props = getPropsForQuote(
|
||||||
|
message.attributes,
|
||||||
|
(id?: string) => message.findAndFormatContact(id),
|
||||||
|
window.ConversationController.getOurConversationIdOrThrow()
|
||||||
|
);
|
||||||
|
|
||||||
const contact = this.quotedMessage.getContact();
|
const contact = this.quotedMessage.getContact();
|
||||||
|
|
||||||
|
|
86
ts/window.d.ts
vendored
86
ts/window.d.ts
vendored
|
@ -19,7 +19,11 @@ import {
|
||||||
ReactionAttributesType,
|
ReactionAttributesType,
|
||||||
ReactionModelType,
|
ReactionModelType,
|
||||||
} from './model-types.d';
|
} from './model-types.d';
|
||||||
import { ContactRecordIdentityState, TextSecureType } from './textsecure.d';
|
import {
|
||||||
|
ContactRecordIdentityState,
|
||||||
|
TextSecureType,
|
||||||
|
DownloadAttachmentType,
|
||||||
|
} from './textsecure.d';
|
||||||
import { Storage } from './textsecure/Storage';
|
import { Storage } from './textsecure/Storage';
|
||||||
import {
|
import {
|
||||||
ChallengeHandler,
|
ChallengeHandler,
|
||||||
|
@ -107,6 +111,7 @@ import { Quote } from './components/conversation/Quote';
|
||||||
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
|
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
|
||||||
import { DisappearingTimeDialog } from './components/conversation/DisappearingTimeDialog';
|
import { DisappearingTimeDialog } from './components/conversation/DisappearingTimeDialog';
|
||||||
import { MIMEType } from './types/MIME';
|
import { MIMEType } from './types/MIME';
|
||||||
|
import { AttachmentType } from './types/Attachment';
|
||||||
import { ElectronLocaleType } from './util/mapToSupportLocale';
|
import { ElectronLocaleType } from './util/mapToSupportLocale';
|
||||||
import { SignalProtocolStore } from './SignalProtocolStore';
|
import { SignalProtocolStore } from './SignalProtocolStore';
|
||||||
import { StartupQueue } from './util/StartupQueue';
|
import { StartupQueue } from './util/StartupQueue';
|
||||||
|
@ -222,16 +227,7 @@ declare global {
|
||||||
getRegionCodeForNumber: (number: string) => string;
|
getRegionCodeForNumber: (number: string) => string;
|
||||||
format: (number: string, format: PhoneNumberFormat) => string;
|
format: (number: string, format: PhoneNumberFormat) => string;
|
||||||
};
|
};
|
||||||
log: {
|
log: LoggerType;
|
||||||
fatal: LoggerType;
|
|
||||||
info: LoggerType;
|
|
||||||
warn: LoggerType;
|
|
||||||
error: LoggerType;
|
|
||||||
debug: LoggerType;
|
|
||||||
trace: LoggerType;
|
|
||||||
fetch: () => Promise<string>;
|
|
||||||
publish: typeof uploadDebugLogs;
|
|
||||||
};
|
|
||||||
nodeSetImmediate: typeof setImmediate;
|
nodeSetImmediate: typeof setImmediate;
|
||||||
normalizeUuids: (obj: any, paths: Array<string>, context: string) => void;
|
normalizeUuids: (obj: any, paths: Array<string>, context: string) => void;
|
||||||
onFullScreenChange: (fullScreen: boolean) => void;
|
onFullScreenChange: (fullScreen: boolean) => void;
|
||||||
|
@ -275,14 +271,6 @@ declare global {
|
||||||
};
|
};
|
||||||
Signal: {
|
Signal: {
|
||||||
Backbone: any;
|
Backbone: any;
|
||||||
AttachmentDownloads: {
|
|
||||||
addJob: <T = unknown>(
|
|
||||||
attachment: unknown,
|
|
||||||
options: unknown
|
|
||||||
) => Promise<T>;
|
|
||||||
start: (options: WhatIsThis) => void;
|
|
||||||
stop: () => void;
|
|
||||||
};
|
|
||||||
Crypto: typeof Crypto;
|
Crypto: typeof Crypto;
|
||||||
Curve: typeof Curve;
|
Curve: typeof Curve;
|
||||||
Data: typeof Data;
|
Data: typeof Data;
|
||||||
|
@ -317,6 +305,9 @@ declare global {
|
||||||
loadStickerData: (sticker: unknown) => WhatIsThis;
|
loadStickerData: (sticker: unknown) => WhatIsThis;
|
||||||
readStickerData: (path: string) => Promise<ArrayBuffer>;
|
readStickerData: (path: string) => Promise<ArrayBuffer>;
|
||||||
upgradeMessageSchema: (attributes: unknown) => WhatIsThis;
|
upgradeMessageSchema: (attributes: unknown) => WhatIsThis;
|
||||||
|
processNewAttachment: (
|
||||||
|
attachment: DownloadAttachmentType
|
||||||
|
) => Promise<AttachmentType>;
|
||||||
|
|
||||||
copyIntoTempDirectory: any;
|
copyIntoTempDirectory: any;
|
||||||
deleteDraftFile: any;
|
deleteDraftFile: any;
|
||||||
|
@ -545,13 +536,6 @@ declare global {
|
||||||
WebAPI: WebAPIConnectType;
|
WebAPI: WebAPIConnectType;
|
||||||
Whisper: WhisperType;
|
Whisper: WhisperType;
|
||||||
|
|
||||||
AccountCache: Record<string, boolean>;
|
|
||||||
AccountJobs: Record<string, Promise<void>>;
|
|
||||||
|
|
||||||
doesAccountCheckJobExist: (number: string) => boolean;
|
|
||||||
checkForSignalAccount: (number: string) => Promise<void>;
|
|
||||||
isSignalAccountCheckComplete: (number: string) => boolean;
|
|
||||||
hasSignalAccount: (number: string) => boolean;
|
|
||||||
getServerTrustRoot: () => WhatIsThis;
|
getServerTrustRoot: () => WhatIsThis;
|
||||||
readyForUpdates: () => void;
|
readyForUpdates: () => void;
|
||||||
logAppLoadedEvent: (options: { processedCount?: number }) => void;
|
logAppLoadedEvent: (options: { processedCount?: number }) => void;
|
||||||
|
@ -648,7 +632,18 @@ export class CanvasVideoRenderer {
|
||||||
constructor(canvas: Ref<HTMLCanvasElement>);
|
constructor(canvas: Ref<HTMLCanvasElement>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LoggerType = (...args: Array<unknown>) => void;
|
export type LoggerType = {
|
||||||
|
fatal: LogFunctionType;
|
||||||
|
info: LogFunctionType;
|
||||||
|
warn: LogFunctionType;
|
||||||
|
error: LogFunctionType;
|
||||||
|
debug: LogFunctionType;
|
||||||
|
trace: LogFunctionType;
|
||||||
|
fetch: () => Promise<string>;
|
||||||
|
publish: typeof uploadDebugLogs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LogFunctionType = (...args: Array<unknown>) => void;
|
||||||
|
|
||||||
export type WhisperType = {
|
export type WhisperType = {
|
||||||
events: {
|
events: {
|
||||||
|
@ -685,7 +680,6 @@ export type WhisperType = {
|
||||||
ConversationUnarchivedToast: WhatIsThis;
|
ConversationUnarchivedToast: WhatIsThis;
|
||||||
ConversationMarkedUnreadToast: WhatIsThis;
|
ConversationMarkedUnreadToast: WhatIsThis;
|
||||||
WallClockListener: WhatIsThis;
|
WallClockListener: WhatIsThis;
|
||||||
MessageRequests: WhatIsThis;
|
|
||||||
BannerView: any;
|
BannerView: any;
|
||||||
RecorderView: any;
|
RecorderView: any;
|
||||||
GroupMemberList: any;
|
GroupMemberList: any;
|
||||||
|
@ -712,42 +706,6 @@ export type WhisperType = {
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
DeliveryReceipts: {
|
|
||||||
add: (receipt: WhatIsThis) => void;
|
|
||||||
forMessage: (conversation: unknown, message: unknown) => Array<WhatIsThis>;
|
|
||||||
onReceipt: (receipt: WhatIsThis) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
ReadReceipts: {
|
|
||||||
add: (receipt: WhatIsThis) => WhatIsThis;
|
|
||||||
forMessage: (conversation: unknown, message: unknown) => Array<WhatIsThis>;
|
|
||||||
onReceipt: (receipt: WhatIsThis) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
ReadSyncs: {
|
|
||||||
add: (sync: WhatIsThis) => WhatIsThis;
|
|
||||||
forMessage: (message: unknown) => WhatIsThis;
|
|
||||||
onReceipt: (receipt: WhatIsThis) => WhatIsThis;
|
|
||||||
};
|
|
||||||
|
|
||||||
ViewSyncs: {
|
|
||||||
add: (sync: WhatIsThis) => WhatIsThis;
|
|
||||||
forMessage: (message: unknown) => Array<WhatIsThis>;
|
|
||||||
onSync: (sync: WhatIsThis) => WhatIsThis;
|
|
||||||
};
|
|
||||||
|
|
||||||
Reactions: {
|
|
||||||
forMessage: (message: unknown) => Array<ReactionModelType>;
|
|
||||||
add: (reaction: ReactionAttributesType) => ReactionModelType;
|
|
||||||
onReaction: (reactionModel: ReactionModelType) => ReactionAttributesType;
|
|
||||||
};
|
|
||||||
|
|
||||||
Deletes: {
|
|
||||||
add: (model: WhatIsThis) => WhatIsThis;
|
|
||||||
forMessage: (message: unknown) => Array<WhatIsThis>;
|
|
||||||
onDelete: (model: WhatIsThis) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
IdenticonSVGView: WhatIsThis;
|
IdenticonSVGView: WhatIsThis;
|
||||||
|
|
||||||
ExpiringMessagesListener: WhatIsThis;
|
ExpiringMessagesListener: WhatIsThis;
|
||||||
|
|
Loading…
Reference in a new issue