Allow stage and send of video, even if we can't get screenshot
This commit is contained in:
parent
117cb074c7
commit
a024ee4b96
21 changed files with 224 additions and 143 deletions
|
@ -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,
|
||||
}),
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
url: '/fixtures/kitten-4-112-112.jpg',
|
||||
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',
|
||||
},
|
||||
url: '/fixtures/kitten-4-112-112.jpg',
|
||||
screenshotPath: '/fixtures/kitten-4-112-112.jpg',
|
||||
}),
|
||||
fakeAttachment({
|
||||
fakeDraftAttachment({
|
||||
contentType: IMAGE_GIF,
|
||||
fileName: 'giphy-GVNv0UpeYm17e',
|
||||
url: '/fixtures/giphy-GVNvOUpeYmI7e.gif',
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
8
ts/model-types.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
|
@ -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 = {
|
||||
const path = await window.Signal.Migrations.writeNewDraftData(
|
||||
attachment.data
|
||||
);
|
||||
|
||||
const screenshotPath = attachment.screenshotData
|
||||
? await window.Signal.Migrations.writeNewDraftData(
|
||||
attachment.screenshotData
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...omit(attachment, ['data', 'screenshotData']),
|
||||
path,
|
||||
screenshotPath,
|
||||
pending: false,
|
||||
};
|
||||
if (attachment.data) {
|
||||
result.path = await window.Signal.Migrations.writeNewDraftData(
|
||||
attachment.data
|
||||
);
|
||||
}
|
||||
if (attachment.screenshotData) {
|
||||
result.screenshotPath = await window.Signal.Migrations.writeNewDraftData(
|
||||
attachment.screenshotData
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -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...`);
|
||||
|
|
Loading…
Reference in a new issue