Allow stage and send of video, even if we can't get screenshot

This commit is contained in:
Scott Nonnenberg 2021-11-15 13:54:33 -08:00 committed by GitHub
parent 117cb074c7
commit a024ee4b96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 224 additions and 143 deletions

View file

@ -13,7 +13,7 @@ import { CompositionArea } from './CompositionArea';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
import { fakeDraftAttachment } from '../test-both/helpers/fakeAttachment';
import { landscapeGreenUrl } from '../storybook/Fixtures';
import { ThemeType } from '../types/Util';
import { RecordingState } from '../state/ducks/audioRecorder';
@ -165,7 +165,7 @@ story.add('SMS-only', () => {
story.add('Attachments', () => {
const props = createProps({
draftAttachments: [
fakeAttachment({
fakeDraftAttachment({
contentType: IMAGE_JPEG,
url: landscapeGreenUrl,
}),

View file

@ -34,7 +34,10 @@ import type { PropsType as GroupV2PendingApprovalActionsPropsType } from './conv
import { GroupV2PendingApprovalActions } from './conversation/GroupV2PendingApprovalActions';
import { AnnouncementsOnlyGroupBanner } from './AnnouncementsOnlyGroupBanner';
import { AttachmentList } from './conversation/AttachmentList';
import type { AttachmentType } from '../types/Attachment';
import type {
AttachmentDraftType,
InMemoryAttachmentDraftType,
} from '../types/Attachment';
import { isImageAttachment } from '../types/Attachment';
import { AudioCapture } from './conversation/AudioCapture';
import { CompositionUpload } from './CompositionUpload';
@ -67,11 +70,11 @@ export type OwnProps = Readonly<{
acceptedMessageRequest?: boolean;
addAttachment: (
conversationId: string,
attachment: AttachmentType
attachment: InMemoryAttachmentDraftType
) => unknown;
addPendingAttachment: (
conversationId: string,
pendingAttachment: AttachmentType
pendingAttachment: AttachmentDraftType
) => unknown;
announcementsOnly?: boolean;
areWeAdmin?: boolean;
@ -80,11 +83,11 @@ export type OwnProps = Readonly<{
cancelRecording: () => unknown;
completeRecording: (
conversationId: string,
onSendAudioRecording?: (rec: AttachmentType) => unknown
onSendAudioRecording?: (rec: InMemoryAttachmentDraftType) => unknown
) => unknown;
compositionApi?: MutableRefObject<CompositionAPIType>;
conversationId: string;
draftAttachments: ReadonlyArray<AttachmentType>;
draftAttachments: ReadonlyArray<AttachmentDraftType>;
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
groupAdmins: Array<ConversationType>;
@ -105,11 +108,11 @@ export type OwnProps = Readonly<{
processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown;
onSelectMediaQuality(isHQ: boolean): unknown;
onSendMessage(options: {
draftAttachments?: ReadonlyArray<AttachmentType>;
draftAttachments?: ReadonlyArray<AttachmentDraftType>;
mentions?: BodyRangesType;
message?: string;
timestamp?: number;
voiceNoteAttachment?: AttachmentType;
voiceNoteAttachment?: InMemoryAttachmentDraftType;
}): unknown;
openConversation(conversationId: string): unknown;
quotedMessageProps?: Omit<
@ -373,7 +376,9 @@ export const CompositionArea = ({
errorRecording={errorRecording}
i18n={i18n}
recordingState={recordingState}
onSendAudioRecording={(voiceNoteAttachment: AttachmentType) => {
onSendAudioRecording={(
voiceNoteAttachment: InMemoryAttachmentDraftType
) => {
onSendMessage({ voiceNoteAttachment });
}}
startRecording={startRecording}

View file

@ -4,7 +4,10 @@
import type { ChangeEventHandler } from 'react';
import React, { forwardRef, useState } from 'react';
import type { AttachmentType } from '../types/Attachment';
import type {
InMemoryAttachmentDraftType,
AttachmentDraftType,
} from '../types/Attachment';
import { AttachmentToastType } from '../types/AttachmentToastType';
import type { LocalizerType } from '../types/Util';
@ -19,14 +22,14 @@ import type { HandleAttachmentsProcessingArgsType } from '../util/handleAttachme
export type PropsType = {
addAttachment: (
conversationId: string,
attachment: AttachmentType
attachment: InMemoryAttachmentDraftType
) => unknown;
addPendingAttachment: (
conversationId: string,
pendingAttachment: AttachmentType
pendingAttachment: AttachmentDraftType
) => unknown;
conversationId: string;
draftAttachments: ReadonlyArray<AttachmentType>;
draftAttachments: ReadonlyArray<AttachmentDraftType>;
i18n: LocalizerType;
processAttachments: (options: HandleAttachmentsProcessingArgsType) => unknown;
removeAttachment: (conversationId: string, filePath: string) => unknown;

View file

@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions';
import { text } from '@storybook/addon-knobs';
import enMessages from '../../_locales/en/messages.json';
import type { AttachmentType } from '../types/Attachment';
import type { AttachmentDraftType } from '../types/Attachment';
import type { PropsType } from './ForwardMessageModal';
import { ForwardMessageModal } from './ForwardMessageModal';
import { IMAGE_JPEG, VIDEO_MP4, stringToMIMEType } from '../types/MIME';
@ -16,15 +16,17 @@ import { getDefaultConversation } from '../test-both/helpers/getDefaultConversat
import { setupI18n } from '../util/setupI18n';
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
const createAttachment = (
props: Partial<AttachmentType> = {}
): AttachmentType => ({
const createDraftAttachment = (
props: Partial<AttachmentDraftType> = {}
): AttachmentDraftType => ({
pending: false,
path: 'fileName.jpg',
contentType: stringToMIMEType(
text('attachment contentType', props.contentType || '')
),
fileName: text('attachment fileName', props.fileName || ''),
screenshot: props.screenshot,
url: text('attachment url', props.url || ''),
screenshotPath: props.pending === false ? props.screenshotPath : undefined,
url: text('attachment url', props.pending === false ? props.url || '' : ''),
size: 3433,
});
@ -81,7 +83,7 @@ story.add('link preview', () => {
date: Date.now(),
domain: 'https://www.signal.org',
url: 'signal.org',
image: createAttachment({
image: createDraftAttachment({
url: '/fixtures/kitten-4-112-112.jpg',
contentType: IMAGE_JPEG,
}),
@ -99,22 +101,19 @@ story.add('media attachments', () => {
<ForwardMessageModal
{...useProps({
attachments: [
createAttachment({
createDraftAttachment({
pending: true,
}),
createDraftAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
}),
createAttachment({
createDraftAttachment({
contentType: VIDEO_MP4,
fileName: 'pixabay-Soap-Bubble-7141.mp4',
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
screenshot: {
height: 112,
width: 112,
url: '/fixtures/kitten-4-112-112.jpg',
contentType: IMAGE_JPEG,
path: 'originalPath',
},
screenshotPath: '/fixtures/kitten-4-112-112.jpg',
}),
],
messageBody: 'cats',

View file

@ -16,7 +16,7 @@ import { animated } from '@react-spring/web';
import classNames from 'classnames';
import { AttachmentList } from './conversation/AttachmentList';
import type { AttachmentType } from '../types/Attachment';
import type { AttachmentDraftType } from '../types/Attachment';
import { Button } from './Button';
import type { InputApi } from './CompositionInput';
import { CompositionInput } from './CompositionInput';
@ -38,13 +38,13 @@ import { filterAndSortConversationsByRecent } from '../util/filterAndSortConvers
import { useAnimated } from '../hooks/useAnimated';
export type DataPropsType = {
attachments?: Array<AttachmentType>;
attachments?: Array<AttachmentDraftType>;
candidateConversations: ReadonlyArray<ConversationType>;
conversationId: string;
doForwardMessage: (
selectedContacts: Array<string>,
messageBody?: string,
attachments?: Array<AttachmentType>,
attachments?: Array<AttachmentDraftType>,
linkPreview?: LinkPreviewType
) => void;
i18n: LocalizerType;
@ -100,7 +100,9 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
const [filteredConversations, setFilteredConversations] = useState(
filterAndSortConversationsByRecent(candidateConversations, '')
);
const [attachmentsToForward, setAttachmentsToForward] = useState(attachments);
const [attachmentsToForward, setAttachmentsToForward] = useState<
Array<AttachmentDraftType>
>(attachments || []);
const [isEditingMessage, setIsEditingMessage] = useState(false);
const [messageBodyText, setMessageBodyText] = useState(messageBody || '');
const [cannotMessage, setCannotMessage] = useState(false);
@ -322,7 +324,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
<AttachmentList
attachments={attachmentsToForward}
i18n={i18n}
onCloseAttachment={(attachment: AttachmentType) => {
onCloseAttachment={(attachment: AttachmentDraftType) => {
const newAttachments = attachmentsToForward.filter(
currentAttachment => currentAttachment !== attachment
);

View file

@ -18,7 +18,7 @@ import {
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import { fakeAttachment } from '../../test-both/helpers/fakeAttachment';
import { fakeDraftAttachment } from '../../test-both/helpers/fakeAttachment';
const i18n = setupI18n('en', enMessages);
@ -36,7 +36,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
story.add('One File', () => {
const props = createProps({
attachments: [
fakeAttachment({
fakeDraftAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
@ -49,24 +49,18 @@ story.add('One File', () => {
story.add('Multiple Visual Attachments', () => {
const props = createProps({
attachments: [
fakeAttachment({
fakeDraftAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
}),
fakeAttachment({
fakeDraftAttachment({
contentType: VIDEO_MP4,
fileName: 'pixabay-Soap-Bubble-7141.mp4',
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
screenshot: {
height: 112,
width: 112,
url: '/fixtures/kitten-4-112-112.jpg',
contentType: IMAGE_JPEG,
path: 'originalpath',
},
screenshotPath: '/fixtures/kitten-4-112-112.jpg',
}),
fakeAttachment({
fakeDraftAttachment({
contentType: IMAGE_GIF,
fileName: 'giphy-GVNv0UpeYm17e',
url: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
@ -80,34 +74,28 @@ story.add('Multiple Visual Attachments', () => {
story.add('Multiple with Non-Visual Types', () => {
const props = createProps({
attachments: [
fakeAttachment({
fakeDraftAttachment({
contentType: IMAGE_JPEG,
fileName: 'tina-rolf-269345-unsplash.jpg',
url: '/fixtures/tina-rolf-269345-unsplash.jpg',
}),
fakeAttachment({
fakeDraftAttachment({
contentType: stringToMIMEType('text/plain'),
fileName: 'lorem-ipsum.txt',
url: '/fixtures/lorem-ipsum.txt',
}),
fakeAttachment({
fakeDraftAttachment({
contentType: AUDIO_MP3,
fileName: 'incompetech-com-Agnus-Dei-X.mp3',
url: '/fixtures/incompetech-com-Agnus-Dei-X.mp3',
}),
fakeAttachment({
fakeDraftAttachment({
contentType: VIDEO_MP4,
fileName: 'pixabay-Soap-Bubble-7141.mp4',
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
screenshot: {
height: 112,
width: 112,
url: '/fixtures/kitten-4-112-112.jpg',
contentType: IMAGE_JPEG,
path: 'originalpath',
},
screenshotPath: '/fixtures/kitten-4-112-112.jpg',
}),
fakeAttachment({
fakeDraftAttachment({
contentType: IMAGE_GIF,
fileName: 'giphy-GVNv0UpeYm17e',
url: '/fixtures/giphy-GVNvOUpeYmI7e.gif',

View file

@ -7,21 +7,20 @@ import { Image } from './Image';
import { StagedGenericAttachment } from './StagedGenericAttachment';
import { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment';
import type { LocalizerType } from '../../types/Util';
import type { AttachmentType } from '../../types/Attachment';
import type { AttachmentDraftType } from '../../types/Attachment';
import {
areAllAttachmentsVisual,
getUrl,
isImageAttachment,
isVideoAttachment,
} from '../../types/Attachment';
export type Props = Readonly<{
attachments: ReadonlyArray<AttachmentType>;
attachments: ReadonlyArray<AttachmentDraftType>;
i18n: LocalizerType;
onAddAttachment?: () => void;
onClickAttachment?: (attachment: AttachmentType) => void;
onClickAttachment?: (attachment: AttachmentDraftType) => void;
onClose?: () => void;
onCloseAttachment: (attachment: AttachmentType) => void;
onCloseAttachment: (attachment: AttachmentDraftType) => void;
}>;
const IMAGE_WIDTH = 120;
@ -31,6 +30,14 @@ const IMAGE_HEIGHT = 120;
const BLANK_VIDEO_THUMBNAIL =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVR42mNiAAAABgADm78GJQAAAABJRU5ErkJggg==';
function getUrl(attachment: AttachmentDraftType): string | undefined {
if (attachment.pending) {
return undefined;
}
return attachment.url;
}
export const AttachmentList = ({
attachments,
i18n,
@ -65,11 +72,17 @@ export const AttachmentList = ({
const isImage = isImageAttachment(attachment);
const isVideo = isVideoAttachment(attachment);
const closeAttachment = () => onCloseAttachment(attachment);
if (isImage || isVideo || attachment.pending) {
const isDownloaded = !attachment.pending;
const imageUrl =
url || (isVideo ? BLANK_VIDEO_THUMBNAIL : undefined);
const clickAttachment = onClickAttachment
? () => onClickAttachment(attachment)
: undefined;
return (
<Image
key={key}
@ -79,17 +92,16 @@ export const AttachmentList = ({
className="module-staged-attachment"
i18n={i18n}
attachment={attachment}
isDownloaded={isDownloaded}
softCorners
playIconOverlay={isVideo}
height={IMAGE_HEIGHT}
width={IMAGE_WIDTH}
url={imageUrl}
closeButton
onClick={onClickAttachment}
onClickClose={onCloseAttachment}
onError={() => {
onCloseAttachment(attachment);
}}
onClick={clickAttachment}
onClickClose={closeAttachment}
onError={closeAttachment}
/>
);
}
@ -99,7 +111,7 @@ export const AttachmentList = ({
key={key}
attachment={attachment}
i18n={i18n}
onClose={onCloseAttachment}
onClose={closeAttachment}
/>
);
})}

View file

@ -5,7 +5,10 @@ import React, { useCallback, useEffect, useState } from 'react';
import * as moment from 'moment';
import { noop } from 'lodash';
import type { AttachmentType } from '../../types/Attachment';
import type {
AttachmentDraftType,
InMemoryAttachmentDraftType,
} from '../../types/Attachment';
import { ConfirmationDialog } from '../ConfirmationDialog';
import type { LocalizerType } from '../../types/Util';
import {
@ -20,7 +23,7 @@ import {
useKeyboardShortcuts,
} from '../../hooks/useKeyboardShortcuts';
type OnSendAudioRecordingType = (rec: AttachmentType) => unknown;
type OnSendAudioRecordingType = (rec: InMemoryAttachmentDraftType) => unknown;
export type PropsType = {
cancelRecording: () => unknown;
@ -29,7 +32,7 @@ export type PropsType = {
conversationId: string,
onSendAudioRecording?: OnSendAudioRecordingType
) => unknown;
draftAttachments: ReadonlyArray<AttachmentType>;
draftAttachments: ReadonlyArray<AttachmentDraftType>;
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
i18n: LocalizerType;

View file

@ -15,6 +15,7 @@ export type Props = {
attachment: AttachmentType;
url?: string;
isDownloaded?: boolean;
className?: string;
height?: number;
width?: number;
@ -145,6 +146,7 @@ export class Image extends React.Component<Props> {
curveTopLeft,
curveTopRight,
darkOverlay,
isDownloaded,
height = 0,
i18n,
noBackground,
@ -165,7 +167,9 @@ export class Image extends React.Component<Props> {
const { caption, pending } = attachment || { caption: null, pending: true };
const canClick = this.canClick();
const imgNotDownloaded = hasNotDownloaded(attachment);
const imgNotDownloaded = isDownloaded
? false
: hasNotDownloaded(attachment);
const resolvedBlurHash = blurHash || defaultBlurHash(theme);

8
ts/model-types.d.ts vendored
View file

@ -22,7 +22,11 @@ import {
} from './messages/MessageSendState';
import { GroupNameCollisionsWithIdsByTitle } from './util/groupMemberNameCollisions';
import { ConversationColorType } from './types/Colors';
import { AttachmentType, ThumbnailType } from './types/Attachment';
import {
AttachmentDraftType,
AttachmentType,
ThumbnailType,
} from './types/Attachment';
import { EmbeddedContactType } from './types/EmbeddedContact';
import { SignalService as Proto } from './protobuf';
import { AvatarDataType } from './types/Avatar';
@ -223,7 +227,7 @@ export type ConversationAttributesType = {
customColorId?: string;
discoveredUnregisteredAt?: number;
draftChanged?: boolean;
draftAttachments?: Array<AttachmentType>;
draftAttachments?: Array<AttachmentDraftType>;
draftBodyRanges?: Array<BodyRangeType>;
draftTimestamp?: number | null;
inbox_position: number;

View file

@ -4,7 +4,7 @@
import type { ThunkAction } from 'redux-thunk';
import * as log from '../../logging/log';
import type { AttachmentType } from '../../types/Attachment';
import type { InMemoryAttachmentDraftType } from '../../types/Attachment';
import { SignalService as Proto } from '../../protobuf';
import type { StateType as RootStateType } from '../reducer';
import { fileToBytes } from '../../util/fileToBytes';
@ -129,7 +129,7 @@ function completeRecordingAction(): CompleteRecordingAction {
function completeRecording(
conversationId: string,
onSendAudioRecording?: (rec: AttachmentType) => unknown
onSendAudioRecording?: (rec: InMemoryAttachmentDraftType) => unknown
): ThunkAction<
void,
RootStateType,
@ -158,7 +158,8 @@ function completeRecording(
}
const data = await fileToBytes(blob);
const voiceNoteAttachment = {
const voiceNoteAttachment: InMemoryAttachmentDraftType = {
pending: false,
contentType: stringToMIMEType(blob.type),
data,
size: data.byteLength,

View file

@ -6,7 +6,10 @@ import type { ThunkAction } from 'redux-thunk';
import * as log from '../../logging/log';
import type { NoopActionType } from './noop';
import type { StateType as RootStateType } from '../reducer';
import type { AttachmentType } from '../../types/Attachment';
import type {
AttachmentDraftType,
InMemoryAttachmentDraftType,
} from '../../types/Attachment';
import type { MessageAttributesType } from '../../model-types.d';
import type { LinkPreviewWithDomain } from '../../types/LinkPreview';
import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation';
@ -15,14 +18,14 @@ import { REMOVE_PREVIEW as REMOVE_LINK_PREVIEW } from './linkPreviews';
import { writeDraftAttachment } from '../../util/writeDraftAttachment';
import { deleteDraftAttachment } from '../../util/deleteDraftAttachment';
import { replaceIndex } from '../../util/replaceIndex';
import { resolveAttachmentOnDisk } from '../../util/resolveAttachmentOnDisk';
import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk';
import type { HandleAttachmentsProcessingArgsType } from '../../util/handleAttachmentsProcessing';
import { handleAttachmentsProcessing } from '../../util/handleAttachmentsProcessing';
// State
export type ComposerStateType = {
attachments: ReadonlyArray<AttachmentType>;
attachments: ReadonlyArray<AttachmentDraftType>;
linkPreviewLoading: boolean;
linkPreviewResult?: LinkPreviewWithDomain;
quotedMessage?: Pick<MessageAttributesType, 'conversationId' | 'quote'>;
@ -40,12 +43,12 @@ const SET_QUOTED_MESSAGE = 'composer/SET_QUOTED_MESSAGE';
type AddPendingAttachmentActionType = {
type: typeof ADD_PENDING_ATTACHMENT;
payload: AttachmentType;
payload: AttachmentDraftType;
};
type ReplaceAttachmentsActionType = {
type: typeof REPLACE_ATTACHMENTS;
payload: ReadonlyArray<AttachmentType>;
payload: ReadonlyArray<AttachmentDraftType>;
};
type ResetComposerActionType = {
@ -99,14 +102,14 @@ export const actions = {
// next in-memory store.
function getAttachmentsFromConversationModel(
conversationId: string
): Array<AttachmentType> {
): Array<AttachmentDraftType> {
const conversation = window.ConversationController.get(conversationId);
return conversation?.get('draftAttachments') || [];
}
function addAttachment(
conversationId: string,
attachment: AttachmentType
attachment: InMemoryAttachmentDraftType
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
return async (dispatch, getState) => {
// We do async operations first so multiple in-process addAttachments don't stomp on
@ -161,7 +164,7 @@ function addAttachment(
function addPendingAttachment(
conversationId: string,
pendingAttachment: AttachmentType
pendingAttachment: AttachmentDraftType
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
return (dispatch, getState) => {
const isSelectedConversation =
@ -240,7 +243,7 @@ function removeAttachment(
function replaceAttachments(
conversationId: string,
attachments: ReadonlyArray<AttachmentType>
attachments: ReadonlyArray<AttachmentDraftType>
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
return (dispatch, getState) => {
// If the call came from a conversation we are no longer in we do not
@ -251,7 +254,7 @@ function replaceAttachments(
dispatch({
type: REPLACE_ATTACHMENTS,
payload: attachments.map(resolveAttachmentOnDisk),
payload: attachments.map(resolveDraftAttachmentOnDisk),
});
};
}

View file

@ -13,15 +13,15 @@ import { getLinkPreview } from '../selectors/linkPreviews';
import { getIntl, getTheme } from '../selectors/user';
import { getEmojiSkinTone } from '../selectors/items';
import { selectRecentEmojis } from '../selectors/emojis';
import type { AttachmentType } from '../../types/Attachment';
import type { AttachmentDraftType } from '../../types/Attachment';
export type SmartForwardMessageModalProps = {
attachments?: Array<AttachmentType>;
attachments?: Array<AttachmentDraftType>;
conversationId: string;
doForwardMessage: (
selectedContacts: Array<string>,
messageBody?: string,
attachments?: Array<AttachmentType>,
attachments?: Array<AttachmentDraftType>,
linkPreview?: LinkPreviewType
) => void;
isSticker: boolean;

View file

@ -1,7 +1,10 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { AttachmentType } from '../../types/Attachment';
import type {
AttachmentType,
AttachmentDraftType,
} from '../../types/Attachment';
import { IMAGE_JPEG } from '../../types/MIME';
export const fakeAttachment = (
@ -13,3 +16,13 @@ export const fakeAttachment = (
size: 10304,
...overrides,
});
export const fakeDraftAttachment = (
overrides: Partial<AttachmentDraftType> = {}
): AttachmentDraftType => ({
pending: false,
contentType: IMAGE_JPEG,
path: 'file.jpg',
size: 10304,
...overrides,
});

View file

@ -9,8 +9,8 @@ import { noopAction } from '../../../state/ducks/noop';
import { reducer as rootReducer } from '../../../state/reducer';
import { IMAGE_JPEG } from '../../../types/MIME';
import type { AttachmentType } from '../../../types/Attachment';
import { fakeAttachment } from '../../helpers/fakeAttachment';
import type { AttachmentDraftType } from '../../../types/Attachment';
import { fakeDraftAttachment } from '../../helpers/fakeAttachment';
describe('both/state/ducks/composer', () => {
const QUOTED_MESSAGE = {
@ -40,8 +40,13 @@ describe('both/state/ducks/composer', () => {
const { replaceAttachments } = actions;
const dispatch = sinon.spy();
const attachments: Array<AttachmentType> = [
{ contentType: IMAGE_JPEG, pending: false, url: '', size: 2433 },
const attachments: Array<AttachmentDraftType> = [
{
contentType: IMAGE_JPEG,
pending: true,
size: 2433,
path: 'image.jpg',
},
];
replaceAttachments('123', attachments)(
dispatch,
@ -57,7 +62,7 @@ describe('both/state/ducks/composer', () => {
it('sets the high quality setting to false when there are no attachments', () => {
const { replaceAttachments } = actions;
const dispatch = sinon.spy();
const attachments: Array<AttachmentType> = [];
const attachments: Array<AttachmentDraftType> = [];
replaceAttachments('123', attachments)(
dispatch,
@ -83,7 +88,7 @@ describe('both/state/ducks/composer', () => {
const { replaceAttachments } = actions;
const dispatch = sinon.spy();
const attachments = [fakeAttachment()];
const attachments = [fakeDraftAttachment()];
replaceAttachments('123', attachments)(
dispatch,
getRootStateFunction('456'),

View file

@ -78,40 +78,46 @@ export type DownloadedAttachmentType = AttachmentType & {
export type BaseAttachmentDraftType = {
blurHash?: string;
contentType: MIME.MIMEType;
fileName: string;
path: string;
screenshotContentType?: string;
screenshotSize?: number;
size: number;
flags?: number;
};
// An ephemeral attachment type, used between user's request to add the attachment as
// a draft and final save on disk and in conversation.draftAttachments.
export type InMemoryAttachmentDraftType =
| ({
data?: Uint8Array;
data: Uint8Array;
pending: false;
screenshotData?: Uint8Array;
fileName?: string;
path?: string;
} & BaseAttachmentDraftType)
| {
contentType: MIME.MIMEType;
fileName: string;
path: string;
fileName?: string;
path?: string;
pending: true;
size: number;
};
// What's stored in conversation.draftAttachments
export type AttachmentDraftType =
| ({
url: string;
url?: string;
screenshotPath?: string;
pending: false;
// Old draft attachments may have a caption, though they are no longer editable
// because we removed the caption editor.
caption?: string;
fileName?: string;
path: string;
} & BaseAttachmentDraftType)
| {
contentType: MIME.MIMEType;
fileName: string;
path: string;
fileName?: string;
path?: string;
pending: true;
size: number;
};
@ -614,6 +620,10 @@ export function getUrl(attachment: AttachmentType): string | undefined {
return attachment.screenshot.url;
}
if (isVideoAttachment(attachment)) {
return undefined;
}
return attachment.url;
}

View file

@ -6,17 +6,20 @@ import {
preProcessAttachment,
processAttachment,
} from './processAttachment';
import type { AttachmentType } from '../types/Attachment';
import type {
AttachmentDraftType,
InMemoryAttachmentDraftType,
} from '../types/Attachment';
import { AttachmentToastType } from '../types/AttachmentToastType';
import * as log from '../logging/log';
export type AddAttachmentActionType = (
conversationId: string,
attachment: AttachmentType
attachment: InMemoryAttachmentDraftType
) => unknown;
export type AddPendingAttachmentActionType = (
conversationId: string,
pendingAttachment: AttachmentType
pendingAttachment: AttachmentDraftType
) => unknown;
export type RemoveAttachmentActionType = (
conversationId: string,
@ -27,7 +30,7 @@ export type HandleAttachmentsProcessingArgsType = {
addAttachment: AddAttachmentActionType;
addPendingAttachment: AddPendingAttachmentActionType;
conversationId: string;
draftAttachments: ReadonlyArray<AttachmentType>;
draftAttachments: ReadonlyArray<AttachmentDraftType>;
files: ReadonlyArray<File>;
onShowToast: (toastType: AttachmentToastType) => unknown;
removeAttachment: RemoveAttachmentActionType;

View file

@ -4,7 +4,10 @@
import path from 'path';
import * as log from '../logging/log';
import type { AttachmentType } from '../types/Attachment';
import type {
AttachmentDraftType,
InMemoryAttachmentDraftType,
} from '../types/Attachment';
import { AttachmentToastType } from '../types/AttachmentToastType';
import { fileToBytes } from './fileToBytes';
import { handleImageAttachment } from './handleImageAttachment';
@ -14,7 +17,9 @@ import { isFileDangerous } from './isFileDangerous';
import { isHeic, isImage, stringToMIMEType } from '../types/MIME';
import { isImageTypeSupported, isVideoTypeSupported } from './GoogleChrome';
export function getPendingAttachment(file: File): AttachmentType | undefined {
export function getPendingAttachment(
file: File
): AttachmentDraftType | undefined {
if (!file) {
return;
}
@ -33,7 +38,7 @@ export function getPendingAttachment(file: File): AttachmentType | undefined {
export function preProcessAttachment(
file: File,
draftAttachments: Array<AttachmentType>
draftAttachments: Array<AttachmentDraftType>
): AttachmentToastType | undefined {
if (!file) {
return;
@ -53,7 +58,7 @@ export function preProcessAttachment(
}
const haveNonImage = draftAttachments.some(
(attachment: AttachmentType) => !isImage(attachment.contentType)
(attachment: AttachmentDraftType) => !isImage(attachment.contentType)
);
// You can't add another attachment if you already have a non-image staged
if (haveNonImage) {
@ -72,10 +77,10 @@ export function preProcessAttachment(
export async function processAttachment(
file: File
): Promise<AttachmentType | void> {
): Promise<InMemoryAttachmentDraftType | void> {
const fileType = stringToMIMEType(file.type);
let attachment: AttachmentType;
let attachment: InMemoryAttachmentDraftType;
try {
if (isImageTypeSupported(fileType) || isHeic(fileType)) {
attachment = await handleImageAttachment(file);

View file

@ -4,11 +4,12 @@
import { pick } from 'lodash';
import * as log from '../logging/log';
import type { AttachmentType } from '../types/Attachment';
import type { AttachmentDraftType } from '../types/Attachment';
import { isVideoAttachment } from '../types/Attachment';
export function resolveAttachmentOnDisk(
attachment: AttachmentType
): AttachmentType {
export function resolveDraftAttachmentOnDisk(
attachment: AttachmentDraftType
): AttachmentDraftType {
let url = '';
if (attachment.pending) {
return attachment;
@ -18,7 +19,7 @@ export function resolveAttachmentOnDisk(
url = window.Signal.Migrations.getAbsoluteDraftPath(
attachment.screenshotPath
);
} else if (attachment.path) {
} else if (!isVideoAttachment(attachment) && attachment.path) {
url = window.Signal.Migrations.getAbsoluteDraftPath(attachment.path);
} else {
log.warn(

View file

@ -2,28 +2,32 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { omit } from 'lodash';
import type { AttachmentType } from '../types/Attachment';
import type {
InMemoryAttachmentDraftType,
AttachmentDraftType,
} from '../types/Attachment';
export async function writeDraftAttachment(
attachment: AttachmentType
): Promise<AttachmentType> {
attachment: InMemoryAttachmentDraftType
): Promise<AttachmentDraftType> {
if (attachment.pending) {
throw new Error('writeDraftAttachment: Cannot write pending attachment');
}
const result: AttachmentType = {
...omit(attachment, ['data', 'screenshotData']),
pending: false,
};
if (attachment.data) {
result.path = await window.Signal.Migrations.writeNewDraftData(
const path = await window.Signal.Migrations.writeNewDraftData(
attachment.data
);
}
if (attachment.screenshotData) {
result.screenshotPath = await window.Signal.Migrations.writeNewDraftData(
const screenshotPath = attachment.screenshotData
? await window.Signal.Migrations.writeNewDraftData(
attachment.screenshotData
);
}
return result;
)
: undefined;
return {
...omit(attachment, ['data', 'screenshotData']),
path,
screenshotPath,
pending: false,
};
}

View file

@ -7,7 +7,7 @@ import { batch as batchDispatch } from 'react-redux';
import { debounce, flatten, omit, throttle } from 'lodash';
import { render } from 'mustache';
import type { AttachmentType } from '../types/Attachment';
import type { AttachmentDraftType, AttachmentType } from '../types/Attachment';
import { isGIF } from '../types/Attachment';
import * as Attachment from '../types/Attachment';
import type { StickerPackType as StickerPackDBType } from '../sql/Interface';
@ -1561,16 +1561,32 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}
const attachments = getAttachmentsForMessage(message.attributes);
const draftAttachments = attachments
.map((item: AttachmentType): AttachmentDraftType | null => {
const { path } = item;
if (!path) {
return null;
}
return {
...item,
path,
pending: false as const,
screenshotPath: item.screenshot?.path,
};
})
.filter(isNotNil);
this.forwardMessageModal = new Whisper.ReactWrapperView({
JSX: window.Signal.State.Roots.createForwardMessageModal(
window.reduxStore,
{
attachments,
attachments: draftAttachments,
conversationId: this.model.id,
doForwardMessage: async (
conversationIds: Array<string>,
messageBody?: string,
includedAttachments?: Array<AttachmentType>,
includedAttachments?: Array<AttachmentDraftType>,
linkPreview?: LinkPreviewType
) => {
try {
@ -1619,7 +1635,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
message: MessageModel,
conversationIds: Array<string>,
messageBody?: string,
attachments?: Array<AttachmentType>,
attachments?: Array<AttachmentDraftType>,
linkPreview?: LinkPreviewType
): Promise<boolean> {
log.info(`maybeForwardMessage/${message.idForLogging()}: Starting...`);