2021-01-04 19:46:24 +00:00
|
|
|
|
// Copyright 2020-2021 Signal Messenger, LLC
|
2020-10-30 20:34:04 +00:00
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
2021-08-09 20:06:21 +00:00
|
|
|
|
import nodePath from 'path';
|
2021-08-11 16:23:21 +00:00
|
|
|
|
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
|
|
|
|
|
|
2021-06-25 16:08:16 +00:00
|
|
|
|
import {
|
|
|
|
|
AttachmentDraftType,
|
|
|
|
|
AttachmentType,
|
|
|
|
|
InMemoryAttachmentDraftType,
|
|
|
|
|
OnDiskAttachmentDraftType,
|
2021-07-14 23:39:52 +00:00
|
|
|
|
isGIF,
|
2021-06-25 16:08:16 +00:00
|
|
|
|
} from '../types/Attachment';
|
2021-07-09 19:36:10 +00:00
|
|
|
|
import type { StickerPackType as StickerPackDBType } from '../sql/Interface';
|
|
|
|
|
import * as Stickers from '../types/Stickers';
|
2021-08-09 20:06:21 +00:00
|
|
|
|
import {
|
|
|
|
|
IMAGE_JPEG,
|
|
|
|
|
IMAGE_WEBP,
|
|
|
|
|
isHeic,
|
|
|
|
|
stringToMIMEType,
|
|
|
|
|
} from '../types/MIME';
|
2021-04-27 22:35:35 +00:00
|
|
|
|
import { ConversationModel } from '../models/conversations';
|
2021-06-16 00:44:14 +00:00
|
|
|
|
import {
|
|
|
|
|
GroupV2PendingMemberType,
|
|
|
|
|
MessageModelCollectionType,
|
2021-06-17 17:15:10 +00:00
|
|
|
|
MessageAttributesType,
|
2021-06-16 00:44:14 +00:00
|
|
|
|
} from '../model-types.d';
|
2021-04-27 22:35:35 +00:00
|
|
|
|
import { LinkPreviewType } from '../types/message/LinkPreviews';
|
2021-08-23 23:14:53 +00:00
|
|
|
|
import { MediaItemType } from '../types/MediaItem';
|
2021-03-18 17:09:27 +00:00
|
|
|
|
import { MessageModel } from '../models/messages';
|
2021-03-22 18:51:53 +00:00
|
|
|
|
import { assert } from '../util/assert';
|
2021-05-13 17:18:51 +00:00
|
|
|
|
import { maybeParseUrl } from '../util/url';
|
2021-05-27 20:17:05 +00:00
|
|
|
|
import { addReportSpamJob } from '../jobs/helpers/addReportSpamJob';
|
|
|
|
|
import { reportSpamJobQueue } from '../jobs/reportSpamJobQueue';
|
2021-06-01 23:30:25 +00:00
|
|
|
|
import { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions';
|
2021-06-07 16:39:13 +00:00
|
|
|
|
import {
|
|
|
|
|
isDirectConversation,
|
|
|
|
|
isGroupV1,
|
|
|
|
|
isMe,
|
|
|
|
|
} from '../util/whatTypeOfConversation';
|
2021-06-22 23:16:50 +00:00
|
|
|
|
import { findAndFormatContact } from '../util/findAndFormatContact';
|
2021-06-22 14:46:42 +00:00
|
|
|
|
import * as Bytes from '../Bytes';
|
2021-06-17 17:15:10 +00:00
|
|
|
|
import {
|
|
|
|
|
canReply,
|
|
|
|
|
getAttachmentsForMessage,
|
|
|
|
|
isOutgoing,
|
|
|
|
|
isTapToView,
|
|
|
|
|
} from '../state/selectors/message';
|
2021-07-29 14:29:07 +00:00
|
|
|
|
import { isMessageUnread } from '../util/isMessageUnread';
|
2021-08-23 23:14:53 +00:00
|
|
|
|
import {
|
|
|
|
|
getConversationSelector,
|
|
|
|
|
getMessagesByConversation,
|
|
|
|
|
} from '../state/selectors/conversations';
|
2021-06-17 21:15:09 +00:00
|
|
|
|
import { ConversationDetailsMembershipList } from '../components/conversation/conversation-details/ConversationDetailsMembershipList';
|
|
|
|
|
import { showSafetyNumberChangeDialog } from '../shims/showSafetyNumberChangeDialog';
|
2021-06-25 16:08:16 +00:00
|
|
|
|
import {
|
|
|
|
|
LinkPreviewImage,
|
|
|
|
|
LinkPreviewResult,
|
|
|
|
|
LinkPreviewWithDomain,
|
|
|
|
|
} from '../types/LinkPreview';
|
2021-06-30 17:00:02 +00:00
|
|
|
|
import * as LinkPreview from '../types/LinkPreview';
|
2021-07-09 19:36:10 +00:00
|
|
|
|
import { SignalService as Proto } from '../protobuf';
|
2021-07-28 00:09:10 +00:00
|
|
|
|
import {
|
|
|
|
|
autoScale,
|
|
|
|
|
handleImageAttachment,
|
|
|
|
|
} from '../util/handleImageAttachment';
|
2021-08-12 18:15:55 +00:00
|
|
|
|
import { ReadStatus } from '../messages/MessageReadStatus';
|
|
|
|
|
import { markViewed } from '../services/MessageUpdater';
|
2021-08-17 15:43:26 +00:00
|
|
|
|
import { viewedReceiptsJobQueue } from '../jobs/viewedReceiptsJobQueue';
|
2021-08-12 18:15:55 +00:00
|
|
|
|
import { viewSyncJobQueue } from '../jobs/viewSyncJobQueue';
|
2021-08-20 01:56:39 +00:00
|
|
|
|
import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
2021-08-13 15:01:56 +00:00
|
|
|
|
import type { WhatIsThis } from '../window.d';
|
2020-09-28 23:46:31 +00:00
|
|
|
|
|
2021-03-22 18:51:53 +00:00
|
|
|
|
type AttachmentOptions = {
|
|
|
|
|
messageId: string;
|
|
|
|
|
attachment: AttachmentType;
|
|
|
|
|
};
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const FIVE_MINUTES = 1000 * 60 * 5;
|
2020-09-28 23:46:31 +00:00
|
|
|
|
const LINK_PREVIEW_TIMEOUT = 60 * 1000;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
window.Whisper = window.Whisper || {};
|
|
|
|
|
|
|
|
|
|
const { Whisper } = window;
|
2021-07-14 23:39:52 +00:00
|
|
|
|
const { Message, MIME, VisualAttachment } = window.Signal.Types;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
copyIntoTempDirectory,
|
|
|
|
|
deleteDraftFile,
|
|
|
|
|
deleteTempFile,
|
|
|
|
|
getAbsoluteAttachmentPath,
|
|
|
|
|
getAbsoluteDraftPath,
|
|
|
|
|
getAbsoluteTempPath,
|
2021-06-03 21:26:56 +00:00
|
|
|
|
loadAttachmentData,
|
2021-04-27 22:35:35 +00:00
|
|
|
|
loadPreviewData,
|
|
|
|
|
loadStickerData,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
openFileInFolder,
|
|
|
|
|
readAttachmentData,
|
|
|
|
|
readDraftData,
|
|
|
|
|
saveAttachmentToDisk,
|
|
|
|
|
upgradeMessageSchema,
|
|
|
|
|
writeNewDraftData,
|
|
|
|
|
} = window.Signal.Migrations;
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
getOlderMessagesByConversation,
|
|
|
|
|
getMessageMetricsForConversation,
|
|
|
|
|
getMessageById,
|
|
|
|
|
getMessagesBySentAt,
|
|
|
|
|
getNewerMessagesByConversation,
|
|
|
|
|
} = window.Signal.Data;
|
|
|
|
|
|
|
|
|
|
Whisper.ExpiredToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('expiredWarning') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.BlockedToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('unblockToSend') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.BlockedGroupToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('unblockGroupToSend') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2021-05-06 00:09:29 +00:00
|
|
|
|
Whisper.CaptchaSolvedToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('verificationComplete') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.CaptchaFailedToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('verificationFailed') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
Whisper.LeftGroupToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('youLeftTheGroup') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2020-11-16 21:34:41 +00:00
|
|
|
|
Whisper.InvalidConversationToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('invalidConversation') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
Whisper.OriginalNotFoundToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('originalMessageNotFound') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.OriginalNoLongerAvailableToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('originalMessageNotAvailable') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.FoundButNotLoadedToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('messageFoundButNotLoaded') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.VoiceNoteLimit = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('voiceNoteLimit') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.VoiceNoteMustBeOnlyAttachmentToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('voiceNoteMustBeOnlyAttachment') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.ConversationArchivedToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('conversationArchived') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.ConversationUnarchivedToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('conversationReturnedToInbox') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2020-10-28 22:54:32 +00:00
|
|
|
|
Whisper.ConversationMarkedUnreadToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('conversationMarkedUnread') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
Whisper.TapToViewExpiredIncomingToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return {
|
|
|
|
|
toastMessage: window.i18n(
|
|
|
|
|
'Message--tap-to-view--incoming--expired-toast'
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.TapToViewExpiredOutgoingToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return {
|
|
|
|
|
toastMessage: window.i18n(
|
|
|
|
|
'Message--tap-to-view--outgoing--expired-toast'
|
|
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2021-07-15 23:48:09 +00:00
|
|
|
|
Whisper.DecryptionErrorToast = Whisper.ToastView.extend({
|
2021-08-03 00:12:49 +00:00
|
|
|
|
className: 'toast toast-clickable decryption-error',
|
2021-07-15 23:48:09 +00:00
|
|
|
|
events: {
|
|
|
|
|
click: 'onClick',
|
2021-08-03 00:12:49 +00:00
|
|
|
|
keydown: 'onKeyDown',
|
2021-07-15 23:48:09 +00:00
|
|
|
|
},
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return {
|
|
|
|
|
toastMessage: window.i18n('decryptionErrorToast'),
|
|
|
|
|
};
|
|
|
|
|
},
|
2021-07-20 20:53:12 +00:00
|
|
|
|
// Note: this is the same thing as ToastView, except it's missing the setTimeout, so it
|
|
|
|
|
// will stick around until the user interacts with it.
|
|
|
|
|
render() {
|
2021-08-03 00:12:49 +00:00
|
|
|
|
const toasts = document.getElementsByClassName('decryption-error');
|
|
|
|
|
if (toasts.length > 1) {
|
|
|
|
|
window.log.info(
|
|
|
|
|
'DecryptionErrorToast: We are second decryption error toast. Closing.'
|
|
|
|
|
);
|
|
|
|
|
this.close();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-20 20:53:12 +00:00
|
|
|
|
this.$el.html(
|
|
|
|
|
window.Mustache.render(
|
|
|
|
|
window._.result(this, 'template', ''),
|
|
|
|
|
window._.result(this, 'render_attributes', '')
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
this.$el.attr('tabIndex', 0);
|
|
|
|
|
this.$el.show();
|
2021-08-03 00:12:49 +00:00
|
|
|
|
if (window.getInteractionMode() === 'keyboard') {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.$el.focus();
|
|
|
|
|
}, 1);
|
|
|
|
|
}
|
2021-07-20 20:53:12 +00:00
|
|
|
|
},
|
2021-07-15 23:48:09 +00:00
|
|
|
|
onClick() {
|
2021-07-20 20:53:12 +00:00
|
|
|
|
this.close();
|
2021-07-15 23:48:09 +00:00
|
|
|
|
window.showDebugLog();
|
|
|
|
|
},
|
2021-08-03 00:12:49 +00:00
|
|
|
|
onKeyDown(event: KeyboardEvent) {
|
|
|
|
|
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.onClick();
|
|
|
|
|
},
|
2021-07-15 23:48:09 +00:00
|
|
|
|
});
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
Whisper.FileSavedToast = Whisper.ToastView.extend({
|
|
|
|
|
className: 'toast toast-clickable',
|
2021-08-16 21:35:54 +00:00
|
|
|
|
initialize(options: Readonly<{ fullPath: string }>) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!options.fullPath) {
|
|
|
|
|
throw new Error('FileSavedToast: name option was not provided!');
|
|
|
|
|
}
|
|
|
|
|
this.fullPath = options.fullPath;
|
|
|
|
|
this.timeout = 10000;
|
|
|
|
|
|
|
|
|
|
if (window.getInteractionMode() === 'keyboard') {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.$el.focus();
|
|
|
|
|
}, 1);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
events: {
|
|
|
|
|
click: 'onClick',
|
|
|
|
|
keydown: 'onKeydown',
|
|
|
|
|
},
|
|
|
|
|
onClick() {
|
|
|
|
|
openFileInFolder(this.fullPath);
|
|
|
|
|
this.close();
|
|
|
|
|
},
|
2021-06-08 14:59:38 +00:00
|
|
|
|
onKeydown(event: KeyboardEvent) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
|
|
|
openFileInFolder(this.fullPath);
|
|
|
|
|
this.close();
|
|
|
|
|
},
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('attachmentSaved') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.ReactionFailedToast = Whisper.ToastView.extend({
|
|
|
|
|
className: 'toast toast-clickable',
|
|
|
|
|
initialize() {
|
|
|
|
|
this.timeout = 4000;
|
|
|
|
|
|
|
|
|
|
if (window.getInteractionMode() === 'keyboard') {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.$el.focus();
|
|
|
|
|
}, 1);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
events: {
|
|
|
|
|
click: 'onClick',
|
|
|
|
|
keydown: 'onKeydown',
|
|
|
|
|
},
|
|
|
|
|
onClick() {
|
|
|
|
|
this.close();
|
|
|
|
|
},
|
2021-06-08 14:59:38 +00:00
|
|
|
|
onKeydown(event: KeyboardEvent) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (event.key !== 'Enter' && event.key !== ' ') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
|
|
|
this.close();
|
|
|
|
|
},
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('Reactions--error') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2021-05-06 00:09:29 +00:00
|
|
|
|
Whisper.DeleteForEveryoneFailedToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('deleteForEveryoneFailed') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
|
Whisper.GroupLinkCopiedToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('GroupLinkManagement--clipboard') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2020-10-02 18:30:43 +00:00
|
|
|
|
Whisper.PinnedConversationsFullToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('pinnedConversationsFull') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
|
|
|
|
|
|
|
|
|
Whisper.MessageBodyTooLongToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('messageBodyTooLong') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.FileSizeToast = Whisper.ToastView.extend({
|
2021-02-26 21:06:37 +00:00
|
|
|
|
template: () => $('#file-size-modal').html(),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
render_attributes() {
|
|
|
|
|
return {
|
|
|
|
|
'file-size-warning': window.i18n('fileSizeWarning'),
|
|
|
|
|
limit: this.model.limit,
|
|
|
|
|
units: this.model.units,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.UnableToLoadToast = Whisper.ToastView.extend({
|
|
|
|
|
render_attributes() {
|
|
|
|
|
return { toastMessage: window.i18n('unableToLoadAttachment') };
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2021-07-20 20:18:35 +00:00
|
|
|
|
Whisper.CannotStartGroupCallToast = Whisper.ToastView.extend({
|
|
|
|
|
template: () => window.i18n('GroupV2--cannot-start-group-call'),
|
|
|
|
|
});
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
Whisper.DangerousFileTypeToast = Whisper.ToastView.extend({
|
2021-02-26 21:06:37 +00:00
|
|
|
|
template: () => window.i18n('dangerousFileType'),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.OneNonImageAtATimeToast = Whisper.ToastView.extend({
|
2021-02-26 21:06:37 +00:00
|
|
|
|
template: () => window.i18n('oneNonImageAtATimeToast'),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.CannotMixImageAndNonImageAttachmentsToast = Whisper.ToastView.extend({
|
2021-02-26 21:06:37 +00:00
|
|
|
|
template: () => window.i18n('cannotMixImageAndNonImageAttachments'),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.MaxAttachmentsToast = Whisper.ToastView.extend({
|
2021-02-26 21:06:37 +00:00
|
|
|
|
template: () => window.i18n('maximumAttachments'),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
});
|
|
|
|
|
|
2021-01-29 22:16:48 +00:00
|
|
|
|
Whisper.AlreadyGroupMemberToast = Whisper.ToastView.extend({
|
2021-02-26 21:06:37 +00:00
|
|
|
|
template: () => window.i18n('GroupV2--join--already-in-group'),
|
2021-01-29 22:16:48 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.AlreadyRequestedToJoinToast = Whisper.ToastView.extend({
|
2021-02-26 21:06:37 +00:00
|
|
|
|
template: () => window.i18n('GroupV2--join--already-awaiting-approval'),
|
2021-01-29 22:16:48 +00:00
|
|
|
|
});
|
|
|
|
|
|
2021-05-27 20:17:05 +00:00
|
|
|
|
const ReportedSpamAndBlockedToast = Whisper.ToastView.extend({
|
|
|
|
|
template: () =>
|
|
|
|
|
window.i18n('MessageRequests--block-and-report-spam-success-toast'),
|
|
|
|
|
});
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
Whisper.ConversationLoadingScreen = Whisper.View.extend({
|
2021-02-26 21:06:37 +00:00
|
|
|
|
template: () => $('#conversation-loading-screen').html(),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
className: 'conversation-loading-screen',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Whisper.ConversationView = Whisper.View.extend({
|
|
|
|
|
className() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
return ['conversation', model.get('type')].join(' ');
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
id() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
return `conversation-${model.cid}`;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
2021-02-26 21:06:37 +00:00
|
|
|
|
template: () => $('#conversation').html(),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
render_attributes() {
|
|
|
|
|
return {
|
|
|
|
|
'send-message': window.i18n('sendMessage'),
|
|
|
|
|
};
|
|
|
|
|
},
|
2021-06-07 17:01:05 +00:00
|
|
|
|
initialize() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
// Events on Conversation model
|
2021-06-16 00:44:14 +00:00
|
|
|
|
this.listenTo(this.model, 'destroy', this.stopListening);
|
|
|
|
|
this.listenTo(this.model, 'newmessage', this.lazyUpdateVerified);
|
|
|
|
|
|
|
|
|
|
// These are triggered by InboxView
|
|
|
|
|
this.listenTo(this.model, 'opened', this.onOpened);
|
|
|
|
|
this.listenTo(this.model, 'scroll-to-message', this.scrollToMessage);
|
|
|
|
|
this.listenTo(this.model, 'unload', (reason: string) =>
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.unload(`model trigger - ${reason}`)
|
|
|
|
|
);
|
2021-06-16 00:44:14 +00:00
|
|
|
|
|
|
|
|
|
// These are triggered by background.ts for keyboard handling
|
|
|
|
|
this.listenTo(this.model, 'focus-composer', this.focusMessageField);
|
|
|
|
|
this.listenTo(this.model, 'open-all-media', this.showAllMedia);
|
|
|
|
|
this.listenTo(this.model, 'begin-recording', this.captureAudio);
|
|
|
|
|
this.listenTo(this.model, 'attach-file', this.onChooseAttachment);
|
|
|
|
|
this.listenTo(this.model, 'escape-pressed', this.resetPanel);
|
|
|
|
|
this.listenTo(this.model, 'show-message-details', this.showMessageDetail);
|
|
|
|
|
this.listenTo(this.model, 'show-contact-modal', this.showContactModal);
|
|
|
|
|
this.listenTo(
|
|
|
|
|
this.model,
|
|
|
|
|
'toggle-reply',
|
|
|
|
|
(messageId: string | undefined) => {
|
|
|
|
|
const target = this.quote || !messageId ? null : messageId;
|
|
|
|
|
this.setQuoteMessage(target);
|
|
|
|
|
}
|
|
|
|
|
);
|
2021-06-08 14:59:38 +00:00
|
|
|
|
this.listenTo(model, 'save-attachment', this.downloadAttachmentWrapper);
|
|
|
|
|
this.listenTo(model, 'delete-message', this.deleteMessage);
|
|
|
|
|
this.listenTo(model, 'remove-link-review', this.removeLinkPreview);
|
|
|
|
|
this.listenTo(model, 'remove-all-draft-attachments', this.clearAttachments);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2021-02-26 21:06:37 +00:00
|
|
|
|
this.lazyUpdateVerified = window._.debounce(
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.updateVerified.bind(model),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
1000 // one second
|
|
|
|
|
);
|
|
|
|
|
this.model.throttledGetProfiles =
|
|
|
|
|
this.model.throttledGetProfiles ||
|
2021-02-26 21:06:37 +00:00
|
|
|
|
window._.throttle(this.model.getProfiles.bind(this.model), FIVE_MINUTES);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2021-02-26 21:06:37 +00:00
|
|
|
|
this.debouncedMaybeGrabLinkPreview = window._.debounce(
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.maybeGrabLinkPreview.bind(this),
|
|
|
|
|
200
|
|
|
|
|
);
|
2021-02-26 21:06:37 +00:00
|
|
|
|
this.debouncedSaveDraft = window._.debounce(this.saveDraft.bind(this), 200);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
this.render();
|
|
|
|
|
|
|
|
|
|
this.loadingScreen = new Whisper.ConversationLoadingScreen();
|
|
|
|
|
this.loadingScreen.render();
|
|
|
|
|
this.loadingScreen.$el.prependTo(this.$('.discussion-container'));
|
|
|
|
|
|
|
|
|
|
this.setupHeader();
|
|
|
|
|
this.setupTimeline();
|
2021-06-25 16:08:16 +00:00
|
|
|
|
this.setupCompositionArea();
|
2020-09-28 23:46:31 +00:00
|
|
|
|
|
|
|
|
|
this.linkPreviewAbortController = null;
|
2021-06-25 16:08:16 +00:00
|
|
|
|
this.updateAttachmentsView();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
events: {
|
|
|
|
|
'click .capture-audio .microphone': 'captureAudio',
|
|
|
|
|
'change input.file-input': 'onChoseAttachment',
|
|
|
|
|
|
|
|
|
|
dragover: 'onDragOver',
|
|
|
|
|
dragleave: 'onDragLeave',
|
|
|
|
|
drop: 'onDrop',
|
|
|
|
|
paste: 'onPaste',
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getMuteExpirationLabel() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
const muteExpiresAt = model.get('muteExpiresAt');
|
|
|
|
|
if (!model.isMuted()) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const today = window.moment(Date.now());
|
|
|
|
|
const expires = window.moment(muteExpiresAt);
|
|
|
|
|
|
|
|
|
|
if (today.isSame(expires, 'day')) {
|
|
|
|
|
// eslint-disable-next-line consistent-return
|
|
|
|
|
return expires.format('hh:mm A');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line consistent-return
|
|
|
|
|
return expires.format('M/D/YY, hh:mm A');
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-05 12:35:33 +00:00
|
|
|
|
setMuteExpiration(ms = 0): void {
|
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
|
|
|
|
model.setMuteExpiration(
|
|
|
|
|
ms >= Number.MAX_SAFE_INTEGER ? ms : Date.now() + ms
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
|
2020-10-02 18:30:43 +00:00
|
|
|
|
setPin(value: boolean) {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2020-10-02 18:30:43 +00:00
|
|
|
|
if (value) {
|
2021-06-15 00:09:37 +00:00
|
|
|
|
const pinnedConversationIds = window.storage.get(
|
2020-11-09 18:30:05 +00:00
|
|
|
|
'pinnedConversationIds',
|
2021-06-15 00:09:37 +00:00
|
|
|
|
new Array<string>()
|
2020-11-09 18:30:05 +00:00
|
|
|
|
);
|
2020-10-10 14:25:17 +00:00
|
|
|
|
|
|
|
|
|
if (pinnedConversationIds.length >= 4) {
|
2020-10-02 18:30:43 +00:00
|
|
|
|
this.showToast(Whisper.PinnedConversationsFullToast);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.pin();
|
2020-10-02 18:30:43 +00:00
|
|
|
|
} else {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.unpin();
|
2020-10-02 18:30:43 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
setupHeader() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2020-10-30 17:52:21 +00:00
|
|
|
|
this.titleView = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'title-wrapper',
|
|
|
|
|
JSX: window.Signal.State.Roots.createConversationHeader(
|
|
|
|
|
window.reduxStore,
|
|
|
|
|
{
|
2021-06-08 14:59:38 +00:00
|
|
|
|
id: model.id,
|
2020-10-30 17:52:21 +00:00
|
|
|
|
|
2021-01-28 00:18:50 +00:00
|
|
|
|
onShowContactModal: this.showContactModal.bind(this),
|
2020-10-30 17:52:21 +00:00
|
|
|
|
onSetDisappearingMessages: (seconds: number) =>
|
|
|
|
|
this.setDisappearingMessages(seconds),
|
|
|
|
|
onDeleteMessages: () => this.destroyMessages(),
|
|
|
|
|
onResetSession: () => this.endSession(),
|
|
|
|
|
onSearchInConversation: () => {
|
|
|
|
|
const { searchInConversation } = window.reduxActions.search;
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const name = isMe(model.attributes)
|
2020-10-30 17:52:21 +00:00
|
|
|
|
? window.i18n('noteToSelf')
|
2021-06-08 14:59:38 +00:00
|
|
|
|
: model.getTitle();
|
|
|
|
|
searchInConversation(model.id, name);
|
2020-10-30 17:52:21 +00:00
|
|
|
|
},
|
2021-08-05 12:35:33 +00:00
|
|
|
|
onSetMuteNotifications: this.setMuteExpiration.bind(this),
|
2020-10-30 17:52:21 +00:00
|
|
|
|
onSetPin: this.setPin.bind(this),
|
|
|
|
|
// These are view only and don't update the Conversation model, so they
|
|
|
|
|
// need a manual update call.
|
|
|
|
|
onOutgoingAudioCallInConversation: async () => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
window.log.info(
|
2020-10-30 17:52:21 +00:00
|
|
|
|
'onOutgoingAudioCallInConversation: about to start an audio call'
|
2020-09-24 20:57:54 +00:00
|
|
|
|
);
|
|
|
|
|
|
2020-10-30 17:52:21 +00:00
|
|
|
|
const isVideoCall = false;
|
|
|
|
|
|
|
|
|
|
if (await this.isCallSafe()) {
|
|
|
|
|
window.log.info(
|
|
|
|
|
'onOutgoingAudioCallInConversation: call is deemed "safe". Making call'
|
|
|
|
|
);
|
|
|
|
|
await window.Signal.Services.calling.startCallingLobby(
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.id,
|
2020-10-30 17:52:21 +00:00
|
|
|
|
isVideoCall
|
|
|
|
|
);
|
|
|
|
|
window.log.info(
|
|
|
|
|
'onOutgoingAudioCallInConversation: started the call'
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
window.log.info(
|
|
|
|
|
'onOutgoingAudioCallInConversation: call is deemed "unsafe". Stopping'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2020-10-30 17:52:21 +00:00
|
|
|
|
onOutgoingVideoCallInConversation: async () => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
window.log.info(
|
2020-10-30 17:52:21 +00:00
|
|
|
|
'onOutgoingVideoCallInConversation: about to start a video call'
|
2020-09-24 20:57:54 +00:00
|
|
|
|
);
|
2020-10-30 17:52:21 +00:00
|
|
|
|
const isVideoCall = true;
|
|
|
|
|
|
2021-07-20 20:18:35 +00:00
|
|
|
|
if (model.get('announcementsOnly') && !model.areWeAdmin()) {
|
|
|
|
|
this.showToast(Whisper.CannotStartGroupCallToast);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-30 17:52:21 +00:00
|
|
|
|
if (await this.isCallSafe()) {
|
|
|
|
|
window.log.info(
|
|
|
|
|
'onOutgoingVideoCallInConversation: call is deemed "safe". Making call'
|
|
|
|
|
);
|
|
|
|
|
await window.Signal.Services.calling.startCallingLobby(
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.id,
|
2020-10-30 17:52:21 +00:00
|
|
|
|
isVideoCall
|
|
|
|
|
);
|
|
|
|
|
window.log.info(
|
|
|
|
|
'onOutgoingVideoCallInConversation: started the call'
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
window.log.info(
|
|
|
|
|
'onOutgoingVideoCallInConversation: call is deemed "unsafe". Stopping'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-05-28 16:15:17 +00:00
|
|
|
|
onShowChatColorEditor: () => {
|
|
|
|
|
this.showChatColorEditor();
|
|
|
|
|
},
|
2021-01-29 21:19:24 +00:00
|
|
|
|
onShowConversationDetails: () => {
|
|
|
|
|
this.showConversationDetails();
|
|
|
|
|
},
|
2020-10-30 17:52:21 +00:00
|
|
|
|
onShowSafetyNumber: () => {
|
|
|
|
|
this.showSafetyNumber();
|
|
|
|
|
},
|
|
|
|
|
onShowAllMedia: () => {
|
|
|
|
|
this.showAllMedia();
|
|
|
|
|
},
|
2021-06-17 21:15:09 +00:00
|
|
|
|
onShowGroupMembers: () => {
|
|
|
|
|
this.showGV1Members();
|
2020-10-30 17:52:21 +00:00
|
|
|
|
},
|
|
|
|
|
onGoBack: () => {
|
|
|
|
|
this.resetPanel();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onArchive: () => {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.setArchived(true);
|
|
|
|
|
model.trigger('unload', 'archive');
|
2020-10-30 17:52:21 +00:00
|
|
|
|
|
|
|
|
|
Whisper.ToastView.show(
|
|
|
|
|
Whisper.ConversationArchivedToast,
|
|
|
|
|
document.body
|
2020-09-24 20:57:54 +00:00
|
|
|
|
);
|
2020-10-30 17:52:21 +00:00
|
|
|
|
},
|
|
|
|
|
onMarkUnread: () => {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.setMarkedUnread(true);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2020-10-30 17:52:21 +00:00
|
|
|
|
Whisper.ToastView.show(
|
|
|
|
|
Whisper.ConversationMarkedUnreadToast,
|
|
|
|
|
document.body
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
onMoveToInbox: () => {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.setArchived(false);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2020-10-30 17:52:21 +00:00
|
|
|
|
Whisper.ToastView.show(
|
|
|
|
|
Whisper.ConversationUnarchivedToast,
|
|
|
|
|
document.body
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
});
|
|
|
|
|
this.$('.conversation-header').append(this.titleView.el);
|
2021-01-29 21:19:24 +00:00
|
|
|
|
window.reduxActions.conversations.setSelectedConversationHeaderTitle();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-06-25 16:08:16 +00:00
|
|
|
|
setupCompositionArea() {
|
|
|
|
|
window.reduxActions.composer.resetComposer();
|
|
|
|
|
|
2021-06-01 23:30:25 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const compositionApi = { current: null };
|
|
|
|
|
this.compositionApi = compositionApi;
|
|
|
|
|
|
|
|
|
|
const micCellEl = $(`
|
|
|
|
|
<div class="capture-audio">
|
|
|
|
|
<button class="microphone"></button>
|
|
|
|
|
</div>
|
|
|
|
|
`)[0];
|
|
|
|
|
|
2021-07-09 19:36:10 +00:00
|
|
|
|
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
const props = {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
id: model.id,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
compositionApi,
|
|
|
|
|
onClickAddPack: () => this.showStickerManager(),
|
|
|
|
|
onPickSticker: (packId: string, stickerId: number) =>
|
|
|
|
|
this.sendStickerMessage({ packId, stickerId }),
|
2020-11-03 01:19:52 +00:00
|
|
|
|
onSubmit: (
|
2021-06-08 14:59:38 +00:00
|
|
|
|
message: string,
|
2021-07-30 18:37:03 +00:00
|
|
|
|
mentions: typeof window.Whisper.BodyRangesType,
|
|
|
|
|
timestamp: number
|
|
|
|
|
) => this.sendMessage(message, mentions, { timestamp }),
|
2020-11-03 01:19:52 +00:00
|
|
|
|
onEditorStateChange: (
|
|
|
|
|
msg: string,
|
|
|
|
|
bodyRanges: Array<typeof window.Whisper.BodyRangeType>,
|
2021-04-27 22:35:35 +00:00
|
|
|
|
caretLocation?: number
|
2020-11-03 01:19:52 +00:00
|
|
|
|
) => this.onEditorStateChange(msg, bodyRanges, caretLocation),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
onTextTooLong: () => this.showToast(Whisper.MessageBodyTooLongToast),
|
|
|
|
|
onChooseAttachment: this.onChooseAttachment.bind(this),
|
2021-06-08 14:59:38 +00:00
|
|
|
|
getQuotedMessage: () => model.get('quotedMessageId'),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
clearQuotedMessage: () => this.setQuoteMessage(null),
|
|
|
|
|
micCellEl,
|
2020-10-06 17:06:34 +00:00
|
|
|
|
onAccept: () => {
|
2021-06-01 23:30:25 +00:00
|
|
|
|
this.syncMessageRequestResponse(
|
|
|
|
|
'onAccept',
|
|
|
|
|
model,
|
|
|
|
|
messageRequestEnum.ACCEPT
|
|
|
|
|
);
|
2020-10-06 17:06:34 +00:00
|
|
|
|
},
|
|
|
|
|
onBlock: () => {
|
2021-06-01 23:30:25 +00:00
|
|
|
|
this.syncMessageRequestResponse(
|
|
|
|
|
'onBlock',
|
|
|
|
|
model,
|
|
|
|
|
messageRequestEnum.BLOCK
|
|
|
|
|
);
|
2020-10-06 17:06:34 +00:00
|
|
|
|
},
|
|
|
|
|
onUnblock: () => {
|
2021-06-01 23:30:25 +00:00
|
|
|
|
this.syncMessageRequestResponse(
|
|
|
|
|
'onUnblock',
|
|
|
|
|
model,
|
|
|
|
|
messageRequestEnum.ACCEPT
|
|
|
|
|
);
|
2020-10-06 17:06:34 +00:00
|
|
|
|
},
|
|
|
|
|
onDelete: () => {
|
2021-06-01 23:30:25 +00:00
|
|
|
|
this.syncMessageRequestResponse(
|
|
|
|
|
'onDelete',
|
|
|
|
|
model,
|
|
|
|
|
messageRequestEnum.DELETE
|
|
|
|
|
);
|
2020-10-06 17:06:34 +00:00
|
|
|
|
},
|
2021-05-27 20:17:05 +00:00
|
|
|
|
onBlockAndReportSpam: () => {
|
2021-06-01 23:30:25 +00:00
|
|
|
|
this.blockAndReportSpam(model);
|
2020-10-06 17:06:34 +00:00
|
|
|
|
},
|
2020-12-01 16:42:35 +00:00
|
|
|
|
onStartGroupMigration: () => this.startMigrationToGV2(),
|
2021-01-29 22:16:48 +00:00
|
|
|
|
onCancelJoinRequest: async () => {
|
|
|
|
|
await window.showConfirmationDialog({
|
|
|
|
|
message: window.i18n(
|
|
|
|
|
'GroupV2--join--cancel-request-to-join--confirmation'
|
|
|
|
|
),
|
|
|
|
|
okText: window.i18n('GroupV2--join--cancel-request-to-join--yes'),
|
|
|
|
|
cancelText: window.i18n('GroupV2--join--cancel-request-to-join--no'),
|
|
|
|
|
resolve: () => {
|
|
|
|
|
this.longRunningTaskWrapper({
|
|
|
|
|
name: 'onCancelJoinRequest',
|
2021-06-08 14:59:38 +00:00
|
|
|
|
task: async () => model.cancelJoinRequest(),
|
2021-01-29 22:16:48 +00:00
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
},
|
2021-06-25 16:08:16 +00:00
|
|
|
|
|
|
|
|
|
onAddAttachment: this.onChooseAttachment.bind(this),
|
|
|
|
|
onClickAttachment: this.onClickAttachment.bind(this),
|
|
|
|
|
onCloseAttachment: this.onCloseAttachment.bind(this),
|
|
|
|
|
onClearAttachments: this.clearAttachments.bind(this),
|
|
|
|
|
onSelectMediaQuality: (isHQ: boolean) => {
|
|
|
|
|
window.reduxActions.composer.setMediaQualitySetting(isHQ);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onClickQuotedMessage: (id?: string) => this.scrollToMessage(id),
|
|
|
|
|
|
|
|
|
|
onCloseLinkPreview: () => {
|
|
|
|
|
this.disableLinkPreviews = true;
|
|
|
|
|
this.removeLinkPreview();
|
|
|
|
|
},
|
2021-07-20 20:18:35 +00:00
|
|
|
|
|
|
|
|
|
openConversation: this.openConversation.bind(this),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.compositionAreaView = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'composition-area-wrapper',
|
|
|
|
|
JSX: window.Signal.State.Roots.createCompositionArea(
|
|
|
|
|
window.reduxStore,
|
|
|
|
|
props
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Finally, add it to the DOM
|
2021-07-20 20:18:35 +00:00
|
|
|
|
this.$('.CompositionArea__placeholder').append(this.compositionAreaView.el);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2020-12-01 16:42:35 +00:00
|
|
|
|
async longRunningTaskWrapper<T>({
|
2020-10-06 17:06:34 +00:00
|
|
|
|
name,
|
|
|
|
|
task,
|
|
|
|
|
}: {
|
|
|
|
|
name: string;
|
2020-12-01 16:42:35 +00:00
|
|
|
|
task: () => Promise<T>;
|
|
|
|
|
}): Promise<T> {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
const idForLogging = model.idForLogging();
|
2021-01-29 22:16:48 +00:00
|
|
|
|
return window.Signal.Util.longRunningTaskWrapper({
|
|
|
|
|
name,
|
|
|
|
|
idForLogging,
|
|
|
|
|
task,
|
|
|
|
|
});
|
2020-10-06 17:06:34 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-03-24 22:06:12 +00:00
|
|
|
|
getMessageActions() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const reactToMessage = (
|
|
|
|
|
messageId: string,
|
|
|
|
|
reaction: { emoji: string; remove: boolean }
|
|
|
|
|
) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.sendReactionMessage(messageId, reaction);
|
|
|
|
|
};
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const replyToMessage = (messageId: string) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.setQuoteMessage(messageId);
|
|
|
|
|
};
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const retrySend = (messageId: string) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.retrySend(messageId);
|
|
|
|
|
};
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const deleteMessage = (messageId: string) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.deleteMessage(messageId);
|
|
|
|
|
};
|
2020-09-29 22:55:56 +00:00
|
|
|
|
const deleteMessageForEveryone = (messageId: string) => {
|
|
|
|
|
this.deleteMessageForEveryone(messageId);
|
|
|
|
|
};
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const showMessageDetail = (messageId: string) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.showMessageDetail(messageId);
|
|
|
|
|
};
|
2020-11-11 17:36:05 +00:00
|
|
|
|
const showContactModal = (contactId: string) => {
|
|
|
|
|
this.showContactModal(contactId);
|
|
|
|
|
};
|
2021-08-16 21:35:54 +00:00
|
|
|
|
const openConversation = (conversationId: string, messageId: string) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.openConversation(conversationId, messageId);
|
|
|
|
|
};
|
2021-08-16 21:35:54 +00:00
|
|
|
|
const showContactDetail = (options: {
|
2021-08-20 01:56:39 +00:00
|
|
|
|
contact: EmbeddedContactType;
|
2021-08-16 21:35:54 +00:00
|
|
|
|
signalAccount?: string;
|
|
|
|
|
}) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.showContactDetail(options);
|
|
|
|
|
};
|
2021-08-16 21:35:54 +00:00
|
|
|
|
const kickOffAttachmentDownload = async (
|
|
|
|
|
options: Readonly<{ messageId: string }>
|
|
|
|
|
) => {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
const message = window.MessageController.getById(options.messageId);
|
|
|
|
|
if (!message) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`kickOffAttachmentDownload: Message ${options.messageId} missing!`
|
|
|
|
|
);
|
2021-01-29 22:58:28 +00:00
|
|
|
|
}
|
|
|
|
|
await message.queueAttachmentDownloads();
|
|
|
|
|
};
|
2021-03-22 18:51:53 +00:00
|
|
|
|
const markAttachmentAsCorrupted = (options: AttachmentOptions) => {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
const message = window.MessageController.getById(options.messageId);
|
|
|
|
|
if (!message) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`markAttachmentAsCorrupted: Message ${options.messageId} missing!`
|
|
|
|
|
);
|
|
|
|
|
}
|
2021-03-22 18:51:53 +00:00
|
|
|
|
message.markAttachmentAsCorrupted(options.attachment);
|
|
|
|
|
};
|
2021-08-12 18:15:55 +00:00
|
|
|
|
const onMarkViewed = (messageId: string): void => {
|
|
|
|
|
const message = window.MessageController.getById(messageId);
|
|
|
|
|
if (!message) {
|
|
|
|
|
throw new Error(`onMarkViewed: Message ${messageId} missing!`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (message.get('readStatus') === ReadStatus.Viewed) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-17 15:43:26 +00:00
|
|
|
|
const senderE164 = message.get('source');
|
|
|
|
|
const senderUuid = message.get('sourceUuid');
|
|
|
|
|
const timestamp = message.get('sent_at');
|
|
|
|
|
|
2021-08-12 18:15:55 +00:00
|
|
|
|
message.set(markViewed(message.attributes, Date.now()));
|
2021-08-17 15:43:26 +00:00
|
|
|
|
|
|
|
|
|
viewedReceiptsJobQueue.add({
|
|
|
|
|
viewedReceipt: {
|
|
|
|
|
messageId,
|
|
|
|
|
senderE164,
|
|
|
|
|
senderUuid,
|
|
|
|
|
timestamp,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2021-08-12 18:15:55 +00:00
|
|
|
|
viewSyncJobQueue.add({
|
|
|
|
|
viewSyncs: [
|
|
|
|
|
{
|
|
|
|
|
messageId,
|
2021-08-17 15:43:26 +00:00
|
|
|
|
senderE164,
|
|
|
|
|
senderUuid,
|
|
|
|
|
timestamp,
|
2021-08-12 18:15:55 +00:00
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
};
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const showVisualAttachment = (options: {
|
2021-07-14 23:39:52 +00:00
|
|
|
|
attachment: AttachmentType;
|
2021-06-08 14:59:38 +00:00
|
|
|
|
messageId: string;
|
|
|
|
|
showSingle?: boolean;
|
|
|
|
|
}) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.showLightbox(options);
|
|
|
|
|
};
|
2021-08-16 21:35:54 +00:00
|
|
|
|
const downloadAttachment = (options: {
|
|
|
|
|
attachment: AttachmentType;
|
|
|
|
|
timestamp: number;
|
|
|
|
|
isDangerous: boolean;
|
|
|
|
|
}) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.downloadAttachment(options);
|
|
|
|
|
};
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const displayTapToViewMessage = (messageId: string) =>
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.displayTapToViewMessage(messageId);
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const showIdentity = (conversationId: string) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.showSafetyNumber(conversationId);
|
|
|
|
|
};
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const openLink = (url: string) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.navigateTo(url);
|
|
|
|
|
};
|
|
|
|
|
const downloadNewVersion = () => {
|
|
|
|
|
this.downloadNewVersion();
|
|
|
|
|
};
|
2021-06-16 00:44:14 +00:00
|
|
|
|
const sendAnyway = (contactId: string, messageId: string) => {
|
2021-07-16 21:27:48 +00:00
|
|
|
|
this.forceSend({ contactId, messageId });
|
2021-06-16 00:44:14 +00:00
|
|
|
|
};
|
|
|
|
|
const showSafetyNumber = (contactId: string) => {
|
|
|
|
|
this.showSafetyNumber(contactId);
|
|
|
|
|
};
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const showExpiredIncomingTapToViewToast = () => {
|
2021-08-12 16:20:22 +00:00
|
|
|
|
window.log.info(
|
|
|
|
|
'Showing expired tap-to-view toast for an incoming message'
|
|
|
|
|
);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.showToast(Whisper.TapToViewExpiredIncomingToast);
|
|
|
|
|
};
|
|
|
|
|
const showExpiredOutgoingTapToViewToast = () => {
|
2021-08-12 16:20:22 +00:00
|
|
|
|
window.log.info(
|
|
|
|
|
'Showing expired tap-to-view toast for an outgoing message'
|
|
|
|
|
);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.showToast(Whisper.TapToViewExpiredOutgoingToast);
|
|
|
|
|
};
|
2021-04-27 22:35:35 +00:00
|
|
|
|
const showForwardMessageModal = this.showForwardMessageModal.bind(this);
|
2021-03-24 22:06:12 +00:00
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
deleteMessage,
|
|
|
|
|
deleteMessageForEveryone,
|
|
|
|
|
displayTapToViewMessage,
|
|
|
|
|
downloadAttachment,
|
|
|
|
|
downloadNewVersion,
|
|
|
|
|
kickOffAttachmentDownload,
|
|
|
|
|
markAttachmentAsCorrupted,
|
2021-08-12 18:15:55 +00:00
|
|
|
|
markViewed: onMarkViewed,
|
2021-03-24 22:06:12 +00:00
|
|
|
|
openConversation,
|
|
|
|
|
openLink,
|
|
|
|
|
reactToMessage,
|
|
|
|
|
replyToMessage,
|
|
|
|
|
retrySend,
|
2021-06-16 00:44:14 +00:00
|
|
|
|
sendAnyway,
|
2021-03-24 22:06:12 +00:00
|
|
|
|
showContactDetail,
|
|
|
|
|
showContactModal,
|
2021-06-16 00:44:14 +00:00
|
|
|
|
showSafetyNumber,
|
2021-03-24 22:06:12 +00:00
|
|
|
|
showExpiredIncomingTapToViewToast,
|
|
|
|
|
showExpiredOutgoingTapToViewToast,
|
2021-04-27 22:35:35 +00:00
|
|
|
|
showForwardMessageModal,
|
2021-03-24 22:06:12 +00:00
|
|
|
|
showIdentity,
|
|
|
|
|
showMessageDetail,
|
|
|
|
|
showVisualAttachment,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setupTimeline() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
const { id } = model;
|
2021-03-24 22:06:12 +00:00
|
|
|
|
|
2021-07-09 19:36:10 +00:00
|
|
|
|
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
2021-04-21 16:31:12 +00:00
|
|
|
|
|
2021-02-18 16:40:26 +00:00
|
|
|
|
const contactSupport = () => {
|
|
|
|
|
const baseUrl =
|
|
|
|
|
'https://support.signal.org/hc/LOCALE/requests/new?desktop&chat_refreshed';
|
|
|
|
|
const locale = window.getLocale();
|
|
|
|
|
const supportLocale = window.Signal.Util.mapToSupportLocale(locale);
|
|
|
|
|
const url = baseUrl.replace('LOCALE', supportLocale);
|
|
|
|
|
|
|
|
|
|
this.navigateTo(url);
|
|
|
|
|
};
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2021-07-30 20:30:59 +00:00
|
|
|
|
const learnMoreAboutDeliveryIssue = () => {
|
|
|
|
|
this.navigateTo('https://support.signal.org/hc/articles/4404859745690');
|
|
|
|
|
};
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
const scrollToQuotedMessage = async (
|
|
|
|
|
options: Readonly<{
|
|
|
|
|
authorId: string;
|
|
|
|
|
sentAt: number;
|
|
|
|
|
}>
|
|
|
|
|
) => {
|
2020-12-11 18:06:04 +00:00
|
|
|
|
const { authorId, sentAt } = options;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const conversationId = model.id;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const messages = await getMessagesBySentAt(sentAt, {
|
|
|
|
|
MessageCollection: Whisper.MessageCollection,
|
|
|
|
|
});
|
2021-08-16 21:35:54 +00:00
|
|
|
|
const message = messages.find(item =>
|
|
|
|
|
Boolean(
|
2020-09-24 20:57:54 +00:00
|
|
|
|
item.get('conversationId') === conversationId &&
|
2021-08-16 21:35:54 +00:00
|
|
|
|
authorId &&
|
|
|
|
|
item.getContactId() === authorId
|
|
|
|
|
)
|
2020-09-24 20:57:54 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!message) {
|
|
|
|
|
this.showToast(Whisper.OriginalNotFoundToast);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.scrollToMessage(message.id);
|
|
|
|
|
};
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const loadOlderMessages = async (oldestMessageId: string) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const {
|
|
|
|
|
messagesAdded,
|
|
|
|
|
setMessagesLoading,
|
2020-12-04 20:41:40 +00:00
|
|
|
|
repairOldestMessage,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
} = window.reduxActions.conversations;
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const conversationId = model.id;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
setMessagesLoading(conversationId, true);
|
|
|
|
|
const finish = this.setInProgressFetch();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const message = await getMessageById(oldestMessageId, {
|
|
|
|
|
Message: Whisper.Message,
|
|
|
|
|
});
|
|
|
|
|
if (!message) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`loadOlderMessages: failed to load message ${oldestMessageId}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const receivedAt = message.get('received_at');
|
2021-01-13 16:32:18 +00:00
|
|
|
|
const sentAt = message.get('sent_at');
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const models = await getOlderMessagesByConversation(conversationId, {
|
|
|
|
|
receivedAt,
|
2021-01-13 16:32:18 +00:00
|
|
|
|
sentAt,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
messageId: oldestMessageId,
|
2021-03-12 18:06:31 +00:00
|
|
|
|
limit: 30,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
MessageCollection: Whisper.MessageCollection,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (models.length < 1) {
|
|
|
|
|
window.log.warn(
|
|
|
|
|
'loadOlderMessages: requested, but loaded no messages'
|
|
|
|
|
);
|
2020-12-04 20:41:40 +00:00
|
|
|
|
repairOldestMessage(conversationId);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cleaned = await this.cleanModels(models);
|
|
|
|
|
const isNewMessage = false;
|
|
|
|
|
messagesAdded(
|
|
|
|
|
id,
|
2021-06-17 17:15:10 +00:00
|
|
|
|
cleaned.map((messageModel: MessageModel) => ({
|
|
|
|
|
...messageModel.attributes,
|
|
|
|
|
})),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
isNewMessage,
|
|
|
|
|
window.isActive()
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setMessagesLoading(conversationId, true);
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
finish();
|
|
|
|
|
}
|
|
|
|
|
};
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const loadNewerMessages = async (newestMessageId: string) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const {
|
|
|
|
|
messagesAdded,
|
|
|
|
|
setMessagesLoading,
|
2020-12-04 20:41:40 +00:00
|
|
|
|
repairNewestMessage,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
} = window.reduxActions.conversations;
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const conversationId = model.id;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
setMessagesLoading(conversationId, true);
|
|
|
|
|
const finish = this.setInProgressFetch();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const message = await getMessageById(newestMessageId, {
|
|
|
|
|
Message: Whisper.Message,
|
|
|
|
|
});
|
|
|
|
|
if (!message) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`loadNewerMessages: failed to load message ${newestMessageId}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const receivedAt = message.get('received_at');
|
2021-01-13 16:32:18 +00:00
|
|
|
|
const sentAt = message.get('sent_at');
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const models = await getNewerMessagesByConversation(model.id, {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
receivedAt,
|
2021-01-13 16:32:18 +00:00
|
|
|
|
sentAt,
|
2021-03-12 18:06:31 +00:00
|
|
|
|
limit: 30,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
MessageCollection: Whisper.MessageCollection,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (models.length < 1) {
|
|
|
|
|
window.log.warn(
|
|
|
|
|
'loadNewerMessages: requested, but loaded no messages'
|
|
|
|
|
);
|
2020-12-04 20:41:40 +00:00
|
|
|
|
repairNewestMessage(conversationId);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cleaned = await this.cleanModels(models);
|
|
|
|
|
const isNewMessage = false;
|
|
|
|
|
messagesAdded(
|
|
|
|
|
id,
|
2021-06-17 17:15:10 +00:00
|
|
|
|
cleaned.map((messageModel: MessageModel) => ({
|
|
|
|
|
...messageModel.attributes,
|
|
|
|
|
})),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
isNewMessage,
|
|
|
|
|
window.isActive()
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setMessagesLoading(conversationId, false);
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
finish();
|
|
|
|
|
}
|
|
|
|
|
};
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const markMessageRead = async (messageId: string) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!window.isActive()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const message = await getMessageById(messageId, {
|
|
|
|
|
Message: Whisper.Message,
|
|
|
|
|
});
|
|
|
|
|
if (!message) {
|
|
|
|
|
throw new Error(`markMessageRead: failed to load message ${messageId}`);
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
await model.markRead(message.get('received_at'));
|
2020-09-24 20:57:54 +00:00
|
|
|
|
};
|
|
|
|
|
|
2021-06-01 23:30:25 +00:00
|
|
|
|
const createMessageRequestResponseHandler = (
|
|
|
|
|
name: string,
|
|
|
|
|
enumValue: number
|
|
|
|
|
): ((conversationId: string) => void) => conversationId => {
|
|
|
|
|
const conversation = window.ConversationController.get(conversationId);
|
|
|
|
|
if (!conversation) {
|
|
|
|
|
assert(
|
|
|
|
|
false,
|
|
|
|
|
`Expected a conversation to be found in ${name}. Doing nothing`
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.syncMessageRequestResponse(name, conversation, enumValue);
|
|
|
|
|
};
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.timelineView = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'timeline-wrapper',
|
|
|
|
|
JSX: window.Signal.State.Roots.createTimeline(window.reduxStore, {
|
|
|
|
|
id,
|
|
|
|
|
|
2021-03-24 22:06:12 +00:00
|
|
|
|
...this.getMessageActions(),
|
|
|
|
|
|
2021-06-01 23:30:25 +00:00
|
|
|
|
acknowledgeGroupMemberNameCollisions: (
|
|
|
|
|
groupNameCollisions: Readonly<GroupNameCollisionsWithIdsByTitle>
|
|
|
|
|
): void => {
|
|
|
|
|
model.acknowledgeGroupMemberNameCollisions(groupNameCollisions);
|
|
|
|
|
},
|
2021-02-18 16:40:26 +00:00
|
|
|
|
contactSupport,
|
2021-07-30 20:30:59 +00:00
|
|
|
|
learnMoreAboutDeliveryIssue,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
loadNewerMessages,
|
|
|
|
|
loadNewestMessages: this.loadNewestMessages.bind(this),
|
|
|
|
|
loadAndScroll: this.loadAndScroll.bind(this),
|
|
|
|
|
loadOlderMessages,
|
|
|
|
|
markMessageRead,
|
2021-06-01 23:30:25 +00:00
|
|
|
|
onBlock: createMessageRequestResponseHandler(
|
|
|
|
|
'onBlock',
|
|
|
|
|
messageRequestEnum.BLOCK
|
|
|
|
|
),
|
|
|
|
|
onBlockAndReportSpam: (conversationId: string) => {
|
|
|
|
|
const conversation = window.ConversationController.get(
|
|
|
|
|
conversationId
|
2021-04-21 16:31:12 +00:00
|
|
|
|
);
|
2021-06-01 23:30:25 +00:00
|
|
|
|
if (!conversation) {
|
|
|
|
|
assert(
|
|
|
|
|
false,
|
|
|
|
|
'Expected a conversation to be found in onBlockAndReportSpam. Doing nothing'
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.blockAndReportSpam(conversation);
|
2021-04-21 16:31:12 +00:00
|
|
|
|
},
|
2021-06-01 23:30:25 +00:00
|
|
|
|
onDelete: createMessageRequestResponseHandler(
|
|
|
|
|
'onDelete',
|
|
|
|
|
messageRequestEnum.DELETE
|
|
|
|
|
),
|
|
|
|
|
onUnblock: createMessageRequestResponseHandler(
|
|
|
|
|
'onUnblock',
|
|
|
|
|
messageRequestEnum.ACCEPT
|
|
|
|
|
),
|
2021-04-21 16:31:12 +00:00
|
|
|
|
onShowContactModal: this.showContactModal.bind(this),
|
2021-06-01 23:30:25 +00:00
|
|
|
|
removeMember: (conversationId: string) => {
|
|
|
|
|
this.longRunningTaskWrapper({
|
|
|
|
|
name: 'removeMember',
|
|
|
|
|
task: () => model.removeFromGroupV2(conversationId),
|
|
|
|
|
});
|
|
|
|
|
},
|
2020-09-24 20:57:54 +00:00
|
|
|
|
scrollToQuotedMessage,
|
2021-04-30 19:40:25 +00:00
|
|
|
|
unblurAvatar: () => {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.unblurAvatar();
|
2021-04-30 19:40:25 +00:00
|
|
|
|
},
|
2021-06-08 14:59:38 +00:00
|
|
|
|
updateSharedGroups: model.throttledUpdateSharedGroups,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.$('.timeline-placeholder').append(this.timelineView.el);
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
showToast(
|
|
|
|
|
ToastView: typeof window.Whisper.ToastView,
|
2021-08-16 21:35:54 +00:00
|
|
|
|
options: WhatIsThis,
|
2021-06-08 14:59:38 +00:00
|
|
|
|
element: Element
|
|
|
|
|
) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const toast = new ToastView(options);
|
|
|
|
|
|
2021-04-27 22:35:35 +00:00
|
|
|
|
if (element) {
|
|
|
|
|
toast.$el.appendTo(element);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
} else {
|
2021-04-27 22:35:35 +00:00
|
|
|
|
const lightboxEl = $('.module-lightbox');
|
|
|
|
|
if (lightboxEl.length > 0) {
|
|
|
|
|
toast.$el.appendTo(lightboxEl);
|
|
|
|
|
} else {
|
|
|
|
|
toast.$el.appendTo(this.$el);
|
|
|
|
|
}
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toast.render();
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-16 00:44:14 +00:00
|
|
|
|
async cleanModels(
|
|
|
|
|
collection: MessageModelCollectionType | Array<MessageModel>
|
|
|
|
|
): Promise<Array<MessageModel>> {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const result = collection
|
2021-08-16 21:35:54 +00:00
|
|
|
|
.filter((message: MessageModel) => Boolean(message.id))
|
|
|
|
|
.map((message: MessageModel) =>
|
2020-09-24 20:57:54 +00:00
|
|
|
|
window.MessageController.register(message.id, message)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const eliminated = collection.length - result.length;
|
|
|
|
|
if (eliminated > 0) {
|
|
|
|
|
window.log.warn(
|
|
|
|
|
`cleanModels: Eliminated ${eliminated} messages without an id`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let max = result.length, i = 0; i < max; i += 1) {
|
|
|
|
|
const message = result[i];
|
|
|
|
|
const { attributes } = message;
|
|
|
|
|
const { schemaVersion } = attributes;
|
|
|
|
|
|
|
|
|
|
if (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
|
|
|
|
|
const upgradedMessage = await upgradeMessageSchema(attributes);
|
|
|
|
|
message.set(upgradedMessage);
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
2021-07-19 20:45:18 +00:00
|
|
|
|
await window.Signal.Data.saveMessage(upgradedMessage);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
async scrollToMessage(messageId: string) {
|
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const message = await getMessageById(messageId, {
|
|
|
|
|
Message: Whisper.Message,
|
|
|
|
|
});
|
|
|
|
|
if (!message) {
|
|
|
|
|
throw new Error(`scrollToMessage: failed to load message ${messageId}`);
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-23 22:57:39 +00:00
|
|
|
|
const state = window.reduxStore.getState();
|
|
|
|
|
|
|
|
|
|
let isInMemory = true;
|
|
|
|
|
|
|
|
|
|
if (!window.MessageController.getById(messageId)) {
|
|
|
|
|
isInMemory = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Message might be in memory, but not in the redux anymore because
|
|
|
|
|
// we call `messageReset()` in `loadAndScroll()`.
|
|
|
|
|
const messagesByConversation = getMessagesByConversation(state)[model.id];
|
|
|
|
|
if (!messagesByConversation?.messageIds.includes(messageId)) {
|
|
|
|
|
isInMemory = false;
|
|
|
|
|
}
|
2021-06-16 00:44:14 +00:00
|
|
|
|
|
|
|
|
|
if (isInMemory) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const { scrollToMessage } = window.reduxActions.conversations;
|
2021-06-08 14:59:38 +00:00
|
|
|
|
scrollToMessage(model.id, messageId);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.loadAndScroll(messageId);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setInProgressFetch() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
let resolvePromise: (value?: unknown) => void;
|
|
|
|
|
model.inProgressFetch = new Promise(resolve => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
resolvePromise = resolve;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const finish = () => {
|
|
|
|
|
resolvePromise();
|
2021-06-01 20:45:43 +00:00
|
|
|
|
this.model.inProgressFetch = null;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return finish;
|
|
|
|
|
},
|
|
|
|
|
|
2021-04-30 22:59:37 +00:00
|
|
|
|
async loadAndScroll(
|
|
|
|
|
messageId: string,
|
|
|
|
|
options?: { disableScroll?: boolean }
|
|
|
|
|
) {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const {
|
|
|
|
|
messagesReset,
|
|
|
|
|
setMessagesLoading,
|
|
|
|
|
} = window.reduxActions.conversations;
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const conversationId = model.id;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
setMessagesLoading(conversationId, true);
|
|
|
|
|
const finish = this.setInProgressFetch();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const message = await getMessageById(messageId, {
|
|
|
|
|
Message: Whisper.Message,
|
|
|
|
|
});
|
|
|
|
|
if (!message) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`loadMoreAndScroll: failed to load message ${messageId}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const receivedAt = message.get('received_at');
|
2021-01-13 16:32:18 +00:00
|
|
|
|
const sentAt = message.get('sent_at');
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const older = await getOlderMessagesByConversation(conversationId, {
|
2021-03-12 18:06:31 +00:00
|
|
|
|
limit: 30,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
receivedAt,
|
2021-01-13 16:32:18 +00:00
|
|
|
|
sentAt,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
messageId,
|
|
|
|
|
MessageCollection: Whisper.MessageCollection,
|
|
|
|
|
});
|
|
|
|
|
const newer = await getNewerMessagesByConversation(conversationId, {
|
2021-03-12 18:06:31 +00:00
|
|
|
|
limit: 30,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
receivedAt,
|
2021-01-13 16:32:18 +00:00
|
|
|
|
sentAt,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
MessageCollection: Whisper.MessageCollection,
|
|
|
|
|
});
|
|
|
|
|
const metrics = await getMessageMetricsForConversation(conversationId);
|
|
|
|
|
|
|
|
|
|
const all = [...older.models, message, ...newer.models];
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const cleaned: Array<MessageModel> = await this.cleanModels(all);
|
2021-04-30 22:59:37 +00:00
|
|
|
|
const scrollToMessageId =
|
|
|
|
|
options && options.disableScroll ? undefined : messageId;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
messagesReset(
|
|
|
|
|
conversationId,
|
2021-06-17 17:15:10 +00:00
|
|
|
|
cleaned.map((messageModel: MessageModel) => ({
|
|
|
|
|
...messageModel.attributes,
|
|
|
|
|
})),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
metrics,
|
|
|
|
|
scrollToMessageId
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setMessagesLoading(conversationId, false);
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
finish();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-04-30 22:59:37 +00:00
|
|
|
|
async loadNewestMessages(
|
|
|
|
|
newestMessageId: string | undefined,
|
|
|
|
|
setFocus: boolean | undefined
|
|
|
|
|
): Promise<void> {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const {
|
|
|
|
|
messagesReset,
|
|
|
|
|
setMessagesLoading,
|
|
|
|
|
} = window.reduxActions.conversations;
|
2021-04-30 22:59:37 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
|
|
|
|
const conversationId = model.id;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
setMessagesLoading(conversationId, true);
|
|
|
|
|
const finish = this.setInProgressFetch();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let scrollToLatestUnread = true;
|
|
|
|
|
|
|
|
|
|
if (newestMessageId) {
|
|
|
|
|
const newestInMemoryMessage = await getMessageById(newestMessageId, {
|
|
|
|
|
Message: Whisper.Message,
|
|
|
|
|
});
|
2021-07-29 14:29:07 +00:00
|
|
|
|
if (newestInMemoryMessage) {
|
|
|
|
|
// If newest in-memory message is unread, scrolling down would mean going to
|
|
|
|
|
// the very bottom, not the oldest unread.
|
|
|
|
|
if (isMessageUnread(newestInMemoryMessage.attributes)) {
|
|
|
|
|
scrollToLatestUnread = false;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
window.log.warn(
|
|
|
|
|
`loadNewestMessages: did not find message ${newestMessageId}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const metrics = await getMessageMetricsForConversation(conversationId);
|
|
|
|
|
|
2021-04-30 22:59:37 +00:00
|
|
|
|
// If this is a message request that has not yet been accepted, we always show the
|
|
|
|
|
// oldest messages, to ensure that the ConversationHero is shown. We don't want to
|
|
|
|
|
// scroll directly to the oldest message, because that could scroll the hero off
|
|
|
|
|
// the screen.
|
|
|
|
|
if (!newestMessageId && !model.getAccepted() && metrics.oldest) {
|
|
|
|
|
this.loadAndScroll(metrics.oldest.id, { disableScroll: true });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (scrollToLatestUnread && metrics.oldestUnread) {
|
|
|
|
|
this.loadAndScroll(metrics.oldestUnread.id, {
|
|
|
|
|
disableScroll: !setFocus,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const messages = await getOlderMessagesByConversation(conversationId, {
|
2021-03-12 18:06:31 +00:00
|
|
|
|
limit: 30,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
MessageCollection: Whisper.MessageCollection,
|
|
|
|
|
});
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const cleaned: Array<MessageModel> = await this.cleanModels(messages);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const scrollToMessageId =
|
|
|
|
|
setFocus && metrics.newest ? metrics.newest.id : undefined;
|
|
|
|
|
|
|
|
|
|
// Because our `getOlderMessages` fetch above didn't specify a receivedAt, we got
|
2021-03-12 18:06:31 +00:00
|
|
|
|
// the most recent 30 messages in the conversation. If it has a conflict with
|
2020-09-24 20:57:54 +00:00
|
|
|
|
// metrics, fetched a bit before, that's likely a race condition. So we tell our
|
|
|
|
|
// reducer to trust the message set we just fetched for determining if we have
|
|
|
|
|
// the newest message loaded.
|
|
|
|
|
const unboundedFetch = true;
|
|
|
|
|
messagesReset(
|
|
|
|
|
conversationId,
|
2021-06-17 17:15:10 +00:00
|
|
|
|
cleaned.map((messageModel: MessageModel) => ({
|
|
|
|
|
...messageModel.attributes,
|
|
|
|
|
})),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
metrics,
|
|
|
|
|
scrollToMessageId,
|
|
|
|
|
unboundedFetch
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setMessagesLoading(conversationId, false);
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
finish();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2020-12-01 16:42:35 +00:00
|
|
|
|
async startMigrationToGV2(): Promise<void> {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
const logId = model.idForLogging();
|
2020-12-01 16:42:35 +00:00
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
if (!isGroupV1(model.attributes)) {
|
2020-12-01 16:42:35 +00:00
|
|
|
|
throw new Error(
|
|
|
|
|
`startMigrationToGV2/${logId}: Cannot start, not a GroupV1 group`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onClose = () => {
|
|
|
|
|
if (this.migrationDialog) {
|
|
|
|
|
this.migrationDialog.remove();
|
|
|
|
|
this.migrationDialog = undefined;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
onClose();
|
|
|
|
|
|
|
|
|
|
const migrate = () => {
|
|
|
|
|
onClose();
|
|
|
|
|
|
|
|
|
|
this.longRunningTaskWrapper({
|
|
|
|
|
name: 'initiateMigrationToGroupV2',
|
2021-06-08 14:59:38 +00:00
|
|
|
|
task: () => window.Signal.Groups.initiateMigrationToGroupV2(model),
|
2020-12-01 16:42:35 +00:00
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2020-12-01 23:45:39 +00:00
|
|
|
|
// Note: this call will throw if, after generating member lists, we are no longer a
|
|
|
|
|
// member or are in the pending member list.
|
2020-12-01 16:42:35 +00:00
|
|
|
|
const {
|
|
|
|
|
droppedGV2MemberIds,
|
|
|
|
|
pendingMembersV2,
|
|
|
|
|
} = await this.longRunningTaskWrapper({
|
|
|
|
|
name: 'getGroupMigrationMembers',
|
2021-06-08 14:59:38 +00:00
|
|
|
|
task: () => window.Signal.Groups.getGroupMigrationMembers(model),
|
2020-12-01 16:42:35 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const invitedMemberIds = pendingMembersV2.map(
|
|
|
|
|
(item: GroupV2PendingMemberType) => item.conversationId
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.migrationDialog = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'group-v1-migration-wrapper',
|
|
|
|
|
JSX: window.Signal.State.Roots.createGroupV1MigrationModal(
|
|
|
|
|
window.reduxStore,
|
|
|
|
|
{
|
2020-12-01 23:45:39 +00:00
|
|
|
|
areWeInvited: false,
|
2020-12-01 16:42:35 +00:00
|
|
|
|
droppedMemberIds: droppedGV2MemberIds,
|
|
|
|
|
hasMigrated: false,
|
|
|
|
|
invitedMemberIds,
|
|
|
|
|
migrate,
|
|
|
|
|
onClose,
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
});
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onChooseAttachment() {
|
|
|
|
|
this.$('input.file-input').click();
|
|
|
|
|
},
|
|
|
|
|
async onChoseAttachment() {
|
|
|
|
|
const fileField = this.$('input.file-input');
|
|
|
|
|
const files = fileField.prop('files');
|
|
|
|
|
|
|
|
|
|
for (let i = 0, max = files.length; i < max; i += 1) {
|
|
|
|
|
const file = files[i];
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
await this.maybeAddAttachment(file);
|
|
|
|
|
this.toggleMicrophone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fileField.val(null);
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
unload(reason: string) {
|
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
window.log.info(
|
|
|
|
|
'unloading conversation',
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.idForLogging(),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
'due to:',
|
|
|
|
|
reason
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const { conversationUnloaded } = window.reduxActions.conversations;
|
|
|
|
|
if (conversationUnloaded) {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
conversationUnloaded(model.id);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.model.get('draftChanged')) {
|
|
|
|
|
if (this.model.hasDraft()) {
|
|
|
|
|
this.model.set({
|
|
|
|
|
draftChanged: false,
|
|
|
|
|
draftTimestamp: Date.now(),
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
this.model.set({
|
|
|
|
|
draftChanged: false,
|
|
|
|
|
draftTimestamp: null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We don't wait here; we need to take down the view
|
|
|
|
|
this.saveModel();
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.updateLastMessage();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.titleView.remove();
|
|
|
|
|
this.timelineView.remove();
|
|
|
|
|
this.compositionAreaView.remove();
|
|
|
|
|
|
|
|
|
|
if (this.captionEditorView) {
|
|
|
|
|
this.captionEditorView.remove();
|
|
|
|
|
}
|
2020-11-11 17:36:05 +00:00
|
|
|
|
if (this.contactModalView) {
|
|
|
|
|
this.contactModalView.remove();
|
|
|
|
|
}
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (this.stickerButtonView) {
|
|
|
|
|
this.stickerButtonView.remove();
|
|
|
|
|
}
|
|
|
|
|
if (this.stickerPreviewModalView) {
|
|
|
|
|
this.stickerPreviewModalView.remove();
|
|
|
|
|
}
|
|
|
|
|
if (this.captureAudioView) {
|
|
|
|
|
this.captureAudioView.remove();
|
|
|
|
|
}
|
|
|
|
|
if (this.lastSeenIndicator) {
|
|
|
|
|
this.lastSeenIndicator.remove();
|
|
|
|
|
}
|
|
|
|
|
if (this.scrollDownButton) {
|
|
|
|
|
this.scrollDownButton.remove();
|
|
|
|
|
}
|
|
|
|
|
if (this.lightboxView) {
|
|
|
|
|
this.lightboxView.remove();
|
|
|
|
|
}
|
|
|
|
|
if (this.lightboxGalleryView) {
|
|
|
|
|
this.lightboxGalleryView.remove();
|
|
|
|
|
}
|
|
|
|
|
if (this.panels && this.panels.length) {
|
|
|
|
|
for (let i = 0, max = this.panels.length; i < max; i += 1) {
|
|
|
|
|
const panel = this.panels[i];
|
|
|
|
|
panel.remove();
|
|
|
|
|
}
|
2020-10-30 17:52:21 +00:00
|
|
|
|
window.reduxActions.conversations.setSelectedConversationPanelDepth(0);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-08-23 18:40:49 +00:00
|
|
|
|
this.removeLinkPreview();
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.remove();
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
navigateTo(url: string) {
|
|
|
|
|
window.location.href = url;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
downloadNewVersion() {
|
2021-08-16 21:35:54 +00:00
|
|
|
|
this.navigateTo('https://signal.org/download');
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
onDragOver(e: WhatIsThis) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.$el.addClass('dropoff');
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
onDragLeave(e: WhatIsThis) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
async onDrop(e: WhatIsThis) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
const { files } = e.originalEvent.dataTransfer;
|
|
|
|
|
for (let i = 0, max = files.length; i < max; i += 1) {
|
|
|
|
|
const file = files[i];
|
2021-08-20 20:33:06 +00:00
|
|
|
|
this.maybeAddAttachment(file);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
onPaste(e: WhatIsThis) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const { items } = e.originalEvent.clipboardData;
|
|
|
|
|
let imgBlob = null;
|
|
|
|
|
for (let i = 0; i < items.length; i += 1) {
|
|
|
|
|
if (items[i].type.split('/')[0] === 'image') {
|
|
|
|
|
imgBlob = items[i].getAsFile();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (imgBlob !== null) {
|
|
|
|
|
const file = imgBlob;
|
|
|
|
|
this.maybeAddAttachment(file);
|
|
|
|
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-04-20 14:46:09 +00:00
|
|
|
|
syncMessageRequestResponse(
|
|
|
|
|
name: string,
|
2021-06-01 23:30:25 +00:00
|
|
|
|
model: ConversationModel,
|
2021-04-20 14:46:09 +00:00
|
|
|
|
messageRequestType: number
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
return this.longRunningTaskWrapper({
|
|
|
|
|
name,
|
|
|
|
|
task: model.syncMessageRequestResponse.bind(model, messageRequestType),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-01 23:30:25 +00:00
|
|
|
|
blockAndReportSpam(model: ConversationModel): Promise<void> {
|
2021-07-09 19:36:10 +00:00
|
|
|
|
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
2021-05-27 20:17:05 +00:00
|
|
|
|
|
|
|
|
|
return this.longRunningTaskWrapper({
|
|
|
|
|
name: 'blockAndReportSpam',
|
|
|
|
|
task: async () => {
|
|
|
|
|
await Promise.all([
|
|
|
|
|
model.syncMessageRequestResponse(messageRequestEnum.BLOCK),
|
|
|
|
|
addReportSpamJob({
|
|
|
|
|
conversation: model.format(),
|
|
|
|
|
getMessageServerGuidsForSpam:
|
|
|
|
|
window.Signal.Data.getMessageServerGuidsForSpam,
|
|
|
|
|
jobQueue: reportSpamJobQueue,
|
|
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
this.showToast(ReportedSpamAndBlockedToast);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
onClickAttachment(attachment: WhatIsThis) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const getProps = () => ({
|
|
|
|
|
url: attachment.url,
|
|
|
|
|
caption: attachment.caption,
|
|
|
|
|
attachment,
|
|
|
|
|
onSave,
|
|
|
|
|
});
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
const onSave = (caption: WhatIsThis) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.model.set({
|
|
|
|
|
draftAttachments: this.model
|
|
|
|
|
.get('draftAttachments')
|
2021-08-16 21:35:54 +00:00
|
|
|
|
.map((item: WhatIsThis) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (
|
|
|
|
|
(item.path && item.path === attachment.path) ||
|
|
|
|
|
(item.screenshotPath &&
|
|
|
|
|
item.screenshotPath === attachment.screenshotPath)
|
|
|
|
|
) {
|
|
|
|
|
return {
|
|
|
|
|
...attachment,
|
|
|
|
|
caption,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return item;
|
|
|
|
|
}),
|
|
|
|
|
draftChanged: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.captionEditorView.remove();
|
|
|
|
|
window.Signal.Backbone.Views.Lightbox.hide();
|
|
|
|
|
|
|
|
|
|
this.updateAttachmentsView();
|
|
|
|
|
this.saveModel();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.captionEditorView = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'attachment-list-wrapper',
|
|
|
|
|
Component: window.Signal.Components.CaptionEditor,
|
|
|
|
|
props: getProps(),
|
|
|
|
|
onClose: () => window.Signal.Backbone.Views.Lightbox.hide(),
|
|
|
|
|
});
|
|
|
|
|
window.Signal.Backbone.Views.Lightbox.show(this.captionEditorView.el);
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-25 16:08:16 +00:00
|
|
|
|
async deleteDraftAttachment(attachment: AttachmentType) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (attachment.screenshotPath) {
|
|
|
|
|
await deleteDraftFile(attachment.screenshotPath);
|
|
|
|
|
}
|
|
|
|
|
if (attachment.path) {
|
|
|
|
|
await deleteDraftFile(attachment.path);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async saveModel() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
window.Signal.Data.updateConversation(model.attributes);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-06-25 16:08:16 +00:00
|
|
|
|
async addAttachment(attachment: InMemoryAttachmentDraftType) {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const onDisk = await this.writeDraftAttachment(attachment);
|
|
|
|
|
|
2021-08-09 20:06:21 +00:00
|
|
|
|
// Remove any pending attachments that were transcoding
|
|
|
|
|
const draftAttachments = (model.get('draftAttachments') || []).filter(
|
|
|
|
|
draftAttachment =>
|
|
|
|
|
!draftAttachment.pending &&
|
2021-08-20 20:33:06 +00:00
|
|
|
|
draftAttachment.fileName !== attachment.fileName
|
2021-08-09 20:06:21 +00:00
|
|
|
|
);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.model.set({
|
|
|
|
|
draftAttachments: [...draftAttachments, onDisk],
|
|
|
|
|
});
|
|
|
|
|
this.updateAttachmentsView();
|
2021-02-26 15:08:59 +00:00
|
|
|
|
|
|
|
|
|
await this.saveModel();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-06-25 16:08:16 +00:00
|
|
|
|
resolveOnDiskAttachment(
|
|
|
|
|
attachment: OnDiskAttachmentDraftType
|
|
|
|
|
): AttachmentDraftType {
|
|
|
|
|
let url = '';
|
|
|
|
|
if (attachment.screenshotPath) {
|
|
|
|
|
url = getAbsoluteDraftPath(attachment.screenshotPath);
|
|
|
|
|
} else if (attachment.path) {
|
|
|
|
|
url = getAbsoluteDraftPath(attachment.path);
|
|
|
|
|
} else {
|
|
|
|
|
window.log.warn(
|
|
|
|
|
'resolveOnDiskAttachment: Attachment was missing both screenshotPath and path fields'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...attachment,
|
|
|
|
|
url,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
async onCloseAttachment(attachment: Pick<AttachmentType, 'path'>) {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
const draftAttachments = model.get('draftAttachments') || [];
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
this.model.set({
|
2021-02-26 21:06:37 +00:00
|
|
|
|
draftAttachments: window._.reject(
|
2020-09-24 20:57:54 +00:00
|
|
|
|
draftAttachments,
|
|
|
|
|
item => item.path === attachment.path
|
|
|
|
|
),
|
|
|
|
|
draftChanged: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.updateAttachmentsView();
|
|
|
|
|
|
|
|
|
|
await this.saveModel();
|
|
|
|
|
await this.deleteDraftAttachment(attachment);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async clearAttachments() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.voiceNoteAttachment = null;
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const draftAttachments = model.get('draftAttachments') || [];
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.model.set({
|
|
|
|
|
draftAttachments: [],
|
|
|
|
|
draftChanged: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.updateAttachmentsView();
|
|
|
|
|
|
|
|
|
|
// We're fine doing this all at once; at most it should be 32 attachments
|
|
|
|
|
await Promise.all([
|
|
|
|
|
this.saveModel(),
|
|
|
|
|
Promise.all(
|
2021-06-08 14:59:38 +00:00
|
|
|
|
draftAttachments.map(attachment =>
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.deleteDraftAttachment(attachment)
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
]);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
hasFiles() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
const draftAttachments = model.get('draftAttachments') || [];
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return draftAttachments.length > 0;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async getFiles() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (this.voiceNoteAttachment) {
|
|
|
|
|
// We don't need to pull these off disk; we return them as-is
|
|
|
|
|
return [this.voiceNoteAttachment];
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const draftAttachments = model.get('draftAttachments') || [];
|
2021-02-26 21:06:37 +00:00
|
|
|
|
const files = window._.compact(
|
2020-09-24 20:57:54 +00:00
|
|
|
|
await Promise.all(
|
2021-06-08 14:59:38 +00:00
|
|
|
|
draftAttachments.map(attachment => this.getFile(attachment))
|
2020-09-24 20:57:54 +00:00
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
return files;
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
async getFile(attachment?: AttachmentType) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!attachment) {
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = await readDraftData(attachment.path);
|
|
|
|
|
if (data.byteLength !== attachment.size) {
|
|
|
|
|
window.log.error(
|
|
|
|
|
`Attachment size from disk ${data.byteLength} did not match attachment size ${attachment.size}`
|
|
|
|
|
);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
2021-02-26 21:06:37 +00:00
|
|
|
|
...window._.pick(attachment, [
|
2020-09-24 20:57:54 +00:00
|
|
|
|
'contentType',
|
|
|
|
|
'fileName',
|
|
|
|
|
'size',
|
|
|
|
|
'caption',
|
|
|
|
|
'blurHash',
|
|
|
|
|
]),
|
|
|
|
|
data,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
arrayBufferFromFile(file: Blob): Promise<ArrayBuffer> {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const FR = new FileReader();
|
2021-08-16 21:35:54 +00:00
|
|
|
|
FR.onload = (e: WhatIsThis) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
resolve(e.target.result);
|
|
|
|
|
};
|
|
|
|
|
FR.onerror = reject;
|
|
|
|
|
FR.onabort = reject;
|
|
|
|
|
FR.readAsArrayBuffer(file);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
showFileSizeError({
|
|
|
|
|
limit,
|
|
|
|
|
units,
|
|
|
|
|
u,
|
|
|
|
|
}: Readonly<{
|
|
|
|
|
limit: WhatIsThis;
|
|
|
|
|
units: WhatIsThis;
|
|
|
|
|
u: WhatIsThis;
|
|
|
|
|
}>) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const toast = new Whisper.FileSizeToast({
|
|
|
|
|
model: { limit, units: units[u] },
|
|
|
|
|
});
|
|
|
|
|
toast.$el.insertAfter(this.$el);
|
|
|
|
|
toast.render();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
updateAttachmentsView() {
|
2021-08-09 20:06:21 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
2021-08-20 20:33:06 +00:00
|
|
|
|
const draftAttachments = model.get('draftAttachments') || [];
|
2021-06-25 16:08:16 +00:00
|
|
|
|
window.reduxActions.composer.replaceAttachments(
|
2021-08-09 20:06:21 +00:00
|
|
|
|
model.get('id'),
|
2021-08-20 20:33:06 +00:00
|
|
|
|
draftAttachments.map(att => this.resolveOnDiskAttachment(att))
|
2021-06-25 16:08:16 +00:00
|
|
|
|
);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.toggleMicrophone();
|
|
|
|
|
if (this.hasFiles()) {
|
|
|
|
|
this.removeLinkPreview();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-25 16:08:16 +00:00
|
|
|
|
async writeDraftAttachment(
|
|
|
|
|
attachment: InMemoryAttachmentDraftType
|
|
|
|
|
): Promise<OnDiskAttachmentDraftType> {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
let toWrite = attachment;
|
|
|
|
|
|
|
|
|
|
if (toWrite.data) {
|
|
|
|
|
const path = await writeNewDraftData(toWrite.data);
|
|
|
|
|
toWrite = {
|
2021-02-26 21:06:37 +00:00
|
|
|
|
...window._.omit(toWrite, ['data']),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
path,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if (toWrite.screenshotData) {
|
|
|
|
|
const screenshotPath = await writeNewDraftData(toWrite.screenshotData);
|
|
|
|
|
toWrite = {
|
2021-02-26 21:06:37 +00:00
|
|
|
|
...window._.omit(toWrite, ['screenshotData']),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
screenshotPath,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return toWrite;
|
|
|
|
|
},
|
|
|
|
|
|
2021-07-28 00:09:10 +00:00
|
|
|
|
async maybeAddAttachment(file: File): Promise<void> {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!file) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const MB = 1000 * 1024;
|
|
|
|
|
if (file.size > 100 * MB) {
|
|
|
|
|
this.showFileSizeError({ limit: 100, units: ['MB'], u: 0 });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (window.Signal.Util.isFileDangerous(file.name)) {
|
|
|
|
|
this.showToast(Whisper.DangerousFileTypeToast);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
|
|
|
|
const draftAttachments = model.get('draftAttachments') || [];
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (draftAttachments.length >= 32) {
|
|
|
|
|
this.showToast(Whisper.MaxAttachmentsToast);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const haveNonImage = window._.any(
|
|
|
|
|
draftAttachments,
|
2021-08-16 21:35:54 +00:00
|
|
|
|
(attachment: WhatIsThis) => !MIME.isImage(attachment.contentType)
|
2020-09-24 20:57:54 +00:00
|
|
|
|
);
|
|
|
|
|
// You can't add another attachment if you already have a non-image staged
|
|
|
|
|
if (haveNonImage) {
|
|
|
|
|
this.showToast(Whisper.OneNonImageAtATimeToast);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-09 20:06:21 +00:00
|
|
|
|
const fileType = stringToMIMEType(file.type);
|
2021-07-28 00:09:10 +00:00
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
// You can't add a non-image attachment if you already have attachments staged
|
2021-07-28 00:09:10 +00:00
|
|
|
|
if (!MIME.isImage(fileType) && draftAttachments.length > 0) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.showToast(Whisper.CannotMixImageAndNonImageAttachmentsToast);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-25 16:08:16 +00:00
|
|
|
|
let attachment: InMemoryAttachmentDraftType;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
try {
|
2021-08-09 20:06:21 +00:00
|
|
|
|
if (
|
|
|
|
|
window.Signal.Util.GoogleChrome.isImageTypeSupported(fileType) ||
|
|
|
|
|
isHeic(fileType)
|
|
|
|
|
) {
|
2021-08-20 20:33:06 +00:00
|
|
|
|
const fileName = nodePath.parse(file.name).name;
|
2021-08-09 20:06:21 +00:00
|
|
|
|
// Add a pending attachment since transcoding may take a while
|
|
|
|
|
this.model.set({
|
|
|
|
|
draftAttachments: [
|
|
|
|
|
...draftAttachments,
|
|
|
|
|
{
|
|
|
|
|
contentType: IMAGE_JPEG,
|
2021-08-20 20:33:06 +00:00
|
|
|
|
fileName,
|
|
|
|
|
path: file.name,
|
2021-08-09 20:06:21 +00:00
|
|
|
|
pending: true,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
this.updateAttachmentsView();
|
|
|
|
|
|
2021-07-28 00:09:10 +00:00
|
|
|
|
attachment = await handleImageAttachment(file);
|
2021-08-20 20:33:06 +00:00
|
|
|
|
|
|
|
|
|
const hasDraftAttachmentPending = (
|
|
|
|
|
model.get('draftAttachments') || []
|
|
|
|
|
).some(
|
|
|
|
|
draftAttachment =>
|
|
|
|
|
draftAttachment.pending && draftAttachment.fileName === fileName
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// User has canceled the draft so we don't need to continue processing
|
|
|
|
|
if (!hasDraftAttachmentPending) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-09-24 20:57:54 +00:00
|
|
|
|
} else if (
|
2021-07-28 00:09:10 +00:00
|
|
|
|
window.Signal.Util.GoogleChrome.isVideoTypeSupported(fileType)
|
2020-09-24 20:57:54 +00:00
|
|
|
|
) {
|
|
|
|
|
attachment = await this.handleVideoAttachment(file);
|
|
|
|
|
} else {
|
|
|
|
|
const data = await this.arrayBufferFromFile(file);
|
|
|
|
|
attachment = {
|
|
|
|
|
data,
|
|
|
|
|
size: data.byteLength,
|
2021-07-28 00:09:10 +00:00
|
|
|
|
contentType: fileType,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
fileName: file.name,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
window.log.error(
|
2021-07-28 00:09:10 +00:00
|
|
|
|
`Was unable to generate thumbnail for fileType ${fileType}`,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
e && e.stack ? e.stack : e
|
|
|
|
|
);
|
|
|
|
|
const data = await this.arrayBufferFromFile(file);
|
|
|
|
|
attachment = {
|
|
|
|
|
data,
|
|
|
|
|
size: data.byteLength,
|
2021-07-28 00:09:10 +00:00
|
|
|
|
contentType: fileType,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
fileName: file.name,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (!this.isSizeOkay(attachment)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
window.log.error(
|
|
|
|
|
'Error ensuring that image is properly sized:',
|
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.showToast(Whisper.UnableToLoadToast);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-26 15:08:59 +00:00
|
|
|
|
try {
|
|
|
|
|
await this.addAttachment(attachment);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
window.log.error(
|
|
|
|
|
'Error saving draft attachment:',
|
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.showToast(Whisper.UnableToLoadToast);
|
|
|
|
|
}
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
isSizeOkay(attachment: Readonly<AttachmentType>) {
|
2021-02-09 20:11:07 +00:00
|
|
|
|
const limitKb = window.Signal.Types.Attachment.getUploadSizeLimitKb(
|
|
|
|
|
attachment.contentType
|
|
|
|
|
);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
// this needs to be cast properly
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
if ((attachment.data.byteLength / 1024).toFixed(4) >= limitKb) {
|
|
|
|
|
const units = ['kB', 'MB', 'GB'];
|
|
|
|
|
let u = -1;
|
|
|
|
|
let limit = limitKb * 1000;
|
|
|
|
|
do {
|
|
|
|
|
limit /= 1000;
|
|
|
|
|
u += 1;
|
|
|
|
|
} while (limit >= 1000 && u < units.length - 1);
|
|
|
|
|
this.showFileSizeError({ limit, units, u });
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
async handleVideoAttachment(
|
|
|
|
|
file: Readonly<File>
|
|
|
|
|
): Promise<InMemoryAttachmentDraftType> {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const objectUrl = URL.createObjectURL(file);
|
|
|
|
|
if (!objectUrl) {
|
|
|
|
|
throw new Error('Failed to create object url for video!');
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const screenshotContentType = 'image/png';
|
|
|
|
|
const screenshotBlob = await VisualAttachment.makeVideoScreenshot({
|
|
|
|
|
objectUrl,
|
|
|
|
|
contentType: screenshotContentType,
|
|
|
|
|
logger: window.log,
|
|
|
|
|
});
|
|
|
|
|
const screenshotData = await VisualAttachment.blobToArrayBuffer(
|
|
|
|
|
screenshotBlob
|
|
|
|
|
);
|
|
|
|
|
const data = await this.arrayBufferFromFile(file);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
fileName: file.name,
|
|
|
|
|
screenshotContentType,
|
|
|
|
|
screenshotData,
|
|
|
|
|
screenshotSize: screenshotData.byteLength,
|
2021-08-16 21:35:54 +00:00
|
|
|
|
contentType: stringToMIMEType(file.type),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
data,
|
|
|
|
|
size: data.byteLength,
|
|
|
|
|
};
|
|
|
|
|
} finally {
|
|
|
|
|
URL.revokeObjectURL(objectUrl);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
markAllAsVerifiedDefault(unverified: ReadonlyArray<ConversationModel>) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return Promise.all(
|
2021-08-16 21:35:54 +00:00
|
|
|
|
unverified.map(contact => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (contact.isUnverified()) {
|
|
|
|
|
return contact.setVerifiedDefault();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
markAllAsApproved(untrusted: ReadonlyArray<ConversationModel>) {
|
|
|
|
|
return Promise.all(untrusted.map(contact => contact.setApproved()));
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
toggleMicrophone() {
|
|
|
|
|
this.compositionApi.current.setShowMic(!this.hasFiles());
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
captureAudio(e?: Event) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.compositionApi.current.isDirty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.hasFiles()) {
|
|
|
|
|
this.showToast(Whisper.VoiceNoteMustBeOnlyAttachmentToast);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.showToast(Whisper.VoiceNoteLimit);
|
|
|
|
|
|
|
|
|
|
// Note - clicking anywhere will close the audio capture panel, due to
|
|
|
|
|
// the onClick handler in InboxView, which calls its closeRecording method.
|
|
|
|
|
|
|
|
|
|
if (this.captureAudioView) {
|
|
|
|
|
this.captureAudioView.remove();
|
|
|
|
|
this.captureAudioView = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.captureAudioView = new Whisper.RecorderView();
|
|
|
|
|
|
|
|
|
|
const view = this.captureAudioView;
|
|
|
|
|
view.render();
|
|
|
|
|
view.on('send', this.handleAudioCapture.bind(this));
|
|
|
|
|
view.on('confirm', this.handleAudioConfirm.bind(this));
|
|
|
|
|
view.on('closed', this.endCaptureAudio.bind(this));
|
|
|
|
|
view.$el.appendTo(this.$('.capture-audio'));
|
|
|
|
|
view.$('.finish').focus();
|
|
|
|
|
this.compositionApi.current.setMicActive(true);
|
|
|
|
|
|
|
|
|
|
this.disableMessageField();
|
|
|
|
|
this.$('.microphone').hide();
|
|
|
|
|
},
|
2021-06-08 14:59:38 +00:00
|
|
|
|
handleAudioConfirm(blob: Blob, lostFocus?: boolean) {
|
2021-01-04 18:47:14 +00:00
|
|
|
|
window.showConfirmationDialog({
|
|
|
|
|
confirmStyle: 'negative',
|
2020-09-24 20:57:54 +00:00
|
|
|
|
cancelText: window.i18n('discard'),
|
|
|
|
|
message: lostFocus
|
|
|
|
|
? window.i18n('voiceRecordingInterruptedBlur')
|
|
|
|
|
: window.i18n('voiceRecordingInterruptedMax'),
|
|
|
|
|
okText: window.i18n('sendAnyway'),
|
|
|
|
|
resolve: async () => {
|
|
|
|
|
await this.handleAudioCapture(blob);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
},
|
2021-06-08 14:59:38 +00:00
|
|
|
|
async handleAudioCapture(blob: Blob) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (this.hasFiles()) {
|
|
|
|
|
throw new Error('A voice note cannot be sent with other attachments');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = await this.arrayBufferFromFile(blob);
|
|
|
|
|
|
|
|
|
|
// These aren't persisted to disk; they are meant to be sent immediately
|
|
|
|
|
this.voiceNoteAttachment = {
|
|
|
|
|
contentType: blob.type,
|
|
|
|
|
data,
|
|
|
|
|
size: data.byteLength,
|
2021-07-09 19:36:10 +00:00
|
|
|
|
flags: Proto.AttachmentPointer.Flags.VOICE_MESSAGE,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Note: The RecorderView removes itself on send
|
|
|
|
|
this.captureAudioView = null;
|
|
|
|
|
|
|
|
|
|
this.sendMessage();
|
|
|
|
|
},
|
|
|
|
|
endCaptureAudio() {
|
|
|
|
|
this.enableMessageField();
|
|
|
|
|
this.$('.microphone').show();
|
|
|
|
|
|
|
|
|
|
// Note: The RecorderView removes itself on close
|
|
|
|
|
this.captureAudioView = null;
|
|
|
|
|
|
|
|
|
|
this.compositionApi.current.setMicActive(false);
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
async onOpened(messageId: string) {
|
2021-05-28 19:11:19 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (messageId) {
|
|
|
|
|
const message = await getMessageById(messageId, {
|
|
|
|
|
Message: Whisper.Message,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (message) {
|
|
|
|
|
this.loadAndScroll(messageId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.log.warn(`onOpened: Did not find message ${messageId}`);
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-28 19:11:19 +00:00
|
|
|
|
const { retryPlaceholders } = window.Signal.Services;
|
|
|
|
|
if (retryPlaceholders) {
|
2021-06-08 21:51:58 +00:00
|
|
|
|
await retryPlaceholders.findByConversationAndMarkOpened(model.id);
|
2021-05-28 19:11:19 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.loadNewestMessages();
|
2021-05-28 19:11:19 +00:00
|
|
|
|
model.updateLastMessage();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
this.focusMessageField();
|
|
|
|
|
|
2021-05-28 19:11:19 +00:00
|
|
|
|
const quotedMessageId = model.get('quotedMessageId');
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (quotedMessageId) {
|
|
|
|
|
this.setQuoteMessage(quotedMessageId);
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-28 19:11:19 +00:00
|
|
|
|
model.fetchLatestGroupV2Data();
|
|
|
|
|
assert(
|
|
|
|
|
model.throttledMaybeMigrateV1Group !== undefined,
|
|
|
|
|
'Conversation model should be initialized'
|
|
|
|
|
);
|
|
|
|
|
model.throttledMaybeMigrateV1Group();
|
2021-05-13 20:57:27 +00:00
|
|
|
|
assert(
|
2021-05-28 19:11:19 +00:00
|
|
|
|
model.throttledFetchSMSOnlyUUID !== undefined,
|
2021-05-13 20:57:27 +00:00
|
|
|
|
'Conversation model should be initialized'
|
|
|
|
|
);
|
2021-05-28 19:11:19 +00:00
|
|
|
|
model.throttledFetchSMSOnlyUUID();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
const statusPromise = this.model.throttledGetProfiles();
|
|
|
|
|
// eslint-disable-next-line more/no-then
|
|
|
|
|
this.statusFetch = statusPromise.then(() =>
|
|
|
|
|
// eslint-disable-next-line more/no-then
|
2021-05-28 19:11:19 +00:00
|
|
|
|
model.updateVerified().then(() => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.statusFetch = null;
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
async retrySend(messageId: string) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
const message = window.MessageController.getById(messageId);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!message) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
throw new Error(`retrySend: Message ${messageId} missing!`);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
await message.retrySend();
|
|
|
|
|
},
|
|
|
|
|
|
2021-04-27 22:35:35 +00:00
|
|
|
|
showForwardMessageModal(messageId: string) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
const message = window.MessageController.getById(messageId);
|
2021-04-27 22:35:35 +00:00
|
|
|
|
if (!message) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
throw new Error(`showForwardMessageModal: Message ${messageId} missing!`);
|
2021-04-27 22:35:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
|
const attachments = getAttachmentsForMessage(message.attributes);
|
2021-04-27 22:35:35 +00:00
|
|
|
|
this.forwardMessageModal = new Whisper.ReactWrapperView({
|
|
|
|
|
JSX: window.Signal.State.Roots.createForwardMessageModal(
|
|
|
|
|
window.reduxStore,
|
|
|
|
|
{
|
|
|
|
|
attachments,
|
|
|
|
|
doForwardMessage: async (
|
|
|
|
|
conversationIds: Array<string>,
|
|
|
|
|
messageBody?: string,
|
|
|
|
|
includedAttachments?: Array<AttachmentType>,
|
|
|
|
|
linkPreview?: LinkPreviewType
|
|
|
|
|
) => {
|
2021-07-20 20:18:35 +00:00
|
|
|
|
try {
|
|
|
|
|
const didForwardSuccessfully = await this.maybeForwardMessage(
|
|
|
|
|
message,
|
|
|
|
|
conversationIds,
|
|
|
|
|
messageBody,
|
|
|
|
|
includedAttachments,
|
|
|
|
|
linkPreview
|
|
|
|
|
);
|
2021-04-27 22:35:35 +00:00
|
|
|
|
|
2021-07-20 20:18:35 +00:00
|
|
|
|
if (didForwardSuccessfully) {
|
|
|
|
|
this.forwardMessageModal.remove();
|
|
|
|
|
this.forwardMessageModal = null;
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
window.log.warn(
|
|
|
|
|
'doForwardMessage',
|
|
|
|
|
err && err.stack ? err.stack : err
|
|
|
|
|
);
|
2021-04-27 22:35:35 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
isSticker: Boolean(message.get('sticker')),
|
|
|
|
|
messageBody: message.getRawText(),
|
|
|
|
|
onClose: () => {
|
|
|
|
|
this.forwardMessageModal.remove();
|
|
|
|
|
this.forwardMessageModal = null;
|
|
|
|
|
this.resetLinkPreview();
|
|
|
|
|
},
|
|
|
|
|
onEditorStateChange: (
|
|
|
|
|
messageText: string,
|
|
|
|
|
_: Array<typeof window.Whisper.BodyRangeType>,
|
|
|
|
|
caretLocation?: number
|
|
|
|
|
) => {
|
|
|
|
|
if (!attachments.length) {
|
|
|
|
|
this.debouncedMaybeGrabLinkPreview(messageText, caretLocation);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onTextTooLong: () =>
|
|
|
|
|
this.showToast(
|
|
|
|
|
Whisper.MessageBodyTooLongToast,
|
|
|
|
|
{},
|
|
|
|
|
document.querySelector('.module-ForwardMessageModal')
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
this.forwardMessageModal.render();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async maybeForwardMessage(
|
|
|
|
|
message: MessageModel,
|
|
|
|
|
conversationIds: Array<string>,
|
|
|
|
|
messageBody?: string,
|
|
|
|
|
attachments?: Array<AttachmentType>,
|
|
|
|
|
linkPreview?: LinkPreviewType
|
|
|
|
|
): Promise<boolean> {
|
2021-06-03 21:26:56 +00:00
|
|
|
|
window.log.info(
|
|
|
|
|
`maybeForwardMessage/${message.idForLogging()}: Starting...`
|
|
|
|
|
);
|
2021-04-27 22:35:35 +00:00
|
|
|
|
const attachmentLookup = new Set();
|
|
|
|
|
if (attachments) {
|
|
|
|
|
attachments.forEach(attachment => {
|
|
|
|
|
attachmentLookup.add(
|
|
|
|
|
`${attachment.fileName}/${attachment.contentType}`
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const conversations = conversationIds.map(id =>
|
|
|
|
|
window.ConversationController.get(id)
|
|
|
|
|
);
|
|
|
|
|
|
2021-07-20 20:18:35 +00:00
|
|
|
|
const cannotSend = conversations.some(
|
|
|
|
|
conversation =>
|
|
|
|
|
conversation?.get('announcementsOnly') && !conversation.areWeAdmin()
|
|
|
|
|
);
|
2021-07-21 20:45:41 +00:00
|
|
|
|
if (cannotSend) {
|
2021-07-20 20:18:35 +00:00
|
|
|
|
throw new Error('Cannot send to group');
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-27 22:35:35 +00:00
|
|
|
|
// Verify that all contacts that we're forwarding
|
|
|
|
|
// to are verified and trusted
|
|
|
|
|
const unverifiedContacts: Array<ConversationModel> = [];
|
|
|
|
|
const untrustedContacts: Array<ConversationModel> = [];
|
|
|
|
|
await Promise.all(
|
|
|
|
|
conversations.map(async conversation => {
|
|
|
|
|
if (conversation) {
|
|
|
|
|
await conversation.updateVerified();
|
|
|
|
|
const unverifieds = conversation.getUnverified();
|
|
|
|
|
if (unverifieds.length) {
|
|
|
|
|
unverifieds.forEach(unverifiedConversation =>
|
|
|
|
|
unverifiedContacts.push(unverifiedConversation)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const untrusted = conversation.getUntrusted();
|
|
|
|
|
if (untrusted.length) {
|
|
|
|
|
untrusted.forEach(untrustedConversation =>
|
|
|
|
|
untrustedContacts.push(untrustedConversation)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// If there are any unverified or untrusted contacts, show the
|
|
|
|
|
// SendAnywayDialog and if we're fine with sending then mark all as
|
|
|
|
|
// verified and trusted and continue the send.
|
|
|
|
|
const iffyConversations = [...unverifiedContacts, ...untrustedContacts];
|
|
|
|
|
if (iffyConversations.length) {
|
|
|
|
|
const forwardMessageModal = document.querySelector<HTMLElement>(
|
|
|
|
|
'.module-ForwardMessageModal'
|
|
|
|
|
);
|
|
|
|
|
if (forwardMessageModal) {
|
|
|
|
|
forwardMessageModal.style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
const sendAnyway = await this.showSendAnywayDialog(iffyConversations);
|
|
|
|
|
|
|
|
|
|
if (!sendAnyway) {
|
|
|
|
|
if (forwardMessageModal) {
|
|
|
|
|
forwardMessageModal.style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let verifyPromise: Promise<void> | undefined;
|
|
|
|
|
let approvePromise: Promise<void> | undefined;
|
|
|
|
|
if (unverifiedContacts.length) {
|
|
|
|
|
verifyPromise = this.markAllAsVerifiedDefault(unverifiedContacts);
|
|
|
|
|
}
|
|
|
|
|
if (untrustedContacts.length) {
|
|
|
|
|
approvePromise = this.markAllAsApproved(untrustedContacts);
|
|
|
|
|
}
|
|
|
|
|
await Promise.all([verifyPromise, approvePromise]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sendMessageOptions = { dontClearDraft: true };
|
2021-06-24 17:38:52 +00:00
|
|
|
|
const baseTimestamp = Date.now();
|
2021-04-27 22:35:35 +00:00
|
|
|
|
|
|
|
|
|
// Actually send the message
|
|
|
|
|
// load any sticker data, attachments, or link previews that we need to
|
|
|
|
|
// send along with the message and do the send to each conversation.
|
|
|
|
|
await Promise.all(
|
2021-06-24 17:38:52 +00:00
|
|
|
|
conversations.map(async (conversation, offset) => {
|
|
|
|
|
const timestamp = baseTimestamp + offset;
|
2021-04-27 22:35:35 +00:00
|
|
|
|
if (conversation) {
|
|
|
|
|
const sticker = message.get('sticker');
|
|
|
|
|
if (sticker) {
|
|
|
|
|
const stickerWithData = await loadStickerData(sticker);
|
2021-06-03 21:26:56 +00:00
|
|
|
|
const stickerNoPath = stickerWithData
|
|
|
|
|
? {
|
|
|
|
|
...stickerWithData,
|
|
|
|
|
data: {
|
|
|
|
|
...stickerWithData.data,
|
|
|
|
|
path: undefined,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
|
2021-04-27 22:35:35 +00:00
|
|
|
|
conversation.sendMessage(
|
2021-07-09 19:36:10 +00:00
|
|
|
|
undefined, // body
|
2021-04-27 22:35:35 +00:00
|
|
|
|
[],
|
2021-07-09 19:36:10 +00:00
|
|
|
|
undefined, // quote
|
2021-04-27 22:35:35 +00:00
|
|
|
|
[],
|
2021-06-03 21:26:56 +00:00
|
|
|
|
stickerNoPath,
|
2021-07-09 19:36:10 +00:00
|
|
|
|
undefined, // BodyRanges
|
2021-06-03 21:26:56 +00:00
|
|
|
|
{ ...sendMessageOptions, timestamp }
|
2021-04-27 22:35:35 +00:00
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
const preview = linkPreview
|
|
|
|
|
? await loadPreviewData([linkPreview])
|
|
|
|
|
: [];
|
2021-06-03 21:26:56 +00:00
|
|
|
|
const attachmentsWithData = await Promise.all(
|
|
|
|
|
(attachments || []).map(async item => ({
|
|
|
|
|
...(await loadAttachmentData(item)),
|
|
|
|
|
path: undefined,
|
|
|
|
|
}))
|
|
|
|
|
);
|
|
|
|
|
const attachmentsToSend = attachmentsWithData.filter(
|
2021-04-27 22:35:35 +00:00
|
|
|
|
(attachment: Partial<AttachmentType>) =>
|
|
|
|
|
attachmentLookup.has(
|
|
|
|
|
`${attachment.fileName}/${attachment.contentType}`
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
conversation.sendMessage(
|
2021-07-09 19:36:10 +00:00
|
|
|
|
messageBody || undefined,
|
2021-04-27 22:35:35 +00:00
|
|
|
|
attachmentsToSend,
|
2021-07-09 19:36:10 +00:00
|
|
|
|
undefined, // quote
|
2021-04-27 22:35:35 +00:00
|
|
|
|
preview,
|
2021-07-09 19:36:10 +00:00
|
|
|
|
undefined, // sticker
|
2021-04-27 22:35:35 +00:00
|
|
|
|
undefined, // BodyRanges
|
2021-06-03 21:26:56 +00:00
|
|
|
|
{ ...sendMessageOptions, timestamp }
|
2021-04-27 22:35:35 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
2021-05-04 14:57:14 +00:00
|
|
|
|
// Cancel any link still pending, even if it didn't make it into the message
|
|
|
|
|
this.resetLinkPreview();
|
2021-04-27 22:35:35 +00:00
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
async showAllMedia() {
|
|
|
|
|
// We fetch more documents than media as they don’t require to be loaded
|
|
|
|
|
// into memory right away. Revisit this once we have infinite scrolling:
|
|
|
|
|
const DEFAULT_MEDIA_FETCH_COUNT = 50;
|
|
|
|
|
const DEFAULT_DOCUMENTS_FETCH_COUNT = 150;
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
const conversationId = model.get('id');
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
const getProps = async () => {
|
|
|
|
|
const rawMedia = await window.Signal.Data.getMessagesWithVisualMediaAttachments(
|
|
|
|
|
conversationId,
|
|
|
|
|
{
|
|
|
|
|
limit: DEFAULT_MEDIA_FETCH_COUNT,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
const rawDocuments = await window.Signal.Data.getMessagesWithFileAttachments(
|
|
|
|
|
conversationId,
|
|
|
|
|
{
|
|
|
|
|
limit: DEFAULT_DOCUMENTS_FETCH_COUNT,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// First we upgrade these messages to ensure that they have thumbnails
|
|
|
|
|
for (let max = rawMedia.length, i = 0; i < max; i += 1) {
|
|
|
|
|
const message = rawMedia[i];
|
|
|
|
|
const { schemaVersion } = message;
|
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
|
if (
|
|
|
|
|
schemaVersion &&
|
|
|
|
|
schemaVersion < Message.VERSION_NEEDED_FOR_DISPLAY
|
|
|
|
|
) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
// Yep, we really do want to wait for each of these
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
|
|
|
|
rawMedia[i] = await upgradeMessageSchema(message);
|
|
|
|
|
// eslint-disable-next-line no-await-in-loop
|
2021-07-19 20:45:18 +00:00
|
|
|
|
await window.Signal.Data.saveMessage(rawMedia[i]);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const media = window._.flatten(
|
|
|
|
|
rawMedia.map(message => {
|
|
|
|
|
const { attachments } = message;
|
|
|
|
|
return (attachments || [])
|
|
|
|
|
.filter(
|
2021-08-16 21:35:54 +00:00
|
|
|
|
(attachment: AttachmentType) =>
|
2020-09-24 20:57:54 +00:00
|
|
|
|
attachment.thumbnail && !attachment.pending && !attachment.error
|
|
|
|
|
)
|
2021-08-16 21:35:54 +00:00
|
|
|
|
.map((attachment: WhatIsThis, index: number) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const { thumbnail } = attachment;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
objectURL: getAbsoluteAttachmentPath(attachment.path),
|
|
|
|
|
thumbnailObjectUrl: thumbnail
|
|
|
|
|
? getAbsoluteAttachmentPath(thumbnail.path)
|
|
|
|
|
: null,
|
|
|
|
|
contentType: attachment.contentType,
|
|
|
|
|
index,
|
|
|
|
|
attachment,
|
|
|
|
|
message,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Unlike visual media, only one non-image attachment is supported
|
|
|
|
|
const documents = rawDocuments
|
|
|
|
|
.filter(message =>
|
|
|
|
|
Boolean(message.attachments && message.attachments.length)
|
|
|
|
|
)
|
|
|
|
|
.map(message => {
|
|
|
|
|
const attachments = message.attachments || [];
|
|
|
|
|
const attachment = attachments[0];
|
|
|
|
|
return {
|
|
|
|
|
contentType: attachment.contentType,
|
|
|
|
|
index: 0,
|
|
|
|
|
attachment,
|
|
|
|
|
message,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
const saveAttachment = async ({
|
|
|
|
|
attachment,
|
|
|
|
|
message,
|
|
|
|
|
}: {
|
|
|
|
|
attachment: AttachmentType;
|
|
|
|
|
message: Pick<MessageAttributesType, 'sent_at'>;
|
|
|
|
|
}) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const timestamp = message.sent_at;
|
|
|
|
|
const fullPath = await window.Signal.Types.Attachment.save({
|
|
|
|
|
attachment,
|
|
|
|
|
readAttachmentData,
|
|
|
|
|
saveAttachmentToDisk,
|
|
|
|
|
timestamp,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (fullPath) {
|
|
|
|
|
this.showToast(Whisper.FileSavedToast, { fullPath });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
const onItemClick = async ({ message, attachment, type }: WhatIsThis) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
switch (type) {
|
|
|
|
|
case 'documents': {
|
|
|
|
|
saveAttachment({ message, attachment });
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'media': {
|
|
|
|
|
const selectedIndex = media.findIndex(
|
|
|
|
|
mediaMessage => mediaMessage.attachment.path === attachment.path
|
|
|
|
|
);
|
|
|
|
|
this.lightboxGalleryView = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'lightbox-wrapper',
|
2021-08-23 23:14:53 +00:00
|
|
|
|
Component: window.Signal.Components.Lightbox,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
props: {
|
|
|
|
|
media,
|
|
|
|
|
onSave: saveAttachment,
|
|
|
|
|
selectedIndex,
|
|
|
|
|
},
|
|
|
|
|
onClose: () => window.Signal.Backbone.Views.Lightbox.hide(),
|
|
|
|
|
});
|
|
|
|
|
window.Signal.Backbone.Views.Lightbox.show(
|
|
|
|
|
this.lightboxGalleryView.el
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
throw new TypeError(`Unknown attachment type: '${type}'`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
documents,
|
|
|
|
|
media,
|
|
|
|
|
onItemClick,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const view = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'panel',
|
|
|
|
|
Component: window.Signal.Components.MediaGallery,
|
|
|
|
|
props: await getProps(),
|
|
|
|
|
onClose: () => {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
unsubscribe();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
});
|
2021-01-29 21:19:24 +00:00
|
|
|
|
view.headerTitle = window.i18n('allMedia');
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
const update = async () => {
|
|
|
|
|
view.update(await getProps());
|
|
|
|
|
};
|
|
|
|
|
|
2021-06-16 00:44:14 +00:00
|
|
|
|
function getMessageIds(): Array<string | undefined> | undefined {
|
|
|
|
|
const state = window.reduxStore.getState();
|
|
|
|
|
const byConversation = state?.conversations?.messagesByConversation;
|
|
|
|
|
const messages = byConversation && byConversation[conversationId];
|
|
|
|
|
if (!messages || !messages.messageIds) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return messages.messageIds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Detect message changes in the current conversation
|
|
|
|
|
let previousMessageList: Array<string | undefined> | undefined;
|
|
|
|
|
previousMessageList = getMessageIds();
|
|
|
|
|
|
|
|
|
|
const unsubscribe = window.reduxStore.subscribe(() => {
|
|
|
|
|
const currentMessageList = getMessageIds();
|
|
|
|
|
if (currentMessageList !== previousMessageList) {
|
|
|
|
|
update();
|
|
|
|
|
previousMessageList = currentMessageList;
|
|
|
|
|
}
|
|
|
|
|
});
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
this.listenBack(view);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
focusMessageField() {
|
|
|
|
|
if (this.panels && this.panels.length) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { compositionApi } = this;
|
|
|
|
|
|
|
|
|
|
if (compositionApi && compositionApi.current) {
|
|
|
|
|
compositionApi.current.focusInput();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
disableMessageField() {
|
|
|
|
|
this.compositionApi.current.setDisabled(true);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
enableMessageField() {
|
|
|
|
|
this.compositionApi.current.setDisabled(false);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
resetEmojiResults() {
|
|
|
|
|
this.compositionApi.current.resetEmojiResults(false);
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-17 21:15:09 +00:00
|
|
|
|
showGV1Members() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
2021-06-17 21:15:09 +00:00
|
|
|
|
const { contactCollection } = model;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2021-06-17 21:15:09 +00:00
|
|
|
|
const memberships =
|
|
|
|
|
contactCollection?.map((conversation: ConversationModel) => {
|
|
|
|
|
return {
|
|
|
|
|
isAdmin: false,
|
|
|
|
|
member: conversation.format(),
|
|
|
|
|
};
|
|
|
|
|
}) || [];
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2021-06-17 21:15:09 +00:00
|
|
|
|
const view = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'group-member-list panel',
|
|
|
|
|
Component: ConversationDetailsMembershipList,
|
|
|
|
|
props: {
|
|
|
|
|
canAddNewMembers: false,
|
|
|
|
|
i18n: window.i18n,
|
|
|
|
|
maxShownMemberCount: 32,
|
|
|
|
|
memberships,
|
|
|
|
|
showContactModal: this.showContactModal.bind(this),
|
|
|
|
|
},
|
2020-09-24 20:57:54 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.listenBack(view);
|
2021-06-17 21:15:09 +00:00
|
|
|
|
view.render();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
forceSend({
|
|
|
|
|
contactId,
|
|
|
|
|
messageId,
|
|
|
|
|
}: Readonly<{ contactId: string; messageId: string }>) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
|
const contact = window.ConversationController.get(contactId)!;
|
2021-06-16 00:44:14 +00:00
|
|
|
|
const message = window.MessageController.getById(messageId);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!message) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
throw new Error(`forceSend: Message ${messageId} missing!`);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-01-04 18:47:14 +00:00
|
|
|
|
window.showConfirmationDialog({
|
|
|
|
|
confirmStyle: 'negative',
|
2020-09-24 20:57:54 +00:00
|
|
|
|
message: window.i18n('identityKeyErrorOnSend', {
|
|
|
|
|
name1: contact.getTitle(),
|
|
|
|
|
name2: contact.getTitle(),
|
|
|
|
|
}),
|
|
|
|
|
okText: window.i18n('sendAnyway'),
|
|
|
|
|
resolve: async () => {
|
|
|
|
|
await contact.updateVerified();
|
|
|
|
|
|
|
|
|
|
if (contact.isUnverified()) {
|
|
|
|
|
await contact.setVerifiedDefault();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const untrusted = await contact.isUntrusted();
|
|
|
|
|
if (untrusted) {
|
|
|
|
|
contact.setApproved();
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-16 00:44:14 +00:00
|
|
|
|
const sendTarget = contact.getSendTarget();
|
|
|
|
|
if (!sendTarget) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`forceSend: Contact ${contact.idForLogging()} had no sendTarget!`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
message.resend(sendTarget);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
showSafetyNumber(id: string) {
|
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
|
|
|
|
let conversation: undefined | ConversationModel;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
if (!id && isDirectConversation(model.attributes)) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
// eslint-disable-next-line prefer-destructuring
|
2021-06-08 14:59:38 +00:00
|
|
|
|
conversation = model;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
} else {
|
|
|
|
|
conversation = window.ConversationController.get(id);
|
|
|
|
|
}
|
|
|
|
|
if (conversation) {
|
|
|
|
|
const view = new Whisper.KeyVerificationPanelView({
|
|
|
|
|
model: conversation,
|
|
|
|
|
});
|
|
|
|
|
this.listenBack(view);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
downloadAttachmentWrapper(messageId: string) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
const message = window.MessageController.getById(messageId);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!message) {
|
|
|
|
|
throw new Error(
|
2021-06-16 00:44:14 +00:00
|
|
|
|
`downloadAttachmentWrapper: Message ${messageId} missing!`
|
2020-09-24 20:57:54 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { attachments, sent_at: timestamp } = message.attributes;
|
|
|
|
|
if (!attachments || attachments.length < 1) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const attachment = attachments[0];
|
|
|
|
|
const { fileName } = attachment;
|
|
|
|
|
|
|
|
|
|
const isDangerous = window.Signal.Util.isFileDangerous(fileName || '');
|
|
|
|
|
|
|
|
|
|
this.downloadAttachment({ attachment, timestamp, isDangerous });
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
async downloadAttachment({
|
|
|
|
|
attachment,
|
|
|
|
|
timestamp,
|
|
|
|
|
isDangerous,
|
|
|
|
|
}: {
|
2021-07-14 23:39:52 +00:00
|
|
|
|
attachment: AttachmentType;
|
|
|
|
|
timestamp: number;
|
2021-06-08 14:59:38 +00:00
|
|
|
|
isDangerous: boolean;
|
|
|
|
|
}) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (isDangerous) {
|
|
|
|
|
this.showToast(Whisper.DangerousFileTypeToast);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fullPath = await window.Signal.Types.Attachment.save({
|
|
|
|
|
attachment,
|
|
|
|
|
readAttachmentData,
|
|
|
|
|
saveAttachmentToDisk,
|
|
|
|
|
timestamp,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (fullPath) {
|
|
|
|
|
this.showToast(Whisper.FileSavedToast, { fullPath });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
async displayTapToViewMessage(messageId: string) {
|
2021-08-12 16:20:22 +00:00
|
|
|
|
window.log.info('displayTapToViewMessage: attempting to display message');
|
|
|
|
|
|
2021-06-16 00:44:14 +00:00
|
|
|
|
const message = window.MessageController.getById(messageId);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!message) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
throw new Error(`displayTapToViewMessage: Message ${messageId} missing!`);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
|
if (!isTapToView(message.attributes)) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
throw new Error(
|
|
|
|
|
`displayTapToViewMessage: Message ${message.idForLogging()} is not a tap to view message`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (message.isErased()) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`displayTapToViewMessage: Message ${message.idForLogging()} is already erased`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
|
const firstAttachment = (message.get('attachments') || [])[0];
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!firstAttachment || !firstAttachment.path) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`displayTapToViewMessage: Message ${message.idForLogging()} had no first attachment with path`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const absolutePath = getAbsoluteAttachmentPath(firstAttachment.path);
|
|
|
|
|
const tempPath = await copyIntoTempDirectory(absolutePath);
|
|
|
|
|
const tempAttachment = {
|
|
|
|
|
...firstAttachment,
|
|
|
|
|
path: tempPath,
|
|
|
|
|
};
|
|
|
|
|
|
2021-07-22 17:07:53 +00:00
|
|
|
|
await message.markViewOnceMessageViewed();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
const closeLightbox = async () => {
|
2021-08-12 16:20:22 +00:00
|
|
|
|
window.log.info('displayTapToViewMessage: attempting to close lightbox');
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!this.lightboxView) {
|
2021-08-12 16:20:22 +00:00
|
|
|
|
window.log.info('displayTapToViewMessage: lightbox was already closed');
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { lightboxView } = this;
|
|
|
|
|
this.lightboxView = null;
|
|
|
|
|
|
|
|
|
|
this.stopListening(message);
|
|
|
|
|
window.Signal.Backbone.Views.Lightbox.hide();
|
|
|
|
|
lightboxView.remove();
|
|
|
|
|
|
|
|
|
|
await deleteTempFile(tempPath);
|
|
|
|
|
};
|
|
|
|
|
this.listenTo(message, 'expired', closeLightbox);
|
|
|
|
|
this.listenTo(message, 'change', () => {
|
|
|
|
|
if (this.lightBoxView) {
|
|
|
|
|
this.lightBoxView.update(getProps());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const getProps = () => {
|
|
|
|
|
const { path, contentType } = tempAttachment;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
objectURL: getAbsoluteTempPath(path),
|
|
|
|
|
contentType,
|
|
|
|
|
onSave: null, // important so download button is omitted
|
|
|
|
|
isViewOnce: true,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
this.lightboxView = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'lightbox-wrapper',
|
|
|
|
|
Component: window.Signal.Components.Lightbox,
|
|
|
|
|
props: getProps(),
|
|
|
|
|
onClose: closeLightbox,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
window.Signal.Backbone.Views.Lightbox.show(this.lightboxView.el);
|
2021-08-12 16:20:22 +00:00
|
|
|
|
|
|
|
|
|
window.log.info('displayTapToViewMessage: showed lightbox');
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
deleteMessage(messageId: string) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
const message = window.MessageController.getById(messageId);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!message) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
throw new Error(`deleteMessage: Message ${messageId} missing!`);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-01-04 18:47:14 +00:00
|
|
|
|
window.showConfirmationDialog({
|
|
|
|
|
confirmStyle: 'negative',
|
2020-09-24 20:57:54 +00:00
|
|
|
|
message: window.i18n('deleteWarning'),
|
|
|
|
|
okText: window.i18n('delete'),
|
|
|
|
|
resolve: () => {
|
|
|
|
|
window.Signal.Data.removeMessage(message.id, {
|
|
|
|
|
Message: Whisper.Message,
|
|
|
|
|
});
|
2021-06-16 00:44:14 +00:00
|
|
|
|
message.cleanup();
|
2021-06-17 17:15:10 +00:00
|
|
|
|
if (isOutgoing(message.attributes)) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.model.decrementSentMessageCount();
|
|
|
|
|
} else {
|
|
|
|
|
this.model.decrementMessageCount();
|
|
|
|
|
}
|
|
|
|
|
this.resetPanel();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2020-09-29 22:55:56 +00:00
|
|
|
|
deleteMessageForEveryone(messageId: string) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
const message = window.MessageController.getById(messageId);
|
2020-09-29 22:55:56 +00:00
|
|
|
|
if (!message) {
|
|
|
|
|
throw new Error(
|
2021-06-16 00:44:14 +00:00
|
|
|
|
`deleteMessageForEveryone: Message ${messageId} missing!`
|
2020-09-29 22:55:56 +00:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-04 18:47:14 +00:00
|
|
|
|
window.showConfirmationDialog({
|
|
|
|
|
confirmStyle: 'negative',
|
2020-09-29 22:55:56 +00:00
|
|
|
|
message: window.i18n('deleteForEveryoneWarning'),
|
|
|
|
|
okText: window.i18n('delete'),
|
|
|
|
|
resolve: async () => {
|
2021-05-06 00:09:29 +00:00
|
|
|
|
try {
|
2021-07-15 23:48:09 +00:00
|
|
|
|
await this.model.sendDeleteForEveryoneMessage({
|
|
|
|
|
id: message.id,
|
|
|
|
|
timestamp: message.get('sent_at'),
|
|
|
|
|
});
|
2021-05-06 00:09:29 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
window.log.error(
|
|
|
|
|
'Error sending delete-for-everyone',
|
2021-06-23 17:01:13 +00:00
|
|
|
|
error && error.stack,
|
2021-05-06 00:09:29 +00:00
|
|
|
|
messageId
|
|
|
|
|
);
|
|
|
|
|
this.showToast(Whisper.DeleteForEveryoneFailedToast);
|
|
|
|
|
}
|
2020-09-29 22:55:56 +00:00
|
|
|
|
this.resetPanel();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
showStickerPackPreview(packId: string, packKey: string) {
|
2021-07-09 19:36:10 +00:00
|
|
|
|
Stickers.downloadEphemeralPack(packId, packKey);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
const props = {
|
|
|
|
|
packId,
|
|
|
|
|
onClose: async () => {
|
|
|
|
|
this.stickerPreviewModalView.remove();
|
|
|
|
|
this.stickerPreviewModalView = null;
|
2021-07-09 19:36:10 +00:00
|
|
|
|
await Stickers.removeEphemeralPack(packId);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.stickerPreviewModalView = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'sticker-preview-modal-wrapper',
|
|
|
|
|
JSX: window.Signal.State.Roots.createStickerPreviewModal(
|
|
|
|
|
window.reduxStore,
|
|
|
|
|
props
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
showLightboxForMedia(
|
2021-08-23 23:14:53 +00:00
|
|
|
|
selectedMediaItem: MediaItemType,
|
|
|
|
|
media: Array<MediaItemType> = [],
|
|
|
|
|
loop = false
|
2021-08-16 21:35:54 +00:00
|
|
|
|
) {
|
|
|
|
|
const onSave = async (options: WhatIsThis = {}) => {
|
2021-01-29 21:19:24 +00:00
|
|
|
|
const fullPath = await window.Signal.Types.Attachment.save({
|
|
|
|
|
attachment: options.attachment,
|
|
|
|
|
index: options.index + 1,
|
|
|
|
|
readAttachmentData,
|
|
|
|
|
saveAttachmentToDisk,
|
|
|
|
|
timestamp: options.message.get('sent_at'),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (fullPath) {
|
|
|
|
|
this.showToast(Whisper.FileSavedToast, { fullPath });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selectedIndex = media.findIndex(
|
|
|
|
|
mediaItem =>
|
|
|
|
|
mediaItem.attachment.path === selectedMediaItem.attachment.path
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.lightboxGalleryView = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'lightbox-wrapper',
|
2021-08-23 23:14:53 +00:00
|
|
|
|
Component: window.Signal.Components.Lightbox,
|
2021-01-29 21:19:24 +00:00
|
|
|
|
props: {
|
2021-08-23 23:14:53 +00:00
|
|
|
|
getConversation: getConversationSelector(window.reduxStore.getState()),
|
|
|
|
|
loop,
|
2021-01-29 21:19:24 +00:00
|
|
|
|
media,
|
2021-08-23 23:14:53 +00:00
|
|
|
|
onForward: this.showForwardMessageModal.bind(this),
|
2021-01-29 21:19:24 +00:00
|
|
|
|
onSave,
|
2021-08-23 23:14:53 +00:00
|
|
|
|
selectedIndex: selectedIndex >= 0 ? selectedIndex : 0,
|
2021-01-29 21:19:24 +00:00
|
|
|
|
},
|
|
|
|
|
onClose: () => window.Signal.Backbone.Views.Lightbox.hide(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
window.Signal.Backbone.Views.Lightbox.show(this.lightboxGalleryView.el);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
showLightbox({
|
|
|
|
|
attachment,
|
|
|
|
|
messageId,
|
|
|
|
|
}: {
|
2021-07-14 23:39:52 +00:00
|
|
|
|
attachment: AttachmentType;
|
2021-01-29 21:19:24 +00:00
|
|
|
|
messageId: string;
|
|
|
|
|
showSingle?: boolean;
|
|
|
|
|
}) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
const message = window.MessageController.getById(messageId);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!message) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
throw new Error(`showLightbox: Message ${messageId} missing!`);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
const sticker = message.get('sticker');
|
|
|
|
|
if (sticker) {
|
|
|
|
|
const { packId, packKey } = sticker;
|
|
|
|
|
this.showStickerPackPreview(packId, packKey);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
|
const { contentType } = attachment;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!window.Signal.Util.GoogleChrome.isImageTypeSupported(contentType) &&
|
|
|
|
|
!window.Signal.Util.GoogleChrome.isVideoTypeSupported(contentType)
|
|
|
|
|
) {
|
|
|
|
|
this.downloadAttachment({ attachment, message });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-14 23:39:52 +00:00
|
|
|
|
const attachments: Array<AttachmentType> = message.get('attachments') || [];
|
|
|
|
|
|
|
|
|
|
const loop = isGIF(attachments);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
const media = attachments
|
2021-07-14 23:39:52 +00:00
|
|
|
|
.filter(item => item.thumbnail && !item.pending && !item.error)
|
|
|
|
|
.map((item, index) => ({
|
|
|
|
|
objectURL: getAbsoluteAttachmentPath(item.path ?? ''),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
path: item.path,
|
|
|
|
|
contentType: item.contentType,
|
2021-07-14 23:39:52 +00:00
|
|
|
|
loop,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
index,
|
2021-08-23 23:14:53 +00:00
|
|
|
|
message: {
|
|
|
|
|
attachments: message.get('attachments'),
|
|
|
|
|
id: message.get('id'),
|
|
|
|
|
conversationId: message.get('conversationId'),
|
|
|
|
|
received_at: message.get('received_at'),
|
|
|
|
|
received_at_ms: message.get('received_at_ms'),
|
|
|
|
|
},
|
2020-09-24 20:57:54 +00:00
|
|
|
|
attachment: item,
|
2021-08-23 23:14:53 +00:00
|
|
|
|
thumbnailObjectUrl:
|
|
|
|
|
item.thumbnail?.objectUrl ||
|
|
|
|
|
getAbsoluteAttachmentPath(item.thumbnail?.path ?? ''),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}));
|
|
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
|
const selectedMedia =
|
|
|
|
|
media.find(item => attachment.path === item.path) || media[0];
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2021-08-23 23:14:53 +00:00
|
|
|
|
this.showLightboxForMedia(selectedMedia, media, loop);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2020-11-11 17:36:05 +00:00
|
|
|
|
showContactModal(contactId: string) {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2020-11-11 17:36:05 +00:00
|
|
|
|
if (this.contactModalView) {
|
|
|
|
|
this.contactModalView.remove();
|
|
|
|
|
this.contactModalView = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.previousFocus = document.activeElement;
|
|
|
|
|
|
|
|
|
|
const hideContactModal = () => {
|
|
|
|
|
if (this.contactModalView) {
|
|
|
|
|
this.contactModalView.remove();
|
|
|
|
|
this.contactModalView = null;
|
|
|
|
|
if (this.previousFocus && this.previousFocus.focus) {
|
|
|
|
|
this.previousFocus.focus();
|
|
|
|
|
this.previousFocus = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.contactModalView = new Whisper.ReactWrapperView({
|
|
|
|
|
JSX: window.Signal.State.Roots.createContactModal(window.reduxStore, {
|
|
|
|
|
contactId,
|
2021-06-08 14:59:38 +00:00
|
|
|
|
currentConversationId: model.id,
|
2020-11-11 17:36:05 +00:00
|
|
|
|
onClose: hideContactModal,
|
|
|
|
|
openConversation: (conversationId: string) => {
|
|
|
|
|
hideContactModal();
|
|
|
|
|
this.openConversation(conversationId);
|
|
|
|
|
},
|
2021-01-29 21:19:24 +00:00
|
|
|
|
removeMember: (conversationId: string) => {
|
|
|
|
|
hideContactModal();
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.removeFromGroupV2(conversationId);
|
2021-01-29 21:19:24 +00:00
|
|
|
|
},
|
2020-11-11 17:36:05 +00:00
|
|
|
|
showSafetyNumber: (conversationId: string) => {
|
|
|
|
|
hideContactModal();
|
|
|
|
|
this.showSafetyNumber(conversationId);
|
|
|
|
|
},
|
2021-01-29 21:19:24 +00:00
|
|
|
|
toggleAdmin: (conversationId: string) => {
|
2020-11-11 17:36:05 +00:00
|
|
|
|
hideContactModal();
|
2021-01-29 21:19:24 +00:00
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const isAdmin = model.isAdmin(conversationId);
|
2021-01-29 21:19:24 +00:00
|
|
|
|
const conversationModel = window.ConversationController.get(
|
|
|
|
|
conversationId
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!conversationModel) {
|
|
|
|
|
window.log.info(
|
|
|
|
|
'conversation_view/toggleAdmin: Could not find conversation to toggle admin privileges'
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.showConfirmationDialog({
|
|
|
|
|
cancelText: window.i18n('cancel'),
|
|
|
|
|
message: isAdmin
|
|
|
|
|
? window.i18n('ContactModal--rm-admin-info', [
|
|
|
|
|
conversationModel.getTitle(),
|
|
|
|
|
])
|
|
|
|
|
: window.i18n('ContactModal--make-admin-info', [
|
|
|
|
|
conversationModel.getTitle(),
|
|
|
|
|
]),
|
|
|
|
|
okText: isAdmin
|
|
|
|
|
? window.i18n('ContactModal--rm-admin')
|
|
|
|
|
: window.i18n('ContactModal--make-admin'),
|
2021-06-08 14:59:38 +00:00
|
|
|
|
resolve: () => model.toggleAdmin(conversationId),
|
2021-01-29 21:19:24 +00:00
|
|
|
|
});
|
2020-11-11 17:36:05 +00:00
|
|
|
|
},
|
2021-05-21 17:27:28 +00:00
|
|
|
|
updateSharedGroups: () => {
|
|
|
|
|
const conversation = window.ConversationController.get(contactId);
|
|
|
|
|
if (conversation && conversation.throttledUpdateSharedGroups) {
|
|
|
|
|
conversation.throttledUpdateSharedGroups();
|
|
|
|
|
}
|
|
|
|
|
},
|
2020-11-11 17:36:05 +00:00
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.contactModalView.render();
|
|
|
|
|
},
|
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
|
showGroupLinkManagement() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
|
const view = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'panel',
|
|
|
|
|
JSX: window.Signal.State.Roots.createGroupLinkManagement(
|
|
|
|
|
window.reduxStore,
|
|
|
|
|
{
|
|
|
|
|
changeHasGroupLink: this.changeHasGroupLink.bind(this),
|
2021-06-08 14:59:38 +00:00
|
|
|
|
conversationId: model.id,
|
2021-01-29 21:19:24 +00:00
|
|
|
|
copyGroupLink: this.copyGroupLink.bind(this),
|
|
|
|
|
generateNewGroupLink: this.generateNewGroupLink.bind(this),
|
|
|
|
|
setAccessControlAddFromInviteLinkSetting: this.setAccessControlAddFromInviteLinkSetting.bind(
|
|
|
|
|
this
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
view.headerTitle = window.i18n('ConversationDetails--group-link');
|
|
|
|
|
|
|
|
|
|
this.listenBack(view);
|
|
|
|
|
view.render();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
showGroupV2Permissions() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
|
const view = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'panel',
|
|
|
|
|
JSX: window.Signal.State.Roots.createGroupV2Permissions(
|
|
|
|
|
window.reduxStore,
|
|
|
|
|
{
|
2021-06-08 14:59:38 +00:00
|
|
|
|
conversationId: model.id,
|
2021-01-29 21:19:24 +00:00
|
|
|
|
setAccessControlAttributesSetting: this.setAccessControlAttributesSetting.bind(
|
|
|
|
|
this
|
|
|
|
|
),
|
|
|
|
|
setAccessControlMembersSetting: this.setAccessControlMembersSetting.bind(
|
|
|
|
|
this
|
|
|
|
|
),
|
2021-07-20 20:18:35 +00:00
|
|
|
|
setAnnouncementsOnly: this.setAnnouncementsOnly.bind(this),
|
2021-01-29 21:19:24 +00:00
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
view.headerTitle = window.i18n('permissions');
|
|
|
|
|
|
|
|
|
|
this.listenBack(view);
|
|
|
|
|
view.render();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
showPendingInvites() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
|
const view = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'panel',
|
|
|
|
|
JSX: window.Signal.State.Roots.createPendingInvites(window.reduxStore, {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
conversationId: model.id,
|
2021-01-29 21:19:24 +00:00
|
|
|
|
ourConversationId: window.ConversationController.getOurConversationId(),
|
|
|
|
|
approvePendingMembership: (conversationId: string) => {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.approvePendingMembershipFromGroupV2(conversationId);
|
2021-01-29 21:19:24 +00:00
|
|
|
|
},
|
|
|
|
|
revokePendingMemberships: conversationIds => {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.revokePendingMembershipsFromGroupV2(conversationIds);
|
2021-01-29 21:19:24 +00:00
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
view.headerTitle = window.i18n('ConversationDetails--requests-and-invites');
|
|
|
|
|
|
|
|
|
|
this.listenBack(view);
|
|
|
|
|
view.render();
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-05 12:35:33 +00:00
|
|
|
|
showConversationNotificationsSettings() {
|
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
|
|
|
|
const view = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'panel',
|
|
|
|
|
JSX: window.Signal.State.Roots.createConversationNotificationsSettings(
|
|
|
|
|
window.reduxStore,
|
|
|
|
|
{
|
|
|
|
|
conversationId: model.id,
|
|
|
|
|
setDontNotifyForMentionsIfMuted: model.setDontNotifyForMentionsIfMuted.bind(
|
|
|
|
|
model
|
|
|
|
|
),
|
|
|
|
|
setMuteExpiration: this.setMuteExpiration.bind(this),
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
view.headerTitle = window.i18n('ConversationDetails--notifications');
|
|
|
|
|
|
|
|
|
|
this.listenBack(view);
|
|
|
|
|
view.render();
|
|
|
|
|
},
|
|
|
|
|
|
2021-05-28 16:15:17 +00:00
|
|
|
|
showChatColorEditor() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
2021-05-28 16:15:17 +00:00
|
|
|
|
|
|
|
|
|
const view = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'panel',
|
|
|
|
|
JSX: window.Signal.State.Roots.createChatColorPicker(window.reduxStore, {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
conversationId: model.get('id'),
|
2021-05-28 16:15:17 +00:00
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
view.headerTitle = window.i18n('ChatColorPicker__menu-title');
|
|
|
|
|
|
|
|
|
|
this.listenBack(view);
|
|
|
|
|
view.render();
|
|
|
|
|
},
|
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
|
showConversationDetails() {
|
2021-07-20 20:18:35 +00:00
|
|
|
|
// Run a getProfiles in case member's capabilities have changed
|
|
|
|
|
// Redux should cover us on the return here so no need to await this.
|
|
|
|
|
this.model.throttledGetProfiles();
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
2021-01-29 21:19:24 +00:00
|
|
|
|
|
2021-07-09 19:36:10 +00:00
|
|
|
|
const messageRequestEnum = Proto.SyncMessage.MessageRequestResponse.Type;
|
2021-01-29 21:19:24 +00:00
|
|
|
|
|
|
|
|
|
// these methods are used in more than one place and should probably be
|
|
|
|
|
// dried up and hoisted to methods on ConversationView
|
|
|
|
|
|
2021-04-28 20:27:16 +00:00
|
|
|
|
const onLeave = () => {
|
2021-01-29 21:19:24 +00:00
|
|
|
|
this.longRunningTaskWrapper({
|
2021-04-28 20:27:16 +00:00
|
|
|
|
name: 'onLeave',
|
2021-06-08 14:59:38 +00:00
|
|
|
|
task: () => model.leaveGroupV2(),
|
2021-01-29 21:19:24 +00:00
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2021-04-28 20:27:16 +00:00
|
|
|
|
const onBlock = () => {
|
2021-06-01 23:30:25 +00:00
|
|
|
|
this.syncMessageRequestResponse(
|
|
|
|
|
'onBlock',
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model,
|
2021-06-01 23:30:25 +00:00
|
|
|
|
messageRequestEnum.BLOCK
|
|
|
|
|
);
|
2021-01-29 21:19:24 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const props = {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
addMembers: model.addMembersV2.bind(model),
|
|
|
|
|
conversationId: model.get('id'),
|
2021-01-29 21:19:24 +00:00
|
|
|
|
loadRecentMediaItems: this.loadRecentMediaItems.bind(this),
|
|
|
|
|
setDisappearingMessages: this.setDisappearingMessages.bind(this),
|
|
|
|
|
showAllMedia: this.showAllMedia.bind(this),
|
|
|
|
|
showContactModal: this.showContactModal.bind(this),
|
2021-05-28 16:15:17 +00:00
|
|
|
|
showGroupChatColorEditor: this.showChatColorEditor.bind(this),
|
2021-01-29 21:19:24 +00:00
|
|
|
|
showGroupLinkManagement: this.showGroupLinkManagement.bind(this),
|
|
|
|
|
showGroupV2Permissions: this.showGroupV2Permissions.bind(this),
|
2021-08-05 12:35:33 +00:00
|
|
|
|
showConversationNotificationsSettings: this.showConversationNotificationsSettings.bind(
|
|
|
|
|
this
|
|
|
|
|
),
|
2021-01-29 21:19:24 +00:00
|
|
|
|
showPendingInvites: this.showPendingInvites.bind(this),
|
|
|
|
|
showLightboxForMedia: this.showLightboxForMedia.bind(this),
|
2021-06-08 14:59:38 +00:00
|
|
|
|
updateGroupAttributes: model.updateGroupAttributesV2.bind(model),
|
2021-04-28 20:27:16 +00:00
|
|
|
|
onLeave,
|
|
|
|
|
onBlock,
|
2021-01-29 21:19:24 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const view = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'conversation-details-pane panel',
|
|
|
|
|
JSX: window.Signal.State.Roots.createConversationDetails(
|
|
|
|
|
window.reduxStore,
|
|
|
|
|
props
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
view.headerTitle = '';
|
|
|
|
|
|
|
|
|
|
this.listenBack(view);
|
|
|
|
|
view.render();
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
showMessageDetail(messageId: string) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
const message = window.MessageController.getById(messageId);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!message) {
|
2021-06-16 00:44:14 +00:00
|
|
|
|
throw new Error(`showMessageDetail: Message ${messageId} missing!`);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!message.isNormalBubble()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-24 22:06:12 +00:00
|
|
|
|
const getProps = () => ({
|
2021-07-19 22:44:49 +00:00
|
|
|
|
...message.getPropsForMessageDetail(
|
|
|
|
|
window.ConversationController.getOurConversationIdOrThrow()
|
|
|
|
|
),
|
2021-03-24 22:06:12 +00:00
|
|
|
|
...this.getMessageActions(),
|
|
|
|
|
});
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const onClose = () => {
|
|
|
|
|
this.stopListening(message, 'change', update);
|
|
|
|
|
this.resetPanel();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const view = new Whisper.ReactWrapperView({
|
|
|
|
|
className: 'panel message-detail-wrapper',
|
2021-03-24 22:06:12 +00:00
|
|
|
|
JSX: window.Signal.State.Roots.createMessageDetail(
|
|
|
|
|
window.reduxStore,
|
|
|
|
|
getProps()
|
|
|
|
|
),
|
2020-09-24 20:57:54 +00:00
|
|
|
|
onClose,
|
|
|
|
|
});
|
|
|
|
|
|
2021-03-24 22:06:12 +00:00
|
|
|
|
const update = () => view.update(getProps());
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.listenTo(message, 'change', update);
|
|
|
|
|
this.listenTo(message, 'expired', onClose);
|
|
|
|
|
// We could listen to all involved contacts, but we'll call that overkill
|
|
|
|
|
|
|
|
|
|
this.listenBack(view);
|
|
|
|
|
view.render();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
showStickerManager() {
|
|
|
|
|
const view = new Whisper.ReactWrapperView({
|
|
|
|
|
className: ['sticker-manager-wrapper', 'panel'].join(' '),
|
|
|
|
|
JSX: window.Signal.State.Roots.createStickerManager(window.reduxStore),
|
|
|
|
|
onClose: () => {
|
|
|
|
|
this.resetPanel();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.listenBack(view);
|
|
|
|
|
view.render();
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
showContactDetail({
|
|
|
|
|
contact,
|
|
|
|
|
signalAccount,
|
|
|
|
|
}: {
|
2021-08-20 01:56:39 +00:00
|
|
|
|
contact: EmbeddedContactType;
|
2021-08-16 21:35:54 +00:00
|
|
|
|
signalAccount?: string;
|
2021-06-08 14:59:38 +00:00
|
|
|
|
}) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const view = new Whisper.ReactWrapperView({
|
|
|
|
|
Component: window.Signal.Components.ContactDetail,
|
|
|
|
|
className: 'contact-detail-pane panel',
|
|
|
|
|
props: {
|
|
|
|
|
contact,
|
|
|
|
|
hasSignalAccount: Boolean(signalAccount),
|
|
|
|
|
onSendMessage: () => {
|
|
|
|
|
if (signalAccount) {
|
|
|
|
|
this.openConversation(signalAccount);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
onClose: () => {
|
|
|
|
|
this.resetPanel();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.listenBack(view);
|
|
|
|
|
},
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
async openConversation(conversationId: string) {
|
|
|
|
|
window.Whisper.events.trigger('showConversation', conversationId);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
listenBack(view: WhatIsThis) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.panels = this.panels || [];
|
|
|
|
|
|
|
|
|
|
if (this.panels.length === 0) {
|
|
|
|
|
this.previousFocus = document.activeElement;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.panels.unshift(view);
|
|
|
|
|
view.$el.insertAfter(this.$('.panel').last());
|
|
|
|
|
view.$el.one('animationend', () => {
|
|
|
|
|
view.$el.addClass('panel--static');
|
|
|
|
|
});
|
2020-10-30 17:52:21 +00:00
|
|
|
|
|
|
|
|
|
window.reduxActions.conversations.setSelectedConversationPanelDepth(
|
|
|
|
|
this.panels.length
|
|
|
|
|
);
|
2021-01-29 21:19:24 +00:00
|
|
|
|
window.reduxActions.conversations.setSelectedConversationHeaderTitle(
|
|
|
|
|
view.headerTitle
|
|
|
|
|
);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
resetPanel() {
|
|
|
|
|
if (!this.panels || !this.panels.length) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const view = this.panels.shift();
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
this.panels.length === 0 &&
|
|
|
|
|
this.previousFocus &&
|
|
|
|
|
this.previousFocus.focus
|
|
|
|
|
) {
|
|
|
|
|
this.previousFocus.focus();
|
|
|
|
|
this.previousFocus = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.panels.length > 0) {
|
|
|
|
|
this.panels[0].$el.fadeIn(250);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
view.$el.addClass('panel--remove').one('transitionend', () => {
|
|
|
|
|
view.remove();
|
|
|
|
|
|
|
|
|
|
if (this.panels.length === 0) {
|
|
|
|
|
// Make sure poppers are positioned properly
|
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
|
|
|
}
|
|
|
|
|
});
|
2020-10-30 17:52:21 +00:00
|
|
|
|
|
|
|
|
|
window.reduxActions.conversations.setSelectedConversationPanelDepth(
|
|
|
|
|
this.panels.length
|
|
|
|
|
);
|
2021-01-29 21:19:24 +00:00
|
|
|
|
window.reduxActions.conversations.setSelectedConversationHeaderTitle(
|
|
|
|
|
this.panels[0]?.headerTitle
|
|
|
|
|
);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
endSession() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
|
|
|
|
model.endSession();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
|
async loadRecentMediaItems(limit: number): Promise<void> {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2021-06-17 17:15:10 +00:00
|
|
|
|
const messages: Array<MessageAttributesType> = await window.Signal.Data.getMessagesWithVisualMediaAttachments(
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.id,
|
2021-01-29 21:19:24 +00:00
|
|
|
|
{
|
|
|
|
|
limit,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const loadedRecentMediaItems = messages
|
|
|
|
|
.filter(message => message.attachments !== undefined)
|
|
|
|
|
.reduce(
|
|
|
|
|
(acc, message) => [
|
|
|
|
|
...acc,
|
2021-06-17 17:15:10 +00:00
|
|
|
|
...(message.attachments || []).map(
|
2021-01-29 21:19:24 +00:00
|
|
|
|
(attachment: AttachmentType, index: number): MediaItemType => {
|
|
|
|
|
const { thumbnail } = attachment;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
objectURL: getAbsoluteAttachmentPath(attachment.path || ''),
|
|
|
|
|
thumbnailObjectUrl: thumbnail
|
|
|
|
|
? getAbsoluteAttachmentPath(thumbnail.path)
|
|
|
|
|
: '',
|
|
|
|
|
contentType: attachment.contentType,
|
|
|
|
|
index,
|
|
|
|
|
attachment,
|
2021-08-23 23:14:53 +00:00
|
|
|
|
message: {
|
|
|
|
|
attachments: message.attachments || [],
|
|
|
|
|
conversationId:
|
|
|
|
|
window.ConversationController.get(message.sourceUuid)?.id ||
|
|
|
|
|
message.conversationId,
|
|
|
|
|
id: message.id,
|
|
|
|
|
received_at: message.received_at,
|
|
|
|
|
received_at_ms: Number(message.received_at_ms),
|
|
|
|
|
},
|
2021-01-29 21:19:24 +00:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
[] as Array<MediaItemType>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
window.reduxActions.conversations.setRecentMediaItems(
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.id,
|
2021-01-29 21:19:24 +00:00
|
|
|
|
loadedRecentMediaItems
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
async setDisappearingMessages(seconds: number) {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
const valueToSet = seconds > 0 ? seconds : undefined;
|
2020-10-06 17:06:34 +00:00
|
|
|
|
|
|
|
|
|
await this.longRunningTaskWrapper({
|
|
|
|
|
name: 'updateExpirationTimer',
|
2021-06-08 14:59:38 +00:00
|
|
|
|
task: async () => model.updateExpirationTimer(valueToSet),
|
2020-10-06 17:06:34 +00:00
|
|
|
|
});
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
|
async changeHasGroupLink(value: boolean) {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
|
await this.longRunningTaskWrapper({
|
|
|
|
|
name: 'toggleGroupLink',
|
2021-06-08 14:59:38 +00:00
|
|
|
|
task: async () => model.toggleGroupLink(value),
|
2021-01-29 21:19:24 +00:00
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async copyGroupLink(groupLink: string) {
|
|
|
|
|
await navigator.clipboard.writeText(groupLink);
|
|
|
|
|
this.showToast(Whisper.GroupLinkCopiedToast);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async generateNewGroupLink() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
|
window.showConfirmationDialog({
|
|
|
|
|
confirmStyle: 'negative',
|
|
|
|
|
message: window.i18n('GroupLinkManagement--confirm-reset'),
|
|
|
|
|
okText: window.i18n('GroupLinkManagement--reset'),
|
|
|
|
|
resolve: async () => {
|
|
|
|
|
await this.longRunningTaskWrapper({
|
|
|
|
|
name: 'refreshGroupLink',
|
2021-06-08 14:59:38 +00:00
|
|
|
|
task: async () => model.refreshGroupLink(),
|
2021-01-29 21:19:24 +00:00
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2021-07-20 20:18:35 +00:00
|
|
|
|
async setAccessControlAddFromInviteLinkSetting(
|
|
|
|
|
value: boolean
|
|
|
|
|
): Promise<void> {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
|
await this.longRunningTaskWrapper({
|
|
|
|
|
name: 'updateAccessControlAddFromInviteLink',
|
2021-06-08 14:59:38 +00:00
|
|
|
|
task: async () => model.updateAccessControlAddFromInviteLink(value),
|
2021-01-29 21:19:24 +00:00
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2021-07-20 20:18:35 +00:00
|
|
|
|
async setAccessControlAttributesSetting(value: number): Promise<void> {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
|
await this.longRunningTaskWrapper({
|
|
|
|
|
name: 'updateAccessControlAttributes',
|
2021-06-08 14:59:38 +00:00
|
|
|
|
task: async () => model.updateAccessControlAttributes(value),
|
2021-01-29 21:19:24 +00:00
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2021-07-20 20:18:35 +00:00
|
|
|
|
async setAccessControlMembersSetting(value: number): Promise<void> {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2021-01-29 21:19:24 +00:00
|
|
|
|
await this.longRunningTaskWrapper({
|
|
|
|
|
name: 'updateAccessControlMembers',
|
2021-06-08 14:59:38 +00:00
|
|
|
|
task: async () => model.updateAccessControlMembers(value),
|
2021-01-29 21:19:24 +00:00
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2021-07-20 20:18:35 +00:00
|
|
|
|
async setAnnouncementsOnly(value: boolean): Promise<void> {
|
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
|
|
|
|
await this.longRunningTaskWrapper({
|
|
|
|
|
name: 'updateAnnouncementsOnly',
|
|
|
|
|
task: async () => model.updateAnnouncementsOnly(value),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
async destroyMessages() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2021-02-26 21:06:37 +00:00
|
|
|
|
window.showConfirmationDialog({
|
2021-05-28 19:14:15 +00:00
|
|
|
|
confirmStyle: 'negative',
|
2021-02-26 21:06:37 +00:00
|
|
|
|
message: window.i18n('deleteConversationConfirmation'),
|
|
|
|
|
okText: window.i18n('delete'),
|
|
|
|
|
resolve: () => {
|
|
|
|
|
this.longRunningTaskWrapper({
|
|
|
|
|
name: 'destroymessages',
|
|
|
|
|
task: async () => {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.trigger('unload', 'delete messages');
|
|
|
|
|
await model.destroyMessages();
|
|
|
|
|
model.updateLastMessage();
|
2021-02-26 21:06:37 +00:00
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
reject: () => {
|
|
|
|
|
window.log.info('destroyMessages: User canceled delete');
|
|
|
|
|
},
|
|
|
|
|
});
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async isCallSafe() {
|
|
|
|
|
const contacts = await this.getUntrustedContacts();
|
|
|
|
|
if (contacts && contacts.length) {
|
|
|
|
|
const callAnyway = await this.showSendAnywayDialog(
|
|
|
|
|
contacts,
|
|
|
|
|
window.i18n('callAnyway')
|
|
|
|
|
);
|
|
|
|
|
if (!callAnyway) {
|
|
|
|
|
window.log.info(
|
|
|
|
|
'Safety number change dialog not accepted, new call not allowed.'
|
|
|
|
|
);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
|
2021-04-27 22:35:35 +00:00
|
|
|
|
showSendAnywayDialog(
|
|
|
|
|
contacts: Array<ConversationModel>,
|
|
|
|
|
confirmText?: string
|
|
|
|
|
) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return new Promise(resolve => {
|
2021-06-17 21:15:09 +00:00
|
|
|
|
showSafetyNumberChangeDialog({
|
2020-09-24 20:57:54 +00:00
|
|
|
|
confirmText,
|
|
|
|
|
contacts,
|
|
|
|
|
reject: () => {
|
|
|
|
|
resolve(false);
|
|
|
|
|
},
|
|
|
|
|
resolve: () => {
|
|
|
|
|
resolve(true);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
async sendReactionMessage(
|
|
|
|
|
messageId: string,
|
|
|
|
|
reaction: { emoji: string; remove: boolean }
|
|
|
|
|
) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const messageModel = messageId
|
|
|
|
|
? await getMessageById(messageId, {
|
|
|
|
|
Message: Whisper.Message,
|
|
|
|
|
})
|
2021-04-05 22:18:19 +00:00
|
|
|
|
: undefined;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
try {
|
2021-04-05 22:18:19 +00:00
|
|
|
|
if (!messageModel) {
|
|
|
|
|
throw new Error('Message not found');
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
await this.model.sendReactionMessage(reaction, {
|
2021-07-15 23:48:09 +00:00
|
|
|
|
messageId,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
targetAuthorUuid: messageModel.getSourceUuid(),
|
|
|
|
|
targetTimestamp: messageModel.get('sent_at'),
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
window.log.error('Error sending reaction', error, messageId, reaction);
|
|
|
|
|
this.showToast(Whisper.ReactionFailedToast);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-07-20 19:31:23 +00:00
|
|
|
|
async sendStickerMessage(options: { packId: string; stickerId: number }) {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
try {
|
|
|
|
|
const contacts = await this.getUntrustedContacts(options);
|
|
|
|
|
|
|
|
|
|
if (contacts && contacts.length) {
|
|
|
|
|
const sendAnyway = await this.showSendAnywayDialog(contacts);
|
|
|
|
|
if (sendAnyway) {
|
|
|
|
|
this.sendStickerMessage({ ...options, force: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-16 21:34:41 +00:00
|
|
|
|
if (this.showInvalidMessageToast()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const { packId, stickerId } = options;
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.sendStickerMessage(packId, stickerId);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
window.log.error(
|
|
|
|
|
'clickSend error:',
|
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-07-30 18:37:03 +00:00
|
|
|
|
async getUntrustedContacts(options: { force?: boolean } = {}) {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
// This will go to the trust store for the latest identity key information,
|
|
|
|
|
// and may result in the display of a new banner for this conversation.
|
2021-06-08 14:59:38 +00:00
|
|
|
|
await model.updateVerified();
|
|
|
|
|
const unverifiedContacts = model.getUnverified();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
if (options.force) {
|
|
|
|
|
if (unverifiedContacts.length) {
|
|
|
|
|
await this.markAllAsVerifiedDefault(unverifiedContacts);
|
|
|
|
|
// We only want force to break us through one layer of checks
|
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
|
options.force = false;
|
|
|
|
|
}
|
|
|
|
|
} else if (unverifiedContacts.length) {
|
|
|
|
|
return unverifiedContacts;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const untrustedContacts = model.getUntrusted();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
if (options.force) {
|
|
|
|
|
if (untrustedContacts.length) {
|
|
|
|
|
await this.markAllAsApproved(untrustedContacts);
|
|
|
|
|
}
|
|
|
|
|
} else if (untrustedContacts.length) {
|
|
|
|
|
return untrustedContacts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
|
2021-03-18 17:09:27 +00:00
|
|
|
|
async setQuoteMessage(messageId: null | string) {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
|
|
|
|
const message: MessageModel | undefined = messageId
|
2020-09-24 20:57:54 +00:00
|
|
|
|
? await getMessageById(messageId, {
|
|
|
|
|
Message: Whisper.Message,
|
|
|
|
|
})
|
2021-04-05 22:18:19 +00:00
|
|
|
|
: undefined;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2021-07-19 22:44:49 +00:00
|
|
|
|
if (
|
|
|
|
|
message &&
|
|
|
|
|
!canReply(
|
|
|
|
|
message.attributes,
|
|
|
|
|
window.ConversationController.getOurConversationIdOrThrow(),
|
|
|
|
|
findAndFormatContact
|
|
|
|
|
)
|
|
|
|
|
) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
if (message && !message.isNormalBubble()) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.quote = null;
|
|
|
|
|
this.quotedMessage = null;
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const existing = model.get('quotedMessageId');
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (existing !== messageId) {
|
|
|
|
|
this.model.set({
|
|
|
|
|
quotedMessageId: messageId,
|
|
|
|
|
draftChanged: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await this.saveModel();
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
if (message) {
|
|
|
|
|
const quotedMessage = window.MessageController.register(
|
|
|
|
|
message.id,
|
|
|
|
|
message
|
|
|
|
|
);
|
|
|
|
|
this.quotedMessage = quotedMessage;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
if (quotedMessage) {
|
|
|
|
|
this.quote = await model.makeQuote(this.quotedMessage);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
|
this.enableMessageField();
|
2020-10-22 21:41:05 +00:00
|
|
|
|
this.focusMessageField();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.renderQuotedMessage();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
renderQuotedMessage() {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!this.quotedMessage) {
|
2021-06-25 16:08:16 +00:00
|
|
|
|
window.reduxActions.composer.setQuotedMessage(undefined);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-25 16:08:16 +00:00
|
|
|
|
window.reduxActions.composer.setQuotedMessage({
|
|
|
|
|
conversationId: model.id,
|
|
|
|
|
quote: this.quote,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
2020-11-16 21:34:41 +00:00
|
|
|
|
showInvalidMessageToast(messageText?: string): boolean {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
|
|
|
|
let ToastView: undefined | typeof window.Whisper.ToastView;
|
2020-11-16 21:34:41 +00:00
|
|
|
|
|
|
|
|
|
if (window.reduxStore.getState().expiration.hasExpired) {
|
|
|
|
|
ToastView = Whisper.ExpiredToast;
|
|
|
|
|
}
|
2021-06-08 14:59:38 +00:00
|
|
|
|
if (!model.isValid()) {
|
2020-11-16 21:34:41 +00:00
|
|
|
|
ToastView = Whisper.InvalidConversationToast;
|
|
|
|
|
}
|
|
|
|
|
if (
|
2021-06-07 16:39:13 +00:00
|
|
|
|
isDirectConversation(this.model.attributes) &&
|
2021-06-15 00:09:37 +00:00
|
|
|
|
(window.storage.blocked.isBlocked(this.model.get('e164')) ||
|
|
|
|
|
window.storage.blocked.isUuidBlocked(this.model.get('uuid')))
|
2020-11-16 21:34:41 +00:00
|
|
|
|
) {
|
|
|
|
|
ToastView = Whisper.BlockedToast;
|
|
|
|
|
}
|
|
|
|
|
if (
|
2021-06-07 16:39:13 +00:00
|
|
|
|
!isDirectConversation(this.model.attributes) &&
|
2021-06-15 00:09:37 +00:00
|
|
|
|
window.storage.blocked.isGroupBlocked(this.model.get('groupId'))
|
2020-11-16 21:34:41 +00:00
|
|
|
|
) {
|
|
|
|
|
ToastView = Whisper.BlockedGroupToast;
|
|
|
|
|
}
|
2021-06-08 14:59:38 +00:00
|
|
|
|
|
|
|
|
|
if (!isDirectConversation(model.attributes) && model.get('left')) {
|
2020-11-16 21:34:41 +00:00
|
|
|
|
ToastView = Whisper.LeftGroupToast;
|
|
|
|
|
}
|
|
|
|
|
if (messageText && messageText.length > MAX_MESSAGE_BODY_LENGTH) {
|
|
|
|
|
ToastView = Whisper.MessageBodyTooLongToast;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ToastView) {
|
|
|
|
|
this.showToast(ToastView);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
},
|
|
|
|
|
|
2021-07-30 18:37:03 +00:00
|
|
|
|
async sendMessage(
|
|
|
|
|
message = '',
|
|
|
|
|
mentions = [],
|
|
|
|
|
options: { timestamp?: number; force?: boolean } = {}
|
|
|
|
|
) {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
2021-07-30 18:37:03 +00:00
|
|
|
|
const timestamp = options.timestamp || Date.now();
|
2021-06-08 14:59:38 +00:00
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.sendStart = Date.now();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const contacts = await this.getUntrustedContacts(options);
|
|
|
|
|
this.disableMessageField();
|
|
|
|
|
|
|
|
|
|
if (contacts && contacts.length) {
|
|
|
|
|
const sendAnyway = await this.showSendAnywayDialog(contacts);
|
|
|
|
|
if (sendAnyway) {
|
2021-07-30 18:37:03 +00:00
|
|
|
|
this.sendMessage(message, mentions, { force: true, timestamp });
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
|
this.enableMessageField();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2020-10-21 16:53:32 +00:00
|
|
|
|
this.enableMessageField();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
window.log.error(
|
|
|
|
|
'sendMessage error:',
|
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
model.clearTypingTimers();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2020-11-16 21:34:41 +00:00
|
|
|
|
if (this.showInvalidMessageToast(message)) {
|
2020-10-21 16:53:32 +00:00
|
|
|
|
this.enableMessageField();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (!message.length && !this.hasFiles() && !this.voiceNoteAttachment) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const attachments = await this.getFiles();
|
2021-07-30 18:37:03 +00:00
|
|
|
|
const sendHQImages =
|
|
|
|
|
window.reduxStore &&
|
|
|
|
|
window.reduxStore.getState().composer.shouldSendHighQualityAttachments;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const sendDelta = Date.now() - this.sendStart;
|
2021-07-30 18:37:03 +00:00
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
window.log.info('Send pre-checks took', sendDelta, 'milliseconds');
|
|
|
|
|
|
2021-08-11 16:23:21 +00:00
|
|
|
|
batchedUpdates(() => {
|
|
|
|
|
model.sendMessage(
|
|
|
|
|
message,
|
|
|
|
|
attachments,
|
|
|
|
|
this.quote,
|
2021-08-13 21:21:14 +00:00
|
|
|
|
this.getLinkPreviewForSend(message),
|
2021-08-11 16:23:21 +00:00
|
|
|
|
undefined, // sticker
|
|
|
|
|
mentions,
|
|
|
|
|
{
|
|
|
|
|
sendHQImages,
|
|
|
|
|
timestamp,
|
|
|
|
|
}
|
|
|
|
|
);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2021-08-11 16:23:21 +00:00
|
|
|
|
this.compositionApi.current.reset();
|
|
|
|
|
model.setMarkedUnread(false);
|
|
|
|
|
this.setQuoteMessage(null);
|
|
|
|
|
this.resetLinkPreview();
|
|
|
|
|
this.clearAttachments();
|
|
|
|
|
window.reduxActions.composer.resetComposer();
|
|
|
|
|
});
|
2020-09-24 20:57:54 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
window.log.error(
|
|
|
|
|
'Error pulling attached files before send',
|
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
2020-10-21 16:53:32 +00:00
|
|
|
|
this.enableMessageField();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2020-11-03 01:19:52 +00:00
|
|
|
|
onEditorStateChange(
|
|
|
|
|
messageText: string,
|
|
|
|
|
bodyRanges: Array<typeof window.Whisper.BodyRangeType>,
|
|
|
|
|
caretLocation?: number
|
|
|
|
|
) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.maybeBumpTyping(messageText);
|
2020-11-03 01:19:52 +00:00
|
|
|
|
this.debouncedSaveDraft(messageText, bodyRanges);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.debouncedMaybeGrabLinkPreview(messageText, caretLocation);
|
|
|
|
|
},
|
|
|
|
|
|
2020-11-03 01:19:52 +00:00
|
|
|
|
async saveDraft(
|
2021-06-08 14:59:38 +00:00
|
|
|
|
messageText: string,
|
2020-11-03 01:19:52 +00:00
|
|
|
|
bodyRanges: Array<typeof window.Whisper.BodyRangeType>
|
|
|
|
|
) {
|
2021-06-08 14:59:38 +00:00
|
|
|
|
const { model }: { model: ConversationModel } = this;
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const trimmed =
|
|
|
|
|
messageText && messageText.length > 0 ? messageText.trim() : '';
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
if (model.get('draft') && (!messageText || trimmed.length === 0)) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.model.set({
|
|
|
|
|
draft: null,
|
|
|
|
|
draftChanged: true,
|
2020-11-03 01:19:52 +00:00
|
|
|
|
draftBodyRanges: [],
|
2020-09-24 20:57:54 +00:00
|
|
|
|
});
|
|
|
|
|
await this.saveModel();
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-08 14:59:38 +00:00
|
|
|
|
if (messageText !== model.get('draft')) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.model.set({
|
|
|
|
|
draft: messageText,
|
|
|
|
|
draftChanged: true,
|
2020-11-03 01:19:52 +00:00
|
|
|
|
draftBodyRanges: bodyRanges,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
});
|
|
|
|
|
await this.saveModel();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2020-11-03 01:19:52 +00:00
|
|
|
|
maybeGrabLinkPreview(message: string, caretLocation?: number) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
// Don't generate link previews if user has turned them off
|
2021-08-18 20:08:14 +00:00
|
|
|
|
if (!window.Events.getLinkPreviewSetting()) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Do nothing if we're offline
|
|
|
|
|
if (!window.textsecure.messaging) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// If we have attachments, don't add link preview
|
|
|
|
|
if (this.hasFiles()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// If we're behind a user-configured proxy, we don't support link previews
|
|
|
|
|
if (window.isBehindProxy()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!message) {
|
|
|
|
|
this.resetLinkPreview();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.disableLinkPreviews) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-30 17:00:02 +00:00
|
|
|
|
const links = LinkPreview.findLinks(message, caretLocation);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
const { currentlyMatchedLink } = this;
|
|
|
|
|
if (links.includes(currentlyMatchedLink)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.currentlyMatchedLink = null;
|
|
|
|
|
this.excludedPreviewUrls = this.excludedPreviewUrls || [];
|
|
|
|
|
|
|
|
|
|
const link = links.find(
|
|
|
|
|
item =>
|
2021-06-30 17:00:02 +00:00
|
|
|
|
LinkPreview.isLinkSafeToPreview(item) &&
|
2020-09-24 20:57:54 +00:00
|
|
|
|
!this.excludedPreviewUrls.includes(item)
|
|
|
|
|
);
|
|
|
|
|
if (!link) {
|
|
|
|
|
this.removeLinkPreview();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.addLinkPreview(link);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
resetLinkPreview() {
|
|
|
|
|
this.disableLinkPreviews = false;
|
|
|
|
|
this.excludedPreviewUrls = [];
|
|
|
|
|
this.removeLinkPreview();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
removeLinkPreview() {
|
2021-08-16 21:35:54 +00:00
|
|
|
|
(this.preview || []).forEach((item: WhatIsThis) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (item.url) {
|
|
|
|
|
URL.revokeObjectURL(item.url);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
this.preview = null;
|
2020-09-28 23:46:31 +00:00
|
|
|
|
this.currentlyMatchedLink = null;
|
|
|
|
|
this.linkPreviewAbortController?.abort();
|
|
|
|
|
this.linkPreviewAbortController = null;
|
2021-08-18 13:34:22 +00:00
|
|
|
|
|
|
|
|
|
window.reduxActions.linkPreviews.removeLinkPreview();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-02-10 22:39:26 +00:00
|
|
|
|
async getStickerPackPreview(
|
|
|
|
|
url: string,
|
2021-06-08 20:55:37 +00:00
|
|
|
|
abortSignal: Readonly<AbortSignal>
|
2021-06-25 16:08:16 +00:00
|
|
|
|
): Promise<null | LinkPreviewResult> {
|
2021-07-09 19:36:10 +00:00
|
|
|
|
const isPackDownloaded = (
|
|
|
|
|
pack?: StickerPackDBType
|
|
|
|
|
): pack is StickerPackDBType => {
|
|
|
|
|
if (!pack) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pack.status === 'downloaded' || pack.status === 'installed';
|
|
|
|
|
};
|
|
|
|
|
const isPackValid = (
|
|
|
|
|
pack?: StickerPackDBType
|
|
|
|
|
): pack is StickerPackDBType => {
|
|
|
|
|
if (!pack) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
pack.status === 'ephemeral' ||
|
2020-09-24 20:57:54 +00:00
|
|
|
|
pack.status === 'downloaded' ||
|
2021-07-09 19:36:10 +00:00
|
|
|
|
pack.status === 'installed'
|
|
|
|
|
);
|
|
|
|
|
};
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2021-07-09 19:36:10 +00:00
|
|
|
|
const dataFromLink = Stickers.getDataFromLink(url);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!dataFromLink) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const { id, key } = dataFromLink;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const keyBytes = window.Signal.Crypto.bytesFromHexString(key);
|
|
|
|
|
const keyBase64 = window.Signal.Crypto.arrayBufferToBase64(keyBytes);
|
|
|
|
|
|
2021-07-09 19:36:10 +00:00
|
|
|
|
const existing = Stickers.getStickerPack(id);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!isPackDownloaded(existing)) {
|
2021-07-09 19:36:10 +00:00
|
|
|
|
await Stickers.downloadEphemeralPack(id, keyBase64);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-02-10 22:39:26 +00:00
|
|
|
|
if (abortSignal.aborted) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-09 19:36:10 +00:00
|
|
|
|
const pack = Stickers.getStickerPack(id);
|
2021-02-10 22:39:26 +00:00
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (!isPackValid(pack)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (pack.key !== keyBase64) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { title, coverStickerId } = pack;
|
|
|
|
|
const sticker = pack.stickers[coverStickerId];
|
|
|
|
|
const data =
|
|
|
|
|
pack.status === 'ephemeral'
|
|
|
|
|
? await window.Signal.Migrations.readTempData(sticker.path)
|
|
|
|
|
: await window.Signal.Migrations.readStickerData(sticker.path);
|
|
|
|
|
|
2021-02-10 22:39:26 +00:00
|
|
|
|
if (abortSignal.aborted) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return {
|
|
|
|
|
title,
|
|
|
|
|
url,
|
|
|
|
|
image: {
|
|
|
|
|
...sticker,
|
|
|
|
|
data,
|
|
|
|
|
size: data.byteLength,
|
2021-07-09 19:36:10 +00:00
|
|
|
|
contentType: IMAGE_WEBP,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
2021-02-10 22:39:26 +00:00
|
|
|
|
description: null,
|
|
|
|
|
date: null,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
window.log.error(
|
|
|
|
|
'getStickerPackPreview error:',
|
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
|
);
|
|
|
|
|
return null;
|
|
|
|
|
} finally {
|
|
|
|
|
if (id) {
|
2021-07-09 19:36:10 +00:00
|
|
|
|
await Stickers.removeEphemeralPack(id);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2021-02-10 22:39:26 +00:00
|
|
|
|
async getGroupPreview(
|
|
|
|
|
url: string,
|
2021-06-08 20:55:37 +00:00
|
|
|
|
abortSignal: Readonly<AbortSignal>
|
2021-06-25 16:08:16 +00:00
|
|
|
|
): Promise<null | LinkPreviewResult> {
|
2021-05-13 17:18:51 +00:00
|
|
|
|
const urlObject = maybeParseUrl(url);
|
|
|
|
|
if (!urlObject) {
|
2021-02-10 22:39:26 +00:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { hash } = urlObject;
|
|
|
|
|
if (!hash) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const groupData = hash.slice(1);
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
inviteLinkPassword,
|
|
|
|
|
masterKey,
|
|
|
|
|
} = window.Signal.Groups.parseGroupLink(groupData);
|
|
|
|
|
|
|
|
|
|
const fields = window.Signal.Groups.deriveGroupFields(
|
2021-06-22 14:46:42 +00:00
|
|
|
|
Bytes.fromBase64(masterKey)
|
2021-02-10 22:39:26 +00:00
|
|
|
|
);
|
2021-06-22 14:46:42 +00:00
|
|
|
|
const id = Bytes.toBase64(fields.id);
|
2021-02-10 22:39:26 +00:00
|
|
|
|
const logId = `groupv2(${id})`;
|
2021-06-22 14:46:42 +00:00
|
|
|
|
const secretParams = Bytes.toBase64(fields.secretParams);
|
2021-02-10 22:39:26 +00:00
|
|
|
|
|
|
|
|
|
window.log.info(`getGroupPreview/${logId}: Fetching pre-join state`);
|
|
|
|
|
const result = await window.Signal.Groups.getPreJoinGroupInfo(
|
|
|
|
|
inviteLinkPassword,
|
|
|
|
|
masterKey
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (abortSignal.aborted) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const title =
|
|
|
|
|
window.Signal.Groups.decryptGroupTitle(result.title, secretParams) ||
|
|
|
|
|
window.i18n('unknownGroup');
|
|
|
|
|
const description =
|
|
|
|
|
result.memberCount === 1 || result.memberCount === undefined
|
|
|
|
|
? window.i18n('GroupV2--join--member-count--single')
|
|
|
|
|
: window.i18n('GroupV2--join--member-count--multiple', {
|
|
|
|
|
count: result.memberCount.toString(),
|
|
|
|
|
});
|
2021-06-25 16:08:16 +00:00
|
|
|
|
let image: undefined | LinkPreviewImage;
|
2021-02-10 22:39:26 +00:00
|
|
|
|
|
|
|
|
|
if (result.avatar) {
|
|
|
|
|
try {
|
|
|
|
|
const data = await window.Signal.Groups.decryptGroupAvatar(
|
|
|
|
|
result.avatar,
|
|
|
|
|
secretParams
|
|
|
|
|
);
|
|
|
|
|
image = {
|
|
|
|
|
data,
|
|
|
|
|
size: data.byteLength,
|
2021-06-25 16:08:16 +00:00
|
|
|
|
contentType: IMAGE_JPEG,
|
2021-03-15 21:27:25 +00:00
|
|
|
|
blurHash: await window.imageToBlurHash(
|
|
|
|
|
new Blob([data], {
|
2021-06-25 16:08:16 +00:00
|
|
|
|
type: IMAGE_JPEG,
|
2021-03-15 21:27:25 +00:00
|
|
|
|
})
|
|
|
|
|
),
|
2021-02-10 22:39:26 +00:00
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorString = error && error.stack ? error.stack : error;
|
|
|
|
|
window.log.error(
|
|
|
|
|
`getGroupPreview/${logId}: Failed to fetch avatar ${errorString}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (abortSignal.aborted) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
title,
|
|
|
|
|
description,
|
|
|
|
|
url,
|
|
|
|
|
image,
|
|
|
|
|
date: null,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
2020-09-28 23:46:31 +00:00
|
|
|
|
async getPreview(
|
|
|
|
|
url: string,
|
2021-06-08 20:55:37 +00:00
|
|
|
|
abortSignal: Readonly<AbortSignal>
|
2021-06-25 16:08:16 +00:00
|
|
|
|
): Promise<null | LinkPreviewResult> {
|
2021-06-30 17:00:02 +00:00
|
|
|
|
if (LinkPreview.isStickerPack(url)) {
|
2021-02-10 22:39:26 +00:00
|
|
|
|
return this.getStickerPackPreview(url, abortSignal);
|
|
|
|
|
}
|
2021-06-30 17:00:02 +00:00
|
|
|
|
if (LinkPreview.isGroupLink(url)) {
|
2021-02-10 22:39:26 +00:00
|
|
|
|
return this.getGroupPreview(url, abortSignal);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-09-28 23:46:31 +00:00
|
|
|
|
// This is already checked elsewhere, but we want to be extra-careful.
|
2021-06-30 17:00:02 +00:00
|
|
|
|
if (!LinkPreview.isLinkSafeToPreview(url)) {
|
2020-09-28 23:46:31 +00:00
|
|
|
|
return null;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-09-28 23:46:31 +00:00
|
|
|
|
const linkPreviewMetadata = await window.textsecure.messaging.fetchLinkPreviewMetadata(
|
|
|
|
|
url,
|
|
|
|
|
abortSignal
|
|
|
|
|
);
|
2021-06-03 00:19:40 +00:00
|
|
|
|
if (!linkPreviewMetadata || abortSignal.aborted) {
|
2020-09-28 23:46:31 +00:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const { title, imageHref, description, date } = linkPreviewMetadata;
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
let image;
|
2021-06-30 17:00:02 +00:00
|
|
|
|
if (imageHref && LinkPreview.isLinkSafeToPreview(imageHref)) {
|
2020-09-28 23:46:31 +00:00
|
|
|
|
let objectUrl: void | string;
|
|
|
|
|
try {
|
|
|
|
|
const fullSizeImage = await window.textsecure.messaging.fetchLinkPreviewImage(
|
|
|
|
|
imageHref,
|
|
|
|
|
abortSignal
|
|
|
|
|
);
|
2021-06-03 00:19:40 +00:00
|
|
|
|
if (abortSignal.aborted) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2020-09-28 23:46:31 +00:00
|
|
|
|
if (!fullSizeImage) {
|
|
|
|
|
throw new Error('Failed to fetch link preview image');
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure that this file is either small enough or is resized to meet our
|
|
|
|
|
// requirements for attachments
|
2021-07-28 00:09:10 +00:00
|
|
|
|
const withBlob = await autoScale({
|
2020-09-28 23:46:31 +00:00
|
|
|
|
contentType: fullSizeImage.contentType,
|
|
|
|
|
file: new Blob([fullSizeImage.data], {
|
|
|
|
|
type: fullSizeImage.contentType,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}),
|
2021-07-28 00:09:10 +00:00
|
|
|
|
fileName: title,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = await this.arrayBufferFromFile(withBlob.file);
|
|
|
|
|
objectUrl = URL.createObjectURL(withBlob.file);
|
|
|
|
|
|
2021-03-15 21:27:25 +00:00
|
|
|
|
const blurHash = await window.imageToBlurHash(withBlob.file);
|
|
|
|
|
|
2020-09-28 23:46:31 +00:00
|
|
|
|
const dimensions = await VisualAttachment.getImageDimensions({
|
|
|
|
|
objectUrl,
|
|
|
|
|
logger: window.log,
|
|
|
|
|
});
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
image = {
|
|
|
|
|
data,
|
|
|
|
|
size: data.byteLength,
|
|
|
|
|
...dimensions,
|
|
|
|
|
contentType: withBlob.file.type,
|
2021-03-15 21:27:25 +00:00
|
|
|
|
blurHash,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
};
|
2020-09-28 23:46:31 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
// We still want to show the preview if we failed to get an image
|
|
|
|
|
window.log.error(
|
|
|
|
|
'getPreview failed to get image for link preview:',
|
|
|
|
|
error.message
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
if (objectUrl) {
|
|
|
|
|
URL.revokeObjectURL(objectUrl);
|
|
|
|
|
}
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 00:19:40 +00:00
|
|
|
|
if (abortSignal.aborted) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return {
|
|
|
|
|
title,
|
|
|
|
|
url,
|
|
|
|
|
image,
|
2020-09-28 23:46:31 +00:00
|
|
|
|
description,
|
|
|
|
|
date,
|
2020-09-24 20:57:54 +00:00
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
2020-09-28 23:46:31 +00:00
|
|
|
|
async addLinkPreview(url: string) {
|
|
|
|
|
if (this.currentlyMatchedLink === url) {
|
|
|
|
|
window.log.warn(
|
|
|
|
|
'addLinkPreview should not be called with the same URL like this'
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-16 21:35:54 +00:00
|
|
|
|
(this.preview || []).forEach((item: WhatIsThis) => {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (item.url) {
|
|
|
|
|
URL.revokeObjectURL(item.url);
|
|
|
|
|
}
|
|
|
|
|
});
|
2021-04-27 22:35:35 +00:00
|
|
|
|
window.reduxActions.linkPreviews.removeLinkPreview();
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.preview = null;
|
|
|
|
|
|
2020-09-28 23:46:31 +00:00
|
|
|
|
// Cancel other in-flight link preview requests.
|
|
|
|
|
if (this.linkPreviewAbortController) {
|
|
|
|
|
window.log.info(
|
|
|
|
|
'addLinkPreview: canceling another in-flight link preview request'
|
|
|
|
|
);
|
|
|
|
|
this.linkPreviewAbortController.abort();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const thisRequestAbortController = new AbortController();
|
|
|
|
|
this.linkPreviewAbortController = thisRequestAbortController;
|
|
|
|
|
|
|
|
|
|
const timeout = setTimeout(() => {
|
|
|
|
|
thisRequestAbortController.abort();
|
|
|
|
|
}, LINK_PREVIEW_TIMEOUT);
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.currentlyMatchedLink = url;
|
|
|
|
|
this.renderLinkPreview();
|
|
|
|
|
|
|
|
|
|
try {
|
2020-09-28 23:46:31 +00:00
|
|
|
|
const result = await this.getPreview(
|
|
|
|
|
url,
|
|
|
|
|
thisRequestAbortController.signal
|
|
|
|
|
);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
|
|
|
|
if (!result) {
|
2020-09-28 23:46:31 +00:00
|
|
|
|
window.log.info(
|
|
|
|
|
'addLinkPreview: failed to load preview (not necessarily a problem)'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// This helps us disambiguate between two kinds of failure:
|
|
|
|
|
//
|
|
|
|
|
// 1. We failed to fetch the preview because of (1) a network failure (2) an
|
|
|
|
|
// invalid response (3) a timeout
|
|
|
|
|
// 2. We failed to fetch the preview because we aborted the request because the
|
|
|
|
|
// user changed the link (e.g., by continuing to type the URL)
|
|
|
|
|
const failedToFetch = this.currentlyMatchedLink === url;
|
|
|
|
|
if (failedToFetch) {
|
|
|
|
|
this.excludedPreviewUrls.push(url);
|
|
|
|
|
this.removeLinkPreview();
|
|
|
|
|
}
|
2020-09-24 20:57:54 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (result.image) {
|
|
|
|
|
const blob = new Blob([result.image.data], {
|
|
|
|
|
type: result.image.contentType,
|
|
|
|
|
});
|
|
|
|
|
result.image.url = URL.createObjectURL(blob);
|
|
|
|
|
} else if (!result.title) {
|
|
|
|
|
// A link preview isn't worth showing unless we have either a title or an image
|
|
|
|
|
this.removeLinkPreview();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-27 22:35:35 +00:00
|
|
|
|
window.reduxActions.linkPreviews.addLinkPreview(result);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
this.preview = [result];
|
|
|
|
|
this.renderLinkPreview();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
window.log.error(
|
|
|
|
|
'Problem loading link preview, disabling.',
|
|
|
|
|
error && error.stack ? error.stack : error
|
|
|
|
|
);
|
|
|
|
|
this.disableLinkPreviews = true;
|
|
|
|
|
this.removeLinkPreview();
|
2020-09-28 23:46:31 +00:00
|
|
|
|
} finally {
|
|
|
|
|
clearTimeout(timeout);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
renderLinkPreview() {
|
2021-04-27 22:35:35 +00:00
|
|
|
|
if (this.forwardMessageModal) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-06-25 16:08:16 +00:00
|
|
|
|
window.reduxActions.composer.setLinkPreviewResult(
|
|
|
|
|
Boolean(this.currentlyMatchedLink),
|
|
|
|
|
this.getLinkPreviewWithDomain()
|
|
|
|
|
);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-08-13 15:01:56 +00:00
|
|
|
|
getLinkPreviewForSend(message: string) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
// Don't generate link previews if user has turned them off
|
|
|
|
|
if (!window.storage.get('linkPreviews', false)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this.preview) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-13 15:01:56 +00:00
|
|
|
|
const urlsInMessage = new Set<string>(LinkPreview.findLinks(message));
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
this.preview
|
|
|
|
|
// This bullet-proofs against sending link previews for URLs that are no longer in
|
|
|
|
|
// the message. This can happen if you have a link preview, then quickly delete
|
|
|
|
|
// the link and send the message.
|
|
|
|
|
.filter(({ url }: Readonly<{ url: string }>) => urlsInMessage.has(url))
|
|
|
|
|
.map((item: WhatIsThis) => {
|
|
|
|
|
if (item.image) {
|
|
|
|
|
// We eliminate the ObjectURL here, unneeded for send or save
|
|
|
|
|
return {
|
|
|
|
|
...item,
|
|
|
|
|
image: window._.omit(item.image, 'url'),
|
|
|
|
|
};
|
|
|
|
|
}
|
2020-09-24 20:57:54 +00:00
|
|
|
|
|
2021-08-13 15:01:56 +00:00
|
|
|
|
return item;
|
|
|
|
|
})
|
|
|
|
|
);
|
2020-09-24 20:57:54 +00:00
|
|
|
|
},
|
|
|
|
|
|
2021-06-25 16:08:16 +00:00
|
|
|
|
getLinkPreviewWithDomain(): LinkPreviewWithDomain | undefined {
|
|
|
|
|
if (!this.preview || !this.preview.length) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [preview] = this.preview;
|
|
|
|
|
return {
|
|
|
|
|
...preview,
|
2021-06-30 17:00:02 +00:00
|
|
|
|
domain: LinkPreview.getDomain(preview.url),
|
2021-06-25 16:08:16 +00:00
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
2020-09-24 20:57:54 +00:00
|
|
|
|
// Called whenever the user changes the message composition field. But only
|
|
|
|
|
// fires if there's content in the message field after the change.
|
2021-06-08 14:59:38 +00:00
|
|
|
|
maybeBumpTyping(messageText: string) {
|
2020-09-24 20:57:54 +00:00
|
|
|
|
if (messageText.length) {
|
|
|
|
|
this.model.throttledBumpTyping();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|