Show lightbox for GIFs
This commit is contained in:
parent
62ab66c1c8
commit
c3bdf3d411
30 changed files with 790 additions and 815 deletions
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { app, dialog, clipboard } from 'electron';
|
||||
|
||||
import * as Errors from '../js/modules/types/errors';
|
||||
import * as Errors from '../ts/types/errors';
|
||||
import { redactAll } from '../ts/util/privacy';
|
||||
import { LocaleMessagesType } from '../ts/types/I18N';
|
||||
import { reallyJsonStringify } from '../ts/util/reallyJsonStringify';
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
"axe-core": "4.1.4",
|
||||
"backbone": "1.4.0",
|
||||
"better-sqlite3": "https://github.com/signalapp/better-sqlite3#2fa02d2484e9f9a10df5ac7ea4617fb2dff30006",
|
||||
"blob-util": "1.3.0",
|
||||
"blob-util": "2.0.2",
|
||||
"blueimp-load-image": "5.14.0",
|
||||
"blurhash": "1.1.3",
|
||||
"classnames": "2.2.5",
|
||||
|
|
|
@ -4084,6 +4084,10 @@ button.module-conversation-details__action-button {
|
|||
line-height: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
video {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
// Only if it's a sticker do we put the outline inside it
|
||||
|
|
|
@ -1,234 +0,0 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
require('mocha-testcheck').install();
|
||||
|
||||
const { assert } = require('chai');
|
||||
|
||||
const Attachment = require('../../../js/modules/types/attachment');
|
||||
const {
|
||||
stringToArrayBuffer,
|
||||
} = require('../../../js/modules/string_to_array_buffer');
|
||||
|
||||
describe('Attachment', () => {
|
||||
describe('replaceUnicodeOrderOverrides', () => {
|
||||
it('should sanitize left-to-right order override character', async () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'test\u202Dfig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'test\uFFFDfig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should sanitize right-to-left order override character', async () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'test\u202Efig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'test\uFFFDfig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should sanitize multiple override characters', async () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'test\u202e\u202dlol\u202efig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'test\uFFFD\uFFFDlol\uFFFDfig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
const hasNoUnicodeOrderOverrides = value =>
|
||||
!value.includes('\u202D') && !value.includes('\u202E');
|
||||
|
||||
check.it(
|
||||
'should ignore non-order-override characters',
|
||||
gen.string.suchThat(hasNoUnicodeOrderOverrides),
|
||||
fileName => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName,
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = Attachment._replaceUnicodeOrderOverridesSync(input);
|
||||
assert.deepEqual(actual, input);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('replaceUnicodeV2', () => {
|
||||
it('should remove all bad characters', async () => {
|
||||
const input = {
|
||||
size: 1111,
|
||||
fileName:
|
||||
'file\u202A\u202B\u202C\u202D\u202E\u2066\u2067\u2068\u2069\u200E\u200F\u061C.jpeg',
|
||||
};
|
||||
const expected = {
|
||||
fileName:
|
||||
'file\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD.jpeg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeV2(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should should leave normal filename alone', async () => {
|
||||
const input = {
|
||||
fileName: 'normal.jpeg',
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
fileName: 'normal.jpeg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeV2(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should handle missing fileName', async () => {
|
||||
const input = {
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeV2(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeSchemaVersion', () => {
|
||||
it('should remove existing schema version', () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
schemaVersion: 1,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = Attachment.removeSchemaVersion({
|
||||
attachment: input,
|
||||
logger: {
|
||||
error: () => null,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateDataToFileSystem', () => {
|
||||
it('should write data to disk and store relative path to it', async () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
data: stringToArrayBuffer('Above us only sky'),
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
contentType: 'image/jpeg',
|
||||
path: 'abc/abcdefgh123456789',
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const expectedAttachmentData = stringToArrayBuffer('Above us only sky');
|
||||
const writeNewAttachmentData = async attachmentData => {
|
||||
assert.deepEqual(attachmentData, expectedAttachmentData);
|
||||
return 'abc/abcdefgh123456789';
|
||||
};
|
||||
|
||||
const actual = await Attachment.migrateDataToFileSystem(input, {
|
||||
writeNewAttachmentData,
|
||||
logger: {
|
||||
warn: () => null,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should skip over (invalid) attachments without data', async () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
contentType: 'image/jpeg',
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
||||
|
||||
const actual = await Attachment.migrateDataToFileSystem(input, {
|
||||
writeNewAttachmentData,
|
||||
logger: {
|
||||
warn: () => null,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should throw error if data is not valid', async () => {
|
||||
const input = {
|
||||
contentType: 'image/jpeg',
|
||||
data: 42,
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
||||
|
||||
try {
|
||||
await Attachment.migrateDataToFileSystem(input, {
|
||||
writeNewAttachmentData,
|
||||
logger: {
|
||||
warn: () => null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
'Expected `attachment.data` to be an array buffer; got: number'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.fail('Unreachable');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -31,6 +31,7 @@ export type Props = {
|
|||
objectURL: string;
|
||||
caption?: string;
|
||||
isViewOnce: boolean;
|
||||
loop?: boolean;
|
||||
onNext?: () => void;
|
||||
onPrevious?: () => void;
|
||||
onSave?: () => void;
|
||||
|
@ -300,6 +301,7 @@ export class Lightbox extends React.Component<Props, State> {
|
|||
contentType,
|
||||
i18n,
|
||||
isViewOnce,
|
||||
loop = false,
|
||||
objectURL,
|
||||
onNext,
|
||||
onPrevious,
|
||||
|
@ -320,7 +322,13 @@ export class Lightbox extends React.Component<Props, State> {
|
|||
<div style={styles.controlsOffsetPlaceholder} />
|
||||
<div style={styles.objectContainer}>
|
||||
{!is.undefined(contentType)
|
||||
? this.renderObject({ objectURL, contentType, i18n, isViewOnce })
|
||||
? this.renderObject({
|
||||
objectURL,
|
||||
contentType,
|
||||
i18n,
|
||||
isViewOnce,
|
||||
loop,
|
||||
})
|
||||
: null}
|
||||
{caption ? <div style={styles.caption}>{caption}</div> : null}
|
||||
</div>
|
||||
|
@ -363,11 +371,13 @@ export class Lightbox extends React.Component<Props, State> {
|
|||
contentType,
|
||||
i18n,
|
||||
isViewOnce,
|
||||
loop,
|
||||
}: {
|
||||
objectURL: string;
|
||||
contentType: MIME.MIMEType;
|
||||
i18n: LocalizerType;
|
||||
isViewOnce: boolean;
|
||||
loop: boolean;
|
||||
}) => {
|
||||
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
|
||||
if (isImageTypeSupported) {
|
||||
|
@ -392,8 +402,8 @@ export class Lightbox extends React.Component<Props, State> {
|
|||
return (
|
||||
<video
|
||||
ref={this.videoRef}
|
||||
loop={isViewOnce}
|
||||
controls={!isViewOnce}
|
||||
loop={loop || isViewOnce}
|
||||
controls={!loop && !isViewOnce}
|
||||
style={styles.object}
|
||||
key={objectURL}
|
||||
>
|
||||
|
|
|
@ -29,6 +29,7 @@ export type Props = {
|
|||
readonly reducedMotion?: boolean;
|
||||
|
||||
onError(): void;
|
||||
showVisualAttachment(): void;
|
||||
kickOffAttachmentDownload(): void;
|
||||
};
|
||||
|
||||
|
@ -48,6 +49,7 @@ export const GIF: React.FC<Props> = props => {
|
|||
),
|
||||
|
||||
onError,
|
||||
showVisualAttachment,
|
||||
kickOffAttachmentDownload,
|
||||
} = props;
|
||||
|
||||
|
@ -191,6 +193,12 @@ export const GIF: React.FC<Props> = props => {
|
|||
onTimeUpdate={onTimeUpdate}
|
||||
onEnded={onEnded}
|
||||
onError={onError}
|
||||
onClick={(event: React.MouseEvent): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
showVisualAttachment();
|
||||
}}
|
||||
className="module-image--gif__video"
|
||||
autoPlay
|
||||
playsInline
|
||||
|
|
|
@ -716,6 +716,12 @@ export class Message extends React.Component<Props, State> {
|
|||
tabIndex={0}
|
||||
reducedMotion={reducedMotion}
|
||||
onError={this.handleImageError}
|
||||
showVisualAttachment={() => {
|
||||
showVisualAttachment({
|
||||
attachment: firstAttachment,
|
||||
messageId: id,
|
||||
});
|
||||
}}
|
||||
kickOffAttachmentDownload={() => {
|
||||
kickOffAttachmentDownload({
|
||||
attachment: firstAttachment,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { Attachment } from '../../../../types/Attachment';
|
||||
import { AttachmentType } from '../../../../types/Attachment';
|
||||
|
||||
export type Message = {
|
||||
id: string;
|
||||
attachments: Array<Attachment>;
|
||||
attachments: Array<AttachmentType>;
|
||||
// Assuming this is for the API
|
||||
// eslint-disable-next-line camelcase
|
||||
received_at: number;
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
const Path = require('path');
|
||||
|
||||
const { assert } = require('chai');
|
||||
|
||||
const Errors = require('../../../js/modules/types/errors');
|
||||
import * as Path from 'path';
|
||||
import { assert } from 'chai';
|
||||
import * as Errors from '../../types/errors';
|
||||
|
||||
const APP_ROOT_PATH = Path.join(__dirname, '..', '..', '..');
|
||||
|
||||
|
@ -26,9 +24,9 @@ describe('Errors', () => {
|
|||
|
||||
it('should return error string representation if stack is missing', () => {
|
||||
const error = new Error('boom');
|
||||
error.stack = null;
|
||||
error.stack = undefined;
|
||||
assert.typeOf(error, 'Error');
|
||||
assert.isNull(error.stack);
|
||||
assert.isUndefined(error.stack);
|
||||
|
||||
const formattedError = Errors.toLogFormat(error);
|
||||
assert.strictEqual(formattedError, 'Error: boom');
|
||||
|
@ -37,7 +35,7 @@ describe('Errors', () => {
|
|||
[0, false, null, undefined].forEach(value => {
|
||||
it(`should return \`${value}\` argument`, () => {
|
||||
const formattedNonError = Errors.toLogFormat(value);
|
||||
assert.strictEqual(formattedNonError, value);
|
||||
assert.strictEqual(formattedNonError, String(value));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,6 +7,7 @@ import * as Attachment from '../../types/Attachment';
|
|||
import * as MIME from '../../types/MIME';
|
||||
import { SignalService } from '../../protobuf';
|
||||
import { stringToArrayBuffer } from '../../../js/modules/string_to_array_buffer';
|
||||
import * as logger from '../../logging/log';
|
||||
|
||||
describe('Attachment', () => {
|
||||
describe('getUploadSizeLimitKb', () => {
|
||||
|
@ -36,7 +37,7 @@ describe('Attachment', () => {
|
|||
|
||||
describe('getFileExtension', () => {
|
||||
it('should return file extension from content type', () => {
|
||||
const input: Attachment.Attachment = {
|
||||
const input: Attachment.AttachmentType = {
|
||||
data: stringToArrayBuffer('foo'),
|
||||
contentType: MIME.IMAGE_GIF,
|
||||
};
|
||||
|
@ -44,7 +45,7 @@ describe('Attachment', () => {
|
|||
});
|
||||
|
||||
it('should return file extension for QuickTime videos', () => {
|
||||
const input: Attachment.Attachment = {
|
||||
const input: Attachment.AttachmentType = {
|
||||
data: stringToArrayBuffer('foo'),
|
||||
contentType: MIME.VIDEO_QUICKTIME,
|
||||
};
|
||||
|
@ -55,7 +56,7 @@ describe('Attachment', () => {
|
|||
describe('getSuggestedFilename', () => {
|
||||
context('for attachment with filename', () => {
|
||||
it('should return existing filename if present', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
fileName: 'funny-cat.mov',
|
||||
data: stringToArrayBuffer('foo'),
|
||||
contentType: MIME.VIDEO_QUICKTIME,
|
||||
|
@ -67,7 +68,7 @@ describe('Attachment', () => {
|
|||
});
|
||||
context('for attachment without filename', () => {
|
||||
it('should generate a filename based on timestamp', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
data: stringToArrayBuffer('foo'),
|
||||
contentType: MIME.VIDEO_QUICKTIME,
|
||||
};
|
||||
|
@ -82,7 +83,7 @@ describe('Attachment', () => {
|
|||
});
|
||||
context('for attachment with index', () => {
|
||||
it('should generate a filename based on timestamp', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
data: stringToArrayBuffer('foo'),
|
||||
contentType: MIME.VIDEO_QUICKTIME,
|
||||
};
|
||||
|
@ -100,7 +101,7 @@ describe('Attachment', () => {
|
|||
|
||||
describe('isVisualMedia', () => {
|
||||
it('should return true for images', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
fileName: 'meme.gif',
|
||||
data: stringToArrayBuffer('gif'),
|
||||
contentType: MIME.IMAGE_GIF,
|
||||
|
@ -109,7 +110,7 @@ describe('Attachment', () => {
|
|||
});
|
||||
|
||||
it('should return true for videos', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
fileName: 'meme.mp4',
|
||||
data: stringToArrayBuffer('mp4'),
|
||||
contentType: MIME.VIDEO_MP4,
|
||||
|
@ -118,7 +119,7 @@ describe('Attachment', () => {
|
|||
});
|
||||
|
||||
it('should return false for voice message attachment', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
fileName: 'Voice Message.aac',
|
||||
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
data: stringToArrayBuffer('voice message'),
|
||||
|
@ -128,7 +129,7 @@ describe('Attachment', () => {
|
|||
});
|
||||
|
||||
it('should return false for other attachments', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
fileName: 'foo.json',
|
||||
data: stringToArrayBuffer('{"foo": "bar"}'),
|
||||
contentType: MIME.APPLICATION_JSON,
|
||||
|
@ -139,7 +140,7 @@ describe('Attachment', () => {
|
|||
|
||||
describe('isFile', () => {
|
||||
it('should return true for JSON', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
fileName: 'foo.json',
|
||||
data: stringToArrayBuffer('{"foo": "bar"}'),
|
||||
contentType: MIME.APPLICATION_JSON,
|
||||
|
@ -148,7 +149,7 @@ describe('Attachment', () => {
|
|||
});
|
||||
|
||||
it('should return false for images', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
fileName: 'meme.gif',
|
||||
data: stringToArrayBuffer('gif'),
|
||||
contentType: MIME.IMAGE_GIF,
|
||||
|
@ -157,7 +158,7 @@ describe('Attachment', () => {
|
|||
});
|
||||
|
||||
it('should return false for videos', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
fileName: 'meme.mp4',
|
||||
data: stringToArrayBuffer('mp4'),
|
||||
contentType: MIME.VIDEO_MP4,
|
||||
|
@ -166,7 +167,7 @@ describe('Attachment', () => {
|
|||
});
|
||||
|
||||
it('should return false for voice message attachment', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
fileName: 'Voice Message.aac',
|
||||
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
data: stringToArrayBuffer('voice message'),
|
||||
|
@ -178,7 +179,7 @@ describe('Attachment', () => {
|
|||
|
||||
describe('isVoiceMessage', () => {
|
||||
it('should return true for voice message attachment', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
fileName: 'Voice Message.aac',
|
||||
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||
data: stringToArrayBuffer('voice message'),
|
||||
|
@ -188,7 +189,7 @@ describe('Attachment', () => {
|
|||
});
|
||||
|
||||
it('should return true for legacy Android voice message attachment', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
data: stringToArrayBuffer('voice message'),
|
||||
contentType: MIME.AUDIO_MP3,
|
||||
};
|
||||
|
@ -196,7 +197,7 @@ describe('Attachment', () => {
|
|||
});
|
||||
|
||||
it('should return false for other attachments', () => {
|
||||
const attachment: Attachment.Attachment = {
|
||||
const attachment: Attachment.AttachmentType = {
|
||||
fileName: 'foo.gif',
|
||||
data: stringToArrayBuffer('foo'),
|
||||
contentType: MIME.IMAGE_GIF,
|
||||
|
@ -204,4 +205,222 @@ describe('Attachment', () => {
|
|||
assert.isFalse(Attachment.isVoiceMessage(attachment));
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceUnicodeOrderOverrides', () => {
|
||||
it('should sanitize left-to-right order override character', async () => {
|
||||
const input = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName: 'test\u202Dfig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName: 'test\uFFFDfig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should sanitize right-to-left order override character', async () => {
|
||||
const input = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName: 'test\u202Efig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName: 'test\uFFFDfig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should sanitize multiple override characters', async () => {
|
||||
const input = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName: 'test\u202e\u202dlol\u202efig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName: 'test\uFFFD\uFFFDlol\uFFFDfig.exe',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeOrderOverrides(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should ignore non-order-override characters', () => {
|
||||
const input = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName: 'abc',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = Attachment._replaceUnicodeOrderOverridesSync(input);
|
||||
assert.deepEqual(actual, input);
|
||||
});
|
||||
|
||||
it('should replace order-override characters', () => {
|
||||
const input = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName: 'abc\u202D\u202E',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = Attachment._replaceUnicodeOrderOverridesSync(input);
|
||||
assert.deepEqual(actual, {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName: 'abc\uFFFD\uFFFD',
|
||||
size: 1111,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceUnicodeV2', () => {
|
||||
it('should remove all bad characters', async () => {
|
||||
const input = {
|
||||
size: 1111,
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName:
|
||||
'file\u202A\u202B\u202C\u202D\u202E\u2066\u2067\u2068\u2069\u200E\u200F\u061C.jpeg',
|
||||
};
|
||||
const expected = {
|
||||
fileName:
|
||||
'file\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD.jpeg',
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeV2(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should should leave normal filename alone', async () => {
|
||||
const input = {
|
||||
fileName: 'normal.jpeg',
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
size: 1111,
|
||||
};
|
||||
const expected = {
|
||||
fileName: 'normal.jpeg',
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeV2(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should handle missing fileName', async () => {
|
||||
const input = {
|
||||
size: 1111,
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
};
|
||||
const expected = {
|
||||
size: 1111,
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
};
|
||||
|
||||
const actual = await Attachment.replaceUnicodeV2(input);
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeSchemaVersion', () => {
|
||||
it('should remove existing schema version', () => {
|
||||
const input = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
schemaVersion: 1,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const actual = Attachment.removeSchemaVersion({
|
||||
attachment: input,
|
||||
logger,
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateDataToFileSystem', () => {
|
||||
it('should write data to disk and store relative path to it', async () => {
|
||||
const input = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
data: stringToArrayBuffer('Above us only sky'),
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
path: 'abc/abcdefgh123456789',
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const expectedAttachmentData = stringToArrayBuffer('Above us only sky');
|
||||
const writeNewAttachmentData = async (attachmentData: ArrayBuffer) => {
|
||||
assert.deepEqual(attachmentData, expectedAttachmentData);
|
||||
return 'abc/abcdefgh123456789';
|
||||
};
|
||||
|
||||
const actual = await Attachment.migrateDataToFileSystem(input, {
|
||||
writeNewAttachmentData,
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should skip over (invalid) attachments without data', async () => {
|
||||
const input = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
||||
|
||||
const actual = await Attachment.migrateDataToFileSystem(input, {
|
||||
writeNewAttachmentData,
|
||||
});
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should throw error if data is not valid', async () => {
|
||||
const input = {
|
||||
contentType: MIME.IMAGE_JPEG,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: 123 as any,
|
||||
fileName: 'foo.jpg',
|
||||
size: 1111,
|
||||
};
|
||||
|
||||
const writeNewAttachmentData = async () => 'abc/abcdefgh123456789';
|
||||
|
||||
await assert.isRejected(
|
||||
Attachment.migrateDataToFileSystem(input, {
|
||||
writeNewAttachmentData,
|
||||
}),
|
||||
'Expected `attachment.data` to be an array buffer; got: number'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
7
ts/textsecure.d.ts
vendored
7
ts/textsecure.d.ts
vendored
|
@ -37,13 +37,6 @@ export type UnprocessedType = {
|
|||
|
||||
export { StorageServiceCallOptionsType, StorageServiceCredentials };
|
||||
|
||||
export type DownloadAttachmentType = Omit<
|
||||
ProcessedAttachment,
|
||||
'digest' | 'key'
|
||||
> & {
|
||||
data: ArrayBuffer;
|
||||
};
|
||||
|
||||
export type TextSecureType = {
|
||||
createTaskWithTimeout: (
|
||||
task: () => Promise<any> | any,
|
||||
|
|
|
@ -62,11 +62,13 @@ import * as Bytes from '../Bytes';
|
|||
import Crypto from './Crypto';
|
||||
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
|
||||
import { ContactBuffer, GroupBuffer } from './ContactsParser';
|
||||
import { DownloadedAttachmentType } from '../types/Attachment';
|
||||
import * as MIME from '../types/MIME';
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
|
||||
import { DownloadAttachmentType, UnprocessedType } from '../textsecure.d';
|
||||
import { UnprocessedType } from '../textsecure.d';
|
||||
import {
|
||||
ProcessedAttachment,
|
||||
ProcessedDataMessage,
|
||||
|
@ -2441,7 +2443,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
|
||||
async downloadAttachment(
|
||||
attachment: ProcessedAttachment
|
||||
): Promise<DownloadAttachmentType> {
|
||||
): Promise<DownloadedAttachmentType> {
|
||||
const cdnId = attachment.cdnId || attachment.cdnKey;
|
||||
const { cdnNumber } = attachment;
|
||||
|
||||
|
@ -2454,7 +2456,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
cdnId,
|
||||
dropNull(cdnNumber)
|
||||
);
|
||||
const { key, digest, size } = attachment;
|
||||
const { key, digest, size, contentType } = attachment;
|
||||
|
||||
if (!digest) {
|
||||
throw new Error('Failure: Ask sender to update Signal and resend.');
|
||||
|
@ -2479,13 +2481,17 @@ class MessageReceiverInner extends EventTarget {
|
|||
|
||||
return {
|
||||
...omit(attachment, 'digest', 'key'),
|
||||
|
||||
contentType: contentType
|
||||
? MIME.fromString(contentType)
|
||||
: MIME.APPLICATION_OCTET_STREAM,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
async handleAttachment(
|
||||
attachment: Proto.IAttachmentPointer
|
||||
): Promise<DownloadAttachmentType> {
|
||||
): Promise<DownloadedAttachmentType> {
|
||||
const cleaned = processAttachment(attachment);
|
||||
return this.downloadAttachment(cleaned);
|
||||
}
|
||||
|
@ -2661,7 +2667,7 @@ export default class MessageReceiver {
|
|||
|
||||
downloadAttachment: (
|
||||
attachment: ProcessedAttachment
|
||||
) => Promise<DownloadAttachmentType>;
|
||||
) => Promise<DownloadedAttachmentType>;
|
||||
|
||||
getStatus: () => SocketStatus;
|
||||
|
||||
|
|
|
@ -3,15 +3,27 @@
|
|||
|
||||
import is from '@sindresorhus/is';
|
||||
import moment from 'moment';
|
||||
import { isNumber, padStart } from 'lodash';
|
||||
import {
|
||||
isNumber,
|
||||
padStart,
|
||||
isArrayBuffer,
|
||||
isFunction,
|
||||
isUndefined,
|
||||
omit,
|
||||
} from 'lodash';
|
||||
import { arrayBufferToBlob, blobToArrayBuffer } from 'blob-util';
|
||||
|
||||
import { LoggerType } from './Logging';
|
||||
import * as MIME from './MIME';
|
||||
import { toLogFormat } from './errors';
|
||||
import { SignalService } from '../protobuf';
|
||||
import {
|
||||
isImageTypeSupported,
|
||||
isVideoTypeSupported,
|
||||
} from '../util/GoogleChrome';
|
||||
import { LocalizerType, ThemeType } from './Util';
|
||||
import * as GoogleChrome from '../util/GoogleChrome';
|
||||
import { scaleImageToLevel } from '../util/scaleImageToLevel';
|
||||
|
||||
const MAX_WIDTH = 300;
|
||||
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
||||
|
@ -39,7 +51,7 @@ export type AttachmentType = {
|
|||
screenshot?: {
|
||||
height: number;
|
||||
width: number;
|
||||
url: string;
|
||||
url?: string;
|
||||
contentType: MIME.MIMEType;
|
||||
path: string;
|
||||
};
|
||||
|
@ -51,11 +63,16 @@ export type AttachmentType = {
|
|||
cdnNumber?: number;
|
||||
cdnId?: string;
|
||||
cdnKey?: string;
|
||||
data?: ArrayBuffer;
|
||||
|
||||
/** Legacy field. Used only for downloading old attachments */
|
||||
id?: number;
|
||||
};
|
||||
|
||||
export type DownloadedAttachmentType = AttachmentType & {
|
||||
data: ArrayBuffer;
|
||||
};
|
||||
|
||||
type BaseAttachmentDraftType = {
|
||||
blurHash?: string;
|
||||
contentType: MIME.MIMEType;
|
||||
|
@ -82,13 +99,411 @@ export type AttachmentDraftType = {
|
|||
export type ThumbnailType = {
|
||||
height: number;
|
||||
width: number;
|
||||
url: string;
|
||||
url?: string;
|
||||
contentType: MIME.MIMEType;
|
||||
path: string;
|
||||
// Only used when quote needed to make an in-memory thumbnail
|
||||
objectUrl?: string;
|
||||
};
|
||||
|
||||
export async function migrateDataToFileSystem(
|
||||
attachment: AttachmentType,
|
||||
{
|
||||
writeNewAttachmentData,
|
||||
}: {
|
||||
writeNewAttachmentData: (data: ArrayBuffer) => Promise<string>;
|
||||
}
|
||||
): Promise<AttachmentType> {
|
||||
if (!isFunction(writeNewAttachmentData)) {
|
||||
throw new TypeError("'writeNewAttachmentData' must be a function");
|
||||
}
|
||||
|
||||
const { data } = attachment;
|
||||
const attachmentHasData = !isUndefined(data);
|
||||
const shouldSkipSchemaUpgrade = !attachmentHasData;
|
||||
if (shouldSkipSchemaUpgrade) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
if (!isArrayBuffer(data)) {
|
||||
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;
|
||||
}
|
||||
|
||||
// // 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.
|
||||
export function isValid(
|
||||
rawAttachment?: AttachmentType
|
||||
): rawAttachment is AttachmentType {
|
||||
// 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:
|
||||
export async function autoOrientJPEG(
|
||||
attachment: AttachmentType,
|
||||
_: unknown,
|
||||
message?: { sendHQImages?: boolean }
|
||||
): Promise<AttachmentType> {
|
||||
if (!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 = {
|
||||
// `digest` is no longer valid for auto-oriented image data, so we discard it:
|
||||
...omit(attachment, 'digest'),
|
||||
data: xcodedDataArrayBuffer,
|
||||
size: xcodedDataArrayBuffer.byteLength,
|
||||
};
|
||||
|
||||
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
|
||||
export function _replaceUnicodeOrderOverridesSync(
|
||||
attachment: AttachmentType
|
||||
): AttachmentType {
|
||||
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;
|
||||
}
|
||||
|
||||
export const replaceUnicodeOrderOverrides = async (
|
||||
attachment: AttachmentType
|
||||
): Promise<AttachmentType> => {
|
||||
return _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;
|
||||
|
||||
export async function replaceUnicodeV2(
|
||||
attachment: AttachmentType
|
||||
): Promise<AttachmentType> {
|
||||
if (!is.string(attachment.fileName)) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const fileName = attachment.fileName.replace(
|
||||
V2_UNWANTED_UNICODE,
|
||||
UNICODE_REPLACEMENT_CHARACTER
|
||||
);
|
||||
|
||||
return {
|
||||
...attachment,
|
||||
fileName,
|
||||
};
|
||||
}
|
||||
|
||||
export function removeSchemaVersion({
|
||||
attachment,
|
||||
logger,
|
||||
}: {
|
||||
attachment: AttachmentType;
|
||||
logger: LoggerType;
|
||||
}): AttachmentType {
|
||||
if (!exports.isValid(attachment)) {
|
||||
logger.error(
|
||||
'Attachment.removeSchemaVersion: Invalid input attachment:',
|
||||
attachment
|
||||
);
|
||||
return attachment;
|
||||
}
|
||||
|
||||
return omit(attachment, 'schemaVersion');
|
||||
}
|
||||
|
||||
export function hasData(attachment: AttachmentType): boolean {
|
||||
return (
|
||||
attachment.data instanceof ArrayBuffer ||
|
||||
ArrayBuffer.isView(attachment.data)
|
||||
);
|
||||
}
|
||||
|
||||
export function loadData(
|
||||
readAttachmentData: (path: string) => Promise<ArrayBuffer>
|
||||
): (attachment?: AttachmentType) => Promise<AttachmentType> {
|
||||
if (!is.function_(readAttachmentData)) {
|
||||
throw new TypeError("'readAttachmentData' must be a function");
|
||||
}
|
||||
|
||||
return async (attachment?: AttachmentType): Promise<AttachmentType> => {
|
||||
if (!isValid(attachment)) {
|
||||
throw new TypeError("'attachment' is not valid");
|
||||
}
|
||||
|
||||
const isAlreadyLoaded = Boolean(attachment.data);
|
||||
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 };
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteData(
|
||||
deleteOnDisk: (path: string) => Promise<void>
|
||||
): (attachment?: AttachmentType) => Promise<void> {
|
||||
if (!is.function_(deleteOnDisk)) {
|
||||
throw new TypeError('deleteData: deleteOnDisk must be a function');
|
||||
}
|
||||
|
||||
return async (attachment?: AttachmentType): Promise<void> => {
|
||||
if (!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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const THUMBNAIL_SIZE = 150;
|
||||
const THUMBNAIL_CONTENT_TYPE = MIME.IMAGE_PNG;
|
||||
|
||||
export async function captureDimensionsAndScreenshot(
|
||||
attachment: AttachmentType,
|
||||
params: {
|
||||
writeNewAttachmentData: (data: ArrayBuffer) => Promise<string>;
|
||||
getAbsoluteAttachmentPath: (path: string) => Promise<string>;
|
||||
makeObjectUrl: (data: ArrayBuffer, contentType: MIME.MIMEType) => string;
|
||||
revokeObjectUrl: (path: string) => void;
|
||||
getImageDimensions: (params: {
|
||||
objectUrl: string;
|
||||
logger: LoggerType;
|
||||
}) => { width: number; height: number };
|
||||
makeImageThumbnail: (params: {
|
||||
size: number;
|
||||
objectUrl: string;
|
||||
contentType: MIME.MIMEType;
|
||||
logger: LoggerType;
|
||||
}) => Promise<Blob>;
|
||||
makeVideoScreenshot: (params: {
|
||||
objectUrl: string;
|
||||
contentType: MIME.MIMEType;
|
||||
logger: LoggerType;
|
||||
}) => Promise<Blob>;
|
||||
logger: LoggerType;
|
||||
}
|
||||
): Promise<AttachmentType> {
|
||||
const { contentType } = attachment;
|
||||
|
||||
const {
|
||||
writeNewAttachmentData,
|
||||
getAbsoluteAttachmentPath,
|
||||
makeObjectUrl,
|
||||
revokeObjectUrl,
|
||||
getImageDimensions: getImageDimensionsFromURL,
|
||||
makeImageThumbnail,
|
||||
makeVideoScreenshot,
|
||||
logger,
|
||||
} = params;
|
||||
|
||||
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 getImageDimensionsFromURL({
|
||||
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: string | undefined;
|
||||
try {
|
||||
const screenshotBuffer = await blobToArrayBuffer(
|
||||
await makeVideoScreenshot({
|
||||
objectUrl: absolutePath,
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
logger,
|
||||
})
|
||||
);
|
||||
screenshotObjectUrl = makeObjectUrl(
|
||||
screenshotBuffer,
|
||||
THUMBNAIL_CONTENT_TYPE
|
||||
);
|
||||
const { width, height } = await getImageDimensionsFromURL({
|
||||
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 {
|
||||
if (screenshotObjectUrl !== undefined) {
|
||||
revokeObjectUrl(screenshotObjectUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI-focused functions
|
||||
|
||||
export function getExtensionForDisplay({
|
||||
|
@ -252,7 +667,7 @@ type DimensionsType = {
|
|||
};
|
||||
|
||||
export function getImageDimensions(
|
||||
attachment: AttachmentType,
|
||||
attachment: Pick<AttachmentType, 'width' | 'height'>,
|
||||
forcedWidth?: number
|
||||
): DimensionsType {
|
||||
const { height, width } = attachment;
|
||||
|
@ -356,28 +771,7 @@ export function getAlt(
|
|||
|
||||
// Migration-related attachment stuff
|
||||
|
||||
export type Attachment = {
|
||||
fileName?: string;
|
||||
flags?: SignalService.AttachmentPointer.Flags;
|
||||
contentType?: MIME.MIMEType;
|
||||
size?: number;
|
||||
data: ArrayBuffer;
|
||||
|
||||
// // Omit unused / deprecated keys:
|
||||
// schemaVersion?: number;
|
||||
// id?: string;
|
||||
// width?: number;
|
||||
// height?: number;
|
||||
// thumbnail?: ArrayBuffer;
|
||||
// key?: ArrayBuffer;
|
||||
// digest?: ArrayBuffer;
|
||||
} & Partial<AttachmentSchemaVersion3>;
|
||||
|
||||
type AttachmentSchemaVersion3 = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export const isVisualMedia = (attachment: Attachment): boolean => {
|
||||
export const isVisualMedia = (attachment: AttachmentType): boolean => {
|
||||
const { contentType } = attachment;
|
||||
|
||||
if (is.undefined(contentType)) {
|
||||
|
@ -391,7 +785,7 @@ export const isVisualMedia = (attachment: Attachment): boolean => {
|
|||
return MIME.isImage(contentType) || MIME.isVideo(contentType);
|
||||
};
|
||||
|
||||
export const isFile = (attachment: Attachment): boolean => {
|
||||
export const isFile = (attachment: AttachmentType): boolean => {
|
||||
const { contentType } = attachment;
|
||||
|
||||
if (is.undefined(contentType)) {
|
||||
|
@ -409,9 +803,7 @@ export const isFile = (attachment: Attachment): boolean => {
|
|||
return true;
|
||||
};
|
||||
|
||||
export const isVoiceMessage = (
|
||||
attachment: Attachment | AttachmentType
|
||||
): boolean => {
|
||||
export const isVoiceMessage = (attachment: AttachmentType): boolean => {
|
||||
const flag = SignalService.AttachmentPointer.Flags.VOICE_MESSAGE;
|
||||
const hasFlag =
|
||||
// eslint-disable-next-line no-bitwise
|
||||
|
@ -438,8 +830,8 @@ export const save = async ({
|
|||
saveAttachmentToDisk,
|
||||
timestamp,
|
||||
}: {
|
||||
attachment: Attachment;
|
||||
index: number;
|
||||
attachment: AttachmentType;
|
||||
index?: number;
|
||||
readAttachmentData: (relativePath: string) => Promise<ArrayBuffer>;
|
||||
saveAttachmentToDisk: (options: {
|
||||
data: ArrayBuffer;
|
||||
|
@ -447,13 +839,15 @@ export const save = async ({
|
|||
}) => Promise<{ name: string; fullPath: string }>;
|
||||
timestamp?: number;
|
||||
}): Promise<string | null> => {
|
||||
if (!attachment.path && !attachment.data) {
|
||||
let data: ArrayBuffer;
|
||||
if (attachment.path) {
|
||||
data = await readAttachmentData(attachment.path);
|
||||
} else if (attachment.data) {
|
||||
data = attachment.data;
|
||||
} else {
|
||||
throw new Error('Attachment had neither path nor data');
|
||||
}
|
||||
|
||||
const data = attachment.path
|
||||
? await readAttachmentData(attachment.path)
|
||||
: attachment.data;
|
||||
const name = getSuggestedFilename({ attachment, timestamp, index });
|
||||
|
||||
const result = await saveAttachmentToDisk({
|
||||
|
@ -473,7 +867,7 @@ export const getSuggestedFilename = ({
|
|||
timestamp,
|
||||
index,
|
||||
}: {
|
||||
attachment: Attachment;
|
||||
attachment: AttachmentType;
|
||||
timestamp?: number | Date;
|
||||
index?: number;
|
||||
}): string => {
|
||||
|
@ -493,7 +887,7 @@ export const getSuggestedFilename = ({
|
|||
};
|
||||
|
||||
export const getFileExtension = (
|
||||
attachment: Attachment
|
||||
attachment: AttachmentType
|
||||
): string | undefined => {
|
||||
if (!attachment.contentType) {
|
||||
return undefined;
|
||||
|
|
|
@ -31,3 +31,7 @@ export const isAudio = (value: string): value is MIMEType =>
|
|||
Boolean(value) && value.startsWith('audio/') && !value.endsWith('aiff');
|
||||
export const isLongMessage = (value: unknown): value is MIMEType =>
|
||||
value === LONG_MESSAGE;
|
||||
|
||||
export const fromString = (value: string): MIMEType => {
|
||||
return value as MIMEType;
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import { Attachment } from './Attachment';
|
||||
import { AttachmentType } from './Attachment';
|
||||
import { ContactType } from './Contact';
|
||||
import { IndexableBoolean, IndexablePresence } from './IndexedDB';
|
||||
|
||||
|
@ -19,7 +19,7 @@ export type IncomingMessage = Readonly<
|
|||
{
|
||||
type: 'incoming';
|
||||
// Required
|
||||
attachments: Array<Attachment>;
|
||||
attachments: Array<AttachmentType>;
|
||||
id: string;
|
||||
received_at: number;
|
||||
|
||||
|
@ -44,7 +44,7 @@ export type OutgoingMessage = Readonly<
|
|||
type: 'outgoing';
|
||||
|
||||
// Required
|
||||
attachments: Array<Attachment>;
|
||||
attachments: Array<AttachmentType>;
|
||||
delivered: number;
|
||||
delivered_to: Array<string>;
|
||||
destination: string; // PhoneNumber
|
||||
|
|
10
ts/types/errors.ts
Normal file
10
ts/types/errors.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export function toLogFormat(error: unknown): string {
|
||||
if (error instanceof Error && error.stack) {
|
||||
return error.stack;
|
||||
}
|
||||
|
||||
return String(error);
|
||||
}
|
|
@ -6,7 +6,7 @@ import * as IndexedDB from '../IndexedDB';
|
|||
import { Message, UserMessage } from '../Message';
|
||||
|
||||
const hasAttachment = (
|
||||
predicate: (value: Attachment.Attachment) => boolean
|
||||
predicate: (value: Attachment.AttachmentType) => boolean
|
||||
) => (message: UserMessage): IndexedDB.IndexablePresence =>
|
||||
IndexedDB.toIndexablePresence(message.attachments.some(predicate));
|
||||
|
||||
|
@ -30,7 +30,7 @@ export const initializeAttachmentMetadata = async (
|
|||
}
|
||||
|
||||
const attachments = message.attachments.filter(
|
||||
(attachment: Attachment.Attachment) =>
|
||||
(attachment: Attachment.AttachmentType) =>
|
||||
attachment.contentType !== 'text/x-signal-plain'
|
||||
);
|
||||
const hasAttachments = IndexedDB.toIndexableBoolean(attachments.length > 0);
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { DownloadAttachmentType } from '../textsecure.d';
|
||||
|
||||
import { AttachmentType } from '../types/Attachment';
|
||||
import { AttachmentType, DownloadedAttachmentType } from '../types/Attachment';
|
||||
|
||||
export async function downloadAttachment(
|
||||
attachmentData: AttachmentType
|
||||
): Promise<DownloadAttachmentType | null> {
|
||||
): Promise<DownloadedAttachmentType | null> {
|
||||
let migratedAttachment: AttachmentType;
|
||||
|
||||
const { id: legacyId } = attachmentData;
|
||||
|
|
|
@ -1896,10 +1896,24 @@
|
|||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "node_modules/blob-util/dist/blob-util.js",
|
||||
"line": " bb.append(ary[i]);",
|
||||
"path": "node_modules/blob-util/dist/blob-util.cjs.js",
|
||||
"line": " builder.append(parts[i]);",
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T18:13:29.628Z"
|
||||
"updated": "2021-07-14T23:12:47.258Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "node_modules/blob-util/dist/blob-util.es.js",
|
||||
"line": " builder.append(parts[i]);",
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2021-07-14T23:12:47.258Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "node_modules/blob-util/dist/blob-util.js",
|
||||
"line": " builder.append(parts[i]);",
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2021-07-14T23:12:47.258Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
|
@ -1907,13 +1921,6 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T18:13:29.628Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "node_modules/blob/index.js",
|
||||
"line": " bb.append(ary[i]);",
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2018-09-19T18:13:29.628Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-wrap(",
|
||||
"path": "node_modules/boom/lib/index.js",
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
AttachmentType,
|
||||
InMemoryAttachmentDraftType,
|
||||
OnDiskAttachmentDraftType,
|
||||
isGIF,
|
||||
} from '../types/Attachment';
|
||||
import type { StickerPackType as StickerPackDBType } from '../sql/Interface';
|
||||
import * as Stickers from '../types/Stickers';
|
||||
|
@ -63,7 +64,7 @@ const LINK_PREVIEW_TIMEOUT = 60 * 1000;
|
|||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const { Whisper } = window;
|
||||
const { Message, MIME, VisualAttachment, Attachment } = window.Signal.Types;
|
||||
const { Message, MIME, VisualAttachment } = window.Signal.Types;
|
||||
|
||||
const {
|
||||
copyIntoTempDirectory,
|
||||
|
@ -783,7 +784,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
message.markAttachmentAsCorrupted(options.attachment);
|
||||
};
|
||||
const showVisualAttachment = (options: {
|
||||
attachment: typeof Attachment;
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
showSingle?: boolean;
|
||||
}) => {
|
||||
|
@ -2803,8 +2804,8 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
timestamp,
|
||||
isDangerous,
|
||||
}: {
|
||||
attachment: typeof Attachment;
|
||||
timestamp: string;
|
||||
attachment: AttachmentType;
|
||||
timestamp: number;
|
||||
isDangerous: boolean;
|
||||
}) {
|
||||
if (isDangerous) {
|
||||
|
@ -3012,7 +3013,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
attachment,
|
||||
messageId,
|
||||
}: {
|
||||
attachment: typeof Attachment;
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
showSingle?: boolean;
|
||||
}) {
|
||||
|
@ -3037,14 +3038,17 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
const attachments = message.get('attachments') || [];
|
||||
const attachments: Array<AttachmentType> = message.get('attachments') || [];
|
||||
|
||||
const loop = isGIF(attachments);
|
||||
|
||||
const media = attachments
|
||||
.filter((item: any) => item.thumbnail && !item.pending && !item.error)
|
||||
.map((item: any, index: number) => ({
|
||||
objectURL: getAbsoluteAttachmentPath(item.path),
|
||||
.filter(item => item.thumbnail && !item.pending && !item.error)
|
||||
.map((item, index) => ({
|
||||
objectURL: getAbsoluteAttachmentPath(item.path ?? ''),
|
||||
path: item.path,
|
||||
contentType: item.contentType,
|
||||
loop,
|
||||
index,
|
||||
message,
|
||||
attachment: item,
|
||||
|
@ -3052,9 +3056,10 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
|
||||
if (media.length === 1) {
|
||||
const props = {
|
||||
objectURL: getAbsoluteAttachmentPath(path),
|
||||
objectURL: getAbsoluteAttachmentPath(path ?? ''),
|
||||
contentType,
|
||||
caption: attachment.caption,
|
||||
loop,
|
||||
onSave: () => {
|
||||
const timestamp = message.get('sent_at');
|
||||
this.downloadAttachment({ attachment, timestamp, message });
|
||||
|
@ -3095,6 +3100,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
|
||||
const props = {
|
||||
media,
|
||||
loop,
|
||||
selectedIndex: selectedIndex >= 0 ? selectedIndex : 0,
|
||||
onSave,
|
||||
};
|
||||
|
|
41
ts/window.d.ts
vendored
41
ts/window.d.ts
vendored
|
@ -18,7 +18,7 @@ import {
|
|||
ReactionAttributesType,
|
||||
ReactionModelType,
|
||||
} from './model-types.d';
|
||||
import { TextSecureType, DownloadAttachmentType } from './textsecure.d';
|
||||
import { TextSecureType } from './textsecure.d';
|
||||
import { Storage } from './textsecure/Storage';
|
||||
import {
|
||||
ChallengeHandler,
|
||||
|
@ -38,7 +38,7 @@ import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util';
|
|||
import * as Attachment from './types/Attachment';
|
||||
import * as MIME from './types/MIME';
|
||||
import * as Contact from './types/Contact';
|
||||
import * as Errors from '../js/modules/types/errors';
|
||||
import * as Errors from './types/errors';
|
||||
import { ConversationController } from './ConversationController';
|
||||
import { ReduxActions } from './state/types';
|
||||
import { createStore } from './state/createStore';
|
||||
|
@ -104,7 +104,7 @@ import { Quote } from './components/conversation/Quote';
|
|||
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
|
||||
import { DisappearingTimeDialog } from './components/DisappearingTimeDialog';
|
||||
import { MIMEType } from './types/MIME';
|
||||
import { AttachmentType } from './types/Attachment';
|
||||
import { DownloadedAttachmentType } from './types/Attachment';
|
||||
import { ElectronLocaleType } from './util/mapToSupportLocale';
|
||||
import { SignalProtocolStore } from './SignalProtocolStore';
|
||||
import { Context as SignalContext } from './context';
|
||||
|
@ -324,8 +324,8 @@ declare global {
|
|||
copyIntoAttachmentsDirectory: (path: string) => Promise<string>;
|
||||
upgradeMessageSchema: (attributes: unknown) => WhatIsThis;
|
||||
processNewAttachment: (
|
||||
attachment: DownloadAttachmentType
|
||||
) => Promise<AttachmentType>;
|
||||
attachment: DownloadedAttachmentType
|
||||
) => Promise<DownloadedAttachmentType>;
|
||||
|
||||
copyIntoTempDirectory: any;
|
||||
deleteDraftFile: (path: string) => Promise<void>;
|
||||
|
@ -339,36 +339,7 @@ declare global {
|
|||
writeNewDraftData: any;
|
||||
};
|
||||
Types: {
|
||||
Attachment: {
|
||||
save: any;
|
||||
path: string;
|
||||
pending: boolean;
|
||||
flags: number;
|
||||
size: number;
|
||||
screenshot: {
|
||||
path: string;
|
||||
};
|
||||
thumbnail: {
|
||||
path: string;
|
||||
objectUrl: string;
|
||||
};
|
||||
contentType: MIMEType;
|
||||
error: unknown;
|
||||
caption: string;
|
||||
|
||||
migrateDataToFileSystem: (
|
||||
attachment: WhatIsThis,
|
||||
options: unknown
|
||||
) => WhatIsThis;
|
||||
|
||||
isVoiceMessage: (attachments: unknown) => boolean;
|
||||
isImage: typeof Attachment.isImage;
|
||||
isGIF: typeof Attachment.isGIF;
|
||||
isVideo: typeof Attachment.isVideo;
|
||||
isAudio: typeof Attachment.isAudio;
|
||||
|
||||
getUploadSizeLimitKb: typeof Attachment.getUploadSizeLimitKb;
|
||||
};
|
||||
Attachment: typeof Attachment;
|
||||
MIME: typeof MIME;
|
||||
Contact: typeof Contact;
|
||||
Conversation: {
|
||||
|
|
27
yarn.lock
27
yarn.lock
|
@ -4721,17 +4721,10 @@ bl@^4.0.1, bl@^4.0.3:
|
|||
inherits "^2.0.4"
|
||||
readable-stream "^3.4.0"
|
||||
|
||||
blob-util@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-1.3.0.tgz#dbb4e8caffd50b5720d347e1169b6369ba34fe95"
|
||||
integrity sha512-cjmYgWj8BQwoX+95rKkWvITL6PiEhSr19sX8qLRu+O6J2qmWmgUvxqhqJn425RFAwLovdDNnsCQ64RRHXjsXSg==
|
||||
dependencies:
|
||||
blob "0.0.4"
|
||||
native-or-lie "1.0.2"
|
||||
|
||||
blob@0.0.4:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921"
|
||||
blob-util@2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
|
||||
integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
|
||||
|
||||
block-stream@*:
|
||||
version "0.0.9"
|
||||
|
@ -11357,12 +11350,6 @@ levn@~0.3.0:
|
|||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
|
||||
lie@*:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.2.0.tgz#4f13f2f8bbb027d383db338c43043545791aa8dc"
|
||||
dependencies:
|
||||
immediate "~3.0.5"
|
||||
|
||||
lie@~3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
|
||||
|
@ -12431,12 +12418,6 @@ napi-build-utils@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508"
|
||||
integrity sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==
|
||||
|
||||
native-or-lie@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/native-or-lie/-/native-or-lie-1.0.2.tgz#c870ee0ba0bf0ff11350595d216cfea68a6d8086"
|
||||
dependencies:
|
||||
lie "*"
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
|
|
Loading…
Reference in a new issue