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='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/expiring_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/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 Util = require('../../ts/util');
|
||||
const LinkPreviews = require('./link_previews');
|
||||
const AttachmentDownloads = require('./attachment_downloads');
|
||||
|
||||
// Components
|
||||
const {
|
||||
|
@ -443,7 +442,6 @@ exports.setup = (options = {}) => {
|
|||
};
|
||||
|
||||
return {
|
||||
AttachmentDownloads,
|
||||
Backbone,
|
||||
Components,
|
||||
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.imageToBlurHash = imageToBlurHash;
|
||||
window.emojiData = require('emoji-datasource');
|
||||
window.filesize = require('filesize');
|
||||
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
|
||||
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;
|
||||
window.loadImage = require('blueimp-load-image');
|
||||
|
|
|
@ -347,7 +347,6 @@
|
|||
<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/message_controller.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_tap_to_view_messages.js' data-cover></script>
|
||||
|
|
|
@ -715,7 +715,7 @@ export class ConversationController {
|
|||
async getConversationForTargetMessage(
|
||||
targetFromId: string,
|
||||
targetTimestamp: number
|
||||
): Promise<boolean | ConversationModel | null | undefined> {
|
||||
): Promise<ConversationModel | null | undefined> {
|
||||
const messages = await getMessagesBySentAt(targetTimestamp, {
|
||||
MessageCollection: window.Whisper.MessageCollection,
|
||||
});
|
||||
|
|
|
@ -43,7 +43,16 @@ import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
|
|||
import { getSendOptions } from './util/getSendOptions';
|
||||
import { BackOff } from './util/BackOff';
|
||||
import { AppViewType } from './state/ducks/app';
|
||||
import { hasErrors, isIncoming } from './state/selectors/message';
|
||||
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;
|
||||
|
||||
|
@ -337,13 +346,15 @@ export async function startApp(): Promise<void> {
|
|||
PASSWORD
|
||||
);
|
||||
accountManager.addEventListener('registration', () => {
|
||||
const ourDeviceId = window.textsecure.storage.user.getDeviceId();
|
||||
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||
const ourUuid = window.textsecure.storage.user.getUuid();
|
||||
const user = {
|
||||
regionCode: window.storage.get('regionCode'),
|
||||
ourConversationId: window.ConversationController.getOurConversationId(),
|
||||
ourDeviceId,
|
||||
ourNumber,
|
||||
ourUuid,
|
||||
ourConversationId: window.ConversationController.getOurConversationId(),
|
||||
regionCode: window.storage.get('regionCode'),
|
||||
};
|
||||
window.Whisper.events.trigger('userChanged', user);
|
||||
|
||||
|
@ -548,7 +559,7 @@ export async function startApp(): Promise<void> {
|
|||
shutdown: async () => {
|
||||
window.log.info('background/shutdown');
|
||||
// Stop background processing
|
||||
window.Signal.AttachmentDownloads.stop();
|
||||
AttachmentDownloads.stop();
|
||||
if (idleDetector) {
|
||||
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
|
||||
// redux when things change in the backbone world.
|
||||
window.reduxActions = {
|
||||
accounts: bindActionCreators(actionCreators.accounts, store.dispatch),
|
||||
app: bindActionCreators(actionCreators.app, store.dispatch),
|
||||
audioPlayer: bindActionCreators(
|
||||
actionCreators.audioPlayer,
|
||||
|
@ -1659,7 +1671,7 @@ export async function startApp(): Promise<void> {
|
|||
const delivered = message.get('delivered');
|
||||
const sentAt = message.get('sent_at');
|
||||
|
||||
if (message.hasErrors()) {
|
||||
if (hasErrors(message.attributes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1887,7 +1899,7 @@ export async function startApp(): Promise<void> {
|
|||
// Clear timer, since we're only called when the timer is expired
|
||||
disconnectTimer = null;
|
||||
|
||||
window.Signal.AttachmentDownloads.stop();
|
||||
AttachmentDownloads.stop();
|
||||
if (messageReceiver) {
|
||||
await messageReceiver.close();
|
||||
}
|
||||
|
@ -2063,7 +2075,7 @@ export async function startApp(): Promise<void> {
|
|||
addQueuedEventListener('fetchLatest', onFetchLatestSync);
|
||||
addQueuedEventListener('keys', onKeysSync);
|
||||
|
||||
window.Signal.AttachmentDownloads.start({
|
||||
AttachmentDownloads.start({
|
||||
getMessageReceiver: () => messageReceiver,
|
||||
logger: window.log,
|
||||
});
|
||||
|
@ -2859,7 +2871,10 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
const message = initIncomingMessage(data, messageDescriptor);
|
||||
|
||||
if (message.isIncoming() && message.get('unidentifiedDeliveryReceived')) {
|
||||
if (
|
||||
isIncoming(message.attributes) &&
|
||||
message.get('unidentifiedDeliveryReceived')
|
||||
) {
|
||||
const sender = message.getContact();
|
||||
|
||||
if (!sender) {
|
||||
|
@ -2890,7 +2905,7 @@ export async function startApp(): Promise<void> {
|
|||
'Queuing incoming reaction for',
|
||||
reaction.targetTimestamp
|
||||
);
|
||||
const reactionModel = window.Whisper.Reactions.add({
|
||||
const reactionModel = Reactions.getSingleton().add({
|
||||
emoji: reaction.emoji,
|
||||
remove: reaction.remove,
|
||||
targetAuthorUuid: reaction.targetAuthorUuid,
|
||||
|
@ -2902,7 +2917,7 @@ export async function startApp(): Promise<void> {
|
|||
}),
|
||||
});
|
||||
// Note: We do not wait for completion here
|
||||
window.Whisper.Reactions.onReaction(reactionModel);
|
||||
Reactions.getSingleton().onReaction(reactionModel);
|
||||
confirm();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -2910,7 +2925,7 @@ export async function startApp(): Promise<void> {
|
|||
if (data.message.delete) {
|
||||
const { delete: del } = data.message;
|
||||
window.log.info('Queuing incoming DOE for', del.targetSentTimestamp);
|
||||
const deleteModel = window.Whisper.Deletes.add({
|
||||
const deleteModel = Deletes.getSingleton().add({
|
||||
targetSentTimestamp: del.targetSentTimestamp,
|
||||
serverTimestamp: data.serverTimestamp,
|
||||
fromId: window.ConversationController.ensureContactIds({
|
||||
|
@ -2919,7 +2934,7 @@ export async function startApp(): Promise<void> {
|
|||
}),
|
||||
});
|
||||
// Note: We do not wait for completion here
|
||||
window.Whisper.Deletes.onDelete(deleteModel);
|
||||
Deletes.getSingleton().onDelete(deleteModel);
|
||||
confirm();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -3184,7 +3199,7 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
|
||||
window.log.info('Queuing sent reaction for', reaction.targetTimestamp);
|
||||
const reactionModel = window.Whisper.Reactions.add({
|
||||
const reactionModel = Reactions.getSingleton().add({
|
||||
emoji: reaction.emoji,
|
||||
remove: reaction.remove,
|
||||
targetAuthorUuid: reaction.targetAuthorUuid,
|
||||
|
@ -3194,7 +3209,7 @@ export async function startApp(): Promise<void> {
|
|||
fromSync: true,
|
||||
});
|
||||
// Note: We do not wait for completion here
|
||||
window.Whisper.Reactions.onReaction(reactionModel);
|
||||
Reactions.getSingleton().onReaction(reactionModel);
|
||||
|
||||
event.confirm();
|
||||
return Promise.resolve();
|
||||
|
@ -3203,13 +3218,13 @@ export async function startApp(): Promise<void> {
|
|||
if (data.message.delete) {
|
||||
const { delete: del } = data.message;
|
||||
window.log.info('Queuing sent DOE for', del.targetSentTimestamp);
|
||||
const deleteModel = window.Whisper.Deletes.add({
|
||||
const deleteModel = Deletes.getSingleton().add({
|
||||
targetSentTimestamp: del.targetSentTimestamp,
|
||||
serverTimestamp: del.serverTimestamp,
|
||||
fromId: window.ConversationController.getOurConversationId(),
|
||||
});
|
||||
// Note: We do not wait for completion here
|
||||
window.Whisper.Deletes.onDelete(deleteModel);
|
||||
Deletes.getSingleton().onDelete(deleteModel);
|
||||
confirm();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -3299,11 +3314,13 @@ export async function startApp(): Promise<void> {
|
|||
window.Signal.Util.Registration.remove();
|
||||
|
||||
const NUMBER_ID_KEY = 'number_id';
|
||||
const UUID_ID_KEY = 'uuid_id';
|
||||
const VERSION_KEY = 'version';
|
||||
const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex';
|
||||
const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete';
|
||||
|
||||
const previousNumberId = window.textsecure.storage.get(NUMBER_ID_KEY);
|
||||
const previousUuidId = window.textsecure.storage.get(UUID_ID_KEY);
|
||||
const lastProcessedIndex = window.textsecure.storage.get(
|
||||
LAST_PROCESSED_INDEX_KEY
|
||||
);
|
||||
|
@ -3327,6 +3344,9 @@ export async function startApp(): Promise<void> {
|
|||
if (previousNumberId !== undefined) {
|
||||
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
|
||||
// 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;
|
||||
window.log.info(`view sync ${source} ${timestamp}`);
|
||||
|
||||
const sync = window.Whisper.ViewSyncs.add({
|
||||
const sync = ViewSyncs.getSingleton().add({
|
||||
source,
|
||||
sourceUuid,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
window.Whisper.ViewSyncs.onSync(sync);
|
||||
ViewSyncs.getSingleton().onSync(sync);
|
||||
}
|
||||
|
||||
async function onFetchLatestSync(ev: WhatIsThis) {
|
||||
|
@ -3855,7 +3875,7 @@ export async function startApp(): Promise<void> {
|
|||
messageRequestResponseType,
|
||||
});
|
||||
|
||||
const sync = window.Whisper.MessageRequests.add({
|
||||
const sync = MessageRequests.getSingleton().add({
|
||||
threadE164,
|
||||
threadUuid,
|
||||
groupId,
|
||||
|
@ -3863,7 +3883,7 @@ export async function startApp(): Promise<void> {
|
|||
type: messageRequestResponseType,
|
||||
});
|
||||
|
||||
window.Whisper.MessageRequests.onResponse(sync);
|
||||
MessageRequests.getSingleton().onResponse(sync);
|
||||
}
|
||||
|
||||
function onReadReceipt(ev: WhatIsThis) {
|
||||
|
@ -3890,14 +3910,14 @@ export async function startApp(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
const receipt = window.Whisper.ReadReceipts.add({
|
||||
const receipt = ReadReceipts.getSingleton().add({
|
||||
reader,
|
||||
timestamp,
|
||||
read_at: readAt,
|
||||
readAt,
|
||||
});
|
||||
|
||||
// Note: We do not wait for completion here
|
||||
window.Whisper.ReadReceipts.onReceipt(receipt);
|
||||
ReadReceipts.getSingleton().onReceipt(receipt);
|
||||
}
|
||||
|
||||
function onReadSync(ev: WhatIsThis) {
|
||||
|
@ -3918,19 +3938,19 @@ export async function startApp(): Promise<void> {
|
|||
timestamp
|
||||
);
|
||||
|
||||
const receipt = window.Whisper.ReadSyncs.add({
|
||||
const receipt = ReadSyncs.getSingleton().add({
|
||||
senderId,
|
||||
sender,
|
||||
senderUuid,
|
||||
timestamp,
|
||||
read_at: readAt,
|
||||
readAt,
|
||||
});
|
||||
|
||||
receipt.on('remove', ev.confirm);
|
||||
|
||||
// Note: Here we wait, because we want read states to be in the database
|
||||
// before we move on.
|
||||
return window.Whisper.ReadSyncs.onReceipt(receipt);
|
||||
return ReadSyncs.getSingleton().onReceipt(receipt);
|
||||
}
|
||||
|
||||
async function onVerified(ev: WhatIsThis) {
|
||||
|
@ -4037,13 +4057,13 @@ export async function startApp(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
const receipt = window.Whisper.DeliveryReceipts.add({
|
||||
const receipt = DeliveryReceipts.getSingleton().add({
|
||||
timestamp,
|
||||
deliveredTo,
|
||||
});
|
||||
|
||||
// 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,
|
||||
url: '/fixtures/kitten-4-112-112.jpg',
|
||||
contentType: IMAGE_JPEG,
|
||||
path: 'originalPath',
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
|
|
@ -60,6 +60,7 @@ story.add('Multiple Visual Attachments', () => {
|
|||
width: 112,
|
||||
url: '/fixtures/kitten-4-112-112.jpg',
|
||||
contentType: IMAGE_JPEG,
|
||||
path: 'originalpath',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -100,6 +101,7 @@ story.add('Multiple with Non-Visual Types', () => {
|
|||
width: 112,
|
||||
url: '/fixtures/kitten-4-112-112.jpg',
|
||||
contentType: IMAGE_JPEG,
|
||||
path: 'originalpath',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -77,7 +77,7 @@ export const AttachmentList = ({
|
|||
<Image
|
||||
key={key}
|
||||
alt={i18n('stagedImageAttachment', [
|
||||
url || attachment.fileName,
|
||||
attachment.fileName || url || index.toString(),
|
||||
])}
|
||||
i18n={i18n}
|
||||
attachment={attachment}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ContactDetail, Props } from './ContactDetail';
|
|||
import { AddressType, ContactFormType } from '../../types/Contact';
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { IMAGE_GIF } from '../../types/MIME';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -73,6 +74,7 @@ const fullContact = {
|
|||
avatar: {
|
||||
avatar: {
|
||||
path: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
|
||||
contentType: IMAGE_GIF,
|
||||
},
|
||||
isProfile: true,
|
||||
},
|
||||
|
@ -208,6 +210,7 @@ story.add('Loading Avatar', () => {
|
|||
contact: {
|
||||
avatar: {
|
||||
avatar: {
|
||||
contentType: IMAGE_GIF,
|
||||
pending: true,
|
||||
},
|
||||
isProfile: true,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { EmbeddedContact, Props } from './EmbeddedContact';
|
|||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { ContactFormType } from '../../types/Contact';
|
||||
import { IMAGE_GIF } from '../../types/MIME';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -36,6 +37,7 @@ const fullContact = {
|
|||
avatar: {
|
||||
avatar: {
|
||||
path: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
|
||||
contentType: IMAGE_GIF,
|
||||
},
|
||||
isProfile: true,
|
||||
},
|
||||
|
@ -134,6 +136,7 @@ story.add('Loading Avatar', () => {
|
|||
avatar: {
|
||||
avatar: {
|
||||
pending: true,
|
||||
contentType: IMAGE_GIF,
|
||||
},
|
||||
isProfile: true,
|
||||
},
|
||||
|
|
|
@ -23,61 +23,55 @@ export type Props = {
|
|||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export class EmbeddedContact extends React.Component<Props> {
|
||||
public render(): JSX.Element {
|
||||
const {
|
||||
contact,
|
||||
i18n,
|
||||
isIncoming,
|
||||
onClick,
|
||||
tabIndex,
|
||||
withContentAbove,
|
||||
withContentBelow,
|
||||
} = this.props;
|
||||
const module = 'embedded-contact';
|
||||
const direction = isIncoming ? 'incoming' : 'outgoing';
|
||||
export const EmbeddedContact: React.FC<Props> = (props: Props) => {
|
||||
const {
|
||||
contact,
|
||||
i18n,
|
||||
isIncoming,
|
||||
onClick,
|
||||
tabIndex,
|
||||
withContentAbove,
|
||||
withContentBelow,
|
||||
} = props;
|
||||
const module = 'embedded-contact';
|
||||
const direction = isIncoming ? 'incoming' : 'outgoing';
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'module-embedded-contact',
|
||||
`module-embedded-contact--${direction}`,
|
||||
withContentAbove
|
||||
? 'module-embedded-contact--with-content-above'
|
||||
: null,
|
||||
withContentBelow
|
||||
? 'module-embedded-contact--with-content-below'
|
||||
: null
|
||||
)}
|
||||
onKeyDown={(event: React.KeyboardEvent) => {
|
||||
if (event.key !== 'Enter' && event.key !== 'Space') {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'module-embedded-contact',
|
||||
`module-embedded-contact--${direction}`,
|
||||
withContentAbove ? 'module-embedded-contact--with-content-above' : null,
|
||||
withContentBelow ? 'module-embedded-contact--with-content-below' : null
|
||||
)}
|
||||
onKeyDown={(event: React.KeyboardEvent) => {
|
||||
if (event.key !== 'Enter' && event.key !== 'Space') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (onClick) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
onClick={(event: React.MouseEvent) => {
|
||||
if (onClick) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
onClick={(event: React.MouseEvent) => {
|
||||
if (onClick) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{renderAvatar({ contact, i18n, size: 52, direction })}
|
||||
<div className="module-embedded-contact__text-container">
|
||||
{renderName({ contact, isIncoming, module })}
|
||||
{renderContactShorthand({ contact, isIncoming, module })}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{renderAvatar({ contact, i18n, size: 52, direction })}
|
||||
<div className="module-embedded-contact__text-container">
|
||||
{renderName({ contact, isIncoming, module })}
|
||||
{renderContactShorthand({ contact, isIncoming, module })}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -260,6 +260,7 @@ story.add('Mixed Content Types', () => {
|
|||
width: 112,
|
||||
url: '/fixtures/kitten-4-112-112.jpg',
|
||||
contentType: IMAGE_JPEG,
|
||||
path: 'originalpath',
|
||||
},
|
||||
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
|
||||
width: 112,
|
||||
|
|
|
@ -76,6 +76,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
canReply: true,
|
||||
canDownload: true,
|
||||
canDeleteForEveryone: overrideProps.canDeleteForEveryone || false,
|
||||
checkForAccount: action('checkForAccount'),
|
||||
clearSelectedMessage: action('clearSelectedMessage'),
|
||||
collapseMetadata: overrideProps.collapseMetadata,
|
||||
conversationColor:
|
||||
|
@ -90,6 +91,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
disableScroll: overrideProps.disableScroll,
|
||||
direction: overrideProps.direction || 'incoming',
|
||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||
downloadAttachment: action('downloadAttachment'),
|
||||
expirationLength:
|
||||
number('expirationLength', overrideProps.expirationLength || 0) ||
|
||||
|
@ -114,6 +116,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
isTapToViewExpired: overrideProps.isTapToViewExpired,
|
||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
onHeightChange: action('onHeightChange'),
|
||||
openConversation: action('openConversation'),
|
||||
openLink: action('openLink'),
|
||||
previews: overrideProps.previews || [],
|
||||
|
|
|
@ -8,7 +8,11 @@ import { drop, groupBy, orderBy, take } from 'lodash';
|
|||
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
||||
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 { Spinner } from '../Spinner';
|
||||
import { MessageBody } from './MessageBody';
|
||||
|
@ -80,15 +84,9 @@ export const MessageStatuses = [
|
|||
] as const;
|
||||
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 type DirectionType = typeof Directions[number];
|
||||
|
||||
export const ConversationTypes = ['direct', 'group'] as const;
|
||||
export type ConversationTypesType = typeof ConversationTypes[number];
|
||||
|
||||
export type AudioAttachmentProps = {
|
||||
id: string;
|
||||
i18n: LocalizerType;
|
||||
|
@ -133,7 +131,7 @@ export type PropsData = {
|
|||
| 'unblurredAvatarPath'
|
||||
>;
|
||||
reducedMotion?: boolean;
|
||||
conversationType: ConversationTypesType;
|
||||
conversationType: ConversationTypeType;
|
||||
attachments?: Array<AttachmentType>;
|
||||
quote?: {
|
||||
conversationColor: ConversationColorType;
|
||||
|
@ -185,6 +183,9 @@ export type PropsHousekeeping = {
|
|||
|
||||
export type PropsActions = {
|
||||
clearSelectedMessage: () => unknown;
|
||||
doubleCheckMissingQuoteReference: (messageId: string) => unknown;
|
||||
onHeightChange: () => unknown;
|
||||
checkForAccount: (identifier: string) => unknown;
|
||||
|
||||
reactToMessage: (
|
||||
id: string,
|
||||
|
@ -405,18 +406,21 @@ export class Message extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
const { expirationLength } = this.props;
|
||||
if (!expirationLength) {
|
||||
return;
|
||||
if (expirationLength) {
|
||||
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 checkFrequency = Math.max(EXPIRATION_CHECK_MINIMUM, increment);
|
||||
|
||||
this.checkExpired();
|
||||
|
||||
this.expirationCheckInterval = setInterval(() => {
|
||||
this.checkExpired();
|
||||
}, checkFrequency);
|
||||
const { contact, checkForAccount } = this.props;
|
||||
if (contact && contact.firstNumber && !contact.isNumberOnSignal) {
|
||||
checkForAccount(contact.firstNumber);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
|
@ -448,12 +452,31 @@ export class Message extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
this.checkExpired();
|
||||
this.checkForHeightChange(prevProps);
|
||||
|
||||
if (canDeleteForEveryone !== prevProps.canDeleteForEveryone) {
|
||||
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 {
|
||||
const { clearSelectedMessage, interactionMode } = this.props;
|
||||
const { isSelected } = this.state;
|
||||
|
@ -1064,7 +1087,9 @@ export class Message extends React.Component<Props, State> {
|
|||
customColor,
|
||||
direction,
|
||||
disableScroll,
|
||||
doubleCheckMissingQuoteReference,
|
||||
i18n,
|
||||
id,
|
||||
quote,
|
||||
scrollToQuotedMessage,
|
||||
} = this.props;
|
||||
|
@ -1104,6 +1129,9 @@ export class Message extends React.Component<Props, State> {
|
|||
referencedMessageNotFound={referencedMessageNotFound}
|
||||
isFromMe={quote.isFromMe}
|
||||
withContentAbove={withContentAbove}
|
||||
doubleCheckMissingQuoteReference={() =>
|
||||
doubleCheckMissingQuoteReference(id)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1127,7 +1155,9 @@ export class Message extends React.Component<Props, State> {
|
|||
conversationType === 'group' && direction === 'incoming';
|
||||
const withContentBelow = withCaption || !collapseMetadata;
|
||||
|
||||
const otherContent = (contact && contact.signalAccount) || withCaption;
|
||||
const otherContent =
|
||||
(contact && contact.firstNumber && contact.isNumberOnSignal) ||
|
||||
withCaption;
|
||||
const tabIndex = otherContent ? 0 : -1;
|
||||
|
||||
return (
|
||||
|
@ -1136,7 +1166,7 @@ export class Message extends React.Component<Props, State> {
|
|||
isIncoming={direction === 'incoming'}
|
||||
i18n={i18n}
|
||||
onClick={() => {
|
||||
showContactDetail({ contact, signalAccount: contact.signalAccount });
|
||||
showContactDetail({ contact, signalAccount: contact.firstNumber });
|
||||
}}
|
||||
withContentAbove={withContentAbove}
|
||||
withContentBelow={withContentBelow}
|
||||
|
@ -1147,18 +1177,18 @@ export class Message extends React.Component<Props, State> {
|
|||
|
||||
public renderSendMessageButton(): JSX.Element | null {
|
||||
const { contact, openConversation, i18n } = this.props;
|
||||
if (!contact || !contact.signalAccount) {
|
||||
if (!contact) {
|
||||
return null;
|
||||
}
|
||||
const { firstNumber, isNumberOnSignal } = contact;
|
||||
if (!firstNumber || !isNumberOnSignal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (contact.signalAccount) {
|
||||
openConversation(contact.signalAccount);
|
||||
}
|
||||
}}
|
||||
onClick={() => openConversation(firstNumber)}
|
||||
className="module-message__send-message-button"
|
||||
>
|
||||
{i18n('sendMessageToContact')}
|
||||
|
@ -2181,15 +2211,15 @@ export class Message extends React.Component<Props, State> {
|
|||
this.audioButtonRef.current.click();
|
||||
}
|
||||
|
||||
if (contact && contact.signalAccount) {
|
||||
openConversation(contact.signalAccount);
|
||||
if (contact && contact.firstNumber && contact.isNumberOnSignal) {
|
||||
openConversation(contact.firstNumber);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (contact) {
|
||||
showContactDetail({ contact, signalAccount: contact.signalAccount });
|
||||
showContactDetail({ contact, signalAccount: contact.firstNumber });
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
|
|
@ -61,26 +61,32 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
sendAnyway: action('onSendAnyway'),
|
||||
showSafetyNumber: action('onShowSafetyNumber'),
|
||||
|
||||
clearSelectedMessage: () => null,
|
||||
checkForAccount: action('checkForAccount'),
|
||||
clearSelectedMessage: action('clearSelectedMessage'),
|
||||
deleteMessage: action('deleteMessage'),
|
||||
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
||||
displayTapToViewMessage: () => null,
|
||||
downloadAttachment: () => null,
|
||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||
downloadAttachment: action('downloadAttachment'),
|
||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
openConversation: () => null,
|
||||
openLink: () => null,
|
||||
reactToMessage: () => null,
|
||||
openConversation: action('openConversation'),
|
||||
openLink: action('openLink'),
|
||||
reactToMessage: action('reactToMessage'),
|
||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||
renderEmojiPicker: () => <div />,
|
||||
replyToMessage: () => null,
|
||||
retrySend: () => null,
|
||||
showContactDetail: () => null,
|
||||
showContactModal: () => null,
|
||||
showExpiredIncomingTapToViewToast: () => null,
|
||||
showExpiredOutgoingTapToViewToast: () => null,
|
||||
showForwardMessageModal: () => null,
|
||||
showVisualAttachment: () => null,
|
||||
replyToMessage: action('replyToMessage'),
|
||||
retrySend: action('retrySend'),
|
||||
showContactDetail: action('showContactDetail'),
|
||||
showContactModal: action('showContactModal'),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
),
|
||||
showExpiredOutgoingTapToViewToast: action(
|
||||
'showExpiredOutgoingTapToViewToast'
|
||||
),
|
||||
showForwardMessageModal: action('showForwardMessageModal'),
|
||||
showVisualAttachment: action('showVisualAttachment'),
|
||||
});
|
||||
|
||||
story.add('Delivered Incoming', () => {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import { GlobalAudioProvider } from '../GlobalAudioContext';
|
||||
import { Avatar } from '../Avatar';
|
||||
|
@ -55,11 +56,13 @@ export type Props = {
|
|||
i18n: LocalizerType;
|
||||
} & Pick<
|
||||
MessagePropsType,
|
||||
| 'checkForAccount'
|
||||
| 'clearSelectedMessage'
|
||||
| 'deleteMessage'
|
||||
| 'deleteMessageForEveryone'
|
||||
| 'displayTapToViewMessage'
|
||||
| 'downloadAttachment'
|
||||
| 'doubleCheckMissingQuoteReference'
|
||||
| 'interactionMode'
|
||||
| 'kickOffAttachmentDownload'
|
||||
| 'markAttachmentAsCorrupted'
|
||||
|
@ -233,12 +236,14 @@ export class MessageDetail extends React.Component<Props> {
|
|||
receivedAt,
|
||||
sentAt,
|
||||
|
||||
checkForAccount,
|
||||
clearSelectedMessage,
|
||||
contactNameColor,
|
||||
deleteMessage,
|
||||
deleteMessageForEveryone,
|
||||
displayTapToViewMessage,
|
||||
downloadAttachment,
|
||||
doubleCheckMissingQuoteReference,
|
||||
i18n,
|
||||
interactionMode,
|
||||
kickOffAttachmentDownload,
|
||||
|
@ -265,6 +270,7 @@ export class MessageDetail extends React.Component<Props> {
|
|||
<GlobalAudioProvider conversationId={message.conversationId}>
|
||||
<Message
|
||||
{...message}
|
||||
checkForAccount={checkForAccount}
|
||||
clearSelectedMessage={clearSelectedMessage}
|
||||
contactNameColor={contactNameColor}
|
||||
deleteMessage={deleteMessage}
|
||||
|
@ -273,10 +279,14 @@ export class MessageDetail extends React.Component<Props> {
|
|||
disableScroll
|
||||
displayTapToViewMessage={displayTapToViewMessage}
|
||||
downloadAttachment={downloadAttachment}
|
||||
doubleCheckMissingQuoteReference={
|
||||
doubleCheckMissingQuoteReference
|
||||
}
|
||||
i18n={i18n}
|
||||
interactionMode={interactionMode}
|
||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
||||
onHeightChange={noop}
|
||||
openConversation={openConversation}
|
||||
openLink={openLink}
|
||||
reactToMessage={reactToMessage}
|
||||
|
|
|
@ -35,39 +35,48 @@ const defaultMessageProps: MessagesProps = {
|
|||
canReply: true,
|
||||
canDeleteForEveryone: true,
|
||||
canDownload: true,
|
||||
clearSelectedMessage: () => null,
|
||||
checkForAccount: action('checkForAccount'),
|
||||
clearSelectedMessage: action('default--clearSelectedMessage'),
|
||||
conversationColor: 'crimson',
|
||||
conversationId: 'conversationId',
|
||||
conversationType: 'direct', // override
|
||||
deleteMessage: () => null,
|
||||
deleteMessageForEveryone: () => null,
|
||||
deleteMessage: action('default--deleteMessage'),
|
||||
deleteMessageForEveryone: action('default--deleteMessageForEveryone'),
|
||||
direction: 'incoming',
|
||||
displayTapToViewMessage: () => null,
|
||||
downloadAttachment: () => null,
|
||||
displayTapToViewMessage: action('default--displayTapToViewMessage'),
|
||||
downloadAttachment: action('default--downloadAttachment'),
|
||||
doubleCheckMissingQuoteReference: action(
|
||||
'default--doubleCheckMissingQuoteReference'
|
||||
),
|
||||
i18n,
|
||||
id: 'messageId',
|
||||
interactionMode: 'keyboard',
|
||||
isBlocked: false,
|
||||
isMessageRequestAccepted: true,
|
||||
kickOffAttachmentDownload: () => null,
|
||||
markAttachmentAsCorrupted: () => null,
|
||||
openConversation: () => null,
|
||||
openLink: () => null,
|
||||
kickOffAttachmentDownload: action('default--kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('default--markAttachmentAsCorrupted'),
|
||||
onHeightChange: action('onHeightChange'),
|
||||
openConversation: action('default--openConversation'),
|
||||
openLink: action('default--openLink'),
|
||||
previews: [],
|
||||
reactToMessage: () => null,
|
||||
reactToMessage: action('default--reactToMessage'),
|
||||
renderEmojiPicker: () => <div />,
|
||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||
replyToMessage: () => null,
|
||||
retrySend: () => null,
|
||||
scrollToQuotedMessage: () => null,
|
||||
selectMessage: () => null,
|
||||
showContactDetail: () => null,
|
||||
showContactModal: () => null,
|
||||
showExpiredIncomingTapToViewToast: () => null,
|
||||
showExpiredOutgoingTapToViewToast: () => null,
|
||||
showForwardMessageModal: () => null,
|
||||
showMessageDetail: () => null,
|
||||
showVisualAttachment: () => null,
|
||||
replyToMessage: action('default--replyToMessage'),
|
||||
retrySend: action('default--retrySend'),
|
||||
scrollToQuotedMessage: action('default--scrollToQuotedMessage'),
|
||||
selectMessage: action('default--selectMessage'),
|
||||
showContactDetail: action('default--showContactDetail'),
|
||||
showContactModal: action('default--showContactModal'),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
),
|
||||
showExpiredOutgoingTapToViewToast: action(
|
||||
'showExpiredOutgoingTapToViewToast'
|
||||
),
|
||||
showForwardMessageModal: action('default--showForwardMessageModal'),
|
||||
showMessageDetail: action('default--showMessageDetail'),
|
||||
showVisualAttachment: action('default--showVisualAttachment'),
|
||||
status: 'sent',
|
||||
text: 'This is really interesting.',
|
||||
timestamp: Date.now(),
|
||||
|
@ -125,6 +134,9 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
),
|
||||
authorTitle: text('authorTitle', overrideProps.authorTitle || ''),
|
||||
conversationColor: overrideProps.conversationColor || 'forest',
|
||||
doubleCheckMissingQuoteReference:
|
||||
overrideProps.doubleCheckMissingQuoteReference ||
|
||||
action('doubleCheckMissingQuoteReference'),
|
||||
i18n,
|
||||
isFromMe: boolean('isFromMe', overrideProps.isFromMe || false),
|
||||
isIncoming: boolean('isIncoming', overrideProps.isIncoming || false),
|
||||
|
|
|
@ -33,6 +33,7 @@ export type Props = {
|
|||
rawAttachment?: QuotedAttachmentType;
|
||||
isViewOnce: boolean;
|
||||
referencedMessageNotFound: boolean;
|
||||
doubleCheckMissingQuoteReference: () => unknown;
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -41,7 +42,7 @@ type State = {
|
|||
|
||||
export type QuotedAttachmentType = {
|
||||
contentType: MIME.MIMEType;
|
||||
fileName: string;
|
||||
fileName?: string;
|
||||
/** Not included in protobuf */
|
||||
isVoiceMessage: boolean;
|
||||
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 = (
|
||||
event: React.KeyboardEvent<HTMLButtonElement>
|
||||
): void => {
|
||||
|
|
|
@ -298,6 +298,7 @@ const actions = () => ({
|
|||
acknowledgeGroupMemberNameCollisions: action(
|
||||
'acknowledgeGroupMemberNameCollisions'
|
||||
),
|
||||
checkForAccount: action('checkForAccount'),
|
||||
clearChangedMessages: action('clearChangedMessages'),
|
||||
clearInvitedConversationsForNewlyCreatedGroup: action(
|
||||
'clearInvitedConversationsForNewlyCreatedGroup'
|
||||
|
@ -327,7 +328,9 @@ const actions = () => ({
|
|||
showVisualAttachment: action('showVisualAttachment'),
|
||||
downloadAttachment: action('downloadAttachment'),
|
||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||
|
||||
onHeightChange: action('onHeightChange'),
|
||||
openLink: action('openLink'),
|
||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
|
|
|
@ -104,6 +104,7 @@ type PropsHousekeepingType = {
|
|||
renderItem: (
|
||||
id: string,
|
||||
conversationId: string,
|
||||
onHeightChange: (messageId: string) => unknown,
|
||||
actions: Record<string, unknown>
|
||||
) => JSX.Element;
|
||||
renderLastSeenIndicator: (id: string) => JSX.Element;
|
||||
|
@ -367,6 +368,22 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
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 => {
|
||||
// Ignore scroll events generated as react-virtualized recursively scrolls and
|
||||
// re-measures to get us where we want to go.
|
||||
|
@ -711,7 +728,7 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
|
|||
style={styleWithWidth}
|
||||
role="row"
|
||||
>
|
||||
{renderItem(messageId, id, this.props)}
|
||||
{renderItem(messageId, id, this.resizeMessage, this.props)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ const getDefaultProps = () => ({
|
|||
interactionMode: 'keyboard' as const,
|
||||
selectMessage: action('selectMessage'),
|
||||
reactToMessage: action('reactToMessage'),
|
||||
checkForAccount: action('checkForAccount'),
|
||||
clearSelectedMessage: action('clearSelectedMessage'),
|
||||
contactSupport: action('contactSupport'),
|
||||
replyToMessage: action('replyToMessage'),
|
||||
|
@ -63,12 +64,14 @@ const getDefaultProps = () => ({
|
|||
showVisualAttachment: action('showVisualAttachment'),
|
||||
downloadAttachment: action('downloadAttachment'),
|
||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
),
|
||||
showExpiredOutgoingTapToViewToast: action(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
),
|
||||
onHeightChange: action('onHeightChange'),
|
||||
openLink: action('openLink'),
|
||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||
downloadNewVersion: action('downloadNewVersion'),
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
import React from 'react';
|
||||
import { LocalizerType, ThemeType } from '../../types/Util';
|
||||
|
||||
import { InteractionModeType } from '../../state/ducks/conversations';
|
||||
import {
|
||||
Message,
|
||||
InteractionModeType,
|
||||
Props as AllMessageProps,
|
||||
PropsActions as MessageActionsType,
|
||||
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
|
||||
|
||||
/* global
|
||||
Whisper,
|
||||
Signal,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
MessageController
|
||||
*/
|
||||
import { isFunction, isNumber, omit } from 'lodash';
|
||||
import { v4 as getGuid } from 'uuid';
|
||||
|
||||
import dataInterface from '../sql/Client';
|
||||
import { downloadAttachment } from '../util/downloadAttachment';
|
||||
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 {
|
||||
getMessageById,
|
||||
getNextAttachmentDownloadJobs,
|
||||
|
@ -19,15 +25,7 @@ const {
|
|||
saveAttachmentDownloadJob,
|
||||
saveMessage,
|
||||
setAttachmentDownloadJobPending,
|
||||
} = require('../../ts/sql/Client').default;
|
||||
const { downloadAttachment } = require('../../ts/util/downloadAttachment');
|
||||
const { stringFromBytes } = require('../../ts/Crypto');
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
stop,
|
||||
addJob,
|
||||
};
|
||||
} = dataInterface;
|
||||
|
||||
const MAX_ATTACHMENT_JOB_PARALLELISM = 3;
|
||||
|
||||
|
@ -36,19 +34,27 @@ const MINUTE = 60 * SECOND;
|
|||
const HOUR = 60 * MINUTE;
|
||||
const TICK_INTERVAL = MINUTE;
|
||||
|
||||
const RETRY_BACKOFF = {
|
||||
const RETRY_BACKOFF: Record<number, number> = {
|
||||
1: 30 * SECOND,
|
||||
2: 30 * MINUTE,
|
||||
3: 6 * HOUR,
|
||||
};
|
||||
|
||||
let enabled = false;
|
||||
let timeout;
|
||||
let getMessageReceiver;
|
||||
let logger;
|
||||
const _activeAttachmentDownloadJobs = {};
|
||||
let timeout: NodeJS.Timeout | null;
|
||||
let getMessageReceiver: () => MessageReceiver | undefined;
|
||||
let logger: LoggerType;
|
||||
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);
|
||||
if (!isFunction(getMessageReceiver)) {
|
||||
throw new Error(
|
||||
|
@ -66,7 +72,7 @@ async function start(options = {}) {
|
|||
_tick();
|
||||
}
|
||||
|
||||
async function stop() {
|
||||
export async function stop(): Promise<void> {
|
||||
// If `.start()` wasn't called - the `logger` is `undefined`
|
||||
if (logger) {
|
||||
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) {
|
||||
throw new Error('attachments_download/addJob: attachment is required');
|
||||
}
|
||||
|
@ -96,7 +105,7 @@ async function addJob(attachment, job = {}) {
|
|||
|
||||
const id = getGuid();
|
||||
const timestamp = Date.now();
|
||||
const toSave = {
|
||||
const toSave: AttachmentDownloadJobType = {
|
||||
...job,
|
||||
id,
|
||||
attachment,
|
||||
|
@ -116,7 +125,7 @@ async function addJob(attachment, job = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
async function _tick() {
|
||||
async function _tick(): Promise<void> {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
|
@ -126,7 +135,7 @@ async function _tick() {
|
|||
timeout = setTimeout(_tick, TICK_INTERVAL);
|
||||
}
|
||||
|
||||
async function _maybeStartJob() {
|
||||
async function _maybeStartJob(): Promise<void> {
|
||||
if (!enabled) {
|
||||
logger.info('attachment_downloads/_maybeStartJob: not enabled, returning');
|
||||
return;
|
||||
|
@ -178,8 +187,13 @@ async function _maybeStartJob() {
|
|||
}
|
||||
}
|
||||
|
||||
async function _runJob(job) {
|
||||
const { id, messageId, attachment, type, index, attempts } = job || {};
|
||||
async function _runJob(job?: AttachmentDownloadJobType): Promise<void> {
|
||||
if (!job) {
|
||||
window.log.warn('_runJob: Job was missing!');
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, messageId, attachment, type, index, attempts } = job;
|
||||
let message;
|
||||
|
||||
try {
|
||||
|
@ -192,16 +206,16 @@ async function _runJob(job) {
|
|||
logger.info(`attachment_downloads/_runJob for job id ${id}`);
|
||||
|
||||
const found =
|
||||
MessageController.getById(messageId) ||
|
||||
window.MessageController.getById(messageId) ||
|
||||
(await getMessageById(messageId, {
|
||||
Message: Whisper.Message,
|
||||
Message: window.Whisper.Message,
|
||||
}));
|
||||
if (!found) {
|
||||
logger.error('_runJob: Source message not found, deleting job');
|
||||
await _finishJob(null, id);
|
||||
return;
|
||||
}
|
||||
message = MessageController.register(found.id, found);
|
||||
message = window.MessageController.register(found.id, found);
|
||||
|
||||
const pending = true;
|
||||
await setAttachmentDownloadJobPending(id, pending);
|
||||
|
@ -231,7 +245,7 @@ async function _runJob(job) {
|
|||
return;
|
||||
}
|
||||
|
||||
const upgradedAttachment = await Signal.Migrations.processNewAttachment(
|
||||
const upgradedAttachment = await window.Signal.Migrations.processNewAttachment(
|
||||
downloaded
|
||||
);
|
||||
|
||||
|
@ -239,11 +253,12 @@ async function _runJob(job) {
|
|||
|
||||
await _finishJob(message, id);
|
||||
} catch (error) {
|
||||
const logId = message ? message.idForLogging() : id || '<no id>';
|
||||
const currentAttempt = (attempts || 0) + 1;
|
||||
|
||||
if (currentAttempt >= 3) {
|
||||
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
|
||||
);
|
||||
|
||||
|
@ -258,7 +273,7 @@ async function _runJob(job) {
|
|||
}
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
|
@ -266,7 +281,8 @@ async function _runJob(job) {
|
|||
...job,
|
||||
pending: 0,
|
||||
attempts: currentAttempt,
|
||||
timestamp: Date.now() + RETRY_BACKOFF[currentAttempt],
|
||||
timestamp:
|
||||
Date.now() + (RETRY_BACKOFF[currentAttempt] || RETRY_BACKOFF[3]),
|
||||
};
|
||||
|
||||
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) {
|
||||
logger.info(`attachment_downloads/_finishJob for job id: ${id}`);
|
||||
await saveMessage(message.attributes, {
|
||||
Message: Whisper.Message,
|
||||
Message: window.Whisper.Message,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -288,18 +307,22 @@ async function _finishJob(message, id) {
|
|||
_maybeStartJob();
|
||||
}
|
||||
|
||||
function getActiveJobCount() {
|
||||
function getActiveJobCount(): number {
|
||||
return Object.keys(_activeAttachmentDownloadJobs).length;
|
||||
}
|
||||
|
||||
function _markAttachmentAsError(attachment) {
|
||||
function _markAttachmentAsError(attachment: AttachmentType): AttachmentType {
|
||||
return {
|
||||
...omit(attachment, ['key', 'digest', 'id']),
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
@ -308,13 +331,17 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
|
|||
|
||||
if (type === 'long-message') {
|
||||
try {
|
||||
const { data } = await Signal.Migrations.loadAttachmentData(attachment);
|
||||
const { data } = await window.Signal.Migrations.loadAttachmentData(
|
||||
attachment
|
||||
);
|
||||
message.set({
|
||||
body: attachment.isError ? message.get('body') : stringFromBytes(data),
|
||||
body: attachment.error ? message.get('body') : stringFromBytes(data),
|
||||
bodyPending: false,
|
||||
});
|
||||
} finally {
|
||||
Signal.Migrations.deleteAttachmentData(attachment.path);
|
||||
if (attachment.path) {
|
||||
window.Signal.Migrations.deleteAttachmentData(attachment.path);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -326,7 +353,7 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
|
|||
`_addAttachmentToMessage: attachments didn't exist or ${index} was too large`
|
||||
);
|
||||
}
|
||||
_checkOldAttachment(attachments, index, attachment, logPrefix);
|
||||
_checkOldAttachment(attachments, index.toString(), logPrefix);
|
||||
|
||||
const newAttachments = [...attachments];
|
||||
newAttachments[index] = attachment;
|
||||
|
@ -348,7 +375,7 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
|
|||
throw new Error(`_addAttachmentToMessage: preview ${index} was falsey`);
|
||||
}
|
||||
|
||||
_checkOldAttachment(item, 'image', attachment, logPrefix);
|
||||
_checkOldAttachment(item, 'image', logPrefix);
|
||||
|
||||
const newPreview = [...preview];
|
||||
newPreview[index] = {
|
||||
|
@ -370,13 +397,13 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
|
|||
}
|
||||
const item = contact[index];
|
||||
if (item && item.avatar && item.avatar.avatar) {
|
||||
_checkOldAttachment(item.avatar, 'avatar', attachment, logPrefix);
|
||||
_checkOldAttachment(item.avatar, 'avatar', logPrefix);
|
||||
|
||||
const newContact = [...contact];
|
||||
newContact[index] = {
|
||||
...contact[index],
|
||||
...item,
|
||||
avatar: {
|
||||
...contact[index].avatar,
|
||||
...item.avatar,
|
||||
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];
|
||||
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];
|
||||
if (oldAttachment && oldAttachment.path) {
|
||||
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 { CallHistoryDetailsFromDiskType } from './types/Calling';
|
||||
import { CustomColorType } from './types/Colors';
|
||||
import {
|
||||
ConversationType,
|
||||
MessageType,
|
||||
LastMessageStatus,
|
||||
} from './state/ducks/conversations';
|
||||
import { DeviceType } from './textsecure/Types';
|
||||
import { SendOptionsType } from './textsecure/SendMessage';
|
||||
import { SendMessageChallengeData } from './textsecure/Errors';
|
||||
|
@ -27,19 +22,19 @@ import { ProfileNameChangeType } from './util/getStringForProfileChange';
|
|||
import { CapabilitiesType } from './textsecure/WebAPI';
|
||||
import { GroupNameCollisionsWithIdsByTitle } from './util/groupMemberNameCollisions';
|
||||
import { ConversationColorType } from './types/Colors';
|
||||
import { AttachmentType, ThumbnailType } from './types/Attachment';
|
||||
import { ContactType } from './types/Contact';
|
||||
|
||||
export type WhatIsThis = any;
|
||||
|
||||
type DeletesAttributesType = {
|
||||
fromId: string;
|
||||
serverTimestamp: number;
|
||||
targetSentTimestamp: number;
|
||||
};
|
||||
|
||||
export declare class DeletesModelType extends Backbone.Model<DeletesAttributesType> {
|
||||
forMessage(message: MessageModel): Array<DeletesModelType>;
|
||||
onDelete(doe: DeletesAttributesType): Promise<void>;
|
||||
}
|
||||
export type LastMessageStatus =
|
||||
| 'paused'
|
||||
| 'error'
|
||||
| 'partial-sent'
|
||||
| 'sending'
|
||||
| 'sent'
|
||||
| 'delivered'
|
||||
| 'read';
|
||||
|
||||
type TaskResultType = any;
|
||||
|
||||
|
@ -77,38 +72,38 @@ export type RetryOptions = Readonly<{
|
|||
}>;
|
||||
|
||||
export type MessageAttributesType = {
|
||||
bodyPending: boolean;
|
||||
bodyRanges: BodyRangesType;
|
||||
callHistoryDetails: CallHistoryDetailsFromDiskType;
|
||||
changedId: string;
|
||||
dataMessage: ArrayBuffer | null;
|
||||
decrypted_at: number;
|
||||
deletedForEveryone: boolean;
|
||||
bodyPending?: boolean;
|
||||
bodyRanges?: BodyRangesType;
|
||||
callHistoryDetails?: CallHistoryDetailsFromDiskType;
|
||||
changedId?: string;
|
||||
dataMessage?: ArrayBuffer | null;
|
||||
decrypted_at?: number;
|
||||
deletedForEveryone?: boolean;
|
||||
deletedForEveryoneTimestamp?: number;
|
||||
delivered: number;
|
||||
delivered_to: Array<string | null>;
|
||||
delivered?: number;
|
||||
delivered_to?: Array<string | null>;
|
||||
errors?: Array<CustomError>;
|
||||
expirationStartTimestamp: number | null;
|
||||
expireTimer: number;
|
||||
expirationStartTimestamp?: number | null;
|
||||
expireTimer?: number;
|
||||
groupMigration?: GroupMigrationType;
|
||||
group_update: {
|
||||
group_update?: {
|
||||
avatarUpdated: boolean;
|
||||
joined: Array<string>;
|
||||
left: string | 'You';
|
||||
name: string;
|
||||
};
|
||||
hasAttachments: boolean;
|
||||
hasFileAttachments: boolean;
|
||||
hasVisualMediaAttachments: boolean;
|
||||
isErased: boolean;
|
||||
isTapToViewInvalid: boolean;
|
||||
isViewOnce: boolean;
|
||||
key_changed: string;
|
||||
local: boolean;
|
||||
logger: unknown;
|
||||
message: unknown;
|
||||
messageTimer: unknown;
|
||||
profileChange: ProfileNameChangeType;
|
||||
hasAttachments?: boolean;
|
||||
hasFileAttachments?: boolean;
|
||||
hasVisualMediaAttachments?: boolean;
|
||||
isErased?: boolean;
|
||||
isTapToViewInvalid?: boolean;
|
||||
isViewOnce?: boolean;
|
||||
key_changed?: string;
|
||||
local?: boolean;
|
||||
logger?: unknown;
|
||||
message?: unknown;
|
||||
messageTimer?: unknown;
|
||||
profileChange?: ProfileNameChangeType;
|
||||
quote?: QuotedMessageType;
|
||||
reactions?: Array<{
|
||||
emoji: string;
|
||||
|
@ -117,20 +112,19 @@ export type MessageAttributesType = {
|
|||
targetTimestamp: number;
|
||||
timestamp: number;
|
||||
}>;
|
||||
read_by: Array<string | null>;
|
||||
requiredProtocolVersion: number;
|
||||
read_by?: Array<string | null>;
|
||||
requiredProtocolVersion?: number;
|
||||
retryOptions?: RetryOptions;
|
||||
sent: boolean;
|
||||
sourceDevice: string | number;
|
||||
snippet: unknown;
|
||||
supportedVersionAtReceive: unknown;
|
||||
synced: boolean;
|
||||
unidentifiedDeliveryReceived: boolean;
|
||||
verified: boolean;
|
||||
verifiedChanged: string;
|
||||
sent?: boolean;
|
||||
sourceDevice?: string | number;
|
||||
supportedVersionAtReceive?: unknown;
|
||||
synced?: boolean;
|
||||
unidentifiedDeliveryReceived?: boolean;
|
||||
verified?: boolean;
|
||||
verifiedChanged?: string;
|
||||
|
||||
id: string;
|
||||
type?:
|
||||
type:
|
||||
| 'call-history'
|
||||
| 'chat-session-refreshed'
|
||||
| 'delivery-issue'
|
||||
|
@ -145,17 +139,22 @@ export type MessageAttributesType = {
|
|||
| 'timer-notification'
|
||||
| 'universal-timer-notification'
|
||||
| 'verified-change';
|
||||
body: string;
|
||||
attachments: Array<WhatIsThis>;
|
||||
preview: Array<WhatIsThis>;
|
||||
sticker: WhatIsThis;
|
||||
body?: string;
|
||||
attachments?: Array<AttachmentType>;
|
||||
preview?: Array<WhatIsThis>;
|
||||
sticker?: {
|
||||
packId: string;
|
||||
stickerId: number;
|
||||
packKey: string;
|
||||
data?: AttachmentType;
|
||||
};
|
||||
sent_at: number;
|
||||
sent_to: Array<string>;
|
||||
unidentifiedDeliveries: Array<string>;
|
||||
contact: Array<WhatIsThis>;
|
||||
sent_to?: Array<string>;
|
||||
unidentifiedDeliveries?: Array<string>;
|
||||
contact?: Array<ContactType>;
|
||||
conversationId: string;
|
||||
recipients: Array<string>;
|
||||
reaction: WhatIsThis;
|
||||
recipients?: Array<string>;
|
||||
reaction?: WhatIsThis;
|
||||
destination?: WhatIsThis;
|
||||
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
|
||||
// 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
|
||||
schemaVersion: number;
|
||||
schemaVersion?: number;
|
||||
// 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.
|
||||
serverGuid?: string;
|
||||
|
@ -182,7 +181,7 @@ export type MessageAttributesType = {
|
|||
source?: string;
|
||||
sourceUuid?: string;
|
||||
|
||||
unread: boolean;
|
||||
unread?: boolean;
|
||||
timestamp: number;
|
||||
|
||||
// Backwards-compatibility with prerelease data schema
|
||||
|
|
|
@ -62,6 +62,14 @@ import {
|
|||
isMe,
|
||||
} from '../util/whatTypeOfConversation';
|
||||
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 */
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
@ -1238,7 +1246,7 @@ export class ConversationModel extends window.Backbone
|
|||
const isNewMessage = true;
|
||||
messagesAdded(
|
||||
this.id,
|
||||
[message.getReduxData()],
|
||||
[{ ...message.attributes }],
|
||||
isNewMessage,
|
||||
window.isActive()
|
||||
);
|
||||
|
@ -1560,7 +1568,7 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
|
||||
const readMessages = messages.filter(
|
||||
m => !m.hasErrors() && m.isIncoming()
|
||||
m => !hasErrors(m.attributes) && isIncoming(m.attributes)
|
||||
);
|
||||
const receiptSpecs = readMessages.map(m => ({
|
||||
senderE164: m.get('source'),
|
||||
|
@ -1570,7 +1578,7 @@ export class ConversationModel extends window.Backbone
|
|||
uuid: m.get('sourceUuid'),
|
||||
}),
|
||||
timestamp: m.get('sent_at'),
|
||||
hasErrors: m.hasErrors(),
|
||||
hasErrors: hasErrors(m.attributes),
|
||||
}));
|
||||
|
||||
if (isLocalAction) {
|
||||
|
@ -2324,27 +2332,6 @@ export class ConversationModel extends window.Backbone
|
|||
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 {
|
||||
if (!this.get('about')) {
|
||||
return undefined;
|
||||
|
@ -3006,9 +2993,9 @@ export class ConversationModel extends window.Backbone
|
|||
}
|
||||
|
||||
async getQuoteAttachment(
|
||||
attachments: Array<WhatIsThis>,
|
||||
preview: Array<WhatIsThis>,
|
||||
sticker: WhatIsThis
|
||||
attachments?: Array<WhatIsThis>,
|
||||
preview?: Array<WhatIsThis>,
|
||||
sticker?: WhatIsThis
|
||||
): Promise<WhatIsThis> {
|
||||
if (attachments && attachments.length) {
|
||||
const validAttachments = filter(
|
||||
|
@ -3104,8 +3091,8 @@ export class ConversationModel extends window.Backbone
|
|||
bodyRanges: quotedMessage.get('bodyRanges'),
|
||||
id: quotedMessage.get('sent_at'),
|
||||
text: body || embeddedContactName,
|
||||
isViewOnce: quotedMessage.isTapToView(),
|
||||
attachments: quotedMessage.isTapToView()
|
||||
isViewOnce: isTapToView(quotedMessage.attributes),
|
||||
attachments: isTapToView(quotedMessage.attributes)
|
||||
? [{ contentType: 'image/jpeg', fileName: null }]
|
||||
: 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');
|
||||
}
|
||||
|
||||
const deleteModel = window.Whisper.Deletes.add({
|
||||
const deleteModel = Deletes.getSingleton().add({
|
||||
targetSentTimestamp: targetTimestamp,
|
||||
fromId: window.ConversationController.getOurConversationId(),
|
||||
});
|
||||
|
@ -3264,7 +3251,7 @@ export class ConversationModel extends window.Backbone
|
|||
// send error.
|
||||
throw new Error('No successful delivery for delete for everyone');
|
||||
}
|
||||
window.Whisper.Deletes.onDelete(deleteModel);
|
||||
Deletes.getSingleton().onDelete(deleteModel);
|
||||
|
||||
return result;
|
||||
}).catch(error => {
|
||||
|
@ -3289,7 +3276,7 @@ export class ConversationModel extends window.Backbone
|
|||
const timestamp = Date.now();
|
||||
const outgoingReaction = { ...reaction, ...target };
|
||||
|
||||
const reactionModel = window.Whisper.Reactions.add({
|
||||
const reactionModel = Reactions.getSingleton().add({
|
||||
...outgoingReaction,
|
||||
fromId: window.ConversationController.getOurConversationId(),
|
||||
timestamp,
|
||||
|
@ -3297,7 +3284,7 @@ export class ConversationModel extends window.Backbone
|
|||
});
|
||||
|
||||
// Apply reaction optimistically
|
||||
const oldReaction = await window.Whisper.Reactions.onReaction(
|
||||
const oldReaction = await Reactions.getSingleton().onReaction(
|
||||
reactionModel
|
||||
);
|
||||
|
||||
|
@ -3367,7 +3354,7 @@ export class ConversationModel extends window.Backbone
|
|||
timestamp,
|
||||
});
|
||||
const result = await message.sendSyncMessageOnly(dataMessage);
|
||||
window.Whisper.Reactions.onReaction(reactionModel);
|
||||
Reactions.getSingleton().onReaction(reactionModel);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -3426,7 +3413,7 @@ export class ConversationModel extends window.Backbone
|
|||
let reverseReaction: ReactionModelType;
|
||||
if (oldReaction) {
|
||||
// Either restore old reaction
|
||||
reverseReaction = window.Whisper.Reactions.add({
|
||||
reverseReaction = Reactions.getSingleton().add({
|
||||
...oldReaction,
|
||||
fromId: window.ConversationController.getOurConversationId(),
|
||||
timestamp,
|
||||
|
@ -3437,7 +3424,7 @@ export class ConversationModel extends window.Backbone
|
|||
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:
|
||||
(previewMessage ? previewMessage.getNotificationText() : '') || '',
|
||||
lastMessageStatus:
|
||||
(previewMessage ? previewMessage.getMessagePropStatus() : null) || null,
|
||||
(previewMessage
|
||||
? getMessagePropStatus(
|
||||
previewMessage.attributes,
|
||||
window.storage.get('read-receipt-setting', false)
|
||||
)
|
||||
: null) || null,
|
||||
timestamp,
|
||||
lastMessageDeletedForEveryone: previewMessage
|
||||
? previewMessage.get('deletedForEveryone')
|
||||
|
@ -5024,7 +5016,7 @@ export class ConversationModel extends window.Backbone
|
|||
return;
|
||||
}
|
||||
|
||||
if (!message.isIncoming() && !reaction) {
|
||||
if (!isIncoming(message.attributes) && !reaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -37,8 +37,8 @@ export function getExpiresAt(
|
|||
'expireTimer' | 'expirationStartTimestamp'
|
||||
>
|
||||
): number | undefined {
|
||||
const expireTimerMs = messageAttrs.expireTimer * 1000;
|
||||
return messageAttrs.expirationStartTimestamp
|
||||
? messageAttrs.expirationStartTimestamp + expireTimerMs
|
||||
const { expireTimer, expirationStartTimestamp } = messageAttrs;
|
||||
return expirationStartTimestamp && expireTimer
|
||||
? expirationStartTimestamp + expireTimer * 1000
|
||||
: undefined;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,6 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// 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 {
|
||||
window.showSettings();
|
||||
}
|
||||
|
|
|
@ -16,12 +16,25 @@ import { StoredJob } from '../jobs/types';
|
|||
import { ReactionType } from '../types/Reactions';
|
||||
import { ConversationColorType, CustomColorType } from '../types/Colors';
|
||||
import { StorageAccessType } from '../types/Storage.d';
|
||||
import { AttachmentType } from '../types/Attachment';
|
||||
|
||||
export type AttachmentDownloadJobTypeType =
|
||||
| 'long-message'
|
||||
| 'attachment'
|
||||
| 'preview'
|
||||
| 'contact'
|
||||
| 'quote'
|
||||
| 'sticker';
|
||||
|
||||
export type AttachmentDownloadJobType = {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
pending: number;
|
||||
attachment: AttachmentType;
|
||||
attempts: number;
|
||||
id: string;
|
||||
index: number;
|
||||
messageId: string;
|
||||
pending: number;
|
||||
timestamp: number;
|
||||
type: AttachmentDownloadJobTypeType;
|
||||
};
|
||||
export type MessageMetricsType = {
|
||||
id: string;
|
||||
|
|
|
@ -3061,7 +3061,7 @@ function saveMessageSync(
|
|||
isErased: isErased ? 1 : 0,
|
||||
isViewOnce: isViewOnce ? 1 : 0,
|
||||
received_at: received_at || null,
|
||||
schemaVersion,
|
||||
schemaVersion: schemaVersion || 0,
|
||||
serverGuid: serverGuid || null,
|
||||
sent_at: sent_at || null,
|
||||
source: source || null,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { actions as accounts } from './ducks/accounts';
|
||||
import { actions as app } from './ducks/app';
|
||||
import { actions as audioPlayer } from './ducks/audioPlayer';
|
||||
import { actions as calling } from './ducks/calling';
|
||||
|
@ -19,6 +20,7 @@ import { actions as user } from './ducks/user';
|
|||
import { ReduxActions } from './types';
|
||||
|
||||
export const actionCreators: ReduxActions = {
|
||||
accounts,
|
||||
app,
|
||||
audioPlayer,
|
||||
calling,
|
||||
|
@ -37,6 +39,7 @@ export const actionCreators: ReduxActions = {
|
|||
};
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
...accounts,
|
||||
...app,
|
||||
...audioPlayer,
|
||||
...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 { assert } from '../../util/assert';
|
||||
import { trigger } from '../../shims/events';
|
||||
import { AttachmentType } from '../../types/Attachment';
|
||||
|
||||
import {
|
||||
AvatarColorType,
|
||||
ConversationColorType,
|
||||
|
@ -29,9 +29,13 @@ import {
|
|||
DefaultConversationColorType,
|
||||
DEFAULT_CONVERSATION_COLOR,
|
||||
} from '../../types/Colors';
|
||||
import { ConversationAttributesType } from '../../model-types.d';
|
||||
import {
|
||||
LastMessageStatus,
|
||||
ConversationAttributesType,
|
||||
MessageAttributesType,
|
||||
} from '../../model-types.d';
|
||||
import { BodyRangeType } from '../../types/Util';
|
||||
import { CallMode, CallHistoryDetailsFromDiskType } from '../../types/Calling';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import { MediaItemType } from '../../components/LightboxGallery';
|
||||
import {
|
||||
getGroupSizeRecommendedLimit,
|
||||
|
@ -41,6 +45,8 @@ import { toggleSelectedContactForGroupAddition } from '../../groups/toggleSelect
|
|||
import { GroupNameCollisionsWithIdsByTitle } from '../../util/groupMemberNameCollisions';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||
|
||||
import { NoopActionType } from './noop';
|
||||
|
||||
// State
|
||||
|
||||
export type DBConversationType = {
|
||||
|
@ -50,16 +56,15 @@ export type DBConversationType = {
|
|||
type: string;
|
||||
};
|
||||
|
||||
export type LastMessageStatus =
|
||||
| 'paused'
|
||||
| 'error'
|
||||
| 'partial-sent'
|
||||
| 'sending'
|
||||
| 'sent'
|
||||
| 'delivered'
|
||||
| 'read';
|
||||
export const InteractionModes = ['mouse', 'keyboard'] as const;
|
||||
export type InteractionModeType = typeof InteractionModes[number];
|
||||
|
||||
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 = {
|
||||
id: string;
|
||||
|
@ -159,52 +164,6 @@ export type CustomError = Error & {
|
|||
identifier?: 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 = {
|
||||
id: string;
|
||||
|
@ -219,7 +178,7 @@ type MessageMetricsType = {
|
|||
};
|
||||
|
||||
export type MessageLookupType = {
|
||||
[key: string]: MessageType;
|
||||
[key: string]: MessageAttributesType;
|
||||
};
|
||||
export type ConversationMessageType = {
|
||||
heightChangeMessageIds: Array<string>;
|
||||
|
@ -460,7 +419,7 @@ export type MessageChangedActionType = {
|
|||
payload: {
|
||||
id: string;
|
||||
conversationId: string;
|
||||
data: MessageType;
|
||||
data: MessageAttributesType;
|
||||
};
|
||||
};
|
||||
export type MessageDeletedActionType = {
|
||||
|
@ -481,7 +440,7 @@ export type MessagesAddedActionType = {
|
|||
type: 'MESSAGES_ADDED';
|
||||
payload: {
|
||||
conversationId: string;
|
||||
messages: Array<MessageType>;
|
||||
messages: Array<MessageAttributesType>;
|
||||
isNewMessage: boolean;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
@ -503,7 +462,7 @@ export type MessagesResetActionType = {
|
|||
type: 'MESSAGES_RESET';
|
||||
payload: {
|
||||
conversationId: string;
|
||||
messages: Array<MessageType>;
|
||||
messages: Array<MessageAttributesType>;
|
||||
metrics: MessageMetricsType;
|
||||
scrollToMessageId?: string;
|
||||
// The set of provided messages should be trusted, even if it conflicts with metrics,
|
||||
|
@ -701,6 +660,7 @@ export const actions = {
|
|||
conversationUnloaded,
|
||||
colorSelected,
|
||||
createGroup,
|
||||
doubleCheckMissingQuoteReference,
|
||||
messageChanged,
|
||||
messageDeleted,
|
||||
messagesAdded,
|
||||
|
@ -990,7 +950,7 @@ function selectMessage(
|
|||
function messageChanged(
|
||||
id: string,
|
||||
conversationId: string,
|
||||
data: MessageType
|
||||
data: MessageAttributesType
|
||||
): MessageChangedActionType {
|
||||
return {
|
||||
type: 'MESSAGE_CHANGED',
|
||||
|
@ -1027,7 +987,7 @@ function messageSizeChanged(
|
|||
}
|
||||
function messagesAdded(
|
||||
conversationId: string,
|
||||
messages: Array<MessageType>,
|
||||
messages: Array<MessageAttributesType>,
|
||||
isNewMessage: boolean,
|
||||
isActive: boolean
|
||||
): MessagesAddedActionType {
|
||||
|
@ -1082,7 +1042,7 @@ function reviewMessageRequestNameCollision(
|
|||
|
||||
function messagesReset(
|
||||
conversationId: string,
|
||||
messages: Array<MessageType>,
|
||||
messages: Array<MessageAttributesType>,
|
||||
metrics: MessageMetricsType,
|
||||
scrollToMessageId?: string,
|
||||
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
|
||||
|
||||
export function getEmptyState(): ConversationsStateType {
|
||||
|
@ -1363,8 +1335,8 @@ export function getEmptyState(): ConversationsStateType {
|
|||
}
|
||||
|
||||
function hasMessageHeightChanged(
|
||||
message: MessageType,
|
||||
previous: MessageType
|
||||
message: MessageAttributesType,
|
||||
previous: MessageAttributesType
|
||||
): boolean {
|
||||
const messageAttachments = message.attachments || [];
|
||||
const previousAttachments = previous.attachments || [];
|
||||
|
@ -1410,13 +1382,6 @@ function hasMessageHeightChanged(
|
|||
return true;
|
||||
}
|
||||
|
||||
const signalAccountChanged =
|
||||
Boolean(message.hasSignalAccount || previous.hasSignalAccount) &&
|
||||
message.hasSignalAccount !== previous.hasSignalAccount;
|
||||
if (signalAccountChanged) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentReactions = message.reactions || [];
|
||||
const lastReactions = previous.reactions || [];
|
||||
const reactionsChanged =
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
} from '../../sql/Interface';
|
||||
import dataInterface from '../../sql/Client';
|
||||
import { makeLookup } from '../../util/makeLookup';
|
||||
import { BodyRangesType } from '../../types/Util';
|
||||
|
||||
import {
|
||||
ConversationUnloadedActionType,
|
||||
|
@ -32,9 +31,7 @@ const {
|
|||
// State
|
||||
|
||||
export type MessageSearchResultType = MessageType & {
|
||||
snippet: string;
|
||||
body: string;
|
||||
bodyRanges: BodyRangesType;
|
||||
snippet?: string;
|
||||
};
|
||||
|
||||
export type MessageSearchResultLookupType = {
|
||||
|
|
|
@ -13,6 +13,7 @@ export type UserStateType = {
|
|||
stickersPath: string;
|
||||
tempPath: string;
|
||||
ourConversationId: string;
|
||||
ourDeviceId: number;
|
||||
ourUuid: string;
|
||||
ourNumber: string;
|
||||
platform: string;
|
||||
|
@ -28,6 +29,7 @@ type UserChangedActionType = {
|
|||
type: 'USER_CHANGED';
|
||||
payload: {
|
||||
ourConversationId?: string;
|
||||
ourDeviceId?: number;
|
||||
ourUuid?: string;
|
||||
ourNumber?: string;
|
||||
regionCode?: string;
|
||||
|
@ -48,6 +50,7 @@ export const actions = {
|
|||
function userChanged(attributes: {
|
||||
interactionMode?: 'mouse' | 'keyboard';
|
||||
ourConversationId?: string;
|
||||
ourDeviceId?: number;
|
||||
ourNumber?: string;
|
||||
ourUuid?: string;
|
||||
regionCode?: string;
|
||||
|
@ -76,6 +79,7 @@ export function getEmptyState(): UserStateType {
|
|||
stickersPath: 'missing',
|
||||
tempPath: 'missing',
|
||||
ourConversationId: 'missing',
|
||||
ourDeviceId: 0,
|
||||
ourUuid: 'missing',
|
||||
ourNumber: 'missing',
|
||||
regionCode: 'missing',
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { reducer as accounts } from './ducks/accounts';
|
||||
import { reducer as app } from './ducks/app';
|
||||
import { reducer as audioPlayer } from './ducks/audioPlayer';
|
||||
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';
|
||||
|
||||
export const reducer = combineReducers({
|
||||
accounts,
|
||||
app,
|
||||
audioPlayer,
|
||||
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,
|
||||
CallsByConversationType,
|
||||
DirectCallStateType,
|
||||
getActiveCall,
|
||||
GroupCallStateType,
|
||||
} from '../ducks/calling';
|
||||
import { CallMode, CallState } from '../../types/Calling';
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
|
||||
export type CallStateType = DirectCallStateType | GroupCallStateType;
|
||||
|
||||
const getCalling = (state: StateType): CallingStateType => state.calling;
|
||||
|
||||
export const isInCall = createSelector(
|
||||
export const getActiveCallState = createSelector(
|
||||
getCalling,
|
||||
(state: CallingStateType): boolean => Boolean(getActiveCall(state))
|
||||
(state: CallingStateType) => state.activeCallState
|
||||
);
|
||||
|
||||
export const getCallsByConversation = createSelector(
|
||||
|
@ -26,10 +28,31 @@ export const getCallsByConversation = createSelector(
|
|||
state.callsByConversation
|
||||
);
|
||||
|
||||
export type CallSelectorType = (
|
||||
conversationId: string
|
||||
) => CallStateType | undefined;
|
||||
export const getCallSelector = createSelector(
|
||||
getCallsByConversation,
|
||||
(callsByConversation: CallsByConversationType) => (conversationId: string) =>
|
||||
getOwn(callsByConversation, conversationId)
|
||||
(callsByConversation: CallsByConversationType): CallSelectorType => (
|
||||
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
|
||||
|
|
|
@ -14,31 +14,35 @@ import {
|
|||
ConversationType,
|
||||
MessageLookupType,
|
||||
MessagesByConversationType,
|
||||
MessageType,
|
||||
OneTimeModalState,
|
||||
PreJoinConversationType,
|
||||
} from '../ducks/conversations';
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
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 { TimelineItemType } from '../../components/conversation/TimelineItem';
|
||||
import { assert } from '../../util/assert';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
||||
import { filterAndSortConversationsByTitle } from '../../util/filterAndSortConversations';
|
||||
import { ContactNameColors, ContactNameColorType } from '../../types/Colors';
|
||||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||
|
||||
import {
|
||||
getInteractionMode,
|
||||
getIntl,
|
||||
getRegionCode,
|
||||
getUserConversationId,
|
||||
getUserNumber,
|
||||
getUserUuid,
|
||||
} from './user';
|
||||
import { getPinnedConversationIds } from './items';
|
||||
import { isInSystemContacts } from '../../util/isInSystemContacts';
|
||||
import { getPinnedConversationIds, getReadReceiptSetting } from './items';
|
||||
import { getPropsForBubble } from './message';
|
||||
import {
|
||||
CallSelectorType,
|
||||
CallStateType,
|
||||
getActiveCall,
|
||||
getCallSelector,
|
||||
} from './calling';
|
||||
import { getAccountSelector, AccountSelectorType } from './accounts';
|
||||
|
||||
let placeholderContact: ConversationType;
|
||||
export const getPlaceholderContact = (): ConversationType => {
|
||||
|
@ -640,66 +644,15 @@ export const getConversationByIdSelector = createSelector(
|
|||
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
|
||||
// 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(
|
||||
getRegionCode,
|
||||
getUserNumber,
|
||||
(): CachedMessageSelectorType => {
|
||||
(): typeof getPropsForBubble => {
|
||||
// Note: memoizee will check all parameters provided, and only run our selector
|
||||
// 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,
|
||||
getConversationSelector,
|
||||
getRegionCode,
|
||||
getReadReceiptSetting,
|
||||
getUserNumber,
|
||||
getInteractionMode,
|
||||
getCallsByConversation,
|
||||
getUserUuid,
|
||||
getUserConversationId,
|
||||
getCallSelector,
|
||||
getActiveCall,
|
||||
getAccountSelector,
|
||||
(
|
||||
messageSelector: CachedMessageSelectorType,
|
||||
messageSelector: typeof getPropsForBubble,
|
||||
messageLookup: MessageLookupType,
|
||||
selectedMessage: SelectedMessageType | undefined,
|
||||
conversationSelector: GetConversationByIdType,
|
||||
regionCode: string,
|
||||
readReceiptSetting: boolean,
|
||||
ourNumber: string,
|
||||
interactionMode: 'keyboard' | 'mouse',
|
||||
callsByConversation: CallsByConversationType
|
||||
ourUuid: string,
|
||||
ourConversationId: string,
|
||||
callSelector: CallSelectorType,
|
||||
activeCall: undefined | CallStateType,
|
||||
accountSelector: AccountSelectorType
|
||||
): GetMessageByIdType => {
|
||||
return (id: string) => {
|
||||
const message = messageLookup[id];
|
||||
|
@ -731,13 +692,17 @@ export const getMessageSelector = createSelector(
|
|||
|
||||
return messageSelector(
|
||||
message,
|
||||
ourNumber,
|
||||
regionCode,
|
||||
interactionMode,
|
||||
conversationSelector,
|
||||
callsByConversation,
|
||||
ourConversationId,
|
||||
ourNumber,
|
||||
ourUuid,
|
||||
regionCode,
|
||||
readReceiptSetting,
|
||||
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
|
||||
);
|
||||
|
||||
export const getReadReceiptSetting = createSelector(
|
||||
getItems,
|
||||
(state: ItemsStateType): boolean => Boolean(state['read-receipt-setting'])
|
||||
);
|
||||
|
||||
export const getPinnedConversationIds = createSelector(
|
||||
getItems,
|
||||
(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,
|
||||
conversationId: message.conversationId,
|
||||
sentAt: message.sent_at,
|
||||
snippet: message.snippet,
|
||||
snippet: message.snippet || '',
|
||||
bodyRanges: bodyRanges.map((bodyRange: BodyRangeType) => {
|
||||
const conversation = conversationSelector(bodyRange.mentionUuid);
|
||||
|
||||
|
@ -152,7 +152,7 @@ export const getCachedSelectorForMessageSearchResult = createSelector(
|
|||
replacementText: conversation.title,
|
||||
};
|
||||
}),
|
||||
body: message.body,
|
||||
body: message.body || '',
|
||||
|
||||
isSelected: Boolean(
|
||||
selectedMessageId && message.id === selectedMessageId
|
||||
|
|
|
@ -15,6 +15,11 @@ export const getUserNumber = createSelector(
|
|||
(state: UserStateType): string => state.ourNumber
|
||||
);
|
||||
|
||||
export const getUserDeviceId = createSelector(
|
||||
getUser,
|
||||
(state: UserStateType): number => state.ourDeviceId
|
||||
);
|
||||
|
||||
export const getRegionCode = createSelector(
|
||||
getUser,
|
||||
(state: UserStateType): string => state.regionCode
|
||||
|
|
|
@ -10,7 +10,10 @@ import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
|
|||
|
||||
import { selectRecentEmojis } from '../selectors/emojis';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import {
|
||||
getConversationSelector,
|
||||
isMissingRequiredProfileSharing,
|
||||
} from '../selectors/conversations';
|
||||
import {
|
||||
getBlessedStickerPacks,
|
||||
getInstalledStickerPacks,
|
||||
|
@ -75,13 +78,8 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
conversationType: conversation.type,
|
||||
isSMSOnly: Boolean(isConversationSMSOnly(conversation)),
|
||||
isFetchingUUID: conversation.isFetchingUUID,
|
||||
isMissingMandatoryProfileSharing: Boolean(
|
||||
!conversation.profileSharing &&
|
||||
window.Signal.RemoteConfig.isEnabled(
|
||||
'desktop.mandatoryProfileSharing'
|
||||
) &&
|
||||
conversation.messageCount &&
|
||||
conversation.messageCount > 0
|
||||
isMissingMandatoryProfileSharing: isMissingRequiredProfileSharing(
|
||||
conversation
|
||||
),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -7,7 +7,10 @@ import {
|
|||
ConversationHeader,
|
||||
OutgoingCallButtonStyle,
|
||||
} from '../../components/conversation/ConversationHeader';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import {
|
||||
getConversationSelector,
|
||||
isMissingRequiredProfileSharing,
|
||||
} from '../selectors/conversations';
|
||||
import { StateType } from '../reducer';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import {
|
||||
|
@ -111,13 +114,8 @@ const mapStateToProps = (state: StateType, ownProps: OwnProps) => {
|
|||
'unblurredAvatarPath',
|
||||
]),
|
||||
conversationTitle: state.conversations.selectedConversationTitle,
|
||||
isMissingMandatoryProfileSharing: Boolean(
|
||||
!conversation.profileSharing &&
|
||||
window.Signal.RemoteConfig.isEnabled(
|
||||
'desktop.mandatoryProfileSharing'
|
||||
) &&
|
||||
conversation.messageCount &&
|
||||
conversation.messageCount > 0
|
||||
isMissingMandatoryProfileSharing: isMissingRequiredProfileSharing(
|
||||
conversation
|
||||
),
|
||||
isSMSOnly: isConversationSMSOnly(conversation),
|
||||
i18n: getIntl(state),
|
||||
|
|
|
@ -33,10 +33,12 @@ export type OwnProps = {
|
|||
} & Pick<
|
||||
MessageDetailProps,
|
||||
| 'clearSelectedMessage'
|
||||
| 'checkForAccount'
|
||||
| 'deleteMessage'
|
||||
| 'deleteMessageForEveryone'
|
||||
| 'displayTapToViewMessage'
|
||||
| 'downloadAttachment'
|
||||
| 'doubleCheckMissingQuoteReference'
|
||||
| 'kickOffAttachmentDownload'
|
||||
| 'markAttachmentAsCorrupted'
|
||||
| 'openConversation'
|
||||
|
@ -66,11 +68,13 @@ const mapStateToProps = (
|
|||
sendAnyway,
|
||||
showSafetyNumber,
|
||||
|
||||
checkForAccount,
|
||||
clearSelectedMessage,
|
||||
deleteMessage,
|
||||
deleteMessageForEveryone,
|
||||
displayTapToViewMessage,
|
||||
downloadAttachment,
|
||||
doubleCheckMissingQuoteReference,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
openConversation,
|
||||
|
@ -108,11 +112,13 @@ const mapStateToProps = (
|
|||
sendAnyway,
|
||||
showSafetyNumber,
|
||||
|
||||
checkForAccount,
|
||||
clearSelectedMessage,
|
||||
deleteMessage,
|
||||
deleteMessageForEveryone,
|
||||
displayTapToViewMessage,
|
||||
downloadAttachment,
|
||||
doubleCheckMissingQuoteReference,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
openConversation,
|
||||
|
|
|
@ -59,6 +59,7 @@ type ExternalProps = {
|
|||
function renderItem(
|
||||
messageId: string,
|
||||
conversationId: string,
|
||||
onHeightChange: (messageId: string) => unknown,
|
||||
actionProps: Record<string, unknown>
|
||||
): JSX.Element {
|
||||
return (
|
||||
|
@ -66,6 +67,7 @@ function renderItem(
|
|||
{...actionProps}
|
||||
conversationId={conversationId}
|
||||
id={messageId}
|
||||
onHeightChange={() => onHeightChange(messageId)}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
renderAudioAttachment={renderAudioAttachment}
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { actions as accounts } from './ducks/accounts';
|
||||
import { actions as app } from './ducks/app';
|
||||
import { actions as audioPlayer } from './ducks/audioPlayer';
|
||||
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';
|
||||
|
||||
export type ReduxActions = {
|
||||
accounts: typeof accounts;
|
||||
app: typeof app;
|
||||
audioPlayer: typeof audioPlayer;
|
||||
calling: typeof calling;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {
|
||||
ConversationType,
|
||||
|
@ -24,20 +25,35 @@ import { getDefaultConversation } from '../../helpers/getDefaultConversation';
|
|||
import { StateType, reducer as rootReducer } from '../../../state/reducer';
|
||||
|
||||
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 => {
|
||||
return rootReducer(undefined, noopAction());
|
||||
};
|
||||
|
||||
function getDefaultMessage(id: string): MessageType {
|
||||
return {
|
||||
id,
|
||||
attachments: [],
|
||||
conversationId: 'conversationId',
|
||||
id,
|
||||
received_at: NOW,
|
||||
sent_at: NOW,
|
||||
source: 'source',
|
||||
sourceUuid: 'sourceUuid',
|
||||
timestamp: NOW,
|
||||
type: 'incoming' as const,
|
||||
received_at: Date.now(),
|
||||
attachments: [],
|
||||
sticker: {},
|
||||
unread: false,
|
||||
};
|
||||
}
|
||||
|
@ -126,7 +142,7 @@ describe('both/state/selectors/search', () => {
|
|||
|
||||
id: searchId,
|
||||
conversationId: toId,
|
||||
sentAt: undefined,
|
||||
sentAt: NOW,
|
||||
snippet: 'snippet',
|
||||
body: 'snippet',
|
||||
bodyRanges: [],
|
||||
|
@ -227,7 +243,7 @@ describe('both/state/selectors/search', () => {
|
|||
|
||||
id: searchId,
|
||||
conversationId: toId,
|
||||
sentAt: undefined,
|
||||
sentAt: NOW,
|
||||
snippet: 'snippet',
|
||||
body: 'snippet',
|
||||
bodyRanges: [],
|
||||
|
|
|
@ -5,6 +5,12 @@ import { assert } from 'chai';
|
|||
import * as sinon from 'sinon';
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import {
|
||||
isEndSession,
|
||||
isGroupUpdate,
|
||||
isIncoming,
|
||||
isOutgoing,
|
||||
} from '../../state/selectors/message';
|
||||
|
||||
describe('Message', () => {
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
@ -122,9 +128,9 @@ describe('Message', () => {
|
|||
it('checks if is incoming message', () => {
|
||||
const messages = new window.Whisper.MessageCollection();
|
||||
let message = messages.add(attributes);
|
||||
assert.notOk(message.isIncoming());
|
||||
assert.notOk(isIncoming(message.attributes));
|
||||
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', () => {
|
||||
const messages = new window.Whisper.MessageCollection();
|
||||
let message = messages.add(attributes);
|
||||
assert.ok(message.isOutgoing());
|
||||
assert.ok(isOutgoing(message.attributes));
|
||||
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', () => {
|
||||
const messages = new window.Whisper.MessageCollection();
|
||||
let message = messages.add(attributes);
|
||||
assert.notOk(message.isGroupUpdate());
|
||||
assert.notOk(isGroupUpdate(message.attributes));
|
||||
|
||||
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', () => {
|
||||
const messages = new window.Whisper.MessageCollection();
|
||||
let message = messages.add(attributes);
|
||||
assert.notOk(message.isEndSession());
|
||||
assert.notOk(isEndSession(message.attributes));
|
||||
|
||||
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', () => {
|
||||
const time = Date.now();
|
||||
const previousTime = time - 1;
|
||||
const conversationId = 'conversation-guid-1';
|
||||
const messageId = 'message-guid-1';
|
||||
const messageIdTwo = 'message-guid-2';
|
||||
|
@ -299,14 +300,15 @@ describe('both/state/ducks/conversations', () => {
|
|||
|
||||
function getDefaultMessage(id: string): MessageType {
|
||||
return {
|
||||
id,
|
||||
attachments: [],
|
||||
conversationId: 'conversationId',
|
||||
id,
|
||||
received_at: previousTime,
|
||||
sent_at: previousTime,
|
||||
source: 'source',
|
||||
sourceUuid: 'sourceUuid',
|
||||
timestamp: previousTime,
|
||||
type: 'incoming' as const,
|
||||
received_at: Date.now(),
|
||||
attachments: [],
|
||||
sticker: {},
|
||||
unread: false,
|
||||
};
|
||||
}
|
||||
|
@ -953,6 +955,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
[messageId]: {
|
||||
...getDefaultMessage(messageId),
|
||||
received_at: time,
|
||||
sent_at: time,
|
||||
},
|
||||
},
|
||||
messagesByConversation: {
|
||||
|
@ -972,6 +975,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
[messageId]: {
|
||||
...getDefaultMessage(messageId),
|
||||
received_at: time,
|
||||
sent_at: time,
|
||||
},
|
||||
},
|
||||
messagesByConversation: {
|
||||
|
@ -983,6 +987,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
newest: {
|
||||
id: messageId,
|
||||
received_at: time,
|
||||
sent_at: time,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1060,6 +1065,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
[messageId]: {
|
||||
...getDefaultMessage(messageId),
|
||||
received_at: time,
|
||||
sent_at: time,
|
||||
},
|
||||
},
|
||||
messagesByConversation: {
|
||||
|
@ -1079,6 +1085,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
[messageId]: {
|
||||
...getDefaultMessage(messageId),
|
||||
received_at: time,
|
||||
sent_at: time,
|
||||
},
|
||||
},
|
||||
messagesByConversation: {
|
||||
|
@ -1090,6 +1097,7 @@ describe('both/state/ducks/conversations', () => {
|
|||
oldest: {
|
||||
id: messageId,
|
||||
received_at: time,
|
||||
sent_at: time,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { IMAGE_GIF } from '../../types/MIME';
|
||||
import { contactSelector, getName } from '../../types/Contact';
|
||||
|
||||
describe('Contact', () => {
|
||||
|
@ -66,7 +67,8 @@ describe('Contact', () => {
|
|||
});
|
||||
describe('contactSelector', () => {
|
||||
const regionCode = '1';
|
||||
const signalAccount = '+1202555000';
|
||||
const firstNumber = '+1202555000';
|
||||
const isNumberOnSignal = false;
|
||||
const getAbsoluteAttachmentPath = (path: string) => `absolute:${path}`;
|
||||
|
||||
it('eliminates avatar if it has had an attachment download error', () => {
|
||||
|
@ -81,6 +83,7 @@ describe('Contact', () => {
|
|||
isProfile: true,
|
||||
avatar: {
|
||||
error: true,
|
||||
contentType: IMAGE_GIF,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -92,12 +95,14 @@ describe('Contact', () => {
|
|||
},
|
||||
organization: 'Somewhere, Inc.',
|
||||
avatar: undefined,
|
||||
signalAccount,
|
||||
firstNumber,
|
||||
isNumberOnSignal,
|
||||
number: undefined,
|
||||
};
|
||||
const actual = contactSelector(contact, {
|
||||
regionCode,
|
||||
signalAccount,
|
||||
firstNumber,
|
||||
isNumberOnSignal,
|
||||
getAbsoluteAttachmentPath,
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
|
@ -115,6 +120,7 @@ describe('Contact', () => {
|
|||
isProfile: true,
|
||||
avatar: {
|
||||
pending: true,
|
||||
contentType: IMAGE_GIF,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -130,14 +136,17 @@ describe('Contact', () => {
|
|||
avatar: {
|
||||
pending: true,
|
||||
path: undefined,
|
||||
contentType: IMAGE_GIF,
|
||||
},
|
||||
},
|
||||
signalAccount,
|
||||
firstNumber,
|
||||
isNumberOnSignal,
|
||||
number: undefined,
|
||||
};
|
||||
const actual = contactSelector(contact, {
|
||||
regionCode,
|
||||
signalAccount,
|
||||
firstNumber,
|
||||
isNumberOnSignal,
|
||||
getAbsoluteAttachmentPath,
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
|
@ -155,6 +164,7 @@ describe('Contact', () => {
|
|||
isProfile: true,
|
||||
avatar: {
|
||||
path: 'somewhere',
|
||||
contentType: IMAGE_GIF,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -169,14 +179,17 @@ describe('Contact', () => {
|
|||
isProfile: true,
|
||||
avatar: {
|
||||
path: 'absolute:somewhere',
|
||||
contentType: IMAGE_GIF,
|
||||
},
|
||||
},
|
||||
signalAccount,
|
||||
firstNumber,
|
||||
isNumberOnSignal: true,
|
||||
number: undefined,
|
||||
};
|
||||
const actual = contactSelector(contact, {
|
||||
regionCode,
|
||||
signalAccount,
|
||||
firstNumber,
|
||||
isNumberOnSignal: true,
|
||||
getAbsoluteAttachmentPath,
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
|
|
|
@ -2489,10 +2489,14 @@ class MessageReceiverInner extends EventTarget {
|
|||
async downloadAttachment(
|
||||
attachment: AttachmentPointerClass
|
||||
): Promise<DownloadAttachmentType> {
|
||||
const encrypted = await this.server.getAttachment(
|
||||
attachment.cdnId || attachment.cdnKey,
|
||||
attachment.cdnNumber || 0
|
||||
);
|
||||
const cdnId = attachment.cdnId || attachment.cdnKey;
|
||||
const { cdnNumber } = attachment;
|
||||
|
||||
if (!cdnId) {
|
||||
throw new Error('downloadAttachment: Attachment was missing cdnId!');
|
||||
}
|
||||
|
||||
const encrypted = await this.server.getAttachment(cdnId, cdnNumber);
|
||||
const { key, digest, size } = attachment;
|
||||
|
||||
if (!digest) {
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
compact,
|
||||
Dictionary,
|
||||
escapeRegExp,
|
||||
isNumber,
|
||||
mapValues,
|
||||
zipObject,
|
||||
} from 'lodash';
|
||||
|
@ -933,7 +934,7 @@ export type WebAPIType = {
|
|||
group: GroupClass,
|
||||
options: GroupCredentialsType
|
||||
) => Promise<void>;
|
||||
getAttachment: (cdnKey: string, cdnNumber: number) => Promise<any>;
|
||||
getAttachment: (cdnKey: string, cdnNumber?: number) => Promise<any>;
|
||||
getAvatar: (path: string) => Promise<any>;
|
||||
getDevices: () => Promise<any>;
|
||||
getGroup: (options: GroupCredentialsType) => Promise<GroupClass>;
|
||||
|
@ -1976,8 +1977,10 @@ export function initialize({
|
|||
return packId;
|
||||
}
|
||||
|
||||
async function getAttachment(cdnKey: string, cdnNumber: number) {
|
||||
const cdnUrl = cdnUrlObject[cdnNumber] || cdnUrlObject['0'];
|
||||
async function getAttachment(cdnKey: string, cdnNumber?: number) {
|
||||
const cdnUrl = isNumber(cdnNumber)
|
||||
? cdnUrlObject[cdnNumber] || cdnUrlObject['0']
|
||||
: cdnUrlObject['0'];
|
||||
// This is going to the CDN, not the service, so we use _outerAjax
|
||||
return _outerAjax(`${cdnUrl}/attachments/${cdnKey}`, {
|
||||
certificateAuthority,
|
||||
|
|
|
@ -21,10 +21,11 @@ const MIN_HEIGHT = 50;
|
|||
// Used for display
|
||||
|
||||
export type AttachmentType = {
|
||||
error?: boolean;
|
||||
blurHash?: string;
|
||||
caption?: string;
|
||||
contentType: MIME.MIMEType;
|
||||
fileName: string;
|
||||
fileName?: string;
|
||||
/** Not included in protobuf, needs to be pulled from flags */
|
||||
isVoiceMessage?: boolean;
|
||||
/** For messages not already on disk, this will be a data url */
|
||||
|
@ -40,16 +41,25 @@ export type AttachmentType = {
|
|||
width: number;
|
||||
url: string;
|
||||
contentType: MIME.MIMEType;
|
||||
};
|
||||
flags?: number;
|
||||
thumbnail?: {
|
||||
height: number;
|
||||
width: number;
|
||||
url: string;
|
||||
contentType: MIME.MIMEType;
|
||||
path: string;
|
||||
};
|
||||
flags?: number;
|
||||
thumbnail?: ThumbnailType;
|
||||
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
|
||||
|
@ -58,7 +68,7 @@ export function getExtensionForDisplay({
|
|||
fileName,
|
||||
contentType,
|
||||
}: {
|
||||
fileName: string;
|
||||
fileName?: string;
|
||||
contentType: MIME.MIMEType;
|
||||
}): string | undefined {
|
||||
if (fileName && fileName.indexOf('.') >= 0) {
|
||||
|
@ -354,7 +364,9 @@ export const isFile = (attachment: Attachment): boolean => {
|
|||
return true;
|
||||
};
|
||||
|
||||
export const isVoiceMessage = (attachment: Attachment): boolean => {
|
||||
export const isVoiceMessage = (
|
||||
attachment: Attachment | AttachmentType
|
||||
): boolean => {
|
||||
const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE;
|
||||
const hasFlag =
|
||||
// eslint-disable-next-line no-bitwise
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { format as formatPhoneNumber } from './PhoneNumber';
|
||||
import { AttachmentType } from './Attachment';
|
||||
|
||||
export type ContactType = {
|
||||
name?: Name;
|
||||
|
@ -10,7 +11,10 @@ export type ContactType = {
|
|||
address?: Array<PostalAddress>;
|
||||
avatar?: Avatar;
|
||||
organization?: string;
|
||||
signalAccount?: string;
|
||||
|
||||
// Populated by selector
|
||||
firstNumber?: string;
|
||||
isNumberOnSignal?: boolean;
|
||||
};
|
||||
|
||||
type Name = {
|
||||
|
@ -60,25 +64,25 @@ export type PostalAddress = {
|
|||
};
|
||||
|
||||
type Avatar = {
|
||||
avatar: Attachment;
|
||||
avatar: AttachmentType;
|
||||
isProfile: boolean;
|
||||
};
|
||||
|
||||
type Attachment = {
|
||||
path?: string;
|
||||
error?: boolean;
|
||||
pending?: boolean;
|
||||
};
|
||||
|
||||
export function contactSelector(
|
||||
contact: ContactType,
|
||||
options: {
|
||||
regionCode: string;
|
||||
signalAccount?: string;
|
||||
firstNumber?: string;
|
||||
isNumberOnSignal?: boolean;
|
||||
getAbsoluteAttachmentPath: (path: string) => string;
|
||||
}
|
||||
): ContactType {
|
||||
const { getAbsoluteAttachmentPath, signalAccount, regionCode } = options;
|
||||
const {
|
||||
getAbsoluteAttachmentPath,
|
||||
firstNumber,
|
||||
isNumberOnSignal,
|
||||
regionCode,
|
||||
} = options;
|
||||
|
||||
let { avatar } = contact;
|
||||
if (avatar && avatar.avatar) {
|
||||
|
@ -99,7 +103,8 @@ export function contactSelector(
|
|||
|
||||
return {
|
||||
...contact,
|
||||
signalAccount,
|
||||
firstNumber,
|
||||
isNumberOnSignal,
|
||||
avatar,
|
||||
number:
|
||||
contact.number &&
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { DeletesModelType } from '../model-types.d';
|
||||
import { DeleteModel } from '../messageModifiers/Deletes';
|
||||
import { MessageModel } from '../models/messages';
|
||||
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
export async function deleteForEveryone(
|
||||
message: MessageModel,
|
||||
doe: DeletesModelType,
|
||||
doe: DeleteModel,
|
||||
shouldPersist = true
|
||||
): Promise<void> {
|
||||
// Make sure the server timestamps for the DOE and the matching message
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { ConversationAttributesType } from '../model-types.d';
|
||||
import { handleMessageSend } from './handleMessageSend';
|
||||
import { sendReadReceiptsFor } from './sendReadReceiptsFor';
|
||||
import { hasErrors } from '../state/selectors/message';
|
||||
|
||||
export async function markConversationRead(
|
||||
conversationAttrs: ConversationAttributesType,
|
||||
|
@ -74,7 +75,7 @@ export async function markConversationRead(
|
|||
uuid: messageSyncData.sourceUuid,
|
||||
}),
|
||||
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 {
|
||||
GroupV2PendingMemberType,
|
||||
MessageModelCollectionType,
|
||||
MessageAttributesType,
|
||||
} from '../model-types.d';
|
||||
import { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||
import { MediaItemType } from '../components/LightboxGallery';
|
||||
import { MessageModel } from '../models/messages';
|
||||
import { MessageType } from '../state/ducks/conversations';
|
||||
import { assert } from '../util/assert';
|
||||
import { maybeParseUrl } from '../util/url';
|
||||
import { addReportSpamJob } from '../jobs/helpers/addReportSpamJob';
|
||||
|
@ -24,6 +24,13 @@ import {
|
|||
isGroupV2,
|
||||
isMe,
|
||||
} from '../util/whatTypeOfConversation';
|
||||
import {
|
||||
canReply,
|
||||
getAttachmentsForMessage,
|
||||
getPropsForQuote,
|
||||
isOutgoing,
|
||||
isTapToView,
|
||||
} from '../state/selectors/message';
|
||||
|
||||
type GetLinkPreviewImageResult = {
|
||||
data: ArrayBuffer;
|
||||
|
@ -910,9 +917,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
const isNewMessage = false;
|
||||
messagesAdded(
|
||||
id,
|
||||
cleaned.map((messageModel: MessageModel) =>
|
||||
messageModel.getReduxData()
|
||||
),
|
||||
cleaned.map((messageModel: MessageModel) => ({
|
||||
...messageModel.attributes,
|
||||
})),
|
||||
isNewMessage,
|
||||
window.isActive()
|
||||
);
|
||||
|
@ -965,9 +972,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
const isNewMessage = false;
|
||||
messagesAdded(
|
||||
id,
|
||||
cleaned.map((messageModel: MessageModel) =>
|
||||
messageModel.getReduxData()
|
||||
),
|
||||
cleaned.map((messageModel: MessageModel) => ({
|
||||
...messageModel.attributes,
|
||||
})),
|
||||
isNewMessage,
|
||||
window.isActive()
|
||||
);
|
||||
|
@ -1210,9 +1217,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
|
||||
messagesReset(
|
||||
conversationId,
|
||||
cleaned.map((messageModel: MessageModel) =>
|
||||
messageModel.getReduxData()
|
||||
),
|
||||
cleaned.map((messageModel: MessageModel) => ({
|
||||
...messageModel.attributes,
|
||||
})),
|
||||
metrics,
|
||||
scrollToMessageId
|
||||
);
|
||||
|
@ -1294,9 +1301,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
const unboundedFetch = true;
|
||||
messagesReset(
|
||||
conversationId,
|
||||
cleaned.map((messageModel: MessageModel) =>
|
||||
messageModel.getReduxData()
|
||||
),
|
||||
cleaned.map((messageModel: MessageModel) => ({
|
||||
...messageModel.attributes,
|
||||
})),
|
||||
metrics,
|
||||
scrollToMessageId,
|
||||
unboundedFetch
|
||||
|
@ -2327,7 +2334,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
throw new Error(`showForwardMessageModal: Message ${messageId} missing!`);
|
||||
}
|
||||
|
||||
const attachments = message.getAttachmentsForMessage();
|
||||
const attachments = getAttachmentsForMessage(message.attributes);
|
||||
this.forwardMessageModal = new Whisper.ReactWrapperView({
|
||||
JSX: window.Signal.State.Roots.createForwardMessageModal(
|
||||
window.reduxStore,
|
||||
|
@ -2557,7 +2564,10 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
const message = rawMedia[i];
|
||||
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
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
rawMedia[i] = await upgradeMessageSchema(message);
|
||||
|
@ -2871,7 +2881,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
throw new Error(`displayTapToViewMessage: Message ${messageId} missing!`);
|
||||
}
|
||||
|
||||
if (!message.isTapToView()) {
|
||||
if (!isTapToView(message.attributes)) {
|
||||
throw new Error(
|
||||
`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) {
|
||||
throw new Error(
|
||||
`displayTapToViewMessage: Message ${message.idForLogging()} had no first attachment with path`
|
||||
|
@ -2955,7 +2965,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
Message: Whisper.Message,
|
||||
});
|
||||
message.cleanup();
|
||||
if (message.isOutgoing()) {
|
||||
if (isOutgoing(message.attributes)) {
|
||||
this.model.decrementSentMessageCount();
|
||||
} else {
|
||||
this.model.decrementMessageCount();
|
||||
|
@ -3531,7 +3541,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
async loadRecentMediaItems(limit: number): Promise<void> {
|
||||
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,
|
||||
{
|
||||
limit,
|
||||
|
@ -3543,7 +3553,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
.reduce(
|
||||
(acc, message) => [
|
||||
...acc,
|
||||
...message.attachments.map(
|
||||
...(message.attachments || []).map(
|
||||
(attachment: AttachmentType, index: number): MediaItemType => {
|
||||
const { thumbnail } = attachment;
|
||||
|
||||
|
@ -3792,7 +3802,12 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
})
|
||||
: undefined;
|
||||
|
||||
if (message && !message.canReply()) {
|
||||
if (
|
||||
message &&
|
||||
!canReply(message.attributes, (id?: string) =>
|
||||
message.findAndFormatContact(id)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -3855,7 +3870,11 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
message.quotedMessage = this.quotedMessage;
|
||||
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();
|
||||
|
||||
|
|
86
ts/window.d.ts
vendored
86
ts/window.d.ts
vendored
|
@ -19,7 +19,11 @@ import {
|
|||
ReactionAttributesType,
|
||||
ReactionModelType,
|
||||
} from './model-types.d';
|
||||
import { ContactRecordIdentityState, TextSecureType } from './textsecure.d';
|
||||
import {
|
||||
ContactRecordIdentityState,
|
||||
TextSecureType,
|
||||
DownloadAttachmentType,
|
||||
} from './textsecure.d';
|
||||
import { Storage } from './textsecure/Storage';
|
||||
import {
|
||||
ChallengeHandler,
|
||||
|
@ -107,6 +111,7 @@ import { Quote } from './components/conversation/Quote';
|
|||
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
|
||||
import { DisappearingTimeDialog } from './components/conversation/DisappearingTimeDialog';
|
||||
import { MIMEType } from './types/MIME';
|
||||
import { AttachmentType } from './types/Attachment';
|
||||
import { ElectronLocaleType } from './util/mapToSupportLocale';
|
||||
import { SignalProtocolStore } from './SignalProtocolStore';
|
||||
import { StartupQueue } from './util/StartupQueue';
|
||||
|
@ -222,16 +227,7 @@ declare global {
|
|||
getRegionCodeForNumber: (number: string) => string;
|
||||
format: (number: string, format: PhoneNumberFormat) => string;
|
||||
};
|
||||
log: {
|
||||
fatal: LoggerType;
|
||||
info: LoggerType;
|
||||
warn: LoggerType;
|
||||
error: LoggerType;
|
||||
debug: LoggerType;
|
||||
trace: LoggerType;
|
||||
fetch: () => Promise<string>;
|
||||
publish: typeof uploadDebugLogs;
|
||||
};
|
||||
log: LoggerType;
|
||||
nodeSetImmediate: typeof setImmediate;
|
||||
normalizeUuids: (obj: any, paths: Array<string>, context: string) => void;
|
||||
onFullScreenChange: (fullScreen: boolean) => void;
|
||||
|
@ -275,14 +271,6 @@ declare global {
|
|||
};
|
||||
Signal: {
|
||||
Backbone: any;
|
||||
AttachmentDownloads: {
|
||||
addJob: <T = unknown>(
|
||||
attachment: unknown,
|
||||
options: unknown
|
||||
) => Promise<T>;
|
||||
start: (options: WhatIsThis) => void;
|
||||
stop: () => void;
|
||||
};
|
||||
Crypto: typeof Crypto;
|
||||
Curve: typeof Curve;
|
||||
Data: typeof Data;
|
||||
|
@ -317,6 +305,9 @@ declare global {
|
|||
loadStickerData: (sticker: unknown) => WhatIsThis;
|
||||
readStickerData: (path: string) => Promise<ArrayBuffer>;
|
||||
upgradeMessageSchema: (attributes: unknown) => WhatIsThis;
|
||||
processNewAttachment: (
|
||||
attachment: DownloadAttachmentType
|
||||
) => Promise<AttachmentType>;
|
||||
|
||||
copyIntoTempDirectory: any;
|
||||
deleteDraftFile: any;
|
||||
|
@ -545,13 +536,6 @@ declare global {
|
|||
WebAPI: WebAPIConnectType;
|
||||
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;
|
||||
readyForUpdates: () => void;
|
||||
logAppLoadedEvent: (options: { processedCount?: number }) => void;
|
||||
|
@ -648,7 +632,18 @@ export class CanvasVideoRenderer {
|
|||
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 = {
|
||||
events: {
|
||||
|
@ -685,7 +680,6 @@ export type WhisperType = {
|
|||
ConversationUnarchivedToast: WhatIsThis;
|
||||
ConversationMarkedUnreadToast: WhatIsThis;
|
||||
WallClockListener: WhatIsThis;
|
||||
MessageRequests: WhatIsThis;
|
||||
BannerView: any;
|
||||
RecorderView: any;
|
||||
GroupMemberList: any;
|
||||
|
@ -712,42 +706,6 @@ export type WhisperType = {
|
|||
) => 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;
|
||||
|
||||
ExpiringMessagesListener: WhatIsThis;
|
||||
|
|
Loading…
Reference in a new issue