Show lightbox for GIFs

This commit is contained in:
Fedor Indutny 2021-07-14 16:39:52 -07:00 committed by GitHub
parent 62ab66c1c8
commit c3bdf3d411
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 790 additions and 815 deletions

View file

@ -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');

View file

@ -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, wed preserve the original image data for users who want to
// retain it but due to reports of data loss, we dont 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 doesnt 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);
}
};

View file

@ -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;
};

View file

@ -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');

View file

@ -1,4 +0,0 @@
// Copyright 2018-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export function toLogFormat(error: any): string;

View file

@ -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();
};

View file

@ -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,

View file

@ -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');