Show lightbox for GIFs
This commit is contained in:
parent
62ab66c1c8
commit
c3bdf3d411
30 changed files with 790 additions and 815 deletions
|
@ -131,11 +131,11 @@ const conversationsSelectors = require('../../ts/state/selectors/conversations')
|
|||
const searchSelectors = require('../../ts/state/selectors/search');
|
||||
|
||||
// Types
|
||||
const AttachmentType = require('./types/attachment');
|
||||
const AttachmentType = require('../../ts/types/Attachment');
|
||||
const VisualAttachment = require('./types/visual_attachment');
|
||||
const Contact = require('../../ts/types/Contact');
|
||||
const Conversation = require('./types/conversation');
|
||||
const Errors = require('./types/errors');
|
||||
const Errors = require('../../ts/types/errors');
|
||||
const MediaGalleryMessage = require('../../ts/components/conversation/media-gallery/types/Message');
|
||||
const MessageType = require('./types/message');
|
||||
const MIME = require('../../ts/types/MIME');
|
||||
|
|
|
@ -1,347 +0,0 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
const is = require('@sindresorhus/is');
|
||||
|
||||
const { arrayBufferToBlob, blobToArrayBuffer } = require('blob-util');
|
||||
const AttachmentTS = require('../../../ts/types/Attachment');
|
||||
const GoogleChrome = require('../../../ts/util/GoogleChrome');
|
||||
const { toLogFormat } = require('./errors');
|
||||
const { scaleImageToLevel } = require('../../../ts/util/scaleImageToLevel');
|
||||
const {
|
||||
migrateDataToFileSystem,
|
||||
} = require('./attachment/migrate_data_to_file_system');
|
||||
|
||||
// // Incoming message attachment fields
|
||||
// {
|
||||
// id: string
|
||||
// contentType: MIMEType
|
||||
// data: ArrayBuffer
|
||||
// digest: ArrayBuffer
|
||||
// fileName?: string
|
||||
// flags: null
|
||||
// key: ArrayBuffer
|
||||
// size: integer
|
||||
// thumbnail: ArrayBuffer
|
||||
// }
|
||||
|
||||
// // Outgoing message attachment fields
|
||||
// {
|
||||
// contentType: MIMEType
|
||||
// data: ArrayBuffer
|
||||
// fileName: string
|
||||
// size: integer
|
||||
// }
|
||||
|
||||
// Returns true if `rawAttachment` is a valid attachment based on our current schema.
|
||||
// Over time, we can expand this definition to become more narrow, e.g. require certain
|
||||
// fields, etc.
|
||||
exports.isValid = rawAttachment => {
|
||||
// NOTE: We cannot use `_.isPlainObject` because `rawAttachment` is
|
||||
// deserialized by protobuf:
|
||||
if (!rawAttachment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Upgrade steps
|
||||
// NOTE: This step strips all EXIF metadata from JPEG images as
|
||||
// part of re-encoding the image:
|
||||
exports.autoOrientJPEG = async (attachment, _, message) => {
|
||||
if (!AttachmentTS.canBeTranscoded(attachment)) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
// If we haven't downloaded the attachment yet, we won't have the data
|
||||
if (!attachment.data) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const dataBlob = await arrayBufferToBlob(
|
||||
attachment.data,
|
||||
attachment.contentType
|
||||
);
|
||||
const xcodedDataBlob = await scaleImageToLevel(
|
||||
dataBlob,
|
||||
message ? message.sendHQImages : false
|
||||
);
|
||||
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 xcodedAttachment = {
|
||||
...attachment,
|
||||
data: xcodedDataArrayBuffer,
|
||||
size: xcodedDataArrayBuffer.byteLength,
|
||||
};
|
||||
|
||||
// `digest` is no longer valid for auto-oriented image data, so we discard it:
|
||||
delete xcodedAttachment.digest;
|
||||
|
||||
return xcodedAttachment;
|
||||
};
|
||||
|
||||
const UNICODE_LEFT_TO_RIGHT_OVERRIDE = '\u202D';
|
||||
const UNICODE_RIGHT_TO_LEFT_OVERRIDE = '\u202E';
|
||||
const UNICODE_REPLACEMENT_CHARACTER = '\uFFFD';
|
||||
const INVALID_CHARACTERS_PATTERN = new RegExp(
|
||||
`[${UNICODE_LEFT_TO_RIGHT_OVERRIDE}${UNICODE_RIGHT_TO_LEFT_OVERRIDE}]`,
|
||||
'g'
|
||||
);
|
||||
// NOTE: Expose synchronous version to do property-based testing using `testcheck`,
|
||||
// which currently doesn’t support async testing:
|
||||
// https://github.com/leebyron/testcheck-js/issues/45
|
||||
exports._replaceUnicodeOrderOverridesSync = attachment => {
|
||||
if (!is.string(attachment.fileName)) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const normalizedFilename = attachment.fileName.replace(
|
||||
INVALID_CHARACTERS_PATTERN,
|
||||
UNICODE_REPLACEMENT_CHARACTER
|
||||
);
|
||||
const newAttachment = { ...attachment, fileName: normalizedFilename };
|
||||
|
||||
return newAttachment;
|
||||
};
|
||||
|
||||
exports.replaceUnicodeOrderOverrides = async attachment =>
|
||||
exports._replaceUnicodeOrderOverridesSync(attachment);
|
||||
|
||||
// \u202A-\u202E is LRE, RLE, PDF, LRO, RLO
|
||||
// \u2066-\u2069 is LRI, RLI, FSI, PDI
|
||||
// \u200E is LRM
|
||||
// \u200F is RLM
|
||||
// \u061C is ALM
|
||||
const V2_UNWANTED_UNICODE = /[\u202A-\u202E\u2066-\u2069\u200E\u200F\u061C]/g;
|
||||
|
||||
exports.replaceUnicodeV2 = async attachment => {
|
||||
if (!is.string(attachment.fileName)) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const fileName = attachment.fileName.replace(
|
||||
V2_UNWANTED_UNICODE,
|
||||
UNICODE_REPLACEMENT_CHARACTER
|
||||
);
|
||||
|
||||
return {
|
||||
...attachment,
|
||||
fileName,
|
||||
};
|
||||
};
|
||||
|
||||
exports.removeSchemaVersion = ({ attachment, logger }) => {
|
||||
if (!exports.isValid(attachment)) {
|
||||
logger.error(
|
||||
'Attachment.removeSchemaVersion: Invalid input attachment:',
|
||||
attachment
|
||||
);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const attachmentWithoutSchemaVersion = { ...attachment };
|
||||
delete attachmentWithoutSchemaVersion.schemaVersion;
|
||||
return attachmentWithoutSchemaVersion;
|
||||
};
|
||||
|
||||
exports.migrateDataToFileSystem = migrateDataToFileSystem;
|
||||
|
||||
// hasData :: Attachment -> Boolean
|
||||
exports.hasData = attachment =>
|
||||
attachment.data instanceof ArrayBuffer || ArrayBuffer.isView(attachment.data);
|
||||
|
||||
// loadData :: (RelativePath -> IO (Promise ArrayBuffer))
|
||||
// Attachment ->
|
||||
// IO (Promise Attachment)
|
||||
exports.loadData = readAttachmentData => {
|
||||
if (!is.function(readAttachmentData)) {
|
||||
throw new TypeError("'readAttachmentData' must be a function");
|
||||
}
|
||||
|
||||
return async attachment => {
|
||||
if (!exports.isValid(attachment)) {
|
||||
throw new TypeError("'attachment' is not valid");
|
||||
}
|
||||
|
||||
const isAlreadyLoaded = exports.hasData(attachment);
|
||||
if (isAlreadyLoaded) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
if (!is.string(attachment.path)) {
|
||||
throw new TypeError("'attachment.path' is required");
|
||||
}
|
||||
|
||||
const data = await readAttachmentData(attachment.path);
|
||||
return { ...attachment, data, size: data.byteLength };
|
||||
};
|
||||
};
|
||||
|
||||
// deleteData :: (RelativePath -> IO Unit)
|
||||
// Attachment ->
|
||||
// IO Unit
|
||||
exports.deleteData = deleteOnDisk => {
|
||||
if (!is.function(deleteOnDisk)) {
|
||||
throw new TypeError('deleteData: deleteOnDisk must be a function');
|
||||
}
|
||||
|
||||
return async attachment => {
|
||||
if (!exports.isValid(attachment)) {
|
||||
throw new TypeError('deleteData: attachment is not valid');
|
||||
}
|
||||
|
||||
const { path, thumbnail, screenshot } = attachment;
|
||||
if (is.string(path)) {
|
||||
await deleteOnDisk(path);
|
||||
}
|
||||
|
||||
if (thumbnail && is.string(thumbnail.path)) {
|
||||
await deleteOnDisk(thumbnail.path);
|
||||
}
|
||||
|
||||
if (screenshot && is.string(screenshot.path)) {
|
||||
await deleteOnDisk(screenshot.path);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
exports.isImage = AttachmentTS.isImage;
|
||||
exports.isVideo = AttachmentTS.isVideo;
|
||||
exports.isGIF = AttachmentTS.isGIF;
|
||||
exports.isAudio = AttachmentTS.isAudio;
|
||||
exports.isVoiceMessage = AttachmentTS.isVoiceMessage;
|
||||
exports.getUploadSizeLimitKb = AttachmentTS.getUploadSizeLimitKb;
|
||||
exports.save = AttachmentTS.save;
|
||||
|
||||
const THUMBNAIL_SIZE = 150;
|
||||
const THUMBNAIL_CONTENT_TYPE = 'image/png';
|
||||
|
||||
exports.captureDimensionsAndScreenshot = async (
|
||||
attachment,
|
||||
{
|
||||
writeNewAttachmentData,
|
||||
getAbsoluteAttachmentPath,
|
||||
makeObjectUrl,
|
||||
revokeObjectUrl,
|
||||
getImageDimensions,
|
||||
makeImageThumbnail,
|
||||
makeVideoScreenshot,
|
||||
logger,
|
||||
}
|
||||
) => {
|
||||
const { contentType } = attachment;
|
||||
|
||||
if (
|
||||
!GoogleChrome.isImageTypeSupported(contentType) &&
|
||||
!GoogleChrome.isVideoTypeSupported(contentType)
|
||||
) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
// If the attachment hasn't been downloaded yet, we won't have a path
|
||||
if (!attachment.path) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const absolutePath = await getAbsoluteAttachmentPath(attachment.path);
|
||||
|
||||
if (GoogleChrome.isImageTypeSupported(contentType)) {
|
||||
try {
|
||||
const { width, height } = await getImageDimensions({
|
||||
objectUrl: absolutePath,
|
||||
logger,
|
||||
});
|
||||
const thumbnailBuffer = await blobToArrayBuffer(
|
||||
await makeImageThumbnail({
|
||||
size: THUMBNAIL_SIZE,
|
||||
objectUrl: absolutePath,
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
logger,
|
||||
})
|
||||
);
|
||||
|
||||
const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer);
|
||||
return {
|
||||
...attachment,
|
||||
width,
|
||||
height,
|
||||
thumbnail: {
|
||||
path: thumbnailPath,
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
width: THUMBNAIL_SIZE,
|
||||
height: THUMBNAIL_SIZE,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'captureDimensionsAndScreenshot:',
|
||||
'error processing image; skipping screenshot generation',
|
||||
toLogFormat(error)
|
||||
);
|
||||
return attachment;
|
||||
}
|
||||
}
|
||||
|
||||
let screenshotObjectUrl;
|
||||
try {
|
||||
const screenshotBuffer = await blobToArrayBuffer(
|
||||
await makeVideoScreenshot({
|
||||
objectUrl: absolutePath,
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
logger,
|
||||
})
|
||||
);
|
||||
screenshotObjectUrl = makeObjectUrl(
|
||||
screenshotBuffer,
|
||||
THUMBNAIL_CONTENT_TYPE
|
||||
);
|
||||
const { width, height } = await getImageDimensions({
|
||||
objectUrl: screenshotObjectUrl,
|
||||
logger,
|
||||
});
|
||||
const screenshotPath = await writeNewAttachmentData(screenshotBuffer);
|
||||
|
||||
const thumbnailBuffer = await blobToArrayBuffer(
|
||||
await makeImageThumbnail({
|
||||
size: THUMBNAIL_SIZE,
|
||||
objectUrl: screenshotObjectUrl,
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
logger,
|
||||
})
|
||||
);
|
||||
|
||||
const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer);
|
||||
|
||||
return {
|
||||
...attachment,
|
||||
screenshot: {
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
path: screenshotPath,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
thumbnail: {
|
||||
path: thumbnailPath,
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
width: THUMBNAIL_SIZE,
|
||||
height: THUMBNAIL_SIZE,
|
||||
},
|
||||
width,
|
||||
height,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'captureDimensionsAndScreenshot: error processing video; skipping screenshot generation',
|
||||
toLogFormat(error)
|
||||
);
|
||||
return attachment;
|
||||
} finally {
|
||||
revokeObjectUrl(screenshotObjectUrl);
|
||||
}
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
const { isArrayBuffer, isFunction, isUndefined, omit } = require('lodash');
|
||||
|
||||
// type Context :: {
|
||||
// writeNewAttachmentData :: ArrayBuffer -> Promise (IO Path)
|
||||
// }
|
||||
//
|
||||
// migrateDataToFileSystem :: Attachment ->
|
||||
// Context ->
|
||||
// Promise Attachment
|
||||
exports.migrateDataToFileSystem = async (
|
||||
attachment,
|
||||
{ writeNewAttachmentData } = {}
|
||||
) => {
|
||||
if (!isFunction(writeNewAttachmentData)) {
|
||||
throw new TypeError("'writeNewAttachmentData' must be a function");
|
||||
}
|
||||
|
||||
const { data } = attachment;
|
||||
const hasData = !isUndefined(data);
|
||||
const shouldSkipSchemaUpgrade = !hasData;
|
||||
if (shouldSkipSchemaUpgrade) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const isValidData = isArrayBuffer(data);
|
||||
if (!isValidData) {
|
||||
throw new TypeError(
|
||||
'Expected `attachment.data` to be an array buffer;' +
|
||||
` got: ${typeof attachment.data}`
|
||||
);
|
||||
}
|
||||
|
||||
const path = await writeNewAttachmentData(data);
|
||||
|
||||
const attachmentWithoutData = omit({ ...attachment, path }, ['data']);
|
||||
return attachmentWithoutData;
|
||||
};
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
const { omit, compact, map } = require('lodash');
|
||||
|
||||
const { toLogFormat } = require('./errors');
|
||||
const { toLogFormat } = require('../../../ts/types/errors');
|
||||
const { SignalService } = require('../../../ts/protobuf');
|
||||
const { parse: parsePhoneNumber } = require('../../../ts/types/PhoneNumber');
|
||||
|
||||
|
|
4
js/modules/types/errors.d.ts
vendored
4
js/modules/types/errors.d.ts
vendored
|
@ -1,4 +0,0 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export function toLogFormat(error: any): string;
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// toLogFormat :: Error -> String
|
||||
exports.toLogFormat = error => {
|
||||
if (!error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error && error.stack) {
|
||||
return error.stack;
|
||||
}
|
||||
|
||||
return error.toString();
|
||||
};
|
|
@ -4,8 +4,8 @@
|
|||
const { isFunction, isObject, isString, omit } = require('lodash');
|
||||
|
||||
const Contact = require('./contact');
|
||||
const Attachment = require('./attachment');
|
||||
const Errors = require('./errors');
|
||||
const Attachment = require('../../../ts/types/Attachment');
|
||||
const Errors = require('../../../ts/types/errors');
|
||||
const SchemaVersion = require('./schema_version');
|
||||
const {
|
||||
initializeAttachmentMetadata,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
const loadImage = require('blueimp-load-image');
|
||||
const { blobToArrayBuffer } = require('blob-util');
|
||||
const { toLogFormat } = require('./errors');
|
||||
const { toLogFormat } = require('../../../ts/types/errors');
|
||||
const {
|
||||
arrayBufferToObjectURL,
|
||||
} = require('../../../ts/util/arrayBufferToObjectURL');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue