put saveAttachemntToDisk outside of component

pull/1783/head
Audric Ackermann 4 years ago
parent 3e4aceabcf
commit 9a380b716b
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -10,7 +10,9 @@ import { AttachmentTypeWithPath } from '../types/Attachment';
// tslint:disable-next-line: no-submodule-imports // tslint:disable-next-line: no-submodule-imports
import useKey from 'react-use/lib/useKey'; import useKey from 'react-use/lib/useKey';
import { showLightBox } from '../state/ducks/conversations'; import { showLightBox } from '../state/ducks/conversations';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { saveAttachmentToDisk } from '../util/attachmentsUtil';
import { getSelectedConversationKey } from '../state/selectors/conversations';
export interface MediaItemType { export interface MediaItemType {
objectURL?: string; objectURL?: string;
@ -25,13 +27,13 @@ export interface MediaItemType {
type Props = { type Props = {
media: Array<MediaItemType>; media: Array<MediaItemType>;
onSave?: (saveData: MediaItemType) => void;
selectedIndex: number; selectedIndex: number;
}; };
export const LightboxGallery = (props: Props) => { export const LightboxGallery = (props: Props) => {
const { media, onSave } = props; const { media } = props;
const [currentIndex, setCurrentIndex] = useState(-1); const [currentIndex, setCurrentIndex] = useState(-1);
const selectedConversation = useSelector(getSelectedConversationKey) as string;
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -56,12 +58,8 @@ export const LightboxGallery = (props: Props) => {
}, [currentIndex, lastIndex]); }, [currentIndex, lastIndex]);
const handleSave = useCallback(() => { const handleSave = useCallback(() => {
if (!onSave) {
return;
}
const mediaItem = media[currentIndex]; const mediaItem = media[currentIndex];
onSave(mediaItem); void saveAttachmentToDisk({ ...mediaItem, conversationId: selectedConversation });
}, [currentIndex, media]); }, [currentIndex, media]);
useKey( useKey(
@ -96,14 +94,13 @@ export const LightboxGallery = (props: Props) => {
const objectURL = selectedMedia?.objectURL || 'images/alert-outline.svg'; const objectURL = selectedMedia?.objectURL || 'images/alert-outline.svg';
const { attachment } = selectedMedia; const { attachment } = selectedMedia;
const saveCallback = onSave ? handleSave : undefined;
const caption = attachment?.caption; const caption = attachment?.caption;
return ( return (
// tslint:disable: use-simple-attributes // tslint:disable: use-simple-attributes
<Lightbox <Lightbox
onPrevious={hasPrevious ? onPrevious : undefined} onPrevious={hasPrevious ? onPrevious : undefined}
onNext={hasNext ? onNext : undefined} onNext={hasNext ? onNext : undefined}
onSave={saveCallback} onSave={handleSave}
objectURL={objectURL} objectURL={objectURL}
caption={caption} caption={caption}
contentType={selectedMedia.contentType} contentType={selectedMedia.contentType}

@ -11,79 +11,70 @@ import {
isVideoAttachment, isVideoAttachment,
} from '../../types/Attachment'; } from '../../types/Attachment';
interface Props { type Props = {
attachments: Array<AttachmentType>; attachments: Array<AttachmentType>;
// onError: () => void; // onError: () => void;
onClickAttachment: (attachment: AttachmentType) => void; onClickAttachment: (attachment: AttachmentType) => void;
onCloseAttachment: (attachment: AttachmentType) => void; onCloseAttachment: (attachment: AttachmentType) => void;
onAddAttachment: () => void; onAddAttachment: () => void;
onClose: () => void; onClose: () => void;
} };
const IMAGE_WIDTH = 120; const IMAGE_WIDTH = 120;
const IMAGE_HEIGHT = 120; const IMAGE_HEIGHT = 120;
export class AttachmentList extends React.Component<Props> { export const AttachmentList = (props: Props) => {
// tslint:disable-next-line max-func-body-length */ const { attachments, onAddAttachment, onClickAttachment, onCloseAttachment, onClose } = props;
public render() {
const {
attachments,
onAddAttachment,
onClickAttachment,
onCloseAttachment,
onClose,
} = this.props;
if (!attachments.length) { if (!attachments.length) {
return null; return null;
} }
const allVisualAttachments = areAllAttachmentsVisual(attachments);
return (
<div className="module-attachments">
{attachments.length > 1 ? (
<div className="module-attachments__header">
<div role="button" onClick={onClose} className="module-attachments__close-button" />
</div>
) : null}
<div className="module-attachments__rail">
{(attachments || []).map((attachment, index) => {
const { contentType } = attachment;
if (isImageTypeSupported(contentType) || isVideoTypeSupported(contentType)) {
const imageKey = getUrl(attachment) || attachment.fileName || index;
const clickCallback = attachments.length > 1 ? onClickAttachment : undefined;
return ( const allVisualAttachments = areAllAttachmentsVisual(attachments);
<Image
key={imageKey}
alt={window.i18n('stagedImageAttachment', [attachment.fileName])}
attachment={attachment}
softCorners={true}
playIconOverlay={isVideoAttachment(attachment)}
height={IMAGE_HEIGHT}
width={IMAGE_WIDTH}
url={getUrl(attachment)}
closeButton={true}
onClick={clickCallback}
onClickClose={onCloseAttachment}
/>
);
}
const genericKey = getUrl(attachment) || attachment.fileName || index; return (
<div className="module-attachments">
{attachments.length > 1 ? (
<div className="module-attachments__header">
<div role="button" onClick={onClose} className="module-attachments__close-button" />
</div>
) : null}
<div className="module-attachments__rail">
{(attachments || []).map((attachment, index) => {
const { contentType } = attachment;
if (isImageTypeSupported(contentType) || isVideoTypeSupported(contentType)) {
const imageKey = getUrl(attachment) || attachment.fileName || index;
const clickCallback = attachments.length > 1 ? onClickAttachment : undefined;
return ( return (
<StagedGenericAttachment <Image
key={genericKey} key={imageKey}
alt={window.i18n('stagedImageAttachment', [attachment.fileName])}
attachment={attachment} attachment={attachment}
onClose={onCloseAttachment} softCorners={true}
playIconOverlay={isVideoAttachment(attachment)}
height={IMAGE_HEIGHT}
width={IMAGE_WIDTH}
url={getUrl(attachment)}
closeButton={true}
onClick={clickCallback}
onClickClose={onCloseAttachment}
/> />
); );
})} }
{allVisualAttachments ? <StagedPlaceholderAttachment onClick={onAddAttachment} /> : null}
</div> const genericKey = getUrl(attachment) || attachment.fileName || index;
return (
<StagedGenericAttachment
key={genericKey}
attachment={attachment}
onClose={onCloseAttachment}
/>
);
})}
{allVisualAttachments ? <StagedPlaceholderAttachment onClick={onAddAttachment} /> : null}
</div> </div>
); </div>
} );
} };

@ -2,12 +2,12 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Spinner } from '../basic/Spinner'; import { Spinner } from '../basic/Spinner';
import { AttachmentType } from '../../types/Attachment'; import { AttachmentTypeWithPath } from '../../types/Attachment';
import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch'; import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
type Props = { type Props = {
alt: string; alt: string;
attachment: AttachmentType; attachment: AttachmentTypeWithPath;
url: string; url: string;
height?: number; height?: number;
@ -28,8 +28,8 @@ type Props = {
playIconOverlay?: boolean; playIconOverlay?: boolean;
softCorners?: boolean; softCorners?: boolean;
onClick?: (attachment: AttachmentType) => void; onClick?: (attachment: AttachmentTypeWithPath) => void;
onClickClose?: (attachment: AttachmentType) => void; onClickClose?: (attachment: AttachmentTypeWithPath) => void;
onError?: () => void; onError?: () => void;
}; };

@ -3,7 +3,7 @@ import classNames from 'classnames';
import { import {
areAllAttachmentsVisual, areAllAttachmentsVisual,
AttachmentType, AttachmentTypeWithPath,
getAlt, getAlt,
getImageDimensions, getImageDimensions,
getThumbnailUrl, getThumbnailUrl,
@ -14,13 +14,13 @@ import {
import { Image } from './Image'; import { Image } from './Image';
type Props = { type Props = {
attachments: Array<AttachmentType>; attachments: Array<AttachmentTypeWithPath>;
withContentAbove?: boolean; withContentAbove?: boolean;
withContentBelow?: boolean; withContentBelow?: boolean;
bottomOverlay?: boolean; bottomOverlay?: boolean;
onError: () => void; onError: () => void;
onClickAttachment?: (attachment: AttachmentType) => void; onClickAttachment?: (attachment: AttachmentTypeWithPath) => void;
}; };
export const ImageGrid = (props: Props) => { export const ImageGrid = (props: Props) => {

@ -10,6 +10,7 @@ import { ContactName } from './ContactName';
import { Quote } from './Quote'; import { Quote } from './Quote';
import { import {
AttachmentTypeWithPath,
canDisplayImage, canDisplayImage,
getExtensionForDisplay, getExtensionForDisplay,
getGridDimensions, getGridDimensions,
@ -43,11 +44,18 @@ import { AudioPlayerWithEncryptedFile } from './H5AudioPlayer';
import { ClickToTrustSender } from './message/ClickToTrustSender'; import { ClickToTrustSender } from './message/ClickToTrustSender';
import { getMessageById } from '../../data/data'; import { getMessageById } from '../../data/data';
import { deleteMessagesById } from '../../interactions/conversationInteractions'; import { deleteMessagesById } from '../../interactions/conversationInteractions';
import { getSelectedMessage } from '../../state/selectors/search';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { StateType } from '../../state/reducer'; import { StateType } from '../../state/reducer';
import { getSelectedMessageIds } from '../../state/selectors/conversations'; import { getSelectedMessageIds } from '../../state/selectors/conversations';
import { showMessageDetailsView, toggleSelectedMessageId } from '../../state/ducks/conversations'; import {
PropsForAttachment,
PropsForMessage,
showLightBox,
showMessageDetailsView,
toggleSelectedMessageId,
} from '../../state/ducks/conversations';
import { saveAttachmentToDisk } from '../../util/attachmentsUtil';
import { LightBoxOptions } from '../session/conversation/SessionConversation';
// Same as MIN_WIDTH in ImageGrid.tsx // Same as MIN_WIDTH in ImageGrid.tsx
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200; const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
@ -63,6 +71,41 @@ const EXPIRED_DELAY = 600;
type Props = MessageRegularProps & { selectedMessages: Array<string> }; type Props = MessageRegularProps & { selectedMessages: Array<string> };
const onClickAttachment = async (onClickProps: {
attachment: AttachmentTypeWithPath;
messageId: string;
}) => {
let index = -1;
const found = await getMessageById(onClickProps.messageId);
if (!found) {
window.log.warn('Such message not found');
return;
}
const msgAttachments = found.getPropsForMessage().attachments;
const media = (msgAttachments || []).map(attachmentForMedia => {
index++;
const messageTimestamp =
found.get('timestamp') || found.get('serverTimestamp') || found.get('received_at');
return {
index: _.clone(index),
objectURL: attachmentForMedia.url || undefined,
contentType: attachmentForMedia.contentType,
attachment: attachmentForMedia,
messageSender: found.getSource(),
messageTimestamp,
messageId: onClickProps.messageId,
};
});
const lightBoxOptions: LightBoxOptions = {
media: media as any,
attachment: onClickProps.attachment,
};
window.inboxStore?.dispatch(showLightBox(lightBoxOptions));
};
class MessageInner extends React.PureComponent<Props, State> { class MessageInner extends React.PureComponent<Props, State> {
public expirationCheckInterval: any; public expirationCheckInterval: any;
public expiredTimeout: any; public expiredTimeout: any;
@ -150,7 +193,6 @@ class MessageInner extends React.PureComponent<Props, State> {
conversationType, conversationType,
direction, direction,
quote, quote,
onClickAttachment,
multiSelectMode, multiSelectMode,
isTrustedForAttachmentDownload, isTrustedForAttachmentDownload,
} = this.props; } = this.props;
@ -191,11 +233,14 @@ class MessageInner extends React.PureComponent<Props, State> {
withContentBelow={withContentBelow} withContentBelow={withContentBelow}
bottomOverlay={!collapseMetadata} bottomOverlay={!collapseMetadata}
onError={this.handleImageError} onError={this.handleImageError}
onClickAttachment={(attachment: AttachmentType) => { onClickAttachment={(attachment: AttachmentTypeWithPath) => {
if (multiSelectMode) { if (multiSelectMode) {
window.inboxStore?.dispatch(toggleSelectedMessageId(id)); window.inboxStore?.dispatch(toggleSelectedMessageId(id));
} else if (onClickAttachment) { } else {
onClickAttachment(attachment); void onClickAttachment({
attachment,
messageId: id,
});
} }
}} }}
/> />
@ -241,10 +286,15 @@ class MessageInner extends React.PureComponent<Props, State> {
role="button" role="button"
className="module-message__generic-attachment__icon" className="module-message__generic-attachment__icon"
onClick={(e: any) => { onClick={(e: any) => {
if (this.props?.onDownload) { e.stopPropagation();
e.stopPropagation();
this.props.onDownload(firstAttachment); const messageTimestamp = this.props.timestamp || this.props.serverTimestamp || 0;
} void saveAttachmentToDisk({
attachment: firstAttachment,
messageTimestamp,
messageSender: this.props.authorPhoneNumber,
conversationId: this.props.convoId,
});
}} }}
> >
{extension ? ( {extension ? (
@ -520,7 +570,6 @@ class MessageInner extends React.PureComponent<Props, State> {
status, status,
isDeletable, isDeletable,
id, id,
onDownload,
isPublic, isPublic,
isOpenGroupV2, isOpenGroupV2,
weAreAdmin, weAreAdmin,
@ -567,9 +616,14 @@ class MessageInner extends React.PureComponent<Props, State> {
{!multipleAttachments && attachments && attachments[0] ? ( {!multipleAttachments && attachments && attachments[0] ? (
<Item <Item
onClick={(e: any) => { onClick={(e: any) => {
if (onDownload) { e.stopPropagation();
onDownload(attachments[0]); const messageTimestamp = this.props.timestamp || this.props.serverTimestamp || 0;
} void saveAttachmentToDisk({
attachment: attachments[0],
messageTimestamp,
messageSender: this.props.authorPhoneNumber,
conversationId: this.props.convoId,
});
}} }}
> >
{window.i18n('downloadAttachment')} {window.i18n('downloadAttachment')}

@ -10,6 +10,7 @@ import { AttachmentTypeWithPath, save } from '../../../types/Attachment';
import { MediaItemType } from '../../LightboxGallery'; import { MediaItemType } from '../../LightboxGallery';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { getSelectedConversationKey } from '../../../state/selectors/conversations'; import { getSelectedConversationKey } from '../../../state/selectors/conversations';
import { saveAttachmentToDisk } from '../../../util/attachmentsUtil';
type Props = { type Props = {
// Required // Required
@ -22,27 +23,6 @@ type Props = {
mediaItem: MediaItemType; mediaItem: MediaItemType;
}; };
const saveAttachment = async ({
attachment,
messageTimestamp,
messageSender,
conversationId,
}: {
attachment: AttachmentTypeWithPath;
messageTimestamp: number;
messageSender: string;
conversationId: string;
}) => {
attachment.url = await getDecryptedMediaUrl(attachment.url, attachment.contentType);
save({
attachment,
document,
getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath,
timestamp: messageTimestamp,
});
await sendDataExtractionNotification(conversationId, messageSender, messageTimestamp);
};
export const DocumentListItem = (props: Props) => { export const DocumentListItem = (props: Props) => {
const { shouldShowSeparator, fileName, fileSize, timestamp } = props; const { shouldShowSeparator, fileName, fileSize, timestamp } = props;
@ -50,7 +30,7 @@ export const DocumentListItem = (props: Props) => {
const selectedConversationKey = useSelector(getSelectedConversationKey) as string; const selectedConversationKey = useSelector(getSelectedConversationKey) as string;
const saveAttachmentCallback = useCallback(() => { const saveAttachmentCallback = useCallback(() => {
void saveAttachment({ void saveAttachmentToDisk({
messageSender: props.mediaItem.messageSender, messageSender: props.mediaItem.messageSender,
messageTimestamp: props.mediaItem.messageTimestamp, messageTimestamp: props.mediaItem.messageTimestamp,
attachment: props.mediaItem.attachment, attachment: props.mediaItem.attachment,

@ -84,7 +84,6 @@ interface Props {
quotedMessageProps?: ReplyingToMessageProps; quotedMessageProps?: ReplyingToMessageProps;
removeQuotedMessage: () => void; removeQuotedMessage: () => void;
textarea: React.RefObject<HTMLDivElement>;
stagedAttachments: Array<StagedAttachmentType>; stagedAttachments: Array<StagedAttachmentType>;
clearAttachments: () => any; clearAttachments: () => any;
removeAttachment: (toRemove: AttachmentType) => void; removeAttachment: (toRemove: AttachmentType) => void;
@ -149,7 +148,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
super(props); super(props);
this.state = getDefaultState(); this.state = getDefaultState();
this.textarea = props.textarea; this.textarea = React.createRef();
this.fileInput = React.createRef(); this.fileInput = React.createRef();
// Emojis // Emojis

@ -70,7 +70,6 @@ interface Props {
} }
export class SessionConversation extends React.Component<Props, State> { export class SessionConversation extends React.Component<Props, State> {
private readonly compositionBoxRef: React.RefObject<HTMLDivElement>;
private readonly messageContainerRef: React.RefObject<HTMLDivElement>; private readonly messageContainerRef: React.RefObject<HTMLDivElement>;
private dragCounter: number; private dragCounter: number;
private publicMembersRefreshTimeout?: NodeJS.Timeout; private publicMembersRefreshTimeout?: NodeJS.Timeout;
@ -87,7 +86,6 @@ export class SessionConversation extends React.Component<Props, State> {
stagedAttachments: [], stagedAttachments: [],
isDraggingFile: false, isDraggingFile: false,
}; };
this.compositionBoxRef = React.createRef();
this.messageContainerRef = React.createRef(); this.messageContainerRef = React.createRef();
this.dragCounter = 0; this.dragCounter = 0;
this.updateMemberList = _.debounce(this.updateMemberListBouncy.bind(this), 1000); this.updateMemberList = _.debounce(this.updateMemberListBouncy.bind(this), 1000);
@ -255,7 +253,6 @@ export class SessionConversation extends React.Component<Props, State> {
removeQuotedMessage={() => { removeQuotedMessage={() => {
void this.replyToMessage(undefined); void this.replyToMessage(undefined);
}} }}
textarea={this.compositionBoxRef}
clearAttachments={this.clearAttachments} clearAttachments={this.clearAttachments}
removeAttachment={this.removeAttachment} removeAttachment={this.removeAttachment}
onChoseAttachments={this.onChoseAttachments} onChoseAttachments={this.onChoseAttachments}
@ -296,14 +293,9 @@ export class SessionConversation extends React.Component<Props, State> {
} }
public getMessagesListProps(): SessionMessageListProps { public getMessagesListProps(): SessionMessageListProps {
const { messagesProps } = this.props;
return { return {
messagesProps,
messageContainerRef: this.messageContainerRef, messageContainerRef: this.messageContainerRef,
replyToMessage: this.replyToMessage, replyToMessage: this.replyToMessage,
onClickAttachment: this.onClickAttachment,
onDownloadAttachment: this.saveAttachment,
}; };
} }
@ -350,36 +342,10 @@ export class SessionConversation extends React.Component<Props, State> {
} }
} }
} }
this.setState({ quotedMessageTimestamp, quotedMessageProps }, () => { this.setState({ quotedMessageTimestamp, quotedMessageProps });
this.compositionBoxRef.current?.focus();
});
} }
} }
private onClickAttachment(attachment: AttachmentTypeWithPath, propsForMessage: PropsForMessage) {
let index = -1;
const media = (propsForMessage.attachments || []).map(attachmentForMedia => {
index++;
const messageTimestamp =
propsForMessage.timestamp || propsForMessage.serverTimestamp || propsForMessage.receivedAt;
return {
index: _.clone(index),
objectURL: attachmentForMedia.url || undefined,
contentType: attachmentForMedia.contentType,
attachment: attachmentForMedia,
messageSender: propsForMessage.authorPhoneNumber,
messageTimestamp,
messageId: propsForMessage.id,
};
});
const lightBoxOptions: LightBoxOptions = {
media: media as any,
attachment,
};
window.inboxStore?.dispatch(showLightBox(lightBoxOptions));
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~ KEYBOARD NAVIGATION ~~~~~~~~~~~~ // ~~~~~~~~~~~ KEYBOARD NAVIGATION ~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -470,35 +436,7 @@ export class SessionConversation extends React.Component<Props, State> {
? media.findIndex(mediaMessage => mediaMessage.attachment.path === attachment.path) ? media.findIndex(mediaMessage => mediaMessage.attachment.path === attachment.path)
: 0; : 0;
console.warn('renderLightBox', { media, attachment }); console.warn('renderLightBox', { media, attachment });
return ( return <LightboxGallery media={media} selectedIndex={selectedIndex} />;
<LightboxGallery media={media} selectedIndex={selectedIndex} onSave={this.saveAttachment} />
);
}
// THIS DOES NOT DOWNLOAD ANYTHING! it just saves it where the user wants
private async saveAttachment({
attachment,
messageTimestamp,
messageSender,
}: {
attachment: AttachmentType;
messageTimestamp: number;
messageSender: string;
}) {
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
attachment.url = await getDecryptedMediaUrl(attachment.url, attachment.contentType);
save({
attachment,
document,
getAbsolutePath: getAbsoluteAttachmentPath,
timestamp: messageTimestamp,
});
await sendDataExtractionNotification(
this.props.selectedConversationKey,
messageSender,
messageTimestamp
);
} }
private async onChoseAttachments(attachmentsFileList: Array<File>) { private async onChoseAttachments(attachmentsFileList: Array<File>) {

@ -7,11 +7,15 @@ import { SessionScrollButton } from '../SessionScrollButton';
import { Constants } from '../../../session'; import { Constants } from '../../../session';
import _ from 'lodash'; import _ from 'lodash';
import { contextMenu } from 'react-contexify'; import { contextMenu } from 'react-contexify';
import { AttachmentType } from '../../../types/Attachment'; import { AttachmentType, AttachmentTypeWithPath } from '../../../types/Attachment';
import { GroupNotification } from '../../conversation/GroupNotification'; import { GroupNotification } from '../../conversation/GroupNotification';
import { GroupInvitation } from '../../conversation/GroupInvitation'; import { GroupInvitation } from '../../conversation/GroupInvitation';
import { import {
fetchMessagesForConversation, fetchMessagesForConversation,
PropsForExpirationTimer,
PropsForGroupInvitation,
PropsForGroupUpdate,
PropsForMessage,
ReduxConversationType, ReduxConversationType,
SortedMessageModelProps, SortedMessageModelProps,
} from '../../../state/ducks/conversations'; } from '../../../state/ducks/conversations';
@ -20,18 +24,25 @@ import { ToastUtils } from '../../../session/utils';
import { TypingBubble } from '../../conversation/TypingBubble'; import { TypingBubble } from '../../conversation/TypingBubble';
import { getConversationController } from '../../../session/conversations'; import { getConversationController } from '../../../session/conversations';
import { MessageModel } from '../../../models/message'; import { MessageModel } from '../../../models/message';
import { MessageRegularProps, QuoteClickOptions } from '../../../models/messageType'; import {
MessageRegularProps,
PropsForDataExtractionNotification,
QuoteClickOptions,
} from '../../../models/messageType';
import { getMessagesBySentAt } from '../../../data/data'; import { getMessagesBySentAt } from '../../../data/data';
import autoBind from 'auto-bind'; import autoBind from 'auto-bind';
import { ConversationTypeEnum } from '../../../models/conversation'; import { ConversationTypeEnum } from '../../../models/conversation';
import { DataExtractionNotification } from '../../conversation/DataExtractionNotification'; import { DataExtractionNotification } from '../../conversation/DataExtractionNotification';
import { StateType } from '../../../state/reducer'; import { StateType } from '../../../state/reducer';
import { connect } from 'react-redux'; import { connect, useSelector } from 'react-redux';
import { import {
getMessagesOfSelectedConversation,
getSelectedConversation, getSelectedConversation,
getSelectedConversationKey, getSelectedConversationKey,
getSelectedMessageIds, getSelectedMessageIds,
isMessageSelectionMode,
} from '../../../state/selectors/conversations'; } from '../../../state/selectors/conversations';
import { saveAttachmentToDisk } from '../../../util/attachmentsUtil';
interface State { interface State {
showScrollButton: boolean; showScrollButton: boolean;
@ -39,24 +50,125 @@ interface State {
nextMessageToPlay: number | undefined; nextMessageToPlay: number | undefined;
} }
export type SessionMessageListProps = {
messageContainerRef: React.RefObject<any>;
replyToMessage: (messageId: number) => Promise<void>;
};
type Props = SessionMessageListProps & { type Props = SessionMessageListProps & {
conversationKey?: string; conversationKey?: string;
selectedMessages: Array<string>; messagesProps: Array<SortedMessageModelProps>;
conversation?: ReduxConversationType; conversation?: ReduxConversationType;
}; };
export type SessionMessageListProps = { const UnreadIndicator = (props: { messageId: string; show: boolean }) => (
messagesProps: Array<SortedMessageModelProps>; <SessionLastSeenIndicator show={props.show} key={`unread-indicator-${props.messageId}`} />
messageContainerRef: React.RefObject<any>; );
const GroupUpdateItem = (props: {
messageId: string;
groupNotificationProps: PropsForGroupUpdate;
showUnreadIndicator: boolean;
}) => {
return (
<React.Fragment key={props.messageId}>
<GroupNotification key={props.messageId} {...props.groupNotificationProps} />
<UnreadIndicator messageId={props.messageId} show={props.showUnreadIndicator} />
</React.Fragment>
);
};
replyToMessage: (messageId: number) => Promise<void>; const GroupInvitationItem = (props: {
onClickAttachment: (attachment: any, message: any) => void; messageId: string;
onDownloadAttachment: (toDownload: { propsForGroupInvitation: PropsForGroupInvitation;
attachment: any; showUnreadIndicator: boolean;
messageTimestamp: number; }) => {
messageSender: string; return (
}) => void; <React.Fragment key={props.messageId}>
<GroupInvitation key={props.messageId} {...props.propsForGroupInvitation} />
<UnreadIndicator messageId={props.messageId} show={props.showUnreadIndicator} />
</React.Fragment>
);
};
const DataExtractionNotificationItem = (props: {
messageId: string;
propsForDataExtractionNotification: PropsForDataExtractionNotification;
showUnreadIndicator: boolean;
}) => {
return (
<React.Fragment key={props.messageId}>
<DataExtractionNotification
key={props.messageId}
{...props.propsForDataExtractionNotification}
/>
<UnreadIndicator messageId={props.messageId} show={props.showUnreadIndicator} />
</React.Fragment>
);
};
const TimerNotificationItem = (props: {
messageId: string;
timerProps: PropsForExpirationTimer;
showUnreadIndicator: boolean;
}) => {
return (
<React.Fragment key={props.messageId}>
<TimerNotification key={props.messageId} {...props.timerProps} />
<UnreadIndicator messageId={props.messageId} show={props.showUnreadIndicator} />
</React.Fragment>
);
};
const GenericMessageItem = (props: {
messageId: string;
messageProps: SortedMessageModelProps;
playableMessageIndex?: number;
showUnreadIndicator: boolean;
}) => {
const multiSelectMode = useSelector(isMessageSelectionMode);
// const selectedConversation = useSelector(getSelectedConversationKey) as string;
const messageId = props.messageId;
console.warn('FIXME audric');
if (!props.messageProps) {
debugger;
}
// const onQuoteClick = props.messageProps.propsForMessage.quote
// ? this.scrollToQuoteMessage
// : async () => {};
const regularProps: MessageRegularProps = {
...props.messageProps.propsForMessage,
// firstMessageOfSeries,
multiSelectMode,
// isQuotedMessageToAnimate: messageId === this.state.animateQuotedMessageId,
// nextMessageToPlay: this.state.nextMessageToPlay,
onReply: props.replyToMessage,
// playNextMessage: this.playNextMessage,
// onQuoteClick,
};
return (
<React.Fragment key={props.messageId}>
<Message
{...regularProps}
playableMessageIndex={props.playableMessageIndex}
multiSelectMode={multiSelectMode}
// onQuoteClick={onQuoteClick}
key={messageId}
/>
<UnreadIndicator messageId={props.messageId} show={props.showUnreadIndicator} />
</React.Fragment>
);
}; };
class SessionMessagesListInner extends React.Component<Props, State> { class SessionMessagesListInner extends React.Component<Props, State> {
@ -171,8 +283,7 @@ class SessionMessagesListInner extends React.Component<Props, State> {
} }
private renderMessages() { private renderMessages() {
const { selectedMessages, messagesProps } = this.props; const { messagesProps } = this.props;
const multiSelectMode = Boolean(selectedMessages.length);
let playableMessageIndex = 0; let playableMessageIndex = 0;
return ( return (
@ -185,62 +296,56 @@ class SessionMessagesListInner extends React.Component<Props, State> {
const groupNotificationProps = messageProps.propsForGroupNotification; const groupNotificationProps = messageProps.propsForGroupNotification;
// IF we found the last read message // IF we found the first unread message
// AND we are not scrolled all the way to the bottom // AND we are not scrolled all the way to the bottom
// THEN, show the unread banner for the current message // THEN, show the unread banner for the current message
const showUnreadIndicator = const showUnreadIndicator =
Boolean(messageProps.firstUnread) && this.getScrollOffsetBottomPx() !== 0; Boolean(messageProps.firstUnread) && this.getScrollOffsetBottomPx() !== 0;
const unreadIndicator = (
<SessionLastSeenIndicator
show={showUnreadIndicator}
key={`unread-indicator-${messageProps.propsForMessage.id}`}
/>
);
if (groupNotificationProps) { if (groupNotificationProps) {
return ( return (
<React.Fragment> <GroupUpdateItem
<GroupNotification key={messageProps.propsForMessage.id}
key={messageProps.propsForMessage.id} groupNotificationProps={groupNotificationProps}
{...groupNotificationProps} messageId={messageProps.propsForMessage.id}
/> showUnreadIndicator={showUnreadIndicator}
{unreadIndicator} />
</React.Fragment>
); );
} }
if (propsForGroupInvitation) { if (propsForGroupInvitation) {
return ( return (
<React.Fragment key={messageProps.propsForMessage.id}> <GroupInvitationItem
<GroupInvitation key={messageProps.propsForMessage.id}
{...propsForGroupInvitation} propsForGroupInvitation={propsForGroupInvitation}
key={messageProps.propsForMessage.id} messageId={messageProps.propsForMessage.id}
/> showUnreadIndicator={showUnreadIndicator}
{unreadIndicator} />
</React.Fragment>
); );
} }
if (propsForDataExtractionNotification) { if (propsForDataExtractionNotification) {
return ( return (
<React.Fragment key={messageProps.propsForMessage.id}> <DataExtractionNotificationItem
<DataExtractionNotification key={messageProps.propsForMessage.id}
{...propsForDataExtractionNotification} propsForDataExtractionNotification={propsForDataExtractionNotification}
key={messageProps.propsForMessage.id} messageId={messageProps.propsForMessage.id}
/> showUnreadIndicator={showUnreadIndicator}
{unreadIndicator} />
</React.Fragment>
); );
} }
if (timerProps) { if (timerProps) {
return ( return (
<React.Fragment key={messageProps.propsForMessage.id}> <TimerNotificationItem
<TimerNotification {...timerProps} key={messageProps.propsForMessage.id} /> key={messageProps.propsForMessage.id}
{unreadIndicator} timerProps={timerProps}
</React.Fragment> messageId={messageProps.propsForMessage.id}
showUnreadIndicator={showUnreadIndicator}
/>
); );
} }
if (!messageProps) { if (!messageProps) {
return; return;
} }
@ -250,68 +355,19 @@ class SessionMessagesListInner extends React.Component<Props, State> {
// firstMessageOfSeries tells us to render the avatar only for the first message // firstMessageOfSeries tells us to render the avatar only for the first message
// in a series of messages from the same user // in a series of messages from the same user
return ( return (
<React.Fragment key={messageProps.propsForMessage.id}> <GenericMessageItem
{this.renderMessage( key={messageProps.propsForMessage.id}
messageProps, playableMessageIndex={playableMessageIndex}
messageProps.firstMessageOfSeries, messageId={messageProps.propsForMessage.id}
multiSelectMode, messageProps={messageProps}
playableMessageIndex showUnreadIndicator={showUnreadIndicator}
)} />
{unreadIndicator}
</React.Fragment>
); );
})} })}
</> </>
); );
} }
private renderMessage(
messageProps: SortedMessageModelProps,
firstMessageOfSeries: boolean,
multiSelectMode: boolean,
playableMessageIndex: number
) {
const messageId = messageProps.propsForMessage.id;
const onClickAttachment = (attachment: AttachmentType) => {
this.props.onClickAttachment(attachment, messageProps.propsForMessage);
};
// tslint:disable-next-line: no-async-without-await
const onQuoteClick = messageProps.propsForMessage.quote
? this.scrollToQuoteMessage
: async () => {};
const onDownload = (attachment: AttachmentType) => {
const messageTimestamp =
messageProps.propsForMessage.timestamp ||
messageProps.propsForMessage.serverTimestamp ||
messageProps.propsForMessage.receivedAt ||
0;
this.props.onDownloadAttachment({
attachment,
messageTimestamp,
messageSender: messageProps.propsForMessage.authorPhoneNumber,
});
};
const regularProps: MessageRegularProps = {
...messageProps.propsForMessage,
firstMessageOfSeries,
multiSelectMode,
isQuotedMessageToAnimate: messageId === this.state.animateQuotedMessageId,
nextMessageToPlay: this.state.nextMessageToPlay,
playableMessageIndex,
onReply: this.props.replyToMessage,
onClickAttachment,
onDownload,
playNextMessage: this.playNextMessage,
onQuoteClick,
};
return <Message {...regularProps} onQuoteClick={onQuoteClick} key={messageId} />;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~ MESSAGE HANDLING ~~~~~~~~~~~~~ // ~~~~~~~~~~~~~ MESSAGE HANDLING ~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -586,9 +642,9 @@ class SessionMessagesListInner extends React.Component<Props, State> {
const mapStateToProps = (state: StateType) => { const mapStateToProps = (state: StateType) => {
return { return {
selectedMessages: getSelectedMessageIds(state),
conversationKey: getSelectedConversationKey(state), conversationKey: getSelectedConversationKey(state),
conversation: getSelectedConversation(state), conversation: getSelectedConversation(state),
messagesProps: getMessagesOfSelectedConversation(state),
}; };
}; };

@ -774,9 +774,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
conversationType: ConversationTypeEnum.PRIVATE, conversationType: ConversationTypeEnum.PRIVATE,
multiSelectMode: false, multiSelectMode: false,
firstMessageOfSeries: false, firstMessageOfSeries: false,
onClickAttachment: noop,
onReply: noop, onReply: noop,
onDownload: noop,
// tslint:disable-next-line: no-async-without-await no-empty // tslint:disable-next-line: no-async-without-await no-empty
onQuoteClick: async () => {}, onQuoteClick: async () => {},
}, },

@ -2,7 +2,7 @@ import { DefaultTheme } from 'styled-components';
import _ from 'underscore'; import _ from 'underscore';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { QuotedAttachmentType } from '../components/conversation/Quote'; import { QuotedAttachmentType } from '../components/conversation/Quote';
import { AttachmentType } from '../types/Attachment'; import { AttachmentType, AttachmentTypeWithPath } from '../types/Attachment';
import { Contact } from '../types/Contact'; import { Contact } from '../types/Contact';
import { ConversationTypeEnum } from './conversation'; import { ConversationTypeEnum } from './conversation';
@ -219,7 +219,7 @@ export interface MessageRegularProps {
/** Note: this should be formatted for display */ /** Note: this should be formatted for display */
authorPhoneNumber: string; authorPhoneNumber: string;
conversationType: ConversationTypeEnum; conversationType: ConversationTypeEnum;
attachments?: Array<AttachmentType>; attachments?: Array<AttachmentTypeWithPath>;
quote?: { quote?: {
text: string; text: string;
attachment?: QuotedAttachmentType; attachment?: QuotedAttachmentType;
@ -246,9 +246,7 @@ export interface MessageRegularProps {
isQuotedMessageToAnimate?: boolean; isQuotedMessageToAnimate?: boolean;
isTrustedForAttachmentDownload: boolean; isTrustedForAttachmentDownload: boolean;
onClickAttachment: (attachment: AttachmentType) => void;
onReply: (messagId: number) => void; onReply: (messagId: number) => void;
onDownload: (attachment: AttachmentType) => void;
onQuoteClick: (options: QuoteClickOptions) => Promise<void>; onQuoteClick: (options: QuoteClickOptions) => Promise<void>;
playableMessageIndex?: number; playableMessageIndex?: number;

@ -2,6 +2,9 @@ import { StagedAttachmentType } from '../components/session/conversation/Session
import { SignalService } from '../protobuf'; import { SignalService } from '../protobuf';
import { Constants } from '../session'; import { Constants } from '../session';
import loadImage from 'blueimp-load-image'; import loadImage from 'blueimp-load-image';
import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager';
import { sendDataExtractionNotification } from '../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage';
import { AttachmentType, AttachmentTypeWithPath, save } from '../types/Attachment';
export interface MaxScaleSize { export interface MaxScaleSize {
maxSize?: number; maxSize?: number;
maxHeight?: number; maxHeight?: number;
@ -135,3 +138,24 @@ export async function readFile(attachment: any): Promise<AttachmentFileType> {
FR.readAsArrayBuffer(attachment.file); FR.readAsArrayBuffer(attachment.file);
}); });
} }
export const saveAttachmentToDisk = async ({
attachment,
messageTimestamp,
messageSender,
conversationId,
}: {
attachment: AttachmentType;
messageTimestamp: number;
messageSender: string;
conversationId: string;
}) => {
const decryptedUrl = await getDecryptedMediaUrl(attachment.url, attachment.contentType);
save({
attachment: { ...attachment, url: decryptedUrl },
document,
getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath,
timestamp: messageTimestamp,
});
await sendDataExtractionNotification(conversationId, messageSender, messageTimestamp);
};

Loading…
Cancel
Save