Merge pull request #1856 from Bilb/fix-attachment-since-electron-upgrade

Fix attachment since electron upgrade
pull/1873/head
Audric Ackermann 4 years ago committed by GitHub
commit a3edb99549
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1879,7 +1879,7 @@ function saveMessage(data) {
return id; return id;
} }
async function saveSeenMessageHashes(arrayOfHashes) { function saveSeenMessageHashes(arrayOfHashes) {
globalInstance.transaction(() => { globalInstance.transaction(() => {
map(arrayOfHashes, hashData => saveSeenMessageHash(hashData)); map(arrayOfHashes, hashData => saveSeenMessageHash(hashData));
})(); })();
@ -1946,7 +1946,7 @@ function cleanSeenMessages() {
}); });
} }
async function saveMessages(arrayOfMessages) { function saveMessages(arrayOfMessages) {
globalInstance.transaction(() => { globalInstance.transaction(() => {
map(arrayOfMessages, message => saveMessage(message)); map(arrayOfMessages, message => saveMessage(message));
})(); })();

@ -87,6 +87,7 @@ export class Linkify extends React.Component<Props> {
message: window.i18n('linkVisitWarningMessage', url), message: window.i18n('linkVisitWarningMessage', url),
okText: window.i18n('open'), okText: window.i18n('open'),
cancelText: window.i18n('copy'), cancelText: window.i18n('copy'),
showExitIcon: true,
onClickOk: openLink, onClickOk: openLink,
onClickClose: () => { onClickClose: () => {
window.inboxStore?.dispatch(updateConfirmModal(null)); window.inboxStore?.dispatch(updateConfirmModal(null));

@ -10,21 +10,41 @@ import {
getUrl, getUrl,
isVideoAttachment, isVideoAttachment,
} from '../../types/Attachment'; } from '../../types/Attachment';
import { useDispatch, useSelector } from 'react-redux';
import {
removeAllStagedAttachmentsInConversation,
removeStagedAttachmentInConversation,
} from '../../state/ducks/stagedAttachments';
import { getSelectedConversationKey } from '../../state/selectors/conversations';
type Props = { type Props = {
attachments: Array<AttachmentType>; attachments: Array<AttachmentType>;
// onError: () => void;
onClickAttachment: (attachment: AttachmentType) => void; onClickAttachment: (attachment: AttachmentType) => void;
onCloseAttachment: (attachment: AttachmentType) => void;
onAddAttachment: () => void; onAddAttachment: () => void;
onClose: () => void;
}; };
const IMAGE_WIDTH = 120; const IMAGE_WIDTH = 120;
const IMAGE_HEIGHT = 120; const IMAGE_HEIGHT = 120;
export const StagedAttachmentList = (props: Props) => { export const StagedAttachmentList = (props: Props) => {
const { attachments, onAddAttachment, onClickAttachment, onCloseAttachment, onClose } = props; const { attachments, onAddAttachment, onClickAttachment } = props;
const dispatch = useDispatch();
const conversationKey = useSelector(getSelectedConversationKey);
const onRemoveAllStaged = () => {
if (!conversationKey) {
return;
}
dispatch(removeAllStagedAttachmentsInConversation({ conversationKey }));
};
const onRemoveByFilename = (filename: string) => {
if (!conversationKey) {
return;
}
dispatch(removeStagedAttachmentInConversation({ conversationKey, filename }));
};
if (!attachments.length) { if (!attachments.length) {
return null; return null;
@ -36,7 +56,11 @@ export const StagedAttachmentList = (props: Props) => {
<div className="module-attachments"> <div className="module-attachments">
{attachments.length > 1 ? ( {attachments.length > 1 ? (
<div className="module-attachments__header"> <div className="module-attachments__header">
<div role="button" onClick={onClose} className="module-attachments__close-button" /> <div
role="button"
onClick={onRemoveAllStaged}
className="module-attachments__close-button"
/>
</div> </div>
) : null} ) : null}
<div className="module-attachments__rail"> <div className="module-attachments__rail">
@ -58,7 +82,9 @@ export const StagedAttachmentList = (props: Props) => {
url={getUrl(attachment)} url={getUrl(attachment)}
closeButton={true} closeButton={true}
onClick={clickCallback} onClick={clickCallback}
onClickClose={onCloseAttachment} onClickClose={() => {
onRemoveByFilename(attachment.fileName);
}}
/> />
); );
} }
@ -69,7 +95,9 @@ export const StagedAttachmentList = (props: Props) => {
<StagedGenericAttachment <StagedGenericAttachment
key={genericKey} key={genericKey}
attachment={attachment} attachment={attachment}
onClose={onCloseAttachment} onClose={() => {
onRemoveByFilename(attachment.fileName);
}}
/> />
); );
})} })}

@ -121,7 +121,13 @@ const InviteContactsDialogInner = (props: Props) => {
); );
if (privateConvo) { if (privateConvo) {
void privateConvo.sendMessage('', null, null, null, groupInvitation); void privateConvo.sendMessage({
body: '',
attachments: undefined,
groupInvitation,
preview: undefined,
quote: undefined,
});
} }
}); });
} }

@ -26,6 +26,7 @@ export interface SessionConfirmDialogProps {
iconSize?: SessionIconSize; iconSize?: SessionIconSize;
theme?: DefaultTheme; theme?: DefaultTheme;
shouldShowConfirm?: boolean | undefined; shouldShowConfirm?: boolean | undefined;
showExitIcon?: boolean | undefined;
} }
const SessionConfirmInner = (props: SessionConfirmDialogProps) => { const SessionConfirmInner = (props: SessionConfirmDialogProps) => {
@ -42,6 +43,7 @@ const SessionConfirmInner = (props: SessionConfirmDialogProps) => {
iconSize, iconSize,
shouldShowConfirm, shouldShowConfirm,
onClickCancel, onClickCancel,
showExitIcon,
} = props; } = props;
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -92,7 +94,7 @@ const SessionConfirmInner = (props: SessionConfirmDialogProps) => {
<SessionWrapperModal <SessionWrapperModal
title={title} title={title}
onClose={onClickClose} onClose={onClickClose}
showExitIcon={false} showExitIcon={showExitIcon}
showHeader={showHeader} showHeader={showHeader}
> >
{!showHeader && <SpacerLG />} {!showHeader && <SpacerLG />}

@ -26,6 +26,7 @@ import { SessionMainPanel } from '../SessionMainPanel';
import { PersistGate } from 'redux-persist/integration/react'; import { PersistGate } from 'redux-persist/integration/react';
import { persistStore } from 'redux-persist'; import { persistStore } from 'redux-persist';
import { TimerOptionsArray } from '../../state/ducks/timerOptions'; import { TimerOptionsArray } from '../../state/ducks/timerOptions';
import { getEmptyStagedAttachmentsState } from '../../state/ducks/stagedAttachments';
// Workaround: A react component's required properties are filtering up through connect() // Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
@ -108,6 +109,7 @@ export class SessionInboxView extends React.Component<any, State> {
timerOptions: { timerOptions: {
timerOptions, timerOptions,
}, },
stagedAttachments: getEmptyStagedAttachmentsState(),
}; };
this.store = createStore(initialState); this.store = createStore(initialState);

@ -1,20 +1,18 @@
import React from 'react'; import React from 'react';
import _, { debounce } from 'lodash'; import _, { debounce } from 'lodash';
import { Attachment, AttachmentType } from '../../../types/Attachment'; import { AttachmentType } from '../../../types/Attachment';
import * as MIME from '../../../types/MIME'; import * as MIME from '../../../types/MIME';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon'; import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
import { SessionEmojiPanel } from './SessionEmojiPanel'; import { SessionEmojiPanel } from './SessionEmojiPanel';
import { SessionRecording } from './SessionRecording'; import { SessionRecording } from './SessionRecording';
import { SignalService } from '../../../protobuf';
import { Constants } from '../../../session'; import { Constants } from '../../../session';
import { toArray } from 'react-emoji-render'; import { toArray } from 'react-emoji-render';
import { Flex } from '../../basic/Flex'; import { Flex } from '../../basic/Flex';
import { StagedAttachmentList } from '../../conversation/AttachmentList'; import { StagedAttachmentList } from '../../conversation/StagedAttachmentList';
import { ToastUtils } from '../../../session/utils'; import { ToastUtils } from '../../../session/utils';
import { AttachmentUtil } from '../../../util'; import { AttachmentUtil } from '../../../util';
import { import {
@ -56,6 +54,7 @@ import {
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { StateType } from '../../../state/reducer'; import { StateType } from '../../../state/reducer';
import { getTheme } from '../../../state/selectors/theme'; import { getTheme } from '../../../state/selectors/theme';
import { removeAllStagedAttachmentsInConversation } from '../../../state/ducks/stagedAttachments';
export interface ReplyingToMessageProps { export interface ReplyingToMessageProps {
convoId: string; convoId: string;
@ -79,8 +78,16 @@ export interface StagedAttachmentType extends AttachmentType {
file: File; file: File;
} }
export type SendMessageType = {
body: string;
attachments: Array<StagedAttachmentType> | undefined;
quote: any | undefined;
preview: any | undefined;
groupInvitation: { url: string | undefined; name: string } | undefined;
};
interface Props { interface Props {
sendMessage: any; sendMessage: (msg: SendMessageType) => void;
draft: string; draft: string;
onLoadVoiceNoteView: any; onLoadVoiceNoteView: any;
@ -89,8 +96,6 @@ interface Props {
selectedConversation: ReduxConversationType | undefined; selectedConversation: ReduxConversationType | undefined;
quotedMessageProps?: ReplyingToMessageProps; quotedMessageProps?: ReplyingToMessageProps;
stagedAttachments: Array<StagedAttachmentType>; stagedAttachments: Array<StagedAttachmentType>;
clearAttachments: () => any;
removeAttachment: (toRemove: AttachmentType) => void;
onChoseAttachments: (newAttachments: Array<File>) => void; onChoseAttachments: (newAttachments: Array<File>) => void;
} }
@ -730,8 +735,6 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
attachments={stagedAttachments} attachments={stagedAttachments}
onClickAttachment={this.onClickAttachment} onClickAttachment={this.onClickAttachment}
onAddAttachment={this.onChooseAttachment} onAddAttachment={this.onChooseAttachment}
onCloseAttachment={this.props.removeAttachment}
onClose={this.props.clearAttachments}
/> />
{this.renderCaptionEditor(showCaptionEditor)} {this.renderCaptionEditor(showCaptionEditor)}
</> </>
@ -872,17 +875,19 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
try { try {
const attachments = await this.getFiles(); const attachments = await this.getFiles();
await this.props.sendMessage( this.props.sendMessage({
messagePlaintext, body: messagePlaintext,
attachments, attachments: attachments || [],
extractedQuotedMessageProps, quote: extractedQuotedMessageProps,
linkPreviews, preview: linkPreviews,
null, groupInvitation: undefined,
{} });
);
this.props.clearAttachments();
window.inboxStore?.dispatch(
removeAllStagedAttachmentsInConversation({
conversationKey: this.props.selectedConversationKey,
})
);
// Empty composition box and stagedAttachments // Empty composition box and stagedAttachments
this.setState({ this.setState({
showEmojiPanel: false, showEmojiPanel: false,
@ -902,8 +907,12 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
} }
// this function is called right before sending a message, to gather really the files behind attachments. // this function is called right before sending a message, to gather really the files behind attachments.
private async getFiles() { private async getFiles(): Promise<Array<any>> {
const { stagedAttachments } = this.props; const { stagedAttachments } = this.props;
if (_.isEmpty(stagedAttachments)) {
return [];
}
// scale them down // scale them down
const files = await Promise.all( const files = await Promise.all(
stagedAttachments.map(attachment => stagedAttachments.map(attachment =>
@ -912,8 +921,12 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
}) })
) )
); );
this.props.clearAttachments(); window.inboxStore?.dispatch(
return files; removeAllStagedAttachmentsInConversation({
conversationKey: this.props.selectedConversationKey,
})
);
return _.compact(files);
} }
private async sendVoiceMessage(audioBlob: Blob) { private async sendVoiceMessage(audioBlob: Blob) {
@ -921,27 +934,27 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
return; return;
} }
const fileBuffer = await new Response(audioBlob).arrayBuffer(); const file = new File([audioBlob], 'audio-blob');
const audioAttachment: Attachment = { const audioAttachment: StagedAttachmentType = {
data: fileBuffer, file,
flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
contentType: MIME.AUDIO_MP3, contentType: MIME.AUDIO_MP3,
size: audioBlob.size, size: audioBlob.size,
fileSize: null,
screenshot: null,
fileName: 'audio-message',
thumbnail: null,
url: '',
isVoiceMessage: true,
}; };
const messageSuccess = this.props.sendMessage( this.props.sendMessage({
'', body: '',
[audioAttachment], attachments: [audioAttachment],
undefined, preview: undefined,
undefined, quote: undefined,
null, groupInvitation: undefined,
{} });
);
if (messageSuccess) {
// success!
}
this.onExitVoiceNoteView(); this.onExitVoiceNoteView();
} }

@ -2,7 +2,11 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { SessionCompositionBox, StagedAttachmentType } from './SessionCompositionBox'; import {
SendMessageType,
SessionCompositionBox,
StagedAttachmentType,
} from './SessionCompositionBox';
import { Constants } from '../../../session'; import { Constants } from '../../../session';
import _ from 'lodash'; import _ from 'lodash';
@ -14,7 +18,7 @@ import styled, { DefaultTheme } from 'styled-components';
import { SessionMessagesListContainer } from './SessionMessagesListContainer'; import { SessionMessagesListContainer } from './SessionMessagesListContainer';
import { LightboxGallery, MediaItemType } from '../../LightboxGallery'; import { LightboxGallery, MediaItemType } from '../../LightboxGallery';
import { AttachmentType, AttachmentTypeWithPath } from '../../../types/Attachment'; import { AttachmentTypeWithPath } from '../../../types/Attachment';
import { ToastUtils, UserUtils } from '../../../session/utils'; import { ToastUtils, UserUtils } from '../../../session/utils';
import * as MIME from '../../../types/MIME'; import * as MIME from '../../../types/MIME';
import { SessionFileDropzone } from './SessionFileDropzone'; import { SessionFileDropzone } from './SessionFileDropzone';
@ -38,10 +42,10 @@ import {
import { SessionButtonColor } from '../SessionButton'; import { SessionButtonColor } from '../SessionButton';
import { updateConfirmModal } from '../../../state/ducks/modalDialog'; import { updateConfirmModal } from '../../../state/ducks/modalDialog';
import { addStagedAttachmentsInConversation } from '../../../state/ducks/stagedAttachments';
interface State { interface State {
showRecordingView: boolean; showRecordingView: boolean;
stagedAttachments: Array<StagedAttachmentType>;
isDraggingFile: boolean; isDraggingFile: boolean;
} }
export interface LightBoxOptions { export interface LightBoxOptions {
@ -61,6 +65,8 @@ interface Props {
// lightbox options // lightbox options
lightBoxOptions?: LightBoxOptions; lightBoxOptions?: LightBoxOptions;
stagedAttachments: Array<StagedAttachmentType>;
} }
const SessionUnreadAboveIndicator = styled.div` const SessionUnreadAboveIndicator = styled.div`
@ -98,7 +104,6 @@ export class SessionConversation extends React.Component<Props, State> {
this.state = { this.state = {
showRecordingView: false, showRecordingView: false,
stagedAttachments: [],
isDraggingFile: false, isDraggingFile: false,
}; };
this.messageContainerRef = React.createRef(); this.messageContainerRef = React.createRef();
@ -159,7 +164,6 @@ export class SessionConversation extends React.Component<Props, State> {
if (newConversationKey !== oldConversationKey) { if (newConversationKey !== oldConversationKey) {
this.setState({ this.setState({
showRecordingView: false, showRecordingView: false,
stagedAttachments: [],
isDraggingFile: false, isDraggingFile: false,
}); });
} }
@ -178,42 +182,16 @@ export class SessionConversation extends React.Component<Props, State> {
} }
} }
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ public sendMessageFn(msg: SendMessageType) {
// ~~~~~~~~~~~~~~ RENDER METHODS ~~~~~~~~~~~~~~ const { selectedConversationKey } = this.props;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public render() {
const { showRecordingView, isDraggingFile, stagedAttachments } = this.state;
const {
selectedConversation,
selectedConversationKey,
messagesProps,
showMessageDetails,
selectedMessages,
isRightPanelShowing,
lightBoxOptions,
} = this.props;
if (!selectedConversation || !messagesProps) {
// return an empty message view
return <MessageView />;
}
const selectionMode = selectedMessages.length > 0;
const conversationModel = getConversationController().get(selectedConversationKey); const conversationModel = getConversationController().get(selectedConversationKey);
const sendMessageFn = (
body: any,
attachments: any,
quote: any,
preview: any,
groupInvitation: any
) => {
if (!conversationModel) { if (!conversationModel) {
return; return;
} }
const sendAndScroll = () => { const sendAndScroll = () => {
void conversationModel.sendMessage(body, attachments, quote, preview, groupInvitation); void conversationModel.sendMessage(msg);
if (this.messageContainerRef.current) { if (this.messageContainerRef.current) {
(this.messageContainerRef (this.messageContainerRef
.current as any).scrollTop = this.messageContainerRef.current?.scrollHeight; .current as any).scrollTop = this.messageContainerRef.current?.scrollHeight;
@ -224,7 +202,7 @@ export class SessionConversation extends React.Component<Props, State> {
const recoveryPhrase = UserUtils.getCurrentRecoveryPhrase(); const recoveryPhrase = UserUtils.getCurrentRecoveryPhrase();
// string replace to fix case where pasted text contains invis characters causing false negatives // string replace to fix case where pasted text contains invis characters causing false negatives
if (body.replace(/\s/g, '').includes(recoveryPhrase.replace(/\s/g, ''))) { if (msg.body.replace(/\s/g, '').includes(recoveryPhrase.replace(/\s/g, ''))) {
window.inboxStore?.dispatch( window.inboxStore?.dispatch(
updateConfirmModal({ updateConfirmModal({
title: window.i18n('sendRecoveryPhraseTitle'), title: window.i18n('sendRecoveryPhraseTitle'),
@ -243,7 +221,29 @@ export class SessionConversation extends React.Component<Props, State> {
} }
window.inboxStore?.dispatch(quoteMessage(undefined)); window.inboxStore?.dispatch(quoteMessage(undefined));
}; }
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~ RENDER METHODS ~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public render() {
const { showRecordingView, isDraggingFile } = this.state;
const {
selectedConversation,
messagesProps,
showMessageDetails,
selectedMessages,
isRightPanelShowing,
lightBoxOptions,
} = this.props;
if (!selectedConversation || !messagesProps) {
// return an empty message view
return <MessageView />;
}
const selectionMode = selectedMessages.length > 0;
return ( return (
<SessionTheme theme={this.props.theme}> <SessionTheme theme={this.props.theme}>
@ -273,12 +273,10 @@ export class SessionConversation extends React.Component<Props, State> {
</div> </div>
<SessionCompositionBox <SessionCompositionBox
sendMessage={sendMessageFn} sendMessage={this.sendMessageFn}
stagedAttachments={stagedAttachments} stagedAttachments={this.props.stagedAttachments}
onLoadVoiceNoteView={this.onLoadVoiceNoteView} onLoadVoiceNoteView={this.onLoadVoiceNoteView}
onExitVoiceNoteView={this.onExitVoiceNoteView} onExitVoiceNoteView={this.onExitVoiceNoteView}
clearAttachments={this.clearAttachments}
removeAttachment={this.removeAttachment}
onChoseAttachments={this.onChoseAttachments} onChoseAttachments={this.onChoseAttachments}
/> />
</div> </div>
@ -332,45 +330,6 @@ export class SessionConversation extends React.Component<Props, State> {
} }
} }
private clearAttachments() {
this.state.stagedAttachments.forEach(attachment => {
if (attachment.url) {
URL.revokeObjectURL(attachment.url);
}
if (attachment.videoUrl) {
URL.revokeObjectURL(attachment.videoUrl);
}
});
this.setState({ stagedAttachments: [] });
}
private removeAttachment(attachment: AttachmentType) {
const { stagedAttachments } = this.state;
const updatedStagedAttachments = (stagedAttachments || []).filter(
m => m.fileName !== attachment.fileName
);
this.setState({ stagedAttachments: updatedStagedAttachments });
}
private addAttachments(newAttachments: Array<StagedAttachmentType>) {
const { stagedAttachments } = this.state;
let newAttachmentsFiltered: Array<StagedAttachmentType> = [];
if (newAttachments?.length > 0) {
if (newAttachments.some(a => a.isVoiceMessage) && stagedAttachments.length > 0) {
throw new Error('A voice note cannot be sent with other attachments');
}
// do not add already added attachments
newAttachmentsFiltered = newAttachments.filter(
a => !stagedAttachments.some(b => b.file.path === a.file.path)
);
}
this.setState({
stagedAttachments: [...stagedAttachments, ...newAttachmentsFiltered],
});
}
private renderLightBox({ media, attachment }: LightBoxOptions) { private renderLightBox({ media, attachment }: LightBoxOptions) {
const selectedIndex = const selectedIndex =
media.length > 1 media.length > 1
@ -399,7 +358,7 @@ export class SessionConversation extends React.Component<Props, State> {
const fileName = file.name; const fileName = file.name;
const contentType = file.type; const contentType = file.type;
const { stagedAttachments } = this.state; const { stagedAttachments } = this.props;
if (window.Signal.Util.isFileDangerous(fileName)) { if (window.Signal.Util.isFileDangerous(fileName)) {
ToastUtils.pushDangerousFileError(); ToastUtils.pushDangerousFileError();
@ -568,6 +527,15 @@ export class SessionConversation extends React.Component<Props, State> {
} }
} }
private addAttachments(newAttachments: Array<StagedAttachmentType>) {
window.inboxStore?.dispatch(
addStagedAttachmentsInConversation({
conversationKey: this.props.selectedConversationKey,
newAttachments,
})
);
}
private handleDrag(e: any) { private handleDrag(e: any) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

@ -10,7 +10,7 @@ import { StagedLinkPreview } from '../../conversation/StagedLinkPreview';
export interface StagedLinkPreviewProps extends StagedLinkPreviewData { export interface StagedLinkPreviewProps extends StagedLinkPreviewData {
onClose: (url: string) => void; onClose: (url: string) => void;
} }
export const LINK_PREVIEW_TIMEOUT = 60 * 1000; export const LINK_PREVIEW_TIMEOUT = 20 * 1000;
export interface GetLinkPreviewResultImage { export interface GetLinkPreviewResultImage {
data: ArrayBuffer; data: ArrayBuffer;

@ -195,6 +195,7 @@ export function init() {
// We can't send ArrayBuffers or BigNumbers (what we get from proto library for dates). // We can't send ArrayBuffers or BigNumbers (what we get from proto library for dates).
function _cleanData(data: any): any { function _cleanData(data: any): any {
const keys = Object.keys(data); const keys = Object.keys(data);
for (let index = 0, max = keys.length; index < max; index += 1) { for (let index = 0, max = keys.length; index < max; index += 1) {
const key = keys[index]; const key = keys[index];
const value = data[key]; const value = data[key];
@ -207,9 +208,17 @@ function _cleanData(data: any): any {
if (_.isFunction(value.toNumber)) { if (_.isFunction(value.toNumber)) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
data[key] = value.toNumber(); data[key] = value.toNumber();
} else if (_.isFunction(value)) {
// just skip a function which has not a toNumber function. We don't want to save a function to the db.
// an attachment comes with a toJson() function
// tslint:disable-next-line: no-dynamic-delete
delete data[key];
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
data[key] = value.map(_cleanData); data[key] = value.map(_cleanData);
} else if (_.isObject(value) && value instanceof File) {
// eslint-disable-next-line no-param-reassign
data[key] = { name: value.name, path: value.path, size: value.size, type: value.type };
} else if (_.isObject(value)) { } else if (_.isObject(value)) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
data[key] = _cleanData(value); data[key] = _cleanData(value);
@ -615,7 +624,8 @@ export async function updateLastHash(data: {
} }
export async function saveMessage(data: MessageAttributes): Promise<string> { export async function saveMessage(data: MessageAttributes): Promise<string> {
const id = await channels.saveMessage(_cleanData(data)); const cleanedData = _cleanData(data);
const id = await channels.saveMessage(cleanedData);
window.Whisper.ExpiringMessagesListener.update(); window.Whisper.ExpiringMessagesListener.update();
return id; return id;
} }

@ -43,7 +43,10 @@ import { OpenGroupRequestCommonType } from '../opengroup/opengroupV2/ApiUtil';
import { getOpenGroupV2FromConversationId } from '../opengroup/utils/OpenGroupUtils'; import { getOpenGroupV2FromConversationId } from '../opengroup/utils/OpenGroupUtils';
import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout'; import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout';
import { perfEnd, perfStart } from '../session/utils/Performance'; import { perfEnd, perfStart } from '../session/utils/Performance';
import { ReplyingToMessageProps } from '../components/session/conversation/SessionCompositionBox'; import {
ReplyingToMessageProps,
SendMessageType,
} from '../components/session/conversation/SessionCompositionBox';
import { ed25519Str } from '../session/onions/onionPath'; import { ed25519Str } from '../session/onions/onionPath';
export enum ConversationTypeEnum { export enum ConversationTypeEnum {
@ -702,13 +705,8 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return null; return null;
} }
} }
public async sendMessage( public async sendMessage(msg: SendMessageType) {
body: string, const { attachments, body, groupInvitation, preview, quote } = msg;
attachments: any,
quote: any,
preview: any,
groupInvitation: any = null
) {
this.clearTypingTimers(); this.clearTypingTimers();
const destination = this.id; const destination = this.id;
@ -745,40 +743,43 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
if (!this.isPublic()) { if (!this.isPublic()) {
messageWithSchema.destination = destination; messageWithSchema.destination = destination;
// set the serverTimestamp only if this conversation is a public one.
messageWithSchema.serverTimestamp = new Date().getTime();
} }
messageWithSchema.source = UserUtils.getOurPubKeyStrFromCache(); messageWithSchema.source = UserUtils.getOurPubKeyStrFromCache();
messageWithSchema.sourceDevice = 1; messageWithSchema.sourceDevice = 1;
// set the serverTimestamp only if this conversation is a public one.
const attributes: MessageAttributesOptionals = { const attributes: MessageAttributesOptionals = {
...messageWithSchema, ...messageWithSchema,
groupInvitation, groupInvitation,
conversationId: this.id, conversationId: this.id,
destination: isPrivate ? destination : undefined, destination: isPrivate ? destination : undefined,
serverTimestamp: this.isPublic() ? new Date().getTime() : undefined,
}; };
const messageModel = await this.addSingleMessage(attributes); const messageModel = await this.addSingleMessage(attributes);
this.set({
lastMessage: messageModel.getNotificationText(),
lastMessageStatus: 'sending',
active_at: now,
});
await this.commit();
// We're offline! // We're offline!
if (!window.textsecure.messaging) { if (!window.textsecure.messaging) {
const error = new Error('Network is not available'); const error = new Error('Network is not available');
error.name = 'SendMessageNetworkError'; error.name = 'SendMessageNetworkError';
(error as any).number = this.id; (error as any).number = this.id;
await messageModel.saveErrors([error]); await messageModel.saveErrors([error]);
return null; await this.commit();
return;
} }
this.set({
lastMessage: messageModel.getNotificationText(),
lastMessageStatus: 'sending',
active_at: now,
});
await this.commit();
this.queueJob(async () => { this.queueJob(async () => {
await this.sendMessageJob(messageModel, expireTimer); await this.sendMessageJob(messageModel, expireTimer);
}); });
return null;
} }
public async bouncyUpdateLastMessage() { public async bouncyUpdateLastMessage() {

@ -0,0 +1,103 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import _ from 'lodash';
import { StagedAttachmentType } from '../../components/session/conversation/SessionCompositionBox';
export type StagedAttachmentsStateType = {
stagedAttachments: { [conversationKey: string]: Array<StagedAttachmentType> };
};
// Reducer
export function getEmptyStagedAttachmentsState(): StagedAttachmentsStateType {
return {
stagedAttachments: {},
};
}
const stagedAttachmentsSlice = createSlice({
name: 'stagedAttachments',
initialState: getEmptyStagedAttachmentsState(),
reducers: {
addStagedAttachmentsInConversation(
state: StagedAttachmentsStateType,
action: PayloadAction<{
conversationKey: string;
newAttachments: Array<StagedAttachmentType>;
}>
) {
const { conversationKey, newAttachments } = action.payload;
if (newAttachments.length === 0) {
return state;
}
const currentStagedAttachments = state.stagedAttachments[conversationKey] || [];
if (newAttachments.some(a => a.isVoiceMessage) && currentStagedAttachments.length > 0) {
window?.log?.warn('A voice note cannot be sent with other attachments');
return state;
}
const allAttachments = _.concat(currentStagedAttachments, newAttachments);
const uniqAttachments = _.uniqBy(allAttachments, m => m.fileName);
state.stagedAttachments[conversationKey] = uniqAttachments;
return state;
},
removeAllStagedAttachmentsInConversation(
state: StagedAttachmentsStateType,
action: PayloadAction<{ conversationKey: string }>
) {
const { conversationKey } = action.payload;
const currentStagedAttachments = state.stagedAttachments[conversationKey];
if (!currentStagedAttachments || _.isEmpty(currentStagedAttachments)) {
return state;
}
currentStagedAttachments.forEach(attachment => {
if (attachment.url) {
URL.revokeObjectURL(attachment.url);
}
if (attachment.videoUrl) {
URL.revokeObjectURL(attachment.videoUrl);
}
});
// tslint:disable-next-line: no-dynamic-delete
delete state.stagedAttachments[conversationKey];
return state;
},
removeStagedAttachmentInConversation(
state: StagedAttachmentsStateType,
action: PayloadAction<{ conversationKey: string; filename: string }>
) {
const { conversationKey, filename } = action.payload;
const currentStagedAttachments = state.stagedAttachments[conversationKey];
if (!currentStagedAttachments || _.isEmpty(currentStagedAttachments)) {
return state;
}
const attachmentToRemove = currentStagedAttachments.find(m => m.fileName === filename);
if (!attachmentToRemove) {
return state;
}
if (attachmentToRemove.url) {
URL.revokeObjectURL(attachmentToRemove.url);
}
if (attachmentToRemove.videoUrl) {
URL.revokeObjectURL(attachmentToRemove.videoUrl);
}
state.stagedAttachments[conversationKey] = state.stagedAttachments[conversationKey].filter(
a => a.fileName !== filename
);
return state;
},
},
});
export const { actions, reducer } = stagedAttachmentsSlice;
export const {
addStagedAttachmentsInConversation,
removeAllStagedAttachmentsInConversation,
removeStagedAttachmentInConversation,
} = actions;

@ -11,6 +11,10 @@ import { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion';
import { modalReducer as modals, ModalState } from './ducks/modalDialog'; import { modalReducer as modals, ModalState } from './ducks/modalDialog';
import { userConfigReducer as userConfig, UserConfigState } from './ducks/userConfig'; import { userConfigReducer as userConfig, UserConfigState } from './ducks/userConfig';
import { timerOptionReducer as timerOptions, TimerOptionsState } from './ducks/timerOptions'; import { timerOptionReducer as timerOptions, TimerOptionsState } from './ducks/timerOptions';
import {
reducer as stagedAttachments,
StagedAttachmentsStateType,
} from './ducks/stagedAttachments';
export type StateType = { export type StateType = {
search: SearchStateType; search: SearchStateType;
@ -23,6 +27,7 @@ export type StateType = {
modals: ModalState; modals: ModalState;
userConfig: UserConfigState; userConfig: UserConfigState;
timerOptions: TimerOptionsState; timerOptions: TimerOptionsState;
stagedAttachments: StagedAttachmentsStateType;
}; };
export const reducers = { export const reducers = {
@ -36,6 +41,7 @@ export const reducers = {
modals, modals,
userConfig, userConfig,
timerOptions, timerOptions,
stagedAttachments,
}; };
// Making this work would require that our reducer signature supported AnyAction, not // Making this work would require that our reducer signature supported AnyAction, not

@ -0,0 +1,28 @@
import { createSelector } from 'reselect';
import { StagedAttachmentType } from '../../components/session/conversation/SessionCompositionBox';
import { StagedAttachmentsStateType } from '../ducks/stagedAttachments';
import { StateType } from '../reducer';
import { getSelectedConversationKey } from './conversations';
export const getStagedAttachmentsState = (state: StateType): StagedAttachmentsStateType =>
state.stagedAttachments;
const getStagedAttachmentsForConversation = (
state: StagedAttachmentsStateType,
conversationKey: string | undefined
) => {
if (!conversationKey) {
return undefined;
}
return state.stagedAttachments[conversationKey] || [];
};
export const getStagedAttachmentsForCurrentConversation = createSelector(
[getSelectedConversationKey, getStagedAttachmentsState],
(
selectedConversationKey: string | undefined,
state: StagedAttachmentsStateType
): Array<StagedAttachmentType> | undefined => {
return getStagedAttachmentsForConversation(state, selectedConversationKey);
}
);

@ -13,6 +13,7 @@ import {
isRightPanelShowing, isRightPanelShowing,
} from '../selectors/conversations'; } from '../selectors/conversations';
import { getOurNumber } from '../selectors/user'; import { getOurNumber } from '../selectors/user';
import { getStagedAttachmentsForCurrentConversation } from '../selectors/stagedAttachments';
const mapStateToProps = (state: StateType) => { const mapStateToProps = (state: StateType) => {
return { return {
@ -25,6 +26,7 @@ const mapStateToProps = (state: StateType) => {
isRightPanelShowing: isRightPanelShowing(state), isRightPanelShowing: isRightPanelShowing(state),
selectedMessages: getSelectedMessageIds(state), selectedMessages: getSelectedMessageIds(state),
lightBoxOptions: getLightBoxOptions(state), lightBoxOptions: getLightBoxOptions(state),
stagedAttachments: getStagedAttachmentsForCurrentConversation(state),
}; };
}; };

@ -90,7 +90,7 @@ export async function autoScale<T extends { contentType: string; file: any }>(
resolve({ resolve({
...attachment, ...attachment,
file: blob, file: new File([blob], 'blob-file'),
}); });
}; };
img.src = url; img.src = url;
@ -99,7 +99,7 @@ export async function autoScale<T extends { contentType: string; file: any }>(
export async function getFile(attachment: StagedAttachmentType, maxMeasurements?: MaxScaleSize) { export async function getFile(attachment: StagedAttachmentType, maxMeasurements?: MaxScaleSize) {
if (!attachment) { if (!attachment) {
return Promise.resolve(); return null;
} }
const attachmentFlags = attachment.isVoiceMessage const attachmentFlags = attachment.isVoiceMessage

2
ts/window.d.ts vendored

@ -70,7 +70,7 @@ declare global {
profileImages: any; profileImages: any;
MediaRecorder: any; MediaRecorder: any;
dataURLToBlobSync: any; dataURLToBlobSync: any;
autoOrientImage: any; autoOrientImage: (fileOrBlobOrURL: string | File | Blob, options: any = {}) => Promise<string>;
contextMenuShown: boolean; contextMenuShown: boolean;
inboxStore?: Store; inboxStore?: Store;
openConversationWithMessages: (args: { openConversationWithMessages: (args: {

Loading…
Cancel
Save