Send related emoji along with Sticker, fix SendMessage types

This commit is contained in:
Scott Nonnenberg 2021-10-05 15:10:08 -07:00 committed by GitHub
parent 3c91dce993
commit bd380086a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 522 additions and 376 deletions

View File

@ -168,14 +168,16 @@ export const createDoesExist = (
export const copyIntoAttachmentsDirectory = (
root: string
): ((sourcePath: string) => Promise<string>) => {
): ((sourcePath: string) => Promise<{ path: string; size: number }>) => {
if (!isString(root)) {
throw new TypeError("'root' must be a path");
}
const userDataPath = getApp().getPath('userData');
return async (sourcePath: string): Promise<string> => {
return async (
sourcePath: string
): Promise<{ path: string; size: number }> => {
if (!isString(sourcePath)) {
throw new TypeError('sourcePath must be a string');
}
@ -196,7 +198,12 @@ export const copyIntoAttachmentsDirectory = (
await fse.ensureFile(normalized);
await fse.copy(sourcePath, normalized);
return relativePath;
const { size } = await fse.stat(normalized);
return {
path: relativePath,
size,
};
};
};

View File

@ -220,6 +220,7 @@ message DataMessage {
optional bytes packKey = 2;
optional uint32 stickerId = 3;
optional AttachmentPointer data = 4;
optional string emoji = 5;
}
message Reaction {

View File

@ -1,4 +1,4 @@
// Copyright 2018-2020 Signal Messenger, LLC
// Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
const fs = require('fs');
@ -222,14 +222,14 @@ describe('Attachments', () => {
});
});
it('returns a function that copies the source path into the attachments directory', async function thisNeeded() {
it('returns a function that copies the source path into the attachments directory and returns its path and size', async function thisNeeded() {
const attachmentsPath = await this.getFakeAttachmentsDirectory();
const someOtherPath = path.join(app.getPath('userData'), 'somethingElse');
await fse.outputFile(someOtherPath, 'hello world');
this.filesToRemove.push(someOtherPath);
const copier = Attachments.copyIntoAttachmentsDirectory(attachmentsPath);
const relativePath = await copier(someOtherPath);
const { path: relativePath, size } = await copier(someOtherPath);
const absolutePath = path.join(attachmentsPath, relativePath);
assert.notEqual(someOtherPath, absolutePath);
@ -237,6 +237,8 @@ describe('Attachments', () => {
await fs.promises.readFile(absolutePath, 'utf8'),
'hello world'
);
assert.strictEqual(size, 'hello world'.length);
});
});

View File

@ -12,17 +12,19 @@ import { AUDIO_MP3, IMAGE_JPEG, VIDEO_MP4 } from '../types/MIME';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
const i18n = setupI18n('en', enMessages);
const stories = storiesOf('Components/Caption Editor', module);
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
attachment: {
attachment: fakeAttachment({
contentType: IMAGE_JPEG,
fileName: '',
url: '',
...overrideProps.attachment,
},
}),
caption: text('caption', overrideProps.caption || ''),
close: action('close'),
i18n,
@ -50,11 +52,11 @@ stories.add('Image with Caption', () => {
stories.add('Video', () => {
const props = createProps({
attachment: {
attachment: fakeAttachment({
contentType: VIDEO_MP4,
fileName: 'pixabay-Soap-Bubble-7141.mp4',
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
},
}),
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
});
@ -63,11 +65,11 @@ stories.add('Video', () => {
stories.add('Video with Caption', () => {
const props = createProps({
attachment: {
attachment: fakeAttachment({
contentType: VIDEO_MP4,
fileName: 'pixabay-Soap-Bubble-7141.mp4',
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
},
}),
caption:
'This is the user-provided caption. We show it overlaid on the image. If it is really long, then it wraps, but it does not get too close to the edges of the image.',
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
@ -78,11 +80,11 @@ stories.add('Video with Caption', () => {
stories.add('Unsupported Attachment Type', () => {
const props = createProps({
attachment: {
attachment: fakeAttachment({
contentType: AUDIO_MP3,
fileName: 'incompetech-com-Agnus-Dei-X.mp3',
url: '/fixtures/incompetech-com-Agnus-Dei-X.mp3',
},
}),
url: '/fixtures/incompetech-com-Agnus-Dei-X.mp3',
});

View File

@ -12,6 +12,9 @@ import { CompositionArea, Props } from './CompositionArea';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
import { landscapeGreenUrl } from '../storybook/Fixtures';
const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/CompositionArea', module);
@ -154,9 +157,10 @@ story.add('SMS-only', () => {
story.add('Attachments', () => {
const props = createProps({
draftAttachments: [
{
fakeAttachment({
contentType: IMAGE_JPEG,
},
url: landscapeGreenUrl,
}),
],
});

View File

@ -23,6 +23,7 @@ const createAttachment = (
fileName: text('attachment fileName', props.fileName || ''),
screenshot: props.screenshot,
url: text('attachment url', props.url || ''),
size: 3433,
});
const story = storiesOf('Components/ForwardMessageModal', module);

View File

@ -19,6 +19,8 @@ import {
stringToMIMEType,
} from '../types/MIME';
import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Lightbox', module);
@ -29,12 +31,12 @@ function createMediaItem(
overrideProps: OverridePropsMediaItemType
): MediaItemType {
return {
attachment: {
attachment: fakeAttachment({
caption: overrideProps.caption || '',
contentType: IMAGE_JPEG,
fileName: overrideProps.objectURL,
url: overrideProps.objectURL,
},
}),
contentType: IMAGE_JPEG,
index: 0,
message: {
@ -63,13 +65,13 @@ story.add('Multimedia', () => {
const props = createProps({
media: [
{
attachment: {
attachment: fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
caption:
'Still from The Lighthouse, starring Robert Pattinson and Willem Defoe.',
},
}),
contentType: IMAGE_JPEG,
index: 0,
message: {
@ -83,11 +85,11 @@ story.add('Multimedia', () => {
objectURL: '/fixtures/tina-rolf-269345-unsplash.jpg',
},
{
attachment: {
attachment: fakeAttachment({
contentType: VIDEO_MP4,
fileName: 'pixabay-Soap-Bubble-7141.mp4',
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
},
}),
contentType: VIDEO_MP4,
index: 1,
message: {
@ -122,11 +124,11 @@ story.add('Missing Media', () => {
const props = createProps({
media: [
{
attachment: {
attachment: fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
},
}),
contentType: IMAGE_JPEG,
index: 0,
message: {

View File

@ -17,6 +17,8 @@ import {
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/AttachmentList', module);
@ -33,11 +35,11 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
story.add('One File', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
},
}),
],
});
return <AttachmentList {...props} />;
@ -46,12 +48,12 @@ story.add('One File', () => {
story.add('Multiple Visual Attachments', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
},
{
}),
fakeAttachment({
contentType: VIDEO_MP4,
fileName: 'pixabay-Soap-Bubble-7141.mp4',
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
@ -62,12 +64,12 @@ story.add('Multiple Visual Attachments', () => {
contentType: IMAGE_JPEG,
path: 'originalpath',
},
},
{
}),
fakeAttachment({
contentType: IMAGE_GIF,
fileName: 'giphy-GVNv0UpeYm17e',
url: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
},
}),
],
});
@ -77,22 +79,22 @@ story.add('Multiple Visual Attachments', () => {
story.add('Multiple with Non-Visual Types', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
},
{
}),
fakeAttachment({
contentType: stringToMIMEType('text/plain'),
fileName: 'lorem-ipsum.txt',
url: '/fixtures/lorem-ipsum.txt',
},
{
}),
fakeAttachment({
contentType: AUDIO_MP3,
fileName: 'incompetech-com-Agnus-Dei-X.mp3',
url: '/fixtures/incompetech-com-Agnus-Dei-X.mp3',
},
{
}),
fakeAttachment({
contentType: VIDEO_MP4,
fileName: 'pixabay-Soap-Bubble-7141.mp4',
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
@ -103,12 +105,12 @@ story.add('Multiple with Non-Visual Types', () => {
contentType: IMAGE_JPEG,
path: 'originalpath',
},
},
{
}),
fakeAttachment({
contentType: IMAGE_GIF,
fileName: 'giphy-GVNv0UpeYm17e',
url: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
},
}),
],
});

View File

@ -13,6 +13,8 @@ import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import { IMAGE_GIF } from '../../types/MIME';
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/ContactDetail', module);
@ -72,10 +74,10 @@ const fullContact = {
},
],
avatar: {
avatar: {
avatar: fakeAttachment({
path: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
contentType: IMAGE_GIF,
},
}),
isProfile: true,
},
email: [
@ -209,10 +211,10 @@ story.add('Loading Avatar', () => {
const props = createProps({
contact: {
avatar: {
avatar: {
avatar: fakeAttachment({
contentType: IMAGE_GIF,
pending: true,
},
}),
isProfile: true,
},
},

View File

@ -13,6 +13,8 @@ import enMessages from '../../../_locales/en/messages.json';
import { ContactFormType } from '../../types/EmbeddedContact';
import { IMAGE_GIF } from '../../types/MIME';
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/EmbeddedContact', module);
@ -35,10 +37,10 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
const fullContact = {
avatar: {
avatar: {
avatar: fakeAttachment({
path: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
contentType: IMAGE_GIF,
},
}),
isProfile: true,
},
email: [
@ -134,10 +136,10 @@ story.add('Loading Avatar', () => {
displayName: 'Jerry Jordan',
},
avatar: {
avatar: {
avatar: fakeAttachment({
pending: true,
contentType: IMAGE_GIF,
},
}),
isProfile: true,
},
},

View File

@ -14,17 +14,21 @@ import { ThemeType } from '../../types/Util';
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
const i18n = setupI18n('en', enMessages);
const story = storiesOf('Components/Conversation/Image', module);
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
alt: text('alt', overrideProps.alt || ''),
attachment: overrideProps.attachment || {
contentType: IMAGE_PNG,
fileName: 'sax.png',
url: pngUrl,
},
attachment:
overrideProps.attachment ||
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
url: pngUrl,
}),
blurHash: text('blurHash', overrideProps.blurHash || ''),
bottomOverlay: boolean('bottomOverlay', overrideProps.bottomOverlay || false),
closeButton: boolean('closeButton', overrideProps.closeButton || false),
@ -99,11 +103,11 @@ story.add('Close Button', () => {
story.add('No Border or Background', () => {
const props = createProps({
attachment: {
attachment: fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
url: pngUrl,
},
}),
noBackground: true,
noBorder: true,
url: pngUrl,

View File

@ -19,6 +19,7 @@ import {
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import { pngUrl, squareStickerUrl } from '../../storybook/Fixtures';
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
const i18n = setupI18n('en', enMessages);
@ -26,13 +27,13 @@ const story = storiesOf('Components/Conversation/ImageGrid', module);
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
attachments: overrideProps.attachments || [
{
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
}),
],
bottomOverlay: boolean('bottomOverlay', overrideProps.bottomOverlay || false),
i18n,
@ -60,20 +61,20 @@ story.add('One Image', () => {
story.add('Two Images', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
{
}),
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
height: 1680,
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
width: 3000,
},
}),
],
});
@ -83,27 +84,27 @@ story.add('Two Images', () => {
story.add('Three Images', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
{
}),
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
height: 1680,
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
width: 3000,
},
{
}),
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
}),
],
});
@ -113,34 +114,34 @@ story.add('Three Images', () => {
story.add('Four Images', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
{
}),
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
height: 1680,
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
width: 3000,
},
{
}),
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
{
}),
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
height: 1680,
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
width: 3000,
},
}),
],
});
@ -150,41 +151,41 @@ story.add('Four Images', () => {
story.add('Five Images', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
{
}),
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
height: 1680,
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
width: 3000,
},
{
}),
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
{
}),
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
height: 1680,
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
width: 3000,
},
{
}),
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
}),
],
});
@ -194,55 +195,55 @@ story.add('Five Images', () => {
story.add('6+ Images', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
{
}),
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
height: 1680,
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
width: 3000,
},
{
}),
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
{
}),
fakeAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
height: 1680,
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
width: 3000,
},
{
}),
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
{
}),
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
{
}),
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
}),
],
});
@ -251,7 +252,7 @@ story.add('6+ Images', () => {
story.add('Mixed Content Types', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: VIDEO_MP4,
fileName: 'pixabay-Soap-Bubble-7141.mp4',
height: 112,
@ -264,24 +265,24 @@ story.add('Mixed Content Types', () => {
},
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
width: 112,
},
{
}),
fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'sax.png',
height: 1200,
url: pngUrl,
width: 800,
},
{
}),
fakeAttachment({
contentType: stringToMIMEType('text/plain'),
fileName: 'lorem-ipsum.txt',
url: '/fixtures/lorem-ipsum.txt',
},
{
}),
fakeAttachment({
contentType: AUDIO_MP3,
fileName: 'incompetech-com-Agnus-Dei-X.mp3',
url: '/fixtures/incompetech-com-Agnus-Dei-X.mp3',
},
}),
],
});
@ -291,13 +292,13 @@ story.add('Mixed Content Types', () => {
story.add('Sticker', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: IMAGE_WEBP,
fileName: 'sticker.webp',
height: 512,
url: squareStickerUrl,
width: 512,
},
}),
],
isSticker: true,
stickerSize: 128,

View File

@ -28,6 +28,8 @@ import enMessages from '../../../_locales/en/messages.json';
import { pngUrl } from '../../storybook/Fixtures';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
const i18n = setupI18n('en', enMessages);
function getJoyReaction() {
@ -444,13 +446,13 @@ story.add('Avatar in Group', () => {
story.add('Sticker', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
url: '/fixtures/512x515-thumbs-up-lincoln.webp',
fileName: '512x515-thumbs-up-lincoln.webp',
contentType: IMAGE_WEBP,
width: 128,
height: 128,
},
}),
],
isSticker: true,
status: 'sent',
@ -524,13 +526,13 @@ story.add('Link Preview', () => {
previews: [
{
domain: 'signal.org',
image: {
image: fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'the-sax.png',
height: 240,
url: pngUrl,
width: 320,
},
}),
isStickerPack: false,
title: 'Signal',
description:
@ -551,13 +553,13 @@ story.add('Link Preview with Small Image', () => {
previews: [
{
domain: 'signal.org',
image: {
image: fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'the-sax.png',
height: 50,
url: pngUrl,
width: 50,
},
}),
isStickerPack: false,
title: 'Signal',
description:
@ -639,13 +641,13 @@ story.add('Link Preview with small image, long description', () => {
previews: [
{
domain: 'signal.org',
image: {
image: fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'the-sax.png',
height: 50,
url: pngUrl,
width: 50,
},
}),
isStickerPack: false,
title: 'Signal',
description: Array(10)
@ -669,13 +671,13 @@ story.add('Link Preview with no date', () => {
previews: [
{
domain: 'signal.org',
image: {
image: fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'the-sax.png',
height: 240,
url: pngUrl,
width: 320,
},
}),
isStickerPack: false,
title: 'Signal',
description:
@ -695,13 +697,13 @@ story.add('Link Preview with too new a date', () => {
previews: [
{
domain: 'signal.org',
image: {
image: fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'the-sax.png',
height: 240,
url: pngUrl,
width: 320,
},
}),
isStickerPack: false,
title: 'Signal',
description:
@ -720,13 +722,13 @@ story.add('Link Preview with too new a date', () => {
story.add('Image', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
},
}),
],
status: 'sent',
});
@ -738,41 +740,41 @@ for (let i = 2; i <= 5; i += 1) {
story.add(`Multiple Images x${i}`, () => {
const props = createProps({
attachments: [
{
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
},
{
}),
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
},
{
}),
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
},
{
}),
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
},
{
}),
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
},
}),
].slice(0, i),
status: 'sent',
});
@ -784,13 +786,13 @@ for (let i = 2; i <= 5; i += 1) {
story.add('Image with Caption', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
},
}),
],
status: 'sent',
text: 'This is my home.',
@ -802,14 +804,14 @@ story.add('Image with Caption', () => {
story.add('GIF', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: VIDEO_MP4,
flags: SignalService.AttachmentPointer.Flags.GIF,
fileName: 'cat-gif.mp4',
url: '/fixtures/cat-gif.mp4',
width: 400,
height: 332,
},
}),
],
status: 'sent',
});
@ -820,14 +822,14 @@ story.add('GIF', () => {
story.add('GIF in a group', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: VIDEO_MP4,
flags: SignalService.AttachmentPointer.Flags.GIF,
fileName: 'cat-gif.mp4',
url: '/fixtures/cat-gif.mp4',
width: 400,
height: 332,
},
}),
],
conversationType: 'group',
status: 'sent',
@ -839,7 +841,7 @@ story.add('GIF in a group', () => {
story.add('Not Downloaded GIF', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: VIDEO_MP4,
flags: SignalService.AttachmentPointer.Flags.GIF,
fileName: 'cat-gif.mp4',
@ -847,7 +849,7 @@ story.add('Not Downloaded GIF', () => {
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
width: 400,
height: 332,
},
}),
],
status: 'sent',
});
@ -858,7 +860,7 @@ story.add('Not Downloaded GIF', () => {
story.add('Pending GIF', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
pending: true,
contentType: VIDEO_MP4,
flags: SignalService.AttachmentPointer.Flags.GIF,
@ -867,7 +869,7 @@ story.add('Pending GIF', () => {
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
width: 400,
height: 332,
},
}),
],
status: 'sent',
});
@ -881,11 +883,11 @@ story.add('Audio', () => {
const messageProps = createProps({
attachments: [
{
fakeAttachment({
contentType: AUDIO_MP3,
fileName: 'incompetech-com-Agnus-Dei-X.mp3',
url: '/fixtures/incompetech-com-Agnus-Dei-X.mp3',
},
}),
],
...(isPlayed
? {
@ -923,11 +925,11 @@ story.add('Audio', () => {
story.add('Long Audio', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: AUDIO_MP3,
fileName: 'long-audio.mp3',
url: '/fixtures/long-audio.mp3',
},
}),
],
status: 'sent',
});
@ -938,11 +940,11 @@ story.add('Long Audio', () => {
story.add('Audio with Caption', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: AUDIO_MP3,
fileName: 'incompetech-com-Agnus-Dei-X.mp3',
url: '/fixtures/incompetech-com-Agnus-Dei-X.mp3',
},
}),
],
status: 'sent',
text: 'This is what I sound like.',
@ -954,10 +956,10 @@ story.add('Audio with Caption', () => {
story.add('Audio with Not Downloaded Attachment', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: AUDIO_MP3,
fileName: 'incompetech-com-Agnus-Dei-X.mp3',
},
}),
],
status: 'sent',
});
@ -968,11 +970,11 @@ story.add('Audio with Not Downloaded Attachment', () => {
story.add('Audio with Pending Attachment', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: AUDIO_MP3,
fileName: 'incompetech-com-Agnus-Dei-X.mp3',
pending: true,
},
}),
],
status: 'sent',
});
@ -983,11 +985,11 @@ story.add('Audio with Pending Attachment', () => {
story.add('Other File Type', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: stringToMIMEType('text/plain'),
fileName: 'my-resume.txt',
url: 'my-resume.txt',
},
}),
],
status: 'sent',
});
@ -998,11 +1000,11 @@ story.add('Other File Type', () => {
story.add('Other File Type with Caption', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: stringToMIMEType('text/plain'),
fileName: 'my-resume.txt',
url: 'my-resume.txt',
},
}),
],
status: 'sent',
text: 'This is what I have done.',
@ -1014,12 +1016,12 @@ story.add('Other File Type with Caption', () => {
story.add('Other File Type with Long Filename', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: stringToMIMEType('text/plain'),
fileName:
'INSERT-APP-NAME_INSERT-APP-APPLE-ID_AppStore_AppsGamesWatch.psd.zip',
url: 'a2/a2334324darewer4234',
},
}),
],
status: 'sent',
text: 'This is what I have done.',
@ -1031,13 +1033,13 @@ story.add('Other File Type with Long Filename', () => {
story.add('TapToView Image', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
},
}),
],
isTapToView: true,
status: 'sent',
@ -1049,13 +1051,13 @@ story.add('TapToView Image', () => {
story.add('TapToView Video', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: VIDEO_MP4,
fileName: 'pixabay-Soap-Bubble-7141.mp4',
height: 128,
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
width: 128,
},
}),
],
isTapToView: true,
status: 'sent',
@ -1067,14 +1069,14 @@ story.add('TapToView Video', () => {
story.add('TapToView GIF', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: VIDEO_MP4,
flags: SignalService.AttachmentPointer.Flags.GIF,
fileName: 'cat-gif.mp4',
url: '/fixtures/cat-gif.mp4',
width: 400,
height: 332,
},
}),
],
isTapToView: true,
status: 'sent',
@ -1086,13 +1088,13 @@ story.add('TapToView GIF', () => {
story.add('TapToView Expired', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
},
}),
],
isTapToView: true,
isTapToViewExpired: true,
@ -1105,13 +1107,13 @@ story.add('TapToView Expired', () => {
story.add('TapToView Error', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
},
}),
],
isTapToView: true,
isTapToViewError: true,
@ -1124,13 +1126,13 @@ story.add('TapToView Error', () => {
story.add('Dangerous File Type', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
contentType: stringToMIMEType(
'application/vnd.microsoft.portable-executable'
),
fileName: 'terrible.exe',
url: 'terrible.exe',
},
}),
],
status: 'sent',
});
@ -1174,13 +1176,13 @@ story.add('@Mentions', () => {
story.add('All the context menus', () => {
const props = createProps({
attachments: [
{
fakeAttachment({
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
fileName: 'tina-rolf-269345-unsplash.jpg',
contentType: IMAGE_JPEG,
width: 128,
height: 128,
},
}),
],
status: 'partial-sent',
canDeleteForEveryone: true,
@ -1194,13 +1196,13 @@ story.add('Not approved, with link preview', () => {
previews: [
{
domain: 'signal.org',
image: {
image: fakeAttachment({
contentType: IMAGE_PNG,
fileName: 'the-sax.png',
height: 240,
url: pngUrl,
width: 320,
},
}),
isStickerPack: false,
title: 'Signal',
description:

View File

@ -33,6 +33,7 @@ const createAttachment = (
),
fileName: text('attachment fileName', props.fileName || ''),
url: '',
size: 14243,
});
story.add('Text File', () => {

View File

@ -32,6 +32,7 @@ const createAttachment = (
),
fileName: text('attachment fileName', props.fileName || ''),
url: text('attachment url', props.url || ''),
size: 24325,
});
const createProps = (overrideProps: Partial<Props> = {}): Props => ({

View File

@ -27,8 +27,8 @@ import type {
AttachmentType,
GroupV1InfoType,
GroupV2InfoType,
PreviewType,
} from '../textsecure/SendMessage';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import type { BodyRangesType } from '../types/Util';
import type { WhatIsThis } from '../window.d';
@ -299,7 +299,7 @@ export class NormalMessageSendJobQueue extends JobQueue<NormalMessageSendJobData
quote,
preview,
sticker,
reaction: null,
reaction: undefined,
deletedForEveryoneTimestamp,
timestamp: messageTimestamp,
expireTimer,
@ -464,7 +464,7 @@ async function getMessageSendData({
expireTimer: undefined | number;
mentions: undefined | BodyRangesType;
messageTimestamp: number;
preview: Array<PreviewType>;
preview: Array<LinkPreviewType>;
profileKey: undefined | Uint8Array;
quote: WhatIsThis;
sticker: WhatIsThis;

View File

@ -23,6 +23,7 @@ import { CapabilityError } from '../types/errors';
import type {
GroupV1InfoType,
GroupV2InfoType,
StickerType,
} from '../textsecure/SendMessage';
import createTaskWithTimeout from '../textsecure/TaskWithTimeout';
import { CallbackResultType } from '../textsecure/Types.d';
@ -60,6 +61,7 @@ import { sendReadReceiptsFor } from '../util/sendReadReceiptsFor';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
import { ReadStatus } from '../messages/MessageReadStatus';
import { SendState, SendStatus } from '../messages/MessageSendState';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import * as durations from '../util/durations';
import {
concat,
@ -3279,8 +3281,8 @@ export class ConversationModel extends window.Backbone
}
const { key } = packData;
const { path, width, height } = stickerData;
const arrayBuffer = await readStickerData(path);
const { emoji, path, width, height } = stickerData;
const data = await readStickerData(path);
// We need this content type to be an image so we can display an `<img>` instead of a
// `<video>` or an error, but it's not critical that we get the full type correct.
@ -3289,11 +3291,13 @@ export class ConversationModel extends window.Backbone
// the MIME type here, but it's okay if we have to use a possibly-incorrect
// fallback.
let contentType: MIMEType;
const sniffedMimeType = sniffImageMimeType(arrayBuffer);
const sniffedMimeType = sniffImageMimeType(data);
if (sniffedMimeType) {
contentType = sniffedMimeType;
} else {
log.warn('Unable to sniff sticker MIME type; falling back to WebP');
log.warn(
'sendStickerMessage: Unable to sniff sticker MIME type; falling back to WebP'
);
contentType = IMAGE_WEBP;
}
@ -3301,9 +3305,10 @@ export class ConversationModel extends window.Backbone
packId,
stickerId,
packKey: key,
emoji,
data: {
size: arrayBuffer.byteLength,
data: arrayBuffer,
size: data.byteLength,
data,
contentType,
width,
height,
@ -3630,8 +3635,8 @@ export class ConversationModel extends window.Backbone
body: string | undefined,
attachments: Array<AttachmentType>,
quote?: QuotedMessageType,
preview?: WhatIsThis,
sticker?: WhatIsThis,
preview?: Array<LinkPreviewType>,
sticker?: StickerType,
mentions?: BodyRangesType,
{
dontClearDraft,

View File

@ -45,7 +45,7 @@ import * as Errors from '../types/errors';
import * as EmbeddedContact from '../types/EmbeddedContact';
import { AttachmentType, isImage, isVideo } from '../types/Attachment';
import * as Attachment from '../types/Attachment';
import { IMAGE_WEBP, stringToMIMEType } from '../types/MIME';
import { stringToMIMEType } from '../types/MIME';
import * as MIME from '../types/MIME';
import { ReadStatus } from '../messages/MessageReadStatus';
import {
@ -117,7 +117,7 @@ import * as LinkPreview from '../types/LinkPreview';
import { SignalService as Proto } from '../protobuf';
import { normalMessageSendJobQueue } from '../jobs/normalMessageSendJobQueue';
import { notificationService } from '../services/notifications';
import type { PreviewType as OutgoingPreviewType } from '../textsecure/SendMessage';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import * as log from '../logging/log';
import * as Bytes from '../Bytes';
import { computeHash } from '../Crypto';
@ -188,7 +188,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
syncPromise?: Promise<CallbackResultType | void>;
cachedOutgoingPreviewData?: Array<OutgoingPreviewType>;
cachedOutgoingPreviewData?: Array<LinkPreviewType>;
cachedOutgoingQuoteData?: WhatIsThis;
@ -2012,14 +2012,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
if (status && (status === 'downloaded' || status === 'installed')) {
try {
const copiedSticker = await copyStickerToAttachments(
packId,
stickerId
);
data = {
...copiedSticker,
contentType: IMAGE_WEBP,
};
data = await copyStickerToAttachments(packId, stickerId);
} catch (error) {
log.error(
`Problem copying sticker (${packId}, ${stickerId}) to attachments:`,

View File

@ -0,0 +1,15 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { AttachmentType } from '../../types/Attachment';
import { IMAGE_JPEG } from '../../types/MIME';
export const fakeAttachment = (
overrides: Partial<AttachmentType> = {}
): AttachmentType => ({
contentType: IMAGE_JPEG,
width: 800,
height: 600,
size: 10304,
...overrides,
});

View File

@ -2,8 +2,9 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { IMAGE_JPEG, VIDEO_MP4 } from '../../types/MIME';
import { AttachmentType } from '../../types/Attachment';
import { VIDEO_MP4 } from '../../types/MIME';
import { fakeAttachment } from '../helpers/fakeAttachment';
import { shouldUseFullSizeLinkPreviewImage } from '../../linkPreviews/shouldUseFullSizeLinkPreviewImage';
@ -15,17 +16,6 @@ describe('shouldUseFullSizeLinkPreviewImage', () => {
isStickerPack: false,
};
const fakeAttachment = (
overrides: Partial<AttachmentType> = {}
): AttachmentType => ({
contentType: IMAGE_JPEG,
fileName: 'foo.jpg',
url: '/tmp/foo.jpg',
width: 800,
height: 600,
...overrides,
});
it('returns false if there is no image', () => {
assert.isFalse(
shouldUseFullSizeLinkPreviewImage({

View File

@ -10,6 +10,7 @@ import { reducer as rootReducer } from '../../../state/reducer';
import { IMAGE_JPEG } from '../../../types/MIME';
import { AttachmentType } from '../../../types/Attachment';
import { fakeAttachment } from '../../helpers/fakeAttachment';
describe('both/state/ducks/composer', () => {
const QUOTED_MESSAGE = {
@ -40,7 +41,7 @@ describe('both/state/ducks/composer', () => {
const dispatch = sinon.spy();
const attachments: Array<AttachmentType> = [
{ contentType: IMAGE_JPEG, pending: false, url: '' },
{ contentType: IMAGE_JPEG, pending: false, url: '', size: 2433 },
];
replaceAttachments('123', attachments)(
dispatch,
@ -82,7 +83,7 @@ describe('both/state/ducks/composer', () => {
const { replaceAttachments } = actions;
const dispatch = sinon.spy();
const attachments: Array<AttachmentType> = [{ contentType: IMAGE_JPEG }];
const attachments = [fakeAttachment()];
replaceAttachments('123', attachments)(
dispatch,
getRootStateFunction('456'),

View File

@ -10,6 +10,7 @@ import {
Section,
} from '../../../components/conversation/media-gallery/groupMediaItemsByDate';
import { MediaItemType } from '../../../types/MediaItem';
import { fakeAttachment } from '../../../test-both/helpers/fakeAttachment';
const testDate = (
year: number,
@ -31,11 +32,11 @@ const toMediaItem = (date: Date): MediaItemType => ({
attachments: [],
sent_at: date.getTime(),
},
attachment: {
attachment: fakeAttachment({
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
},
}),
});
describe('groupMediaItemsByDate', () => {
@ -74,11 +75,11 @@ describe('groupMediaItemsByDate', () => {
attachments: [],
sent_at: 1523534400000,
},
attachment: {
attachment: fakeAttachment({
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
},
}),
},
{
objectURL: 'Thu, 12 Apr 2018 00:01:00 GMT',
@ -91,11 +92,11 @@ describe('groupMediaItemsByDate', () => {
attachments: [],
sent_at: 1523491260000,
},
attachment: {
attachment: fakeAttachment({
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
},
}),
},
],
},
@ -113,11 +114,11 @@ describe('groupMediaItemsByDate', () => {
attachments: [],
sent_at: 1523491140000,
},
attachment: {
attachment: fakeAttachment({
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
},
}),
},
],
},
@ -135,11 +136,11 @@ describe('groupMediaItemsByDate', () => {
attachments: [],
sent_at: 1523232060000,
},
attachment: {
attachment: fakeAttachment({
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
},
}),
},
],
},
@ -157,11 +158,11 @@ describe('groupMediaItemsByDate', () => {
attachments: [],
sent_at: 1523231940000,
},
attachment: {
attachment: fakeAttachment({
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
},
}),
},
{
objectURL: 'Sun, 01 Apr 2018 00:01:00 GMT',
@ -174,11 +175,11 @@ describe('groupMediaItemsByDate', () => {
attachments: [],
sent_at: 1522540860000,
},
attachment: {
attachment: fakeAttachment({
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
},
}),
},
],
},
@ -198,11 +199,11 @@ describe('groupMediaItemsByDate', () => {
attachments: [],
sent_at: 1522540740000,
},
attachment: {
attachment: fakeAttachment({
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
},
}),
},
{
objectURL: 'Thu, 01 Mar 2018 14:00:00 GMT',
@ -215,11 +216,11 @@ describe('groupMediaItemsByDate', () => {
attachments: [],
sent_at: 1519912800000,
},
attachment: {
attachment: fakeAttachment({
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
},
}),
},
],
},
@ -239,11 +240,11 @@ describe('groupMediaItemsByDate', () => {
attachments: [],
sent_at: 1298937540000,
},
attachment: {
attachment: fakeAttachment({
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
},
}),
},
{
objectURL: 'Tue, 01 Feb 2011 10:00:00 GMT',
@ -256,11 +257,11 @@ describe('groupMediaItemsByDate', () => {
attachments: [],
sent_at: 1296554400000,
},
attachment: {
attachment: fakeAttachment({
fileName: 'fileName',
contentType: IMAGE_JPEG,
url: 'url',
},
}),
},
],
},

View File

@ -9,6 +9,8 @@ import { SignalService } from '../../protobuf';
import * as Bytes from '../../Bytes';
import * as logger from '../../logging/log';
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
describe('Attachment', () => {
describe('getUploadSizeLimitKb', () => {
const { getUploadSizeLimitKb } = Attachment;
@ -37,18 +39,18 @@ describe('Attachment', () => {
describe('getFileExtension', () => {
it('should return file extension from content type', () => {
const input: Attachment.AttachmentType = {
const input: Attachment.AttachmentType = fakeAttachment({
data: Bytes.fromString('foo'),
contentType: MIME.IMAGE_GIF,
};
});
assert.strictEqual(Attachment.getFileExtension(input), 'gif');
});
it('should return file extension for QuickTime videos', () => {
const input: Attachment.AttachmentType = {
const input: Attachment.AttachmentType = fakeAttachment({
data: Bytes.fromString('foo'),
contentType: MIME.VIDEO_QUICKTIME,
};
});
assert.strictEqual(Attachment.getFileExtension(input), 'mov');
});
});
@ -56,11 +58,11 @@ describe('Attachment', () => {
describe('getSuggestedFilename', () => {
context('for attachment with filename', () => {
it('should return existing filename if present', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
fileName: 'funny-cat.mov',
data: Bytes.fromString('foo'),
contentType: MIME.VIDEO_QUICKTIME,
};
});
const actual = Attachment.getSuggestedFilename({ attachment });
const expected = 'funny-cat.mov';
assert.strictEqual(actual, expected);
@ -68,10 +70,10 @@ describe('Attachment', () => {
});
context('for attachment without filename', () => {
it('should generate a filename based on timestamp', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
data: Bytes.fromString('foo'),
contentType: MIME.VIDEO_QUICKTIME,
};
});
const timestamp = new Date(new Date(0).getTimezoneOffset() * 60 * 1000);
const actual = Attachment.getSuggestedFilename({
attachment,
@ -83,10 +85,10 @@ describe('Attachment', () => {
});
context('for attachment with index', () => {
it('should generate a filename based on timestamp', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
data: Bytes.fromString('foo'),
contentType: MIME.VIDEO_QUICKTIME,
};
});
const timestamp = new Date(new Date(0).getTimezoneOffset() * 60 * 1000);
const actual = Attachment.getSuggestedFilename({
attachment,
@ -101,107 +103,107 @@ describe('Attachment', () => {
describe('isVisualMedia', () => {
it('should return true for images', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
fileName: 'meme.gif',
data: Bytes.fromString('gif'),
contentType: MIME.IMAGE_GIF,
};
});
assert.isTrue(Attachment.isVisualMedia(attachment));
});
it('should return true for videos', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
fileName: 'meme.mp4',
data: Bytes.fromString('mp4'),
contentType: MIME.VIDEO_MP4,
};
});
assert.isTrue(Attachment.isVisualMedia(attachment));
});
it('should return false for voice message attachment', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
fileName: 'Voice Message.aac',
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
data: Bytes.fromString('voice message'),
contentType: MIME.AUDIO_AAC,
};
});
assert.isFalse(Attachment.isVisualMedia(attachment));
});
it('should return false for other attachments', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
fileName: 'foo.json',
data: Bytes.fromString('{"foo": "bar"}'),
contentType: MIME.APPLICATION_JSON,
};
});
assert.isFalse(Attachment.isVisualMedia(attachment));
});
});
describe('isFile', () => {
it('should return true for JSON', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
fileName: 'foo.json',
data: Bytes.fromString('{"foo": "bar"}'),
contentType: MIME.APPLICATION_JSON,
};
});
assert.isTrue(Attachment.isFile(attachment));
});
it('should return false for images', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
fileName: 'meme.gif',
data: Bytes.fromString('gif'),
contentType: MIME.IMAGE_GIF,
};
});
assert.isFalse(Attachment.isFile(attachment));
});
it('should return false for videos', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
fileName: 'meme.mp4',
data: Bytes.fromString('mp4'),
contentType: MIME.VIDEO_MP4,
};
});
assert.isFalse(Attachment.isFile(attachment));
});
it('should return false for voice message attachment', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
fileName: 'Voice Message.aac',
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
data: Bytes.fromString('voice message'),
contentType: MIME.AUDIO_AAC,
};
});
assert.isFalse(Attachment.isFile(attachment));
});
});
describe('isVoiceMessage', () => {
it('should return true for voice message attachment', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
fileName: 'Voice Message.aac',
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
data: Bytes.fromString('voice message'),
contentType: MIME.AUDIO_AAC,
};
});
assert.isTrue(Attachment.isVoiceMessage(attachment));
});
it('should return true for legacy Android voice message attachment', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
data: Bytes.fromString('voice message'),
contentType: MIME.AUDIO_MP3,
};
});
assert.isTrue(Attachment.isVoiceMessage(attachment));
});
it('should return false for other attachments', () => {
const attachment: Attachment.AttachmentType = {
const attachment: Attachment.AttachmentType = fakeAttachment({
fileName: 'foo.gif',
data: Bytes.fromString('foo'),
contentType: MIME.IMAGE_GIF,
};
});
assert.isFalse(Attachment.isVoiceMessage(attachment));
});
});

View File

@ -15,6 +15,7 @@ import {
getName,
parseAndWriteAvatar,
} from '../../types/EmbeddedContact';
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
describe('Contact', () => {
const NUMBER = '+12025550099';
@ -127,10 +128,10 @@ describe('Contact', () => {
organization: 'Somewhere, Inc.',
avatar: {
isProfile: true,
avatar: {
avatar: fakeAttachment({
error: true,
contentType: IMAGE_GIF,
},
}),
},
};
const expected = {
@ -164,10 +165,10 @@ describe('Contact', () => {
organization: 'Somewhere, Inc.',
avatar: {
isProfile: true,
avatar: {
avatar: fakeAttachment({
pending: true,
contentType: IMAGE_GIF,
},
}),
},
};
const expected = {
@ -179,11 +180,11 @@ describe('Contact', () => {
organization: 'Somewhere, Inc.',
avatar: {
isProfile: true,
avatar: {
avatar: fakeAttachment({
pending: true,
path: undefined,
contentType: IMAGE_GIF,
},
}),
},
firstNumber,
isNumberOnSignal,
@ -208,10 +209,10 @@ describe('Contact', () => {
organization: 'Somewhere, Inc.',
avatar: {
isProfile: true,
avatar: {
avatar: fakeAttachment({
path: 'somewhere',
contentType: IMAGE_GIF,
},
}),
},
};
const expected = {
@ -223,10 +224,10 @@ describe('Contact', () => {
organization: 'Somewhere, Inc.',
avatar: {
isProfile: true,
avatar: {
avatar: fakeAttachment({
path: 'absolute:somewhere',
contentType: IMAGE_GIF,
},
}),
},
firstNumber,
isNumberOnSignal: true,
@ -363,10 +364,10 @@ describe('Contact', () => {
it('writes avatar to disk', async () => {
const upgradeAttachment = async () => {
return {
return fakeAttachment({
path: 'abc/abcdefg',
contentType: IMAGE_PNG,
};
});
};
const upgradeVersion = parseAndWriteAvatar(upgradeAttachment);
@ -430,10 +431,10 @@ describe('Contact', () => {
avatar: {
otherKey: 'otherValue',
isProfile: false,
avatar: {
avatar: fakeAttachment({
contentType: IMAGE_PNG,
path: 'abc/abcdefg',
},
}),
},
};

View File

@ -5,7 +5,6 @@
/* eslint-disable class-methods-use-this */
/* eslint-disable more/no-then */
/* eslint-disable no-bitwise */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable max-classes-per-file */
import { Dictionary } from 'lodash';
@ -23,6 +22,9 @@ import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import { SenderKeys } from '../LibSignalStores';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import { MIMETypeToString } from '../types/MIME';
import * as Attachment from '../types/Attachment';
import {
ChallengeType,
GroupCredentialsType,
@ -77,12 +79,6 @@ export type SendOptionsType = {
online?: boolean;
};
export type PreviewType = {
url: string;
title: string;
image?: AttachmentType;
};
type QuoteAttachmentType = {
thumbnail?: AttachmentType;
attachmentPointer?: Proto.IAttachmentPointer;
@ -103,22 +99,65 @@ type GroupCallUpdateType = {
eraId: string;
};
export type StickerType = {
packId: string;
stickerId: number;
packKey: string;
data: Readonly<AttachmentType>;
emoji?: string;
attachmentPointer?: Proto.IAttachmentPointer;
};
export type QuoteType = {
id?: number;
authorUuid?: string;
text?: string;
attachments?: Array<AttachmentType>;
bodyRanges?: BodyRangesType;
};
export type ReactionType = {
emoji?: string;
remove?: boolean;
targetAuthorUuid?: string;
targetTimestamp?: number;
};
export type AttachmentType = {
size: number;
data: Uint8Array;
contentType: string;
fileName: string;
flags: number;
width: number;
height: number;
caption: string;
fileName?: string;
flags?: number;
width?: number;
height?: number;
caption?: string;
attachmentPointer?: Proto.IAttachmentPointer;
blurHash?: string;
};
function makeAttachmentSendReady(
attachment: Attachment.AttachmentType
): AttachmentType | undefined {
const { data } = attachment;
if (!data) {
throw new Error(
'makeAttachmentSendReady: Missing data, returning undefined'
);
}
return {
...attachment,
contentType: MIMETypeToString(attachment.contentType),
data,
};
}
export type MessageOptionsType = {
attachments?: ReadonlyArray<AttachmentType> | null;
body?: string;
@ -130,12 +169,12 @@ export type MessageOptionsType = {
};
groupV2?: GroupV2InfoType;
needsSync?: boolean;
preview?: ReadonlyArray<PreviewType> | null;
preview?: ReadonlyArray<LinkPreviewType>;
profileKey?: Uint8Array;
quote?: any;
quote?: QuoteType;
recipients: ReadonlyArray<string>;
sticker?: any;
reaction?: any;
sticker?: StickerType;
reaction?: ReactionType;
deletedForEveryoneTimestamp?: number;
timestamp: number;
mentions?: BodyRangesType;
@ -147,11 +186,11 @@ export type GroupSendOptionsType = {
groupV2?: GroupV2InfoType;
groupV1?: GroupV1InfoType;
messageText?: string;
preview?: any;
preview?: ReadonlyArray<LinkPreviewType>;
profileKey?: Uint8Array;
quote?: any;
reaction?: any;
sticker?: any;
quote?: QuoteType;
reaction?: ReactionType;
sticker?: StickerType;
deletedForEveryoneTimestamp?: number;
timestamp: number;
mentions?: BodyRangesType;
@ -159,7 +198,7 @@ export type GroupSendOptionsType = {
};
class Message {
attachments: ReadonlyArray<any>;
attachments: ReadonlyArray<AttachmentType>;
body?: string;
@ -176,28 +215,17 @@ class Message {
needsSync?: boolean;
preview: any;
preview?: ReadonlyArray<LinkPreviewType>;
profileKey?: Uint8Array;
quote?: {
id?: number;
authorUuid?: string;
text?: string;
attachments?: Array<AttachmentType>;
bodyRanges?: BodyRangesType;
};
quote?: QuoteType;
recipients: ReadonlyArray<string>;
sticker?: any;
sticker?: StickerType;
reaction?: {
emoji?: string;
remove?: boolean;
targetAuthorUuid?: string;
targetTimestamp?: number;
};
reaction?: ReactionType;
timestamp: number;
@ -325,6 +353,7 @@ class Message {
proto.sticker.packId = Bytes.fromHex(this.sticker.packId);
proto.sticker.packKey = Bytes.fromBase64(this.sticker.packKey);
proto.sticker.stickerId = this.sticker.stickerId;
proto.sticker.emoji = this.sticker.emoji;
if (this.sticker.attachmentPointer) {
proto.sticker.data = this.sticker.attachmentPointer;
@ -345,7 +374,9 @@ class Message {
item.url = preview.url;
item.description = preview.description || null;
item.date = preview.date || null;
item.image = preview.image || null;
if (preview.attachmentPointer) {
item.image = preview.attachmentPointer;
}
return item;
});
}
@ -364,7 +395,9 @@ class Message {
const quotedAttachment = new QuotedAttachment();
quotedAttachment.contentType = attachment.contentType;
quotedAttachment.fileName = attachment.fileName;
if (attachment.fileName) {
quotedAttachment.fileName = attachment.fileName;
}
if (attachment.attachmentPointer) {
quotedAttachment.thumbnail = attachment.attachmentPointer;
}
@ -442,10 +475,10 @@ export default class MessageSender {
this.pendingMessages = {};
}
async queueJobForIdentifier(
async queueJobForIdentifier<T>(
identifier: string,
runJob: () => Promise<any>
): Promise<void> {
runJob: () => Promise<T>
): Promise<T> {
const { id } = await window.ConversationController.getOrCreateAndWait(
identifier,
'private'
@ -546,8 +579,10 @@ export default class MessageSender {
}
async uploadAttachments(message: Message): Promise<void> {
return Promise.all(
message.attachments.map(this.makeAttachmentPointer.bind(this))
await Promise.all(
message.attachments.map(attachment =>
this.makeAttachmentPointer(attachment)
)
)
.then(attachmentPointers => {
// eslint-disable-next-line no-param-reassign
@ -565,12 +600,20 @@ export default class MessageSender {
async uploadLinkPreviews(message: Message): Promise<void> {
try {
const preview = await Promise.all(
(message.preview || []).map(async (item: PreviewType) => ({
...item,
image: item.image
? await this.makeAttachmentPointer(item.image)
: undefined,
}))
(message.preview || []).map(async (item: Readonly<LinkPreviewType>) => {
if (!item.image) {
return item;
}
const attachment = makeAttachmentSendReady(item.image);
if (!attachment) {
return item;
}
return {
...item,
attachmentPointer: await this.makeAttachmentPointer(attachment),
};
})
);
// eslint-disable-next-line no-param-reassign
message.preview = preview;
@ -968,10 +1011,10 @@ export default class MessageSender {
identifier: string;
messageText: string | undefined;
attachments: ReadonlyArray<AttachmentType> | undefined;
quote: unknown;
preview: ReadonlyArray<PreviewType> | undefined;
sticker: unknown;
reaction: unknown;
quote?: QuoteType;
preview?: ReadonlyArray<LinkPreviewType> | undefined;
sticker?: StickerType;
reaction?: ReactionType;
deletedForEveryoneTimestamp: number | undefined;
timestamp: number;
expireTimer: number | undefined;
@ -2080,7 +2123,7 @@ export default class MessageSender {
profileKeyVersion?: string;
profileKeyCredentialRequest?: string;
}> = {}
): Promise<any> {
): Promise<ReturnType<WebAPIType['getProfile']>> {
const { accessKey } = options;
if (accessKey) {
@ -2100,15 +2143,20 @@ export default class MessageSender {
return this.server.getUuidsForE164s(numbers);
}
async getAvatar(path: string): Promise<any> {
async getAvatar(path: string): Promise<ReturnType<WebAPIType['getAvatar']>> {
return this.server.getAvatar(path);
}
async getSticker(packId: string, stickerId: number): Promise<any> {
async getSticker(
packId: string,
stickerId: number
): Promise<ReturnType<WebAPIType['getSticker']>> {
return this.server.getSticker(packId, stickerId);
}
async getStickerPackManifest(packId: string): Promise<any> {
async getStickerPackManifest(
packId: string
): Promise<ReturnType<WebAPIType['getStickerPackManifest']>> {
return this.server.getStickerPackManifest(packId);
}
@ -2184,7 +2232,7 @@ export default class MessageSender {
async makeProxiedRequest(
url: string,
options?: Readonly<ProxiedRequestOptionsType>
): Promise<any> {
): Promise<ReturnType<WebAPIType['makeProxiedRequest']>> {
return this.server.makeProxiedRequest(url, options);
}

View File

@ -678,7 +678,8 @@ export type ProfileType = Readonly<{
username?: string;
uuid?: string;
credential?: string;
capabilities?: unknown;
capabilities?: CapabilitiesType;
paymentAddress?: string;
}>;
export type GetIceServersResultType = Readonly<{

View File

@ -52,6 +52,7 @@ export async function downloadAttachment(
return {
...omit(attachment, 'digest', 'key'),
size,
contentType: contentType
? MIME.stringToMIMEType(contentType)
: MIME.APPLICATION_OCTET_STREAM,

View File

@ -42,7 +42,7 @@ export type AttachmentType = {
isVoiceMessage?: boolean;
/** For messages not already on disk, this will be a data url */
url?: string;
size?: number;
size: number;
fileSize?: string;
pending?: boolean;
width?: number;
@ -95,6 +95,7 @@ export type InMemoryAttachmentDraftType =
fileName: string;
path: string;
pending: true;
size: number;
};
export type AttachmentDraftType =
@ -109,6 +110,7 @@ export type AttachmentDraftType =
fileName: string;
path: string;
pending: true;
size: number;
};
export type ThumbnailType = {
@ -621,7 +623,9 @@ export function isImage(attachments?: ReadonlyArray<AttachmentType>): boolean {
);
}
export function isImageAttachment(attachment?: AttachmentType): boolean {
export function isImageAttachment(
attachment?: Pick<AttachmentType, 'contentType'>
): boolean {
return Boolean(
attachment &&
attachment.contentType &&
@ -630,8 +634,8 @@ export function isImageAttachment(attachment?: AttachmentType): boolean {
}
export function canBeTranscoded(
attachment?: AttachmentType
): attachment is AttachmentType {
attachment?: Pick<AttachmentType, 'contentType'>
): boolean {
return Boolean(
attachment &&
isImageAttachment(attachment) &&

View File

@ -6,6 +6,9 @@ export type MIMEType = string & { _mimeTypeBrand: never };
export const stringToMIMEType = (value: string): MIMEType => {
return value as MIMEType;
};
export const MIMETypeToString = (value: MIMEType): string => {
return value as string;
};
export const APPLICATION_OCTET_STREAM = stringToMIMEType(
'application/octet-stream'

View File

@ -11,6 +11,9 @@ import { makeLookup } from '../util/makeLookup';
import { maybeParseUrl } from '../util/url';
import * as Bytes from '../Bytes';
import { deriveStickerPackKey, decryptAttachment } from '../Crypto';
import { IMAGE_WEBP, MIMEType } from './MIME';
import { sniffImageMimeType } from '../util/sniffImageMimeType';
import type { AttachmentType } from './Attachment';
import type {
StickerType,
StickerPackType,
@ -749,21 +752,41 @@ export function getSticker(
export async function copyStickerToAttachments(
packId: string,
stickerId: number
): Promise<StickerType | undefined> {
): Promise<AttachmentType> {
const sticker = getSticker(packId, stickerId);
if (!sticker) {
return undefined;
throw new Error(
`copyStickerToAttachments: Failed to find sticker ${packId}/${stickerId}`
);
}
const { path } = sticker;
const absolutePath = window.Signal.Migrations.getAbsoluteStickerPath(path);
const newPath = await window.Signal.Migrations.copyIntoAttachmentsDirectory(
absolutePath
const { path: stickerPath } = sticker;
const absolutePath = window.Signal.Migrations.getAbsoluteStickerPath(
stickerPath
);
const {
path,
size,
} = await window.Signal.Migrations.copyIntoAttachmentsDirectory(absolutePath);
const data = window.Signal.Migrations.loadAttachmentData(path);
let contentType: MIMEType;
const sniffedMimeType = sniffImageMimeType(data);
if (sniffedMimeType) {
contentType = sniffedMimeType;
} else {
log.warn(
'copyStickerToAttachments: Unable to sniff sticker MIME type; falling back to WebP'
);
contentType = IMAGE_WEBP;
}
return {
...sticker,
path: newPath,
contentType,
path,
size,
};
}

View File

@ -9,6 +9,6 @@ export type LinkPreviewType = {
domain: string;
url: string;
isStickerPack: boolean;
image?: AttachmentType;
image?: Readonly<AttachmentType>;
date?: number;
};

View File

@ -114,19 +114,21 @@ export async function getProfile(
});
}
const identityKey = Bytes.fromBase64(profile.identityKey);
const changed = await window.textsecure.storage.protocol.saveIdentity(
new Address(targetUuid, 1),
identityKey,
false
);
if (changed) {
// save identity will close all sessions except for .1, so we
// must close that one manually.
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
await window.textsecure.storage.protocol.archiveSession(
new QualifiedAddress(ourUuid, new Address(targetUuid, 1))
if (profile.identityKey) {
const identityKey = Bytes.fromBase64(profile.identityKey);
const changed = await window.textsecure.storage.protocol.saveIdentity(
new Address(targetUuid, 1),
identityKey,
false
);
if (changed) {
// save identity will close all sessions except for .1, so we
// must close that one manually.
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
await window.textsecure.storage.protocol.archiveSession(
new QualifiedAddress(ourUuid, new Address(targetUuid, 1))
);
}
}
const accessKey = c.get('accessKey');
@ -238,15 +240,22 @@ export async function getProfile(
}
}
try {
await c.setEncryptedProfileName(profile.name);
} catch (error) {
log.warn(
'getProfile decryption failure:',
c.idForLogging(),
error && error.stack ? error.stack : error
);
await c.set({
if (profile.name) {
try {
await c.setEncryptedProfileName(profile.name);
} catch (error) {
log.warn(
'getProfile decryption failure:',
c.idForLogging(),
error && error.stack ? error.stack : error
);
await c.set({
profileName: undefined,
profileFamilyName: undefined,
});
}
} else {
c.set({
profileName: undefined,
profileFamilyName: undefined,
});

View File

@ -25,6 +25,7 @@ export function getPendingAttachment(file: File): AttachmentType | undefined {
return {
contentType: fileType,
fileName,
size: file.size,
path: file.name,
pending: true,
};

View File

@ -20,6 +20,7 @@ import {
MIMEType,
stringToMIMEType,
} from '../types/MIME';
import { sniffImageMimeType } from '../util/sniffImageMimeType';
import { ConversationModel } from '../models/conversations';
import {
GroupV2PendingMemberType,
@ -3499,6 +3500,17 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
return null;
}
let contentType: MIMEType;
const sniffedMimeType = sniffImageMimeType(data);
if (sniffedMimeType) {
contentType = sniffedMimeType;
} else {
log.warn(
'getStickerPackPreview: Unable to sniff sticker MIME type; falling back to WebP'
);
contentType = IMAGE_WEBP;
}
return {
date: null,
description: null,
@ -3506,7 +3518,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
...sticker,
data,
size: data.byteLength,
contentType: IMAGE_WEBP,
contentType,
},
title,
url,

4
ts/window.d.ts vendored
View File

@ -329,7 +329,9 @@ declare global {
width: number;
height: number;
};
copyIntoAttachmentsDirectory: (path: string) => Promise<string>;
copyIntoAttachmentsDirectory: (
path: string
) => Promise<{ path: string; size: number }>;
upgradeMessageSchema: (attributes: unknown) => WhatIsThis;
processNewAttachment: (
attachment: DownloadedAttachmentType