Option to send photos as high quality
This commit is contained in:
parent
6c56d5a5f1
commit
01eabf9ec6
44 changed files with 1263 additions and 363 deletions
|
@ -719,30 +719,6 @@ Signal Desktop makes use of the following open source projects.
|
|||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
## blueimp-canvas-to-blob
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright © 2012 Sebastian Tschan, https://blueimp.net
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## blueimp-load-image
|
||||
|
||||
MIT License
|
||||
|
|
|
@ -5605,5 +5605,29 @@
|
|||
"ConversationDetailsHeader--add-group-description": {
|
||||
"message": "Add group description...",
|
||||
"description": "Placeholder text in the details header for those that can edit the group description"
|
||||
},
|
||||
"MediaQualitySelector--button": {
|
||||
"message": "Select media quality",
|
||||
"description": "aria-label for the media quality selector button"
|
||||
},
|
||||
"MediaQualitySelector--title": {
|
||||
"message": "Media Quality",
|
||||
"description": "Popup selector title"
|
||||
},
|
||||
"MediaQualitySelector--standard-quality-title": {
|
||||
"message": "Standard",
|
||||
"description": "Title for option for standard quality"
|
||||
},
|
||||
"MediaQualitySelector--standard-quality-description": {
|
||||
"message": "Faster, less data",
|
||||
"description": "Description of standard quality selector"
|
||||
},
|
||||
"MediaQualitySelector--high-quality-title": {
|
||||
"message": "High",
|
||||
"description": "Title for option for high quality"
|
||||
},
|
||||
"MediaQualitySelector--high-quality-description": {
|
||||
"message": "Slower, more data",
|
||||
"description": "Description of high quality selector"
|
||||
}
|
||||
}
|
||||
|
|
1
images/icons/v2/hq-outline-24.svg
Normal file
1
images/icons/v2/hq-outline-24.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m11.2 15.8 3-3.8 3.8 5.2h-12l3-3.9z"/><path d="m19.7 3.5a.9.9 0 0 1 .8.8v15.4a.9.9 0 0 1 -.8.8h-15.4a.9.9 0 0 1 -.8-.8v-15.4a.9.9 0 0 1 .8-.8zm0-1.5h-15.4a2.3 2.3 0 0 0 -2.3 2.3v15.4a2.3 2.3 0 0 0 2.3 2.3h15.4a2.3 2.3 0 0 0 2.3-2.3v-15.4a2.3 2.3 0 0 0 -2.3-2.3z"/></svg>
|
After Width: | Height: | Size: 339 B |
1
images/icons/v2/hq-solid-24.svg
Normal file
1
images/icons/v2/hq-solid-24.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m19.7 2h-15.4a2.3 2.3 0 0 0 -2.3 2.3v15.4a2.3 2.3 0 0 0 2.3 2.3h15.4a2.3 2.3 0 0 0 2.3-2.3v-15.4a2.3 2.3 0 0 0 -2.3-2.3zm-14.4 15.5 3.3-4.4 2.4 2.9 3.4-4.3 4.3 5.8z"/></svg>
|
After Width: | Height: | Size: 242 B |
1
images/icons/v2/sq-24.svg
Normal file
1
images/icons/v2/sq-24.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><circle cx="21.2" cy="7.4" r="1"/><circle cx="3" cy="3" r="1"/><circle cx="3" cy="7.5" r="1"/><circle cx="7.5" cy="3" r="1"/><circle cx="12" cy="3" r="1"/><circle cx="16.5" cy="3" r="1"/><circle cx="21" cy="3" r="1"/><circle cx="21" cy="12" r="1"/><circle cx="21" cy="16.5" r="1"/><circle cx="21" cy="21" r="1"/><circle cx="16.5" cy="21" r="1"/><path d="m12.8 10.5h-10a.7.7 0 0 0 -.8.7v10a.7.7 0 0 0 .8.8h10a.7.7 0 0 0 .7-.8v-10a.7.7 0 0 0 -.7-.7zm-9.6 7.8 2.3-3.1 1.6 2.1 2.3-3.2 3 4.2z"/></svg>
|
After Width: | Height: | Size: 556 B |
|
@ -1,43 +0,0 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
const loadImage = require('blueimp-load-image');
|
||||
|
||||
const DEFAULT_JPEG_QUALITY = 0.85;
|
||||
|
||||
// File | Blob | URLString -> LoadImageOptions -> Promise<DataURLString>
|
||||
//
|
||||
// Documentation for `options` (`LoadImageOptions`):
|
||||
// https://github.com/blueimp/JavaScript-Load-Image/tree/v2.18.0#options
|
||||
exports.autoOrientImage = (fileOrBlobOrURL, options = {}) => {
|
||||
const optionsWithDefaults = {
|
||||
type: 'image/jpeg',
|
||||
quality: DEFAULT_JPEG_QUALITY,
|
||||
...options,
|
||||
canvas: true,
|
||||
orientation: true,
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
loadImage(
|
||||
fileOrBlobOrURL,
|
||||
canvasOrError => {
|
||||
if (canvasOrError.type === 'error') {
|
||||
const error = new Error('autoOrientImage: Failed to process image');
|
||||
error.originalError = canvasOrError;
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = canvasOrError;
|
||||
const dataURL = canvas.toDataURL(
|
||||
optionsWithDefaults.type,
|
||||
optionsWithDefaults.quality
|
||||
);
|
||||
|
||||
resolve(dataURL);
|
||||
},
|
||||
optionsWithDefaults
|
||||
);
|
||||
});
|
||||
};
|
|
@ -3,16 +3,12 @@
|
|||
|
||||
const is = require('@sindresorhus/is');
|
||||
|
||||
const {
|
||||
arrayBufferToBlob,
|
||||
blobToArrayBuffer,
|
||||
dataURLToBlob,
|
||||
} = require('blob-util');
|
||||
const { arrayBufferToBlob, blobToArrayBuffer } = require('blob-util');
|
||||
const AttachmentTS = require('../../../ts/types/Attachment');
|
||||
const GoogleChrome = require('../../../ts/util/GoogleChrome');
|
||||
const MIME = require('../../../ts/types/MIME');
|
||||
const { toLogFormat } = require('./errors');
|
||||
const { autoOrientImage } = require('../auto_orient_image');
|
||||
const { scaleImageToLevel } = require('../../../ts/util/scaleImageToLevel');
|
||||
const {
|
||||
migrateDataToFileSystem,
|
||||
} = require('./attachment/migrate_data_to_file_system');
|
||||
|
@ -54,7 +50,7 @@ exports.isValid = rawAttachment => {
|
|||
// Upgrade steps
|
||||
// NOTE: This step strips all EXIF metadata from JPEG images as
|
||||
// part of re-encoding the image:
|
||||
exports.autoOrientJPEG = async attachment => {
|
||||
exports.autoOrientJPEG = async (attachment, _, message) => {
|
||||
if (!MIME.isJPEG(attachment.contentType)) {
|
||||
return attachment;
|
||||
}
|
||||
|
@ -68,24 +64,27 @@ exports.autoOrientJPEG = async attachment => {
|
|||
attachment.data,
|
||||
attachment.contentType
|
||||
);
|
||||
const newDataBlob = await dataURLToBlob(await autoOrientImage(dataBlob));
|
||||
const newDataArrayBuffer = await blobToArrayBuffer(newDataBlob);
|
||||
const xcodedDataBlob = await scaleImageToLevel(
|
||||
dataBlob,
|
||||
message.sendHQImages
|
||||
);
|
||||
const xcodedDataArrayBuffer = await blobToArrayBuffer(xcodedDataBlob);
|
||||
|
||||
// IMPORTANT: We overwrite the existing `data` `ArrayBuffer` losing the original
|
||||
// image data. Ideally, we’d preserve the original image data for users who want to
|
||||
// retain it but due to reports of data loss, we don’t want to overburden IndexedDB
|
||||
// by potentially doubling stored image data.
|
||||
// See: https://github.com/signalapp/Signal-Desktop/issues/1589
|
||||
const newAttachment = {
|
||||
const xcodedAttachment = {
|
||||
...attachment,
|
||||
data: newDataArrayBuffer,
|
||||
size: newDataArrayBuffer.byteLength,
|
||||
data: xcodedDataArrayBuffer,
|
||||
size: xcodedDataArrayBuffer.byteLength,
|
||||
};
|
||||
|
||||
// `digest` is no longer valid for auto-oriented image data, so we discard it:
|
||||
delete newAttachment.digest;
|
||||
delete xcodedAttachment.digest;
|
||||
|
||||
return newAttachment;
|
||||
return xcodedAttachment;
|
||||
};
|
||||
|
||||
const UNICODE_LEFT_TO_RIGHT_OVERRIDE = '\u202D';
|
||||
|
|
|
@ -170,7 +170,7 @@ exports._withSchemaVersion = ({ schemaVersion, upgrade }) => {
|
|||
// Promise Message
|
||||
exports._mapAttachments = upgradeAttachment => async (message, context) => {
|
||||
const upgradeWithContext = attachment =>
|
||||
upgradeAttachment(attachment, context);
|
||||
upgradeAttachment(attachment, context, message);
|
||||
const attachments = await Promise.all(
|
||||
(message.attachments || []).map(upgradeWithContext)
|
||||
);
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* global document, URL, Blob */
|
||||
|
||||
const loadImage = require('blueimp-load-image');
|
||||
const dataURLToBlobSync = require('blueimp-canvas-to-blob');
|
||||
const { blobToArrayBuffer } = require('blob-util');
|
||||
const { toLogFormat } = require('./errors');
|
||||
const {
|
||||
arrayBufferToObjectURL,
|
||||
} = require('../../../ts/util/arrayBufferToObjectURL');
|
||||
const { canvasToBlob } = require('../../../ts/util/canvasToBlob');
|
||||
|
||||
exports.blobToArrayBuffer = blobToArrayBuffer;
|
||||
|
||||
|
@ -40,7 +40,7 @@ exports.makeImageThumbnail = ({
|
|||
new Promise((resolve, reject) => {
|
||||
const image = document.createElement('img');
|
||||
|
||||
image.addEventListener('load', () => {
|
||||
image.addEventListener('load', async () => {
|
||||
// using components/blueimp-load-image
|
||||
|
||||
// first, make the correct size
|
||||
|
@ -63,9 +63,12 @@ exports.makeImageThumbnail = ({
|
|||
minHeight: size,
|
||||
});
|
||||
|
||||
const blob = dataURLToBlobSync(canvas.toDataURL(contentType));
|
||||
|
||||
resolve(blob);
|
||||
try {
|
||||
const blob = await canvasToBlob(canvas, contentType);
|
||||
resolve(blob);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
image.addEventListener('error', error => {
|
||||
|
@ -88,7 +91,7 @@ exports.makeVideoScreenshot = ({
|
|||
video.currentTime = 1.0;
|
||||
}
|
||||
|
||||
function capture() {
|
||||
async function capture() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
|
@ -96,12 +99,15 @@ exports.makeVideoScreenshot = ({
|
|||
.getContext('2d')
|
||||
.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const image = dataURLToBlobSync(canvas.toDataURL(contentType));
|
||||
|
||||
video.addEventListener('loadeddata', seek);
|
||||
video.removeEventListener('seeked', capture);
|
||||
|
||||
resolve(image);
|
||||
try {
|
||||
const image = canvasToBlob(canvas, contentType);
|
||||
resolve(image);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
video.addEventListener('loadeddata', seek);
|
||||
|
|
|
@ -78,7 +78,6 @@
|
|||
"backbone": "1.4.0",
|
||||
"better-sqlite3": "https://github.com/signalapp/better-sqlite3#2fa02d2484e9f9a10df5ac7ea4617fb2dff30006",
|
||||
"blob-util": "1.3.0",
|
||||
"blueimp-canvas-to-blob": "3.14.0",
|
||||
"blueimp-load-image": "5.14.0",
|
||||
"blurhash": "1.1.3",
|
||||
"classnames": "2.2.5",
|
||||
|
|
25
patches/@types+blueimp-load-image+5.14.1.patch
Normal file
25
patches/@types+blueimp-load-image+5.14.1.patch
Normal file
|
@ -0,0 +1,25 @@
|
|||
diff --git a/node_modules/@types/blueimp-load-image/index.d.ts b/node_modules/@types/blueimp-load-image/index.d.ts
|
||||
index 285505b..da92b91 100644
|
||||
--- a/node_modules/@types/blueimp-load-image/index.d.ts
|
||||
+++ b/node_modules/@types/blueimp-load-image/index.d.ts
|
||||
@@ -9,7 +9,7 @@
|
||||
declare namespace loadImage {
|
||||
type LoadImageCallback = (eventOrImage: Event | HTMLCanvasElement | HTMLImageElement, data?: MetaData) => void;
|
||||
type LoadImageResult = MetaData & {
|
||||
- image: HTMLImageElement | FileReader | false;
|
||||
+ image: HTMLImageElement | HTMLCanvasElement;
|
||||
};
|
||||
|
||||
type ParseMetaDataCallback = (data: MetaData) => void;
|
||||
@@ -122,6 +122,11 @@ interface LoadImage {
|
||||
) => void;
|
||||
|
||||
blobSlice: (this: Blob, start?: number, end?: number) => Blob;
|
||||
+
|
||||
+ scale: (
|
||||
+ img: HTMLImageElement | HTMLCanvasElement,
|
||||
+ options?: loadImage.LoadImageOptions
|
||||
+ ) => HTMLImageElement | HTMLCanvasElement;
|
||||
}
|
||||
|
||||
declare const loadImage: LoadImage;
|
|
@ -484,14 +484,11 @@ try {
|
|||
window.nodeSetImmediate(() => {});
|
||||
}, 1000);
|
||||
|
||||
const { autoOrientImage } = require('./js/modules/auto_orient_image');
|
||||
const { imageToBlurHash } = require('./ts/util/imageToBlurHash');
|
||||
const { isGroupCallingEnabled } = require('./ts/util/isGroupCallingEnabled');
|
||||
const { isValidGuid } = require('./ts/util/isValidGuid');
|
||||
const { ActiveWindowService } = require('./ts/services/ActiveWindowService');
|
||||
|
||||
window.autoOrientImage = autoOrientImage;
|
||||
window.dataURLToBlobSync = require('blueimp-canvas-to-blob');
|
||||
window.imageToBlurHash = imageToBlurHash;
|
||||
window.emojiData = require('emoji-datasource');
|
||||
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
|
||||
|
|
|
@ -10575,6 +10575,11 @@ $contact-modal-padding: 18px;
|
|||
}
|
||||
}
|
||||
|
||||
.react-contextmenu-item--disabled.react-contextmenu-item--selected {
|
||||
background-color: inherit;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.react-contextmenu-item.react-contextmenu-item--active.react-contextmenu-item--checked:before,
|
||||
.react-contextmenu-item.react-contextmenu-item--selected.react-contextmenu-item--checked:before {
|
||||
color: $color-black;
|
||||
|
|
116
stylesheets/components/MediaQualitySelector.scss
Normal file
116
stylesheets/components/MediaQualitySelector.scss
Normal file
|
@ -0,0 +1,116 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.MediaQualitySelector {
|
||||
&__popper {
|
||||
@extend %module-composition-popper;
|
||||
padding: 12px 16px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&__title {
|
||||
@include font-body-1-bold;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
@include button-reset();
|
||||
align-items: center;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
width: 32px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
@include light-theme {
|
||||
@include color-svg('../images/icons/v2/sq-24.svg', $color-gray-75);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg('../images/icons/v2/sq-24.svg', $color-gray-15);
|
||||
}
|
||||
}
|
||||
|
||||
&--hq {
|
||||
&::after {
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/hq-outline-24.svg',
|
||||
$color-gray-75
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/hq-solid-24.svg',
|
||||
$color-gray-15
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
opacity: 1;
|
||||
|
||||
@include light-theme() {
|
||||
background-color: $color-gray-05;
|
||||
}
|
||||
|
||||
@include dark-theme() {
|
||||
background-color: $color-gray-75;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__option {
|
||||
@include button-reset();
|
||||
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
height: 42px;
|
||||
margin: 2px 0;
|
||||
min-width: 200px;
|
||||
|
||||
&--checkmark {
|
||||
height: 12px;
|
||||
margin: 0 6px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
@include color-svg('../images/icons/v2/check-24.svg', $color-ultramarine);
|
||||
}
|
||||
|
||||
&--title {
|
||||
@include font-body-2;
|
||||
}
|
||||
|
||||
&--description {
|
||||
@include font-subtitle;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include light-theme() {
|
||||
background-color: $color-gray-05;
|
||||
}
|
||||
|
||||
@include dark-theme() {
|
||||
background-color: $color-gray-65;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 1px 1px $color-ultramarine;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,6 +49,7 @@
|
|||
@import './components/GroupDescription.scss';
|
||||
@import './components/GroupDialog.scss';
|
||||
@import './components/GroupInput.scss';
|
||||
@import './components/MediaQualitySelector.scss';
|
||||
@import './components/MessageAudio.scss';
|
||||
@import './components/Modal.scss';
|
||||
@import './components/SafetyNumberChangeDialog.scss';
|
||||
|
|
|
@ -10,6 +10,7 @@ export type ConfigKeyType =
|
|||
| 'desktop.groupCalling'
|
||||
| 'desktop.gv2'
|
||||
| 'desktop.mandatoryProfileSharing'
|
||||
| 'desktop.mediaQuality.levels'
|
||||
| 'desktop.messageRequests'
|
||||
| 'desktop.retryReceiptLifespan'
|
||||
| 'desktop.retryRespondMaxAge'
|
||||
|
|
|
@ -984,6 +984,7 @@ export async function startApp(): Promise<void> {
|
|||
store.dispatch
|
||||
),
|
||||
calling: bindActionCreators(actionCreators.calling, store.dispatch),
|
||||
composer: bindActionCreators(actionCreators.composer, store.dispatch),
|
||||
conversations: bindActionCreators(
|
||||
actionCreators.conversations,
|
||||
store.dispatch
|
||||
|
|
|
@ -32,6 +32,25 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
i18n,
|
||||
micCellEl,
|
||||
onChooseAttachment: action('onChooseAttachment'),
|
||||
// AttachmentList
|
||||
draftAttachments: [],
|
||||
onAddAttachment: action('onAddAttachment'),
|
||||
onClearAttachments: action('onClearAttachments'),
|
||||
onClickAttachment: action('onClickAttachment'),
|
||||
onCloseAttachment: action('onCloseAttachment'),
|
||||
// StagedLinkPreview
|
||||
linkPreviewLoading: Boolean(overrideProps.linkPreviewLoading),
|
||||
linkPreviewResult: overrideProps.linkPreviewResult,
|
||||
onCloseLinkPreview: action('onCloseLinkPreview'),
|
||||
// Quote
|
||||
quotedMessageProps: overrideProps.quotedMessageProps,
|
||||
onClickQuotedMessage: action('onClickQuotedMessage'),
|
||||
setQuotedMessage: action('setQuotedMessage'),
|
||||
// MediaQualitySelector
|
||||
onSelectMediaQuality: action('onSelectMediaQuality'),
|
||||
shouldSendHighQualityAttachments: Boolean(
|
||||
overrideProps.shouldSendHighQualityAttachments
|
||||
),
|
||||
// CompositionInput
|
||||
onSubmit: action('onSubmit'),
|
||||
onEditorStateChange: action('onEditorStateChange'),
|
||||
|
|
|
@ -31,6 +31,12 @@ import { MandatoryProfileSharingActions } from './conversation/MandatoryProfileS
|
|||
import { countStickers } from './stickers/lib';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { EmojiPickDataType } from './emoji/EmojiPicker';
|
||||
import { AttachmentType, isImageAttachment } from '../types/Attachment';
|
||||
import { AttachmentList } from './conversation/AttachmentList';
|
||||
import { MediaQualitySelector } from './MediaQualitySelector';
|
||||
import { Quote, Props as QuoteProps } from './conversation/Quote';
|
||||
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
||||
import { LinkPreviewWithDomain } from '../types/LinkPreview';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
|
@ -50,14 +56,24 @@ export type OwnProps = {
|
|||
setDisabled: (disabled: boolean) => void;
|
||||
setShowMic: (showMic: boolean) => void;
|
||||
setMicActive: (micActive: boolean) => void;
|
||||
attSlotRef: React.RefObject<HTMLDivElement>;
|
||||
reset: InputApi['reset'];
|
||||
resetEmojiResults: InputApi['resetEmojiResults'];
|
||||
}>;
|
||||
readonly micCellEl?: HTMLElement;
|
||||
readonly attCellEl?: HTMLElement;
|
||||
readonly attachmentListEl?: HTMLElement;
|
||||
readonly draftAttachments: Array<AttachmentType>;
|
||||
readonly shouldSendHighQualityAttachments: boolean;
|
||||
onChooseAttachment(): unknown;
|
||||
onAddAttachment(): unknown;
|
||||
onClickAttachment(): unknown;
|
||||
onCloseAttachment(): unknown;
|
||||
onClearAttachments(): unknown;
|
||||
onSelectMediaQuality(isHQ: boolean): unknown;
|
||||
readonly quotedMessageProps?: QuoteProps;
|
||||
onClickQuotedMessage(): unknown;
|
||||
setQuotedMessage(message: undefined): unknown;
|
||||
linkPreviewLoading: boolean;
|
||||
linkPreviewResult?: LinkPreviewWithDomain;
|
||||
onCloseLinkPreview(): unknown;
|
||||
};
|
||||
|
||||
export type Props = Pick<
|
||||
|
@ -103,9 +119,25 @@ const emptyElement = (el: HTMLElement) => {
|
|||
|
||||
export const CompositionArea = ({
|
||||
i18n,
|
||||
attachmentListEl,
|
||||
micCellEl,
|
||||
onChooseAttachment,
|
||||
// AttachmentList
|
||||
draftAttachments,
|
||||
onAddAttachment,
|
||||
onClearAttachments,
|
||||
onClickAttachment,
|
||||
onCloseAttachment,
|
||||
// StagedLinkPreview
|
||||
linkPreviewLoading,
|
||||
linkPreviewResult,
|
||||
onCloseLinkPreview,
|
||||
// Quote
|
||||
quotedMessageProps,
|
||||
onClickQuotedMessage,
|
||||
setQuotedMessage,
|
||||
// MediaQualitySelector
|
||||
onSelectMediaQuality,
|
||||
shouldSendHighQualityAttachments,
|
||||
// CompositionInput
|
||||
onSubmit,
|
||||
compositionApi,
|
||||
|
@ -198,9 +230,6 @@ export const CompositionArea = ({
|
|||
receivedPacks,
|
||||
}) > 0;
|
||||
|
||||
// A ref to grab a slot where backbone can insert link previews and attachments
|
||||
const attSlotRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
if (compositionApi) {
|
||||
// Using a React.MutableRefObject, so we need to reassign this prop.
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
|
@ -210,7 +239,6 @@ export const CompositionArea = ({
|
|||
setDisabled,
|
||||
setShowMic,
|
||||
setMicActive,
|
||||
attSlotRef,
|
||||
reset: () => {
|
||||
if (inputApiRef.current) {
|
||||
inputApiRef.current.reset();
|
||||
|
@ -251,27 +279,31 @@ export const CompositionArea = ({
|
|||
return noop;
|
||||
}, [micCellRef, micCellEl, large, dirty, showMic]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const { current: attSlot } = attSlotRef;
|
||||
if (attSlot && attachmentListEl) {
|
||||
attSlot.appendChild(attachmentListEl);
|
||||
}
|
||||
const showMediaQualitySelector = draftAttachments.some(isImageAttachment);
|
||||
|
||||
return noop;
|
||||
}, [attSlotRef, attachmentListEl]);
|
||||
|
||||
const emojiButtonFragment = (
|
||||
<div className="module-composition-area__button-cell">
|
||||
<EmojiButton
|
||||
i18n={i18n}
|
||||
doSend={handleForceSend}
|
||||
onPickEmoji={insertEmoji}
|
||||
onClose={focusInput}
|
||||
recentEmojis={recentEmojis}
|
||||
skinTone={skinTone}
|
||||
onSetSkinTone={onSetSkinTone}
|
||||
/>
|
||||
</div>
|
||||
const leftHandSideButtonsFragment = (
|
||||
<>
|
||||
<div className="module-composition-area__button-cell">
|
||||
<EmojiButton
|
||||
i18n={i18n}
|
||||
doSend={handleForceSend}
|
||||
onPickEmoji={insertEmoji}
|
||||
onClose={focusInput}
|
||||
recentEmojis={recentEmojis}
|
||||
skinTone={skinTone}
|
||||
onSetSkinTone={onSetSkinTone}
|
||||
/>
|
||||
</div>
|
||||
{showMediaQualitySelector ? (
|
||||
<div className="module-composition-area__button-cell">
|
||||
<MediaQualitySelector
|
||||
i18n={i18n}
|
||||
isHighQuality={shouldSendHighQualityAttachments}
|
||||
onSelectQuality={onSelectMediaQuality}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
const micButtonFragment = showMic ? (
|
||||
|
@ -480,15 +512,52 @@ export const CompositionArea = ({
|
|||
'module-composition-area__row',
|
||||
'module-composition-area__row--column'
|
||||
)}
|
||||
ref={attSlotRef}
|
||||
/>
|
||||
>
|
||||
{quotedMessageProps && (
|
||||
<div className="quote-wrapper">
|
||||
<Quote
|
||||
{...quotedMessageProps}
|
||||
i18n={i18n}
|
||||
onClick={onClickQuotedMessage}
|
||||
onClose={() => {
|
||||
// This one is for redux...
|
||||
setQuotedMessage(undefined);
|
||||
// and this is for conversation_view.
|
||||
clearQuotedMessage();
|
||||
}}
|
||||
withContentAbove
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{linkPreviewLoading && (
|
||||
<div className="preview-wrapper">
|
||||
<StagedLinkPreview
|
||||
{...(linkPreviewResult || {})}
|
||||
i18n={i18n}
|
||||
onClose={onCloseLinkPreview}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{draftAttachments.length ? (
|
||||
<div className="module-composition-area__attachment-list">
|
||||
<AttachmentList
|
||||
attachments={draftAttachments}
|
||||
i18n={i18n}
|
||||
onAddAttachment={onAddAttachment}
|
||||
onClickAttachment={onClickAttachment}
|
||||
onClose={onClearAttachments}
|
||||
onCloseAttachment={onCloseAttachment}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-composition-area__row',
|
||||
large ? 'module-composition-area__row--padded' : null
|
||||
)}
|
||||
>
|
||||
{!large ? emojiButtonFragment : null}
|
||||
{!large ? leftHandSideButtonsFragment : null}
|
||||
<div className="module-composition-area__input">
|
||||
<CompositionInput
|
||||
i18n={i18n}
|
||||
|
@ -523,7 +592,7 @@ export const CompositionArea = ({
|
|||
'module-composition-area__row--control-row'
|
||||
)}
|
||||
>
|
||||
{emojiButtonFragment}
|
||||
{leftHandSideButtonsFragment}
|
||||
{stickerButtonFragment}
|
||||
{attButton}
|
||||
{!dirty ? micButtonFragment : null}
|
||||
|
|
|
@ -269,7 +269,6 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
domain={linkPreview.url}
|
||||
i18n={i18n}
|
||||
image={linkPreview.image}
|
||||
isLoaded
|
||||
onClose={() => removeLinkPreview()}
|
||||
title={linkPreview.title}
|
||||
/>
|
||||
|
|
34
ts/components/MediaQualitySelector.stories.tsx
Normal file
34
ts/components/MediaQualitySelector.stories.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { MediaQualitySelector, PropsType } from './MediaQualitySelector';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
|
||||
const story = storiesOf('Components/MediaQualitySelector', module);
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
i18n,
|
||||
isHighQuality: boolean('isHighQuality', Boolean(overrideProps.isHighQuality)),
|
||||
onSelectQuality: action('onSelectQuality'),
|
||||
});
|
||||
|
||||
story.add('Standard Quality', () => (
|
||||
<MediaQualitySelector {...createProps()} />
|
||||
));
|
||||
|
||||
story.add('High Quality', () => (
|
||||
<MediaQualitySelector
|
||||
{...createProps({
|
||||
isHighQuality: true,
|
||||
})}
|
||||
/>
|
||||
));
|
152
ts/components/MediaQualitySelector.tsx
Normal file
152
ts/components/MediaQualitySelector.tsx
Normal file
|
@ -0,0 +1,152 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { KeyboardEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { noop } from 'lodash';
|
||||
import { createPortal } from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
isHighQuality: boolean;
|
||||
onSelectQuality: (isHQ: boolean) => unknown;
|
||||
};
|
||||
|
||||
export const MediaQualitySelector = ({
|
||||
i18n,
|
||||
isHighQuality,
|
||||
onSelectQuality,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [menuShowing, setMenuShowing] = useState(false);
|
||||
const [popperRoot, setPopperRoot] = useState<HTMLElement | null>(null);
|
||||
|
||||
// We use regular MouseEvent below, and this one uses React.MouseEvent
|
||||
const handleClick = (ev: KeyboardEvent | React.MouseEvent) => {
|
||||
setMenuShowing(true);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setMenuShowing(false);
|
||||
}, [setMenuShowing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (menuShowing) {
|
||||
const root = document.createElement('div');
|
||||
setPopperRoot(root);
|
||||
document.body.appendChild(root);
|
||||
const handleOutsideClick = (event: MouseEvent) => {
|
||||
if (!root.contains(event.target as Node)) {
|
||||
handleClose();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleOutsideClick);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(root);
|
||||
document.removeEventListener('click', handleOutsideClick);
|
||||
setPopperRoot(null);
|
||||
};
|
||||
}
|
||||
|
||||
return noop;
|
||||
}, [menuShowing, setPopperRoot, handleClose]);
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<button
|
||||
aria-label={i18n('MediaQualitySelector--button')}
|
||||
className={classNames({
|
||||
MediaQualitySelector__button: true,
|
||||
'MediaQualitySelector__button--hq': isHighQuality,
|
||||
'MediaQualitySelector__button--active': menuShowing,
|
||||
})}
|
||||
onClick={handleClick}
|
||||
ref={ref}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
</Reference>
|
||||
{menuShowing && popperRoot
|
||||
? createPortal(
|
||||
<Popper placement="top-start" positionFixed>
|
||||
{({ ref, style, placement }) => (
|
||||
<div
|
||||
className="MediaQualitySelector__popper"
|
||||
data-placement={placement}
|
||||
ref={ref}
|
||||
style={style}
|
||||
>
|
||||
<div className="MediaQualitySelector__title">
|
||||
{i18n('MediaQualitySelector--title')}
|
||||
</div>
|
||||
<button
|
||||
aria-label={i18n(
|
||||
'MediaQualitySelector--standard-quality-title'
|
||||
)}
|
||||
className="MediaQualitySelector__option"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelectQuality(false);
|
||||
setMenuShowing(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
'MediaQualitySelector__option--checkmark': true,
|
||||
'MediaQualitySelector__option--selected': !isHighQuality,
|
||||
})}
|
||||
/>
|
||||
<div>
|
||||
<div className="MediaQualitySelector__option--title">
|
||||
{i18n('MediaQualitySelector--standard-quality-title')}
|
||||
</div>
|
||||
<div className="MediaQualitySelector__option--description">
|
||||
{i18n(
|
||||
'MediaQualitySelector--standard-quality-description'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label={i18n(
|
||||
'MediaQualitySelector--high-quality-title'
|
||||
)}
|
||||
className="MediaQualitySelector__option"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelectQuality(true);
|
||||
setMenuShowing(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
'MediaQualitySelector__option--checkmark': true,
|
||||
'MediaQualitySelector__option--selected': isHighQuality,
|
||||
})}
|
||||
/>
|
||||
<div>
|
||||
<div className="MediaQualitySelector__option--title">
|
||||
{i18n('MediaQualitySelector--high-quality-title')}
|
||||
</div>
|
||||
<div className="MediaQualitySelector__option--description">
|
||||
{i18n('MediaQualitySelector--high-quality-description')}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Popper>,
|
||||
popperRoot
|
||||
)
|
||||
: null}
|
||||
</Manager>
|
||||
);
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
@ -6,7 +6,7 @@ import moment, { Moment } from 'moment';
|
|||
import { isLinkPreviewDateValid } from '../../linkPreviews/isLinkPreviewDateValid';
|
||||
|
||||
type Props = {
|
||||
date: null | number;
|
||||
date?: null | number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, date, text, withKnobs } from '@storybook/addon-knobs';
|
||||
import { date, text, withKnobs } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { AttachmentType } from '../../types/Attachment';
|
||||
|
@ -36,7 +36,6 @@ const createAttachment = (
|
|||
});
|
||||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
isLoaded: boolean('isLoaded', overrideProps.isLoaded !== false),
|
||||
title: text(
|
||||
'title',
|
||||
typeof overrideProps.title === 'string'
|
||||
|
@ -57,9 +56,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
});
|
||||
|
||||
story.add('Loading', () => {
|
||||
const props = createProps({
|
||||
isLoaded: false,
|
||||
});
|
||||
const props = createProps({ domain: '' });
|
||||
|
||||
return <StagedLinkPreview {...props} />;
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
@ -11,11 +11,10 @@ import { AttachmentType, isImageAttachment } from '../../types/Attachment';
|
|||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type Props = {
|
||||
isLoaded: boolean;
|
||||
title: string;
|
||||
description: null | string;
|
||||
date: null | number;
|
||||
domain: string;
|
||||
title?: string;
|
||||
description?: null | string;
|
||||
date?: null | number;
|
||||
domain?: string;
|
||||
image?: AttachmentType;
|
||||
|
||||
i18n: LocalizerType;
|
||||
|
@ -23,7 +22,6 @@ export type Props = {
|
|||
};
|
||||
|
||||
export const StagedLinkPreview: React.FC<Props> = ({
|
||||
isLoaded,
|
||||
onClose,
|
||||
i18n,
|
||||
title,
|
||||
|
@ -33,6 +31,7 @@ export const StagedLinkPreview: React.FC<Props> = ({
|
|||
domain,
|
||||
}: Props) => {
|
||||
const isImage = isImageAttachment(image);
|
||||
const isLoaded = Boolean(domain);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -46,7 +45,7 @@ export const StagedLinkPreview: React.FC<Props> = ({
|
|||
{i18n('loadingPreview')}
|
||||
</div>
|
||||
) : null}
|
||||
{isLoaded && image && isImage ? (
|
||||
{isLoaded && image && isImage && domain ? (
|
||||
<div className="module-staged-link-preview__icon-container">
|
||||
<Image
|
||||
alt={i18n('stagedPreviewThumbnail', [domain])}
|
||||
|
|
7
ts/model-types.d.ts
vendored
7
ts/model-types.d.ts
vendored
|
@ -56,12 +56,13 @@ export type QuotedMessageType = {
|
|||
// `author` is an old attribute that holds the author's E164. We shouldn't use it for
|
||||
// new messages, but old messages might have this attribute.
|
||||
author?: string;
|
||||
authorUuid: string;
|
||||
bodyRanges: BodyRangesType;
|
||||
authorUuid?: string;
|
||||
bodyRanges?: BodyRangesType;
|
||||
id: string;
|
||||
referencedMessageNotFound: boolean;
|
||||
isViewOnce: boolean;
|
||||
text: string;
|
||||
text?: string;
|
||||
messageId: string;
|
||||
};
|
||||
|
||||
export type RetryOptions = Readonly<{
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
import { ProfileKeyCredentialRequestContext } from 'zkgroup';
|
||||
import { compact, sample } from 'lodash';
|
||||
import {
|
||||
MessageModelCollectionType,
|
||||
WhatIsThis,
|
||||
MessageAttributesType,
|
||||
ReactionModelType,
|
||||
ConversationAttributesType,
|
||||
MessageAttributesType,
|
||||
MessageModelCollectionType,
|
||||
QuotedMessageType,
|
||||
ReactionModelType,
|
||||
VerificationOptions,
|
||||
WhatIsThis,
|
||||
} from '../model-types.d';
|
||||
import { CallMode, CallHistoryDetailsType } from '../types/Calling';
|
||||
import { CallbackResultType, GroupV2InfoType } from '../textsecure/SendMessage';
|
||||
|
@ -40,7 +41,6 @@ import {
|
|||
verifyAccessKey,
|
||||
} from '../Crypto';
|
||||
import * as Bytes from '../Bytes';
|
||||
import { DataMessageClass } from '../textsecure.d';
|
||||
import { BodyRangesType } from '../types/Util';
|
||||
import { getTextWithMentions } from '../util';
|
||||
import { migrateColor } from '../util/migrateColor';
|
||||
|
@ -3083,7 +3083,7 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
async makeQuote(
|
||||
quotedMessage: typeof window.Whisper.MessageType
|
||||
): Promise<DataMessageClass.Quote> {
|
||||
): Promise<QuotedMessageType> {
|
||||
const { getName } = Contact;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const contact = quotedMessage.getContact()!;
|
||||
|
@ -3100,13 +3100,15 @@ export class ConversationModel extends window.Backbone
|
|||
|
||||
return {
|
||||
authorUuid: contact.get('uuid'),
|
||||
bodyRanges: quotedMessage.get('bodyRanges'),
|
||||
id: quotedMessage.get('sent_at'),
|
||||
text: body || embeddedContactName,
|
||||
isViewOnce: isTapToView(quotedMessage.attributes),
|
||||
attachments: isTapToView(quotedMessage.attributes)
|
||||
? [{ contentType: 'image/jpeg', fileName: null }]
|
||||
: await this.getQuoteAttachment(attachments, preview, sticker),
|
||||
bodyRanges: quotedMessage.get('bodyRanges'),
|
||||
id: String(quotedMessage.get('sent_at')),
|
||||
isViewOnce: isTapToView(quotedMessage.attributes),
|
||||
messageId: quotedMessage.get('id'),
|
||||
referencedMessageNotFound: false,
|
||||
text: body || embeddedContactName,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -3476,10 +3478,13 @@ export class ConversationModel extends window.Backbone
|
|||
mentions?: BodyRangesType,
|
||||
{
|
||||
dontClearDraft,
|
||||
sendHQImages,
|
||||
timestamp,
|
||||
}: { dontClearDraft: boolean; timestamp?: number } = {
|
||||
dontClearDraft: false,
|
||||
}
|
||||
}: {
|
||||
dontClearDraft?: boolean;
|
||||
sendHQImages?: boolean;
|
||||
timestamp?: number;
|
||||
} = {}
|
||||
): void {
|
||||
if (this.isGroupV1AndDisabled()) {
|
||||
return;
|
||||
|
@ -3530,6 +3535,7 @@ export class ConversationModel extends window.Backbone
|
|||
recipients,
|
||||
sticker,
|
||||
bodyRanges: mentions,
|
||||
sendHQImages,
|
||||
});
|
||||
|
||||
if (isDirectConversation(this.attributes)) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { actions as accounts } from './ducks/accounts';
|
|||
import { actions as app } from './ducks/app';
|
||||
import { actions as audioPlayer } from './ducks/audioPlayer';
|
||||
import { actions as calling } from './ducks/calling';
|
||||
import { actions as composer } from './ducks/composer';
|
||||
import { actions as conversations } from './ducks/conversations';
|
||||
import { actions as emojis } from './ducks/emojis';
|
||||
import { actions as expiration } from './ducks/expiration';
|
||||
|
@ -24,6 +25,7 @@ export const actionCreators: ReduxActions = {
|
|||
app,
|
||||
audioPlayer,
|
||||
calling,
|
||||
composer,
|
||||
conversations,
|
||||
emojis,
|
||||
expiration,
|
||||
|
@ -43,6 +45,7 @@ export const mapDispatchToProps = {
|
|||
...app,
|
||||
...audioPlayer,
|
||||
...calling,
|
||||
...composer,
|
||||
...conversations,
|
||||
...emojis,
|
||||
...expiration,
|
||||
|
|
168
ts/state/ducks/composer.ts
Normal file
168
ts/state/ducks/composer.ts
Normal file
|
@ -0,0 +1,168 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { AttachmentType } from '../../types/Attachment';
|
||||
import { MessageAttributesType } from '../../model-types.d';
|
||||
import { LinkPreviewWithDomain } from '../../types/LinkPreview';
|
||||
|
||||
// State
|
||||
|
||||
export type ComposerStateType = {
|
||||
attachments: ReadonlyArray<AttachmentType>;
|
||||
linkPreviewLoading: boolean;
|
||||
linkPreviewResult?: LinkPreviewWithDomain;
|
||||
quotedMessage?: Pick<MessageAttributesType, 'conversationId' | 'quote'>;
|
||||
shouldSendHighQualityAttachments: boolean;
|
||||
};
|
||||
|
||||
// Actions
|
||||
|
||||
const REPLACE_ATTACHMENTS = 'composer/REPLACE_ATTACHMENTS';
|
||||
const RESET_COMPOSER = 'composer/RESET_COMPOSER';
|
||||
const SET_HIGH_QUALITY_SETTING = 'composer/SET_HIGH_QUALITY_SETTING';
|
||||
const SET_LINK_PREVIEW_RESULT = 'composer/SET_LINK_PREVIEW_RESULT';
|
||||
const SET_QUOTED_MESSAGE = 'composer/SET_QUOTED_MESSAGE';
|
||||
|
||||
type ReplaceAttachmentsActionType = {
|
||||
type: typeof REPLACE_ATTACHMENTS;
|
||||
payload: ReadonlyArray<AttachmentType>;
|
||||
};
|
||||
|
||||
type ResetComposerActionType = {
|
||||
type: typeof RESET_COMPOSER;
|
||||
};
|
||||
|
||||
type SetHighQualitySettingActionType = {
|
||||
type: typeof SET_HIGH_QUALITY_SETTING;
|
||||
payload: boolean;
|
||||
};
|
||||
|
||||
type SetLinkPreviewResultActionType = {
|
||||
type: typeof SET_LINK_PREVIEW_RESULT;
|
||||
payload: {
|
||||
isLoading: boolean;
|
||||
linkPreview?: LinkPreviewWithDomain;
|
||||
};
|
||||
};
|
||||
|
||||
type SetQuotedMessageActionType = {
|
||||
type: typeof SET_QUOTED_MESSAGE;
|
||||
payload?: Pick<MessageAttributesType, 'conversationId' | 'quote'>;
|
||||
};
|
||||
|
||||
type ComposerActionType =
|
||||
| ReplaceAttachmentsActionType
|
||||
| ResetComposerActionType
|
||||
| SetHighQualitySettingActionType
|
||||
| SetLinkPreviewResultActionType
|
||||
| SetQuotedMessageActionType;
|
||||
|
||||
// Action Creators
|
||||
|
||||
export const actions = {
|
||||
replaceAttachments,
|
||||
resetComposer,
|
||||
setLinkPreviewResult,
|
||||
setMediaQualitySetting,
|
||||
setQuotedMessage,
|
||||
};
|
||||
|
||||
function replaceAttachments(
|
||||
payload: ReadonlyArray<AttachmentType>
|
||||
): ReplaceAttachmentsActionType {
|
||||
return {
|
||||
type: REPLACE_ATTACHMENTS,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function resetComposer(): ResetComposerActionType {
|
||||
return {
|
||||
type: RESET_COMPOSER,
|
||||
};
|
||||
}
|
||||
|
||||
function setLinkPreviewResult(
|
||||
isLoading: boolean,
|
||||
linkPreview?: LinkPreviewWithDomain
|
||||
): SetLinkPreviewResultActionType {
|
||||
return {
|
||||
type: SET_LINK_PREVIEW_RESULT,
|
||||
payload: {
|
||||
isLoading,
|
||||
linkPreview,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setMediaQualitySetting(
|
||||
payload: boolean
|
||||
): SetHighQualitySettingActionType {
|
||||
return {
|
||||
type: SET_HIGH_QUALITY_SETTING,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function setQuotedMessage(
|
||||
payload?: Pick<MessageAttributesType, 'conversationId' | 'quote'>
|
||||
): SetQuotedMessageActionType {
|
||||
return {
|
||||
type: SET_QUOTED_MESSAGE,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
// Reducer
|
||||
|
||||
export function getEmptyState(): ComposerStateType {
|
||||
return {
|
||||
attachments: [],
|
||||
linkPreviewLoading: false,
|
||||
shouldSendHighQualityAttachments: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function reducer(
|
||||
state: Readonly<ComposerStateType> = getEmptyState(),
|
||||
action: Readonly<ComposerActionType>
|
||||
): ComposerStateType {
|
||||
if (action.type === RESET_COMPOSER) {
|
||||
return getEmptyState();
|
||||
}
|
||||
|
||||
if (action.type === REPLACE_ATTACHMENTS) {
|
||||
const { payload: attachments } = action;
|
||||
return {
|
||||
...state,
|
||||
attachments,
|
||||
...(attachments.length
|
||||
? {}
|
||||
: { shouldSendHighQualityAttachments: false }),
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_HIGH_QUALITY_SETTING) {
|
||||
return {
|
||||
...state,
|
||||
shouldSendHighQualityAttachments: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_QUOTED_MESSAGE) {
|
||||
return {
|
||||
...state,
|
||||
quotedMessage: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_LINK_PREVIEW_RESULT) {
|
||||
return {
|
||||
...state,
|
||||
linkPreviewLoading: action.payload.isLoading,
|
||||
linkPreviewResult: action.payload.linkPreview,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
|
@ -7,6 +7,7 @@ import { reducer as accounts } from './ducks/accounts';
|
|||
import { reducer as app } from './ducks/app';
|
||||
import { reducer as audioPlayer } from './ducks/audioPlayer';
|
||||
import { reducer as calling } from './ducks/calling';
|
||||
import { reducer as composer } from './ducks/composer';
|
||||
import { reducer as conversations } from './ducks/conversations';
|
||||
import { reducer as emojis } from './ducks/emojis';
|
||||
import { reducer as expiration } from './ducks/expiration';
|
||||
|
@ -25,6 +26,7 @@ export const reducer = combineReducers({
|
|||
app,
|
||||
audioPlayer,
|
||||
calling,
|
||||
composer,
|
||||
conversations,
|
||||
emojis,
|
||||
expiration,
|
||||
|
|
|
@ -9,11 +9,12 @@ import { StateType } from '../reducer';
|
|||
import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
|
||||
|
||||
import { selectRecentEmojis } from '../selectors/emojis';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getIntl, getUserConversationId } from '../selectors/user';
|
||||
import {
|
||||
getConversationSelector,
|
||||
isMissingRequiredProfileSharing,
|
||||
} from '../selectors/conversations';
|
||||
import { getPropsForQuote } from '../selectors/message';
|
||||
import {
|
||||
getBlessedStickerPacks,
|
||||
getInstalledStickerPacks,
|
||||
|
@ -25,12 +26,14 @@ import {
|
|||
|
||||
type ExternalProps = {
|
||||
id: string;
|
||||
onClickQuotedMessage: (id?: string) => unknown;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { id } = props;
|
||||
const { id, onClickQuotedMessage } = props;
|
||||
|
||||
const conversation = getConversationSelector(state)(id);
|
||||
const conversationSelector = getConversationSelector(state);
|
||||
const conversation = conversationSelector(id);
|
||||
if (!conversation) {
|
||||
throw new Error(`Conversation id ${id} not found!`);
|
||||
}
|
||||
|
@ -54,6 +57,14 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
get(state.items, ['showStickerPickerHint'], false) &&
|
||||
receivedPacks.length > 0;
|
||||
|
||||
const {
|
||||
attachments: draftAttachments,
|
||||
linkPreviewLoading,
|
||||
linkPreviewResult,
|
||||
quotedMessage,
|
||||
shouldSendHighQualityAttachments,
|
||||
} = state.composer;
|
||||
|
||||
const recentEmojis = selectRecentEmojis(state);
|
||||
|
||||
return {
|
||||
|
@ -61,6 +72,23 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
i18n: getIntl(state),
|
||||
draftText,
|
||||
draftBodyRanges,
|
||||
// AttachmentsList
|
||||
draftAttachments,
|
||||
// MediaQualitySelector
|
||||
shouldSendHighQualityAttachments,
|
||||
// StagedLinkPreview
|
||||
linkPreviewLoading,
|
||||
linkPreviewResult,
|
||||
// Quote
|
||||
quotedMessageProps: quotedMessage
|
||||
? getPropsForQuote(
|
||||
quotedMessage,
|
||||
conversationSelector,
|
||||
getUserConversationId(state)
|
||||
)
|
||||
: undefined,
|
||||
onClickQuotedMessage: () =>
|
||||
onClickQuotedMessage(quotedMessage?.quote?.messageId),
|
||||
// Emojis
|
||||
recentEmojis,
|
||||
skinTone: get(state, ['items', 'skinTone'], 0),
|
||||
|
|
|
@ -5,6 +5,7 @@ import { actions as accounts } from './ducks/accounts';
|
|||
import { actions as app } from './ducks/app';
|
||||
import { actions as audioPlayer } from './ducks/audioPlayer';
|
||||
import { actions as calling } from './ducks/calling';
|
||||
import { actions as composer } from './ducks/composer';
|
||||
import { actions as conversations } from './ducks/conversations';
|
||||
import { actions as emojis } from './ducks/emojis';
|
||||
import { actions as expiration } from './ducks/expiration';
|
||||
|
@ -23,6 +24,7 @@ export type ReduxActions = {
|
|||
app: typeof app;
|
||||
audioPlayer: typeof audioPlayer;
|
||||
calling: typeof calling;
|
||||
composer: typeof composer;
|
||||
conversations: typeof conversations;
|
||||
emojis: typeof emojis;
|
||||
expiration: typeof expiration;
|
||||
|
|
120
ts/test-both/state/ducks/composer_test.ts
Normal file
120
ts/test-both/state/ducks/composer_test.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { actions, getEmptyState, reducer } from '../../../state/ducks/composer';
|
||||
|
||||
import { IMAGE_JPEG } from '../../../types/MIME';
|
||||
import { AttachmentType } from '../../../types/Attachment';
|
||||
|
||||
describe('both/state/ducks/composer', () => {
|
||||
const QUOTED_MESSAGE = {
|
||||
conversationId: '123',
|
||||
quote: {
|
||||
attachments: [],
|
||||
id: '456',
|
||||
isViewOnce: false,
|
||||
messageId: '789',
|
||||
referencedMessageNotFound: false,
|
||||
},
|
||||
};
|
||||
|
||||
describe('replaceAttachments', () => {
|
||||
it('replaces the attachments state', () => {
|
||||
const { replaceAttachments } = actions;
|
||||
const state = getEmptyState();
|
||||
const attachments: Array<AttachmentType> = [{ contentType: IMAGE_JPEG }];
|
||||
const nextState = reducer(state, replaceAttachments(attachments));
|
||||
|
||||
assert.deepEqual(nextState.attachments, attachments);
|
||||
});
|
||||
|
||||
it('sets the high quality setting to false when there are no attachments', () => {
|
||||
const { replaceAttachments } = actions;
|
||||
const state = getEmptyState();
|
||||
const attachments: Array<AttachmentType> = [];
|
||||
const nextState = reducer(
|
||||
{ ...state, shouldSendHighQualityAttachments: true },
|
||||
replaceAttachments(attachments)
|
||||
);
|
||||
|
||||
assert.deepEqual(nextState.attachments, attachments);
|
||||
assert.isFalse(nextState.shouldSendHighQualityAttachments);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetComposer', () => {
|
||||
it('returns composer back to empty state', () => {
|
||||
const { resetComposer } = actions;
|
||||
const nextState = reducer(
|
||||
{
|
||||
attachments: [],
|
||||
linkPreviewLoading: true,
|
||||
quotedMessage: QUOTED_MESSAGE,
|
||||
shouldSendHighQualityAttachments: true,
|
||||
},
|
||||
resetComposer()
|
||||
);
|
||||
|
||||
assert.deepEqual(nextState, getEmptyState());
|
||||
});
|
||||
});
|
||||
|
||||
describe('setLinkPreviewResult', () => {
|
||||
it('sets loading state when loading', () => {
|
||||
const { setLinkPreviewResult } = actions;
|
||||
const state = getEmptyState();
|
||||
const nextState = reducer(state, setLinkPreviewResult(true));
|
||||
|
||||
assert.isTrue(nextState.linkPreviewLoading);
|
||||
});
|
||||
|
||||
it('sets the link preview result', () => {
|
||||
const { setLinkPreviewResult } = actions;
|
||||
const state = getEmptyState();
|
||||
const nextState = reducer(
|
||||
state,
|
||||
setLinkPreviewResult(false, {
|
||||
domain: 'https://www.signal.org/',
|
||||
title: 'Signal >> Careers',
|
||||
url: 'https://www.signal.org/workworkwork',
|
||||
description:
|
||||
'Join an organization that empowers users by making private communication simple.',
|
||||
date: null,
|
||||
})
|
||||
);
|
||||
|
||||
assert.isFalse(nextState.linkPreviewLoading);
|
||||
assert.equal(nextState.linkPreviewResult?.title, 'Signal >> Careers');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMediaQualitySetting', () => {
|
||||
it('toggles the media quality setting', () => {
|
||||
const { setMediaQualitySetting } = actions;
|
||||
const state = getEmptyState();
|
||||
|
||||
assert.isFalse(state.shouldSendHighQualityAttachments);
|
||||
|
||||
const nextState = reducer(state, setMediaQualitySetting(true));
|
||||
|
||||
assert.isTrue(nextState.shouldSendHighQualityAttachments);
|
||||
|
||||
const nextNextState = reducer(nextState, setMediaQualitySetting(false));
|
||||
|
||||
assert.isFalse(nextNextState.shouldSendHighQualityAttachments);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setQuotedMessage', () => {
|
||||
it('sets the quoted message', () => {
|
||||
const { setQuotedMessage } = actions;
|
||||
const state = getEmptyState();
|
||||
const nextState = reducer(state, setQuotedMessage(QUOTED_MESSAGE));
|
||||
|
||||
assert.equal(nextState.quotedMessage?.conversationId, '123');
|
||||
assert.equal(nextState.quotedMessage?.quote?.id, '456');
|
||||
});
|
||||
});
|
||||
});
|
27
ts/test-electron/util/canvasToBlob_test.ts
Normal file
27
ts/test-electron/util/canvasToBlob_test.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { canvasToBlob } from '../../util/canvasToBlob';
|
||||
|
||||
describe('canvasToBlob', () => {
|
||||
it('converts a canvas to an Blob', async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 100;
|
||||
canvas.height = 200;
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) {
|
||||
throw new Error('Test setup error: cannot get canvas rendering context');
|
||||
}
|
||||
context.fillStyle = '#ff9900';
|
||||
context.fillRect(10, 10, 20, 20);
|
||||
|
||||
const result = await canvasToBlob(canvas);
|
||||
|
||||
// These are just smoke tests.
|
||||
assert.instanceOf(result, Blob);
|
||||
assert.isAtLeast(result.size, 50);
|
||||
});
|
||||
});
|
|
@ -43,6 +43,7 @@ export type AttachmentType = {
|
|||
contentType: MIME.MIMEType;
|
||||
path: string;
|
||||
};
|
||||
screenshotPath?: string;
|
||||
flags?: number;
|
||||
thumbnail?: ThumbnailType;
|
||||
isCorrupted?: boolean;
|
||||
|
@ -52,6 +53,29 @@ export type AttachmentType = {
|
|||
cdnKey?: string;
|
||||
};
|
||||
|
||||
type BaseAttachmentDraftType = {
|
||||
blurHash?: string;
|
||||
contentType: MIME.MIMEType;
|
||||
fileName: string;
|
||||
screenshotContentType?: string;
|
||||
screenshotSize?: number;
|
||||
size: number;
|
||||
};
|
||||
|
||||
export type InMemoryAttachmentDraftType = {
|
||||
data?: ArrayBuffer;
|
||||
screenshotData?: ArrayBuffer;
|
||||
} & BaseAttachmentDraftType;
|
||||
|
||||
export type OnDiskAttachmentDraftType = {
|
||||
path?: string;
|
||||
screenshotPath?: string;
|
||||
} & BaseAttachmentDraftType;
|
||||
|
||||
export type AttachmentDraftType = {
|
||||
url: string;
|
||||
} & BaseAttachmentDraftType;
|
||||
|
||||
export type ThumbnailType = {
|
||||
height: number;
|
||||
width: number;
|
||||
|
|
20
ts/types/LinkPreview.ts
Normal file
20
ts/types/LinkPreview.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { AttachmentType } from './Attachment';
|
||||
|
||||
export type LinkPreviewImage = AttachmentType & {
|
||||
data: ArrayBuffer;
|
||||
};
|
||||
|
||||
export type LinkPreviewResult = {
|
||||
title: string;
|
||||
url: string;
|
||||
image?: LinkPreviewImage;
|
||||
description: string | null;
|
||||
date: number | null;
|
||||
};
|
||||
|
||||
export type LinkPreviewWithDomain = {
|
||||
domain: string;
|
||||
} & LinkPreviewResult;
|
31
ts/util/autoOrientImage.ts
Normal file
31
ts/util/autoOrientImage.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import loadImage, { LoadImageOptions } from 'blueimp-load-image';
|
||||
import { IMAGE_JPEG } from '../types/MIME';
|
||||
import { canvasToBlob } from './canvasToBlob';
|
||||
|
||||
const DEFAULT_JPEG_QUALITY = 0.85;
|
||||
|
||||
export async function autoOrientImage(blob: Blob): Promise<Blob> {
|
||||
const options: LoadImageOptions = {
|
||||
canvas: true,
|
||||
orientation: true,
|
||||
};
|
||||
|
||||
try {
|
||||
const data = await loadImage(blob, options);
|
||||
const { image } = data;
|
||||
if (image instanceof HTMLCanvasElement) {
|
||||
// We `return await`, instead of just `return`, so we capture the rejection in this
|
||||
// try/catch block. See [this blog post][0] for more background.
|
||||
// [0]: https://jakearchibald.com/2017/await-vs-return-vs-return-await/
|
||||
return await canvasToBlob(image, IMAGE_JPEG, DEFAULT_JPEG_QUALITY);
|
||||
}
|
||||
throw new Error('image not a canvas');
|
||||
} catch (err) {
|
||||
const error = new Error('autoOrientImage: Failed to process image');
|
||||
error.originalError = err;
|
||||
throw error;
|
||||
}
|
||||
}
|
|
@ -1,17 +1,11 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { canvasToBlob } from './canvasToBlob';
|
||||
|
||||
export async function canvasToArrayBuffer(
|
||||
canvas: HTMLCanvasElement
|
||||
): Promise<ArrayBuffer> {
|
||||
const blob: Blob = await new Promise<Blob>((resolve, reject) => {
|
||||
canvas.toBlob(result => {
|
||||
if (result) {
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(new Error("Couldn't convert the canvas to a Blob"));
|
||||
}
|
||||
}, 'image/webp');
|
||||
});
|
||||
const blob = await canvasToBlob(canvas);
|
||||
return blob.arrayBuffer();
|
||||
}
|
||||
|
|
29
ts/util/canvasToBlob.ts
Normal file
29
ts/util/canvasToBlob.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { IMAGE_JPEG } from '../types/MIME';
|
||||
|
||||
/**
|
||||
* Similar to [the built-in `toBlob` method][0], but returns a Promise.
|
||||
*
|
||||
* [0]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
|
||||
*/
|
||||
export async function canvasToBlob(
|
||||
canvas: HTMLCanvasElement,
|
||||
mimeType = IMAGE_JPEG,
|
||||
quality?: number
|
||||
): Promise<Blob> {
|
||||
return new Promise((resolve, reject) =>
|
||||
canvas.toBlob(
|
||||
result => {
|
||||
if (result) {
|
||||
resolve(result);
|
||||
} else {
|
||||
reject(new Error("Couldn't convert the canvas to a Blob"));
|
||||
}
|
||||
},
|
||||
mimeType,
|
||||
quality
|
||||
)
|
||||
);
|
||||
}
|
|
@ -1913,19 +1913,6 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T18:13:29.628Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "node_modules/blueimp-canvas-to-blob/js/canvas-to-blob.js",
|
||||
"line": " bb.append(arrayBuffer)",
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T18:13:29.628Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "node_modules/blueimp-canvas-to-blob/js/canvas-to-blob.min.js",
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T18:13:29.628Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-wrap(",
|
||||
"path": "node_modules/boom/lib/index.js",
|
||||
|
@ -13436,14 +13423,6 @@
|
|||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Doesn't refer to a DOM element."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CompositionArea.js",
|
||||
"line": " const attSlotRef = React.useRef(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-10-26T19:12:24.410Z",
|
||||
"reasonDetail": "Needed for the composition area."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/CompositionArea.js",
|
||||
|
|
145
ts/util/scaleImageToLevel.ts
Normal file
145
ts/util/scaleImageToLevel.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import loadImage from 'blueimp-load-image';
|
||||
|
||||
import { IMAGE_JPEG } from '../types/MIME';
|
||||
import { canvasToBlob } from './canvasToBlob';
|
||||
import { getValue } from '../RemoteConfig';
|
||||
|
||||
enum MediaQualityLevels {
|
||||
One = 1,
|
||||
Two = 2,
|
||||
Three = 3,
|
||||
}
|
||||
|
||||
const DEFAULT_LEVEL = MediaQualityLevels.One;
|
||||
|
||||
const MiB = 1024 * 1024;
|
||||
|
||||
const DEFAULT_LEVEL_DATA = {
|
||||
maxDimensions: 1600,
|
||||
quality: 0.7,
|
||||
size: MiB,
|
||||
};
|
||||
|
||||
const MEDIA_QUALITY_LEVEL_DATA = new Map([
|
||||
[MediaQualityLevels.One, DEFAULT_LEVEL_DATA],
|
||||
[
|
||||
MediaQualityLevels.Two,
|
||||
{
|
||||
maxDimensions: 2048,
|
||||
quality: 0.75,
|
||||
size: MiB * 1.5,
|
||||
},
|
||||
],
|
||||
[
|
||||
MediaQualityLevels.Three,
|
||||
{
|
||||
maxDimensions: 4096,
|
||||
quality: 0.75,
|
||||
size: MiB * 3,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const SCALABLE_DIMENSIONS = [3072, 2048, 1600, 1024, 768];
|
||||
const MIN_DIMENSIONS = 512;
|
||||
|
||||
function parseCountryValues(values: string): Map<string, MediaQualityLevels> {
|
||||
const map = new Map<string, MediaQualityLevels>();
|
||||
values.split(',').forEach(value => {
|
||||
const [countryCode, level] = value.split(':');
|
||||
map.set(
|
||||
countryCode,
|
||||
Number(level) === 2 ? MediaQualityLevels.Two : MediaQualityLevels.One
|
||||
);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
function getMediaQualityLevel(): MediaQualityLevels {
|
||||
const values = getValue('desktop.mediaQuality.levels');
|
||||
if (!values) {
|
||||
return DEFAULT_LEVEL;
|
||||
}
|
||||
const countryValues = parseCountryValues(values);
|
||||
const e164 = window.textsecure.storage.user.getNumber();
|
||||
if (!e164) {
|
||||
return DEFAULT_LEVEL;
|
||||
}
|
||||
const parsedPhoneNumber = window.libphonenumber.util.parseNumber(e164);
|
||||
|
||||
if (!parsedPhoneNumber.isValidNumber) {
|
||||
return DEFAULT_LEVEL;
|
||||
}
|
||||
|
||||
const level = countryValues.get(parsedPhoneNumber.countryCode);
|
||||
if (level) {
|
||||
return level;
|
||||
}
|
||||
|
||||
return countryValues.get('*') || DEFAULT_LEVEL;
|
||||
}
|
||||
|
||||
async function getCanvasBlob(
|
||||
image: HTMLCanvasElement,
|
||||
dimensions: number,
|
||||
quality: number
|
||||
): Promise<Blob> {
|
||||
const canvas = loadImage.scale(image, {
|
||||
canvas: true,
|
||||
maxHeight: dimensions,
|
||||
maxWidth: dimensions,
|
||||
});
|
||||
if (!(canvas instanceof HTMLCanvasElement)) {
|
||||
throw new Error('image not a canvas');
|
||||
}
|
||||
return canvasToBlob(canvas, IMAGE_JPEG, quality);
|
||||
}
|
||||
|
||||
export async function scaleImageToLevel(
|
||||
fileOrBlobOrURL: File | Blob,
|
||||
sendAsHighQuality?: boolean
|
||||
): Promise<Blob> {
|
||||
let image: HTMLCanvasElement;
|
||||
try {
|
||||
const data = await loadImage(fileOrBlobOrURL, {
|
||||
canvas: true,
|
||||
orientation: true,
|
||||
});
|
||||
if (!(data.image instanceof HTMLCanvasElement)) {
|
||||
throw new Error('image not a canvas');
|
||||
}
|
||||
({ image } = data);
|
||||
if (!(image instanceof HTMLCanvasElement)) {
|
||||
throw new Error('image not a canvas');
|
||||
}
|
||||
} catch (err) {
|
||||
const error = new Error('scaleImageToLevel: Failed to process image');
|
||||
error.originalError = err;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const level = sendAsHighQuality
|
||||
? MediaQualityLevels.Three
|
||||
: getMediaQualityLevel();
|
||||
const { maxDimensions, quality, size } =
|
||||
MEDIA_QUALITY_LEVEL_DATA.get(level) || DEFAULT_LEVEL_DATA;
|
||||
|
||||
for (let i = 0; i < SCALABLE_DIMENSIONS.length; i += 1) {
|
||||
const scalableDimensions = SCALABLE_DIMENSIONS[i];
|
||||
if (maxDimensions < scalableDimensions) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We need these operations to be in serial
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const blob = await getCanvasBlob(image, scalableDimensions, quality);
|
||||
if (blob.size <= size) {
|
||||
return blob;
|
||||
}
|
||||
}
|
||||
|
||||
return getCanvasBlob(image, MIN_DIMENSIONS, quality);
|
||||
}
|
|
@ -3,7 +3,13 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { AttachmentType } from '../types/Attachment';
|
||||
import {
|
||||
AttachmentDraftType,
|
||||
AttachmentType,
|
||||
InMemoryAttachmentDraftType,
|
||||
OnDiskAttachmentDraftType,
|
||||
} from '../types/Attachment';
|
||||
import { IMAGE_JPEG } from '../types/MIME';
|
||||
import { ConversationModel } from '../models/conversations';
|
||||
import {
|
||||
GroupV2PendingMemberType,
|
||||
|
@ -28,30 +34,19 @@ import * as Bytes from '../Bytes';
|
|||
import {
|
||||
canReply,
|
||||
getAttachmentsForMessage,
|
||||
getPropsForQuote,
|
||||
isOutgoing,
|
||||
isTapToView,
|
||||
} from '../state/selectors/message';
|
||||
import { getMessagesByConversation } from '../state/selectors/conversations';
|
||||
import { ConversationDetailsMembershipList } from '../components/conversation/conversation-details/ConversationDetailsMembershipList';
|
||||
import { showSafetyNumberChangeDialog } from '../shims/showSafetyNumberChangeDialog';
|
||||
|
||||
type GetLinkPreviewImageResult = {
|
||||
data: ArrayBuffer;
|
||||
size: number;
|
||||
contentType: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
blurHash: string;
|
||||
};
|
||||
|
||||
type GetLinkPreviewResult = {
|
||||
title: string;
|
||||
url: string;
|
||||
image?: GetLinkPreviewImageResult;
|
||||
description: string | null;
|
||||
date: number | null;
|
||||
};
|
||||
import { autoOrientImage } from '../util/autoOrientImage';
|
||||
import { canvasToBlob } from '../util/canvasToBlob';
|
||||
import {
|
||||
LinkPreviewImage,
|
||||
LinkPreviewResult,
|
||||
LinkPreviewWithDomain,
|
||||
} from '../types/LinkPreview';
|
||||
|
||||
type AttachmentOptions = {
|
||||
messageId: string;
|
||||
|
@ -421,21 +416,12 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
this.loadingScreen.render();
|
||||
this.loadingScreen.$el.prependTo(this.$('.discussion-container'));
|
||||
|
||||
const attachmentListEl = $(
|
||||
'<div class="module-composition-area__attachment-list"></div>'
|
||||
);
|
||||
|
||||
this.attachmentListView = new Whisper.ReactWrapperView({
|
||||
el: attachmentListEl,
|
||||
Component: window.Signal.Components.AttachmentList,
|
||||
props: this.getPropsForAttachmentList(),
|
||||
});
|
||||
|
||||
this.setupHeader();
|
||||
this.setupTimeline();
|
||||
this.setupCompositionArea({ attachmentListEl: attachmentListEl[0] });
|
||||
this.setupCompositionArea();
|
||||
|
||||
this.linkPreviewAbortController = null;
|
||||
this.updateAttachmentsView();
|
||||
},
|
||||
|
||||
events: {
|
||||
|
@ -615,7 +601,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
window.reduxActions.conversations.setSelectedConversationHeaderTitle();
|
||||
},
|
||||
|
||||
setupCompositionArea({ attachmentListEl }: any) {
|
||||
setupCompositionArea() {
|
||||
window.reduxActions.composer.resetComposer();
|
||||
|
||||
const { model }: { model: ConversationModel } = this;
|
||||
|
||||
const compositionApi = { current: null };
|
||||
|
@ -650,7 +638,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
getQuotedMessage: () => model.get('quotedMessageId'),
|
||||
clearQuotedMessage: () => this.setQuoteMessage(null),
|
||||
micCellEl,
|
||||
attachmentListEl,
|
||||
onAccept: () => {
|
||||
this.syncMessageRequestResponse(
|
||||
'onAccept',
|
||||
|
@ -698,6 +685,21 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
},
|
||||
});
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
};
|
||||
|
||||
this.compositionAreaView = new Whisper.ReactWrapperView({
|
||||
|
@ -1444,9 +1446,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
this.timelineView.remove();
|
||||
this.compositionAreaView.remove();
|
||||
|
||||
if (this.attachmentListView) {
|
||||
this.attachmentListView.remove();
|
||||
}
|
||||
if (this.captionEditorView) {
|
||||
this.captionEditorView.remove();
|
||||
}
|
||||
|
@ -1468,9 +1467,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
if (this.scrollDownButton) {
|
||||
this.scrollDownButton.remove();
|
||||
}
|
||||
if (this.quoteView) {
|
||||
this.quoteView.remove();
|
||||
}
|
||||
if (this.lightboxView) {
|
||||
this.lightboxView.remove();
|
||||
}
|
||||
|
@ -1587,37 +1583,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
});
|
||||
},
|
||||
|
||||
getPropsForAttachmentList() {
|
||||
const { model }: { model: ConversationModel } = this;
|
||||
const draftAttachments = model.get('draftAttachments') || [];
|
||||
|
||||
return {
|
||||
// In conversation model/redux
|
||||
attachments: draftAttachments.map(attachment => {
|
||||
let url = '';
|
||||
if (attachment.screenshotPath) {
|
||||
url = getAbsoluteDraftPath(attachment.screenshotPath);
|
||||
} else if (attachment.path) {
|
||||
url = getAbsoluteDraftPath(attachment.path);
|
||||
} else {
|
||||
window.log.warn(
|
||||
'getPropsForAttachmentList: Attachment was missing both screenshotPath and path fields'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...attachment,
|
||||
url,
|
||||
};
|
||||
}),
|
||||
// Passed in from ConversationView
|
||||
onAddAttachment: this.onChooseAttachment.bind(this),
|
||||
onClickAttachment: this.onClickAttachment.bind(this),
|
||||
onCloseAttachment: this.onCloseAttachment.bind(this),
|
||||
onClose: this.clearAttachments.bind(this),
|
||||
};
|
||||
},
|
||||
|
||||
onClickAttachment(attachment: any) {
|
||||
const getProps = () => ({
|
||||
url: attachment.url,
|
||||
|
@ -1663,9 +1628,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
window.Signal.Backbone.Views.Lightbox.show(this.captionEditorView.el);
|
||||
},
|
||||
|
||||
async deleteDraftAttachment(
|
||||
attachment: Readonly<{ screenshotPath?: string; path?: string }>
|
||||
) {
|
||||
async deleteDraftAttachment(attachment: AttachmentType) {
|
||||
if (attachment.screenshotPath) {
|
||||
await deleteDraftFile(attachment.screenshotPath);
|
||||
}
|
||||
|
@ -1679,7 +1642,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
window.Signal.Data.updateConversation(model.attributes);
|
||||
},
|
||||
|
||||
async addAttachment(attachment: any) {
|
||||
async addAttachment(attachment: InMemoryAttachmentDraftType) {
|
||||
const { model }: { model: ConversationModel } = this;
|
||||
const onDisk = await this.writeDraftAttachment(attachment);
|
||||
|
||||
|
@ -1692,6 +1655,26 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
await this.saveModel();
|
||||
},
|
||||
|
||||
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,
|
||||
};
|
||||
},
|
||||
|
||||
async onCloseAttachment(attachment: any) {
|
||||
const { model }: { model: ConversationModel } = this;
|
||||
const draftAttachments = model.get('draftAttachments') || [];
|
||||
|
@ -1801,14 +1784,21 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
},
|
||||
|
||||
updateAttachmentsView() {
|
||||
this.attachmentListView.update(this.getPropsForAttachmentList());
|
||||
const draftAttachments = this.model.get('draftAttachments') || [];
|
||||
window.reduxActions.composer.replaceAttachments(
|
||||
draftAttachments.map((att: AttachmentType) =>
|
||||
this.resolveOnDiskAttachment(att)
|
||||
)
|
||||
);
|
||||
this.toggleMicrophone();
|
||||
if (this.hasFiles()) {
|
||||
this.removeLinkPreview();
|
||||
}
|
||||
},
|
||||
|
||||
async writeDraftAttachment(attachment: any) {
|
||||
async writeDraftAttachment(
|
||||
attachment: InMemoryAttachmentDraftType
|
||||
): Promise<OnDiskAttachmentDraftType> {
|
||||
let toWrite = attachment;
|
||||
|
||||
if (toWrite.data) {
|
||||
|
@ -1869,7 +1859,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
let attachment;
|
||||
let attachment: InMemoryAttachmentDraftType;
|
||||
|
||||
try {
|
||||
if (window.Signal.Util.GoogleChrome.isImageTypeSupported(file.type)) {
|
||||
|
@ -1949,7 +1939,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
return true;
|
||||
},
|
||||
|
||||
async handleVideoAttachment(file: any) {
|
||||
async handleVideoAttachment(file: any): Promise<InMemoryAttachmentDraftType> {
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
if (!objectUrl) {
|
||||
throw new Error('Failed to create object url for video!');
|
||||
|
@ -1980,11 +1970,10 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
async handleImageAttachment(file: any) {
|
||||
async handleImageAttachment(file: any): Promise<InMemoryAttachmentDraftType> {
|
||||
const blurHash = await window.imageToBlurHash(file);
|
||||
if (MIME.isJPEG(file.type)) {
|
||||
const rotatedDataUrl = await window.autoOrientImage(file);
|
||||
const rotatedBlob = window.dataURLToBlobSync(rotatedDataUrl);
|
||||
const rotatedBlob = await autoOrientImage(file);
|
||||
const { contentType, file: resizedBlob, fileName } = await this.autoScale(
|
||||
{
|
||||
contentType: file.type,
|
||||
|
@ -1992,7 +1981,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
file: rotatedBlob,
|
||||
}
|
||||
);
|
||||
const data = await await VisualAttachment.blobToArrayBuffer(resizedBlob);
|
||||
const data = await VisualAttachment.blobToArrayBuffer(resizedBlob);
|
||||
|
||||
return {
|
||||
fileName: fileName || file.name,
|
||||
|
@ -2008,7 +1997,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
fileName: file.name,
|
||||
file,
|
||||
});
|
||||
const data = await await VisualAttachment.blobToArrayBuffer(resizedBlob);
|
||||
const data = await VisualAttachment.blobToArrayBuffer(resizedBlob);
|
||||
return {
|
||||
fileName: fileName || file.name,
|
||||
contentType,
|
||||
|
@ -2028,7 +2017,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
const img = document.createElement('img');
|
||||
img.onload = () => {
|
||||
img.onload = async () => {
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
const maxSize = 6000 * 1024;
|
||||
|
@ -2054,7 +2043,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
const targetContentType = 'image/jpeg';
|
||||
const targetContentType = IMAGE_JPEG;
|
||||
const canvas = window.loadImage.scale(img, {
|
||||
canvas: true,
|
||||
maxWidth,
|
||||
|
@ -2066,9 +2055,9 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
let blob;
|
||||
do {
|
||||
i -= 1;
|
||||
blob = window.dataURLToBlobSync(
|
||||
canvas.toDataURL(targetContentType, quality)
|
||||
);
|
||||
// We want to do these operations in serial.
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
blob = await canvasToBlob(canvas, targetContentType, quality);
|
||||
quality = (quality * maxSize) / blob.size;
|
||||
// NOTE: During testing with a large image, we observed the
|
||||
// `quality` value being > 1. Should we clamp it to [0.5, 1.0]?
|
||||
|
@ -3780,11 +3769,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
await this.saveModel();
|
||||
}
|
||||
|
||||
if (this.quoteView) {
|
||||
this.quoteView.remove();
|
||||
this.quoteView = null;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
const quotedMessage = window.MessageController.register(
|
||||
message.id,
|
||||
|
@ -3806,47 +3790,15 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
renderQuotedMessage() {
|
||||
const { model }: { model: ConversationModel } = this;
|
||||
|
||||
if (this.quoteView) {
|
||||
this.quoteView.remove();
|
||||
this.quoteView = null;
|
||||
}
|
||||
if (!this.quotedMessage) {
|
||||
window.reduxActions.composer.setQuotedMessage(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const props = getPropsForQuote(
|
||||
{
|
||||
conversationId: model.id,
|
||||
quote: this.quote,
|
||||
},
|
||||
findAndFormatContact,
|
||||
window.ConversationController.getOurConversationIdOrThrow()
|
||||
);
|
||||
|
||||
const contact = this.quotedMessage.getContact();
|
||||
|
||||
this.quoteView = new Whisper.ReactWrapperView({
|
||||
className: 'quote-wrapper',
|
||||
Component: window.Signal.Components.Quote,
|
||||
elCallback: (el: any) =>
|
||||
this.$(this.compositionApi.current.attSlotRef.current).prepend(el),
|
||||
props: {
|
||||
...props,
|
||||
withContentAbove: true,
|
||||
onClick: () => this.scrollToMessage(this.quotedMessage.id),
|
||||
onClose: () => {
|
||||
// This can't be the normal 'onClose' because that is always run when this
|
||||
// view is removed from the DOM, and would clear the draft quote.
|
||||
this.setQuoteMessage(null);
|
||||
},
|
||||
},
|
||||
window.reduxActions.composer.setQuotedMessage({
|
||||
conversationId: model.id,
|
||||
quote: this.quote,
|
||||
});
|
||||
|
||||
if (contact) {
|
||||
this.quoteView.listenTo(contact, 'change', () => {
|
||||
this.renderQuotedMessage();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showInvalidMessageToast(messageText?: string): boolean {
|
||||
|
@ -3939,7 +3891,13 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
this.quote,
|
||||
this.getLinkPreview(),
|
||||
undefined, // sticker
|
||||
mentions
|
||||
mentions,
|
||||
{
|
||||
sendHQImages:
|
||||
window.reduxStore &&
|
||||
window.reduxStore.getState().composer
|
||||
.shouldSendHighQualityAttachments,
|
||||
}
|
||||
);
|
||||
|
||||
this.compositionApi.current.reset();
|
||||
|
@ -3947,6 +3905,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
this.setQuoteMessage(null);
|
||||
this.resetLinkPreview();
|
||||
this.clearAttachments();
|
||||
window.reduxActions.composer.resetComposer();
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'Error pulling attached files before send',
|
||||
|
@ -4068,7 +4027,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
async getStickerPackPreview(
|
||||
url: string,
|
||||
abortSignal: Readonly<AbortSignal>
|
||||
): Promise<null | GetLinkPreviewResult> {
|
||||
): Promise<null | LinkPreviewResult> {
|
||||
const isPackDownloaded = (pack: any) =>
|
||||
pack && (pack.status === 'downloaded' || pack.status === 'installed');
|
||||
const isPackValid = (pack: any) =>
|
||||
|
@ -4144,7 +4103,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
async getGroupPreview(
|
||||
url: string,
|
||||
abortSignal: Readonly<AbortSignal>
|
||||
): Promise<null | GetLinkPreviewResult> {
|
||||
): Promise<null | LinkPreviewResult> {
|
||||
const urlObject = maybeParseUrl(url);
|
||||
if (!urlObject) {
|
||||
return null;
|
||||
|
@ -4187,7 +4146,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
: window.i18n('GroupV2--join--member-count--multiple', {
|
||||
count: result.memberCount.toString(),
|
||||
});
|
||||
let image: undefined | GetLinkPreviewImageResult;
|
||||
let image: undefined | LinkPreviewImage;
|
||||
|
||||
if (result.avatar) {
|
||||
try {
|
||||
|
@ -4198,10 +4157,10 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
image = {
|
||||
data,
|
||||
size: data.byteLength,
|
||||
contentType: 'image/jpeg',
|
||||
contentType: IMAGE_JPEG,
|
||||
blurHash: await window.imageToBlurHash(
|
||||
new Blob([data], {
|
||||
type: 'image/jpeg',
|
||||
type: IMAGE_JPEG,
|
||||
})
|
||||
),
|
||||
};
|
||||
|
@ -4229,7 +4188,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
async getPreview(
|
||||
url: string,
|
||||
abortSignal: Readonly<AbortSignal>
|
||||
): Promise<null | GetLinkPreviewResult> {
|
||||
): Promise<null | LinkPreviewResult> {
|
||||
if (window.Signal.LinkPreviews.isStickerPack(url)) {
|
||||
return this.getStickerPackPreview(url, abortSignal);
|
||||
}
|
||||
|
@ -4410,32 +4369,10 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
if (this.forwardMessageModal) {
|
||||
return;
|
||||
}
|
||||
if (this.previewView) {
|
||||
this.previewView.remove();
|
||||
this.previewView = null;
|
||||
}
|
||||
if (!this.currentlyMatchedLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
const first = (this.preview && this.preview[0]) || null;
|
||||
const props = {
|
||||
...first,
|
||||
domain: first && window.Signal.LinkPreviews.getDomain(first.url),
|
||||
isLoaded: Boolean(first),
|
||||
onClose: () => {
|
||||
this.disableLinkPreviews = true;
|
||||
this.removeLinkPreview();
|
||||
},
|
||||
};
|
||||
|
||||
this.previewView = new Whisper.ReactWrapperView({
|
||||
className: 'preview-wrapper',
|
||||
Component: window.Signal.Components.StagedLinkPreview,
|
||||
elCallback: (el: any) =>
|
||||
this.$(this.compositionApi.current.attSlotRef.current).prepend(el),
|
||||
props,
|
||||
});
|
||||
window.reduxActions.composer.setLinkPreviewResult(
|
||||
Boolean(this.currentlyMatchedLink),
|
||||
this.getLinkPreviewWithDomain()
|
||||
);
|
||||
},
|
||||
|
||||
getLinkPreview() {
|
||||
|
@ -4461,6 +4398,18 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
});
|
||||
},
|
||||
|
||||
getLinkPreviewWithDomain(): LinkPreviewWithDomain | undefined {
|
||||
if (!this.preview || !this.preview.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [preview] = this.preview;
|
||||
return {
|
||||
...preview,
|
||||
domain: window.Signal.LinkPreviews.getDomain(preview.url),
|
||||
};
|
||||
},
|
||||
|
||||
// Called whenever the user changes the message composition field. But only
|
||||
// fires if there's content in the message field after the change.
|
||||
maybeBumpTyping(messageText: string) {
|
||||
|
|
4
ts/window.d.ts
vendored
4
ts/window.d.ts
vendored
|
@ -150,8 +150,6 @@ declare global {
|
|||
|
||||
moment: typeof moment;
|
||||
imageToBlurHash: typeof imageToBlurHash;
|
||||
autoOrientImage: any;
|
||||
dataURLToBlobSync: any;
|
||||
loadImage: any;
|
||||
isBehindProxy: () => boolean;
|
||||
getAutoLaunch: () => boolean;
|
||||
|
@ -220,7 +218,7 @@ declare global {
|
|||
getRegionCodeForNumber: (number: string) => string;
|
||||
parseNumber: (
|
||||
e164: string,
|
||||
defaultRegionCode: string
|
||||
defaultRegionCode?: string
|
||||
) =>
|
||||
| { isValidNumber: false; error: unknown }
|
||||
| {
|
||||
|
|
|
@ -4728,11 +4728,6 @@ bluebird@^3.3.5, bluebird@^3.5.4, bluebird@^3.5.5:
|
|||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
|
||||
integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
|
||||
|
||||
blueimp-canvas-to-blob@3.14.0:
|
||||
version "3.14.0"
|
||||
resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.14.0.tgz#ea075ffbfb1436607b0c75e951fb1ceb3ca0288e"
|
||||
integrity sha512-i6I2CiX1VR8YwUNYBo+dM8tg89ns4TTHxSpWjaDeHKcYS3yFalpLCwDaY21/EsJMufLy2tnG4j0JN5L8OVNkKQ==
|
||||
|
||||
blueimp-load-image@5.14.0:
|
||||
version "5.14.0"
|
||||
resolved "https://registry.yarnpkg.com/blueimp-load-image/-/blueimp-load-image-5.14.0.tgz#e8086415e580df802c33ff0da6b37a8d20205cc6"
|
||||
|
|
Loading…
Add table
Reference in a new issue