Moves showLightbox to redux

This commit is contained in:
Josh Perez 2022-12-09 21:02:22 -05:00 committed by GitHub
parent 3a246656e3
commit 635a59a473
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 584 additions and 504 deletions

View file

@ -158,6 +158,7 @@ import type AccountManager from './textsecure/AccountManager';
import { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate'; import { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate';
import { StoryViewModeType, StoryViewTargetType } from './types/Stories'; import { StoryViewModeType, StoryViewTargetType } from './types/Stories';
import { downloadOnboardingStory } from './util/downloadOnboardingStory'; import { downloadOnboardingStory } from './util/downloadOnboardingStory';
import { saveAttachmentFromMessage } from './util/saveAttachment';
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
@ -1114,6 +1115,7 @@ export async function startApp(): Promise<void> {
store.dispatch store.dispatch
), ),
items: bindActionCreators(actionCreators.items, store.dispatch), items: bindActionCreators(actionCreators.items, store.dispatch),
lightbox: bindActionCreators(actionCreators.lightbox, store.dispatch),
linkPreviews: bindActionCreators( linkPreviews: bindActionCreators(
actionCreators.linkPreviews, actionCreators.linkPreviews,
store.dispatch store.dispatch
@ -1656,10 +1658,10 @@ export async function startApp(): Promise<void> {
const { selectedMessage } = state.conversations; const { selectedMessage } = state.conversations;
if (selectedMessage) { if (selectedMessage) {
conversation.trigger('save-attachment', selectedMessage);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
saveAttachmentFromMessage(selectedMessage);
return; return;
} }
} }

View file

@ -31,6 +31,7 @@ type PropsType = {
renderStories: (closeView: () => unknown) => JSX.Element; renderStories: (closeView: () => unknown) => JSX.Element;
hasSelectedStoryData: boolean; hasSelectedStoryData: boolean;
renderStoryViewer: (closeView: () => unknown) => JSX.Element; renderStoryViewer: (closeView: () => unknown) => JSX.Element;
renderLightbox: () => JSX.Element | null;
requestVerification: ( requestVerification: (
type: 'sms' | 'voice', type: 'sms' | 'voice',
number: string, number: string,
@ -77,6 +78,7 @@ export function App({
renderCustomizingPreferredReactionsModal, renderCustomizingPreferredReactionsModal,
renderGlobalModalContainer, renderGlobalModalContainer,
renderLeftPane, renderLeftPane,
renderLightbox,
renderStories, renderStories,
renderStoryViewer, renderStoryViewer,
requestVerification, requestVerification,
@ -179,6 +181,7 @@ export function App({
<ToastManager hideToast={hideToast} i18n={i18n} toast={toast} /> <ToastManager hideToast={hideToast} i18n={i18n} toast={toast} />
{renderGlobalModalContainer()} {renderGlobalModalContainer()}
{renderCallManager()} {renderCallManager()}
{renderLightbox()}
{isShowingStoriesView && renderStories(toggleStoriesView)} {isShowingStoriesView && renderStories(toggleStoriesView)}
{hasSelectedStoryData && {hasSelectedStoryData &&
renderStoryViewer(() => viewStory({ closeViewer: true }))} renderStoryViewer(() => viewStory({ closeViewer: true }))}

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React from 'react';
import { noop } from 'lodash';
import type { AvatarColorType } from '../types/Colors'; import type { AvatarColorType } from '../types/Colors';
import { AvatarPreview } from './AvatarPreview'; import { AvatarPreview } from './AvatarPreview';
@ -26,7 +27,13 @@ export function AvatarLightbox({
onClose, onClose,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
return ( return (
<Lightbox close={onClose} i18n={i18n} media={[]}> <Lightbox
closeLightbox={onClose}
i18n={i18n}
media={[]}
isViewOnce
toggleForwardMessageModal={noop}
>
<AvatarPreview <AvatarPreview
avatarColor={avatarColor} avatarColor={avatarColor}
avatarPath={avatarPath} avatarPath={avatarPath}

View file

@ -55,12 +55,12 @@ function createMediaItem(
} }
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
close: action('close'), closeLightbox: action('closeLightbox'),
i18n, i18n,
isViewOnce: Boolean(overrideProps.isViewOnce), isViewOnce: Boolean(overrideProps.isViewOnce),
media: overrideProps.media || [], media: overrideProps.media || [],
onSave: action('onSave'),
selectedIndex: number('selectedIndex', overrideProps.selectedIndex || 0), selectedIndex: number('selectedIndex', overrideProps.selectedIndex || 0),
toggleForwardMessageModal: action('toggleForwardMessageModal'),
}); });
export function Multimedia(): JSX.Element { export function Multimedia(): JSX.Element {
@ -305,10 +305,6 @@ CustomChildren.story = {
name: 'Custom children', name: 'Custom children',
}; };
export function Forwarding(): JSX.Element {
return <Lightbox {...createProps({})} onForward={action('onForward')} />;
}
export function ConversationHeader(): JSX.Element { export function ConversationHeader(): JSX.Element {
return ( return (
<Lightbox <Lightbox

View file

@ -9,32 +9,27 @@ import { createPortal } from 'react-dom';
import { noop } from 'lodash'; import { noop } from 'lodash';
import { useSpring, animated, to } from '@react-spring/web'; import { useSpring, animated, to } from '@react-spring/web';
import * as GoogleChrome from '../util/GoogleChrome';
import type { AttachmentType } from '../types/Attachment';
import { isGIF } from '../types/Attachment';
import { Avatar, AvatarSize } from './Avatar';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import { IMAGE_PNG, isImage, isVideo } from '../types/MIME';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import type { MediaItemType, MediaItemMessageType } from '../types/MediaItem'; import type { MediaItemType, MediaItemMessageType } from '../types/MediaItem';
import { formatDuration } from '../util/formatDuration'; import * as GoogleChrome from '../util/GoogleChrome';
import { useRestoreFocus } from '../hooks/useRestoreFocus';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { Avatar, AvatarSize } from './Avatar';
import { IMAGE_PNG, isImage, isVideo } from '../types/MIME';
import { formatDuration } from '../util/formatDuration';
import { isGIF } from '../types/Attachment';
import { saveAttachment } from '../util/saveAttachment';
import { useRestoreFocus } from '../hooks/useRestoreFocus';
export type PropsType = { export type PropsType = {
children?: ReactNode; children?: ReactNode;
close: () => void; closeLightbox: () => unknown;
getConversation?: (id: string) => ConversationType; getConversation?: (id: string) => ConversationType;
i18n: LocalizerType; i18n: LocalizerType;
isViewOnce?: boolean; isViewOnce?: boolean;
media: Array<MediaItemType>; media: Array<MediaItemType>;
onForward?: (messageId: string) => void;
onSave?: (options: {
attachment: AttachmentType;
message: MediaItemMessageType;
index: number;
}) => void;
selectedIndex?: number; selectedIndex?: number;
toggleForwardMessageModal: (messageId: string) => unknown;
}; };
const ZOOM_SCALE = 3; const ZOOM_SCALE = 3;
@ -53,14 +48,13 @@ const INITIAL_IMAGE_TRANSFORM = {
export function Lightbox({ export function Lightbox({
children, children,
close, closeLightbox,
getConversation, getConversation,
media, media,
i18n, i18n,
isViewOnce = false, isViewOnce = false,
onForward,
onSave,
selectedIndex: initialSelectedIndex = 0, selectedIndex: initialSelectedIndex = 0,
toggleForwardMessageModal,
}: PropsType): JSX.Element | null { }: PropsType): JSX.Element | null {
const [root, setRoot] = React.useState<HTMLElement | undefined>(); const [root, setRoot] = React.useState<HTMLElement | undefined>();
const [selectedIndex, setSelectedIndex] = const [selectedIndex, setSelectedIndex] =
@ -138,31 +132,39 @@ export function Lightbox({
const handleSave = ( const handleSave = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent> event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => { ) => {
if (isViewOnce) {
return;
}
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
const mediaItem = media[selectedIndex]; const mediaItem = media[selectedIndex];
const { attachment, message, index } = mediaItem; const { attachment, message, index } = mediaItem;
onSave?.({ attachment, message, index }); saveAttachment(attachment, message.sent_at, index + 1);
}; };
const handleForward = ( const handleForward = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent> event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => { ) => {
if (isViewOnce) {
return;
}
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
close(); closeLightbox();
const mediaItem = media[selectedIndex]; const mediaItem = media[selectedIndex];
onForward?.(mediaItem.message.id); toggleForwardMessageModal(mediaItem.message.id);
}; };
const onKeyDown = useCallback( const onKeyDown = useCallback(
(event: KeyboardEvent) => { (event: KeyboardEvent) => {
switch (event.key) { switch (event.key) {
case 'Escape': { case 'Escape': {
close(); closeLightbox();
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -181,14 +183,14 @@ export function Lightbox({
default: default:
} }
}, },
[close, onNext, onPrevious] [closeLightbox, onNext, onPrevious]
); );
const onClose = (event: React.MouseEvent<HTMLElement>) => { const onClose = (event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
close(); closeLightbox();
}; };
const playVideo = useCallback(() => { const playVideo = useCallback(() => {
@ -521,7 +523,7 @@ export function Lightbox({
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
close(); closeLightbox();
}} }}
onKeyUp={(event: React.KeyboardEvent<HTMLDivElement>) => { onKeyUp={(event: React.KeyboardEvent<HTMLDivElement>) => {
if ( if (
@ -531,7 +533,7 @@ export function Lightbox({
return; return;
} }
close(); closeLightbox();
}} }}
ref={containerRef} ref={containerRef}
role="presentation" role="presentation"
@ -553,7 +555,7 @@ export function Lightbox({
<div /> <div />
)} )}
<div className="Lightbox__controls"> <div className="Lightbox__controls">
{onForward ? ( {!isViewOnce ? (
<button <button
aria-label={i18n('forwardMessage')} aria-label={i18n('forwardMessage')}
className="Lightbox__button Lightbox__button--forward" className="Lightbox__button Lightbox__button--forward"
@ -561,7 +563,7 @@ export function Lightbox({
type="button" type="button"
/> />
) : null} ) : null}
{onSave ? ( {!isViewOnce ? (
<button <button
aria-label={i18n('save')} aria-label={i18n('save')}
className="Lightbox__button Lightbox__button--save" className="Lightbox__button Lightbox__button--save"
@ -572,7 +574,7 @@ export function Lightbox({
<button <button
aria-label={i18n('close')} aria-label={i18n('close')}
className="Lightbox__button Lightbox__button--close" className="Lightbox__button Lightbox__button--close"
onClick={close} onClick={closeLightbox}
type="button" type="button"
/> />
</div> </div>

View file

@ -49,9 +49,7 @@ const MESSAGE_DEFAULT_PROPS = {
checkForAccount: shouldNeverBeCalled, checkForAccount: shouldNeverBeCalled,
clearSelectedMessage: shouldNeverBeCalled, clearSelectedMessage: shouldNeverBeCalled,
containerWidthBreakpoint: WidthBreakpoint.Medium, containerWidthBreakpoint: WidthBreakpoint.Medium,
displayTapToViewMessage: shouldNeverBeCalled,
doubleCheckMissingQuoteReference: shouldNeverBeCalled, doubleCheckMissingQuoteReference: shouldNeverBeCalled,
downloadAttachment: shouldNeverBeCalled,
isBlocked: false, isBlocked: false,
isMessageRequestAccepted: true, isMessageRequestAccepted: true,
kickOffAttachmentDownload: shouldNeverBeCalled, kickOffAttachmentDownload: shouldNeverBeCalled,
@ -69,8 +67,9 @@ const MESSAGE_DEFAULT_PROPS = {
showContactModal: shouldNeverBeCalled, showContactModal: shouldNeverBeCalled,
showExpiredIncomingTapToViewToast: shouldNeverBeCalled, showExpiredIncomingTapToViewToast: shouldNeverBeCalled,
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled, showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
showLightbox: shouldNeverBeCalled,
showLightboxForViewOnceMedia: shouldNeverBeCalled,
showMessageDetail: shouldNeverBeCalled, showMessageDetail: shouldNeverBeCalled,
showVisualAttachment: shouldNeverBeCalled,
startConversation: shouldNeverBeCalled, startConversation: shouldNeverBeCalled,
theme: ThemeType.dark, theme: ThemeType.dark,
viewStory: shouldNeverBeCalled, viewStory: shouldNeverBeCalled,

View file

@ -1,18 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Toast } from './Toast';
type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
};
export function ToastUnableToLoadAttachment({
i18n,
onClose,
}: PropsType): JSX.Element {
return <Toast onClose={onClose}>{i18n('unableToLoadAttachment')}</Toast>;
}

View file

@ -87,6 +87,7 @@ import { PaymentEventKind } from '../../types/Payment';
import type { AnyPaymentEvent } from '../../types/Payment'; import type { AnyPaymentEvent } from '../../types/Payment';
import { Emojify } from './Emojify'; import { Emojify } from './Emojify';
import { getPaymentEventDescription } from '../../messages/helpers'; import { getPaymentEventDescription } from '../../messages/helpers';
import { saveAttachment } from '../../util/saveAttachment';
const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 10; const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 10;
const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18; const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18;
@ -318,16 +319,11 @@ export type PropsActions = {
messageId: string; messageId: string;
}) => void; }) => void;
markViewed(messageId: string): void; markViewed(messageId: string): void;
showVisualAttachment: (options: { showLightbox: (options: {
attachment: AttachmentType; attachment: AttachmentType;
messageId: string; messageId: string;
}) => void; }) => void;
downloadAttachment: (options: { showLightboxForViewOnceMedia: (messageId: string) => unknown;
attachment: AttachmentType;
timestamp: number;
isDangerous: boolean;
}) => void;
displayTapToViewMessage: (messageId: string) => unknown;
openLink: (url: string) => void; openLink: (url: string) => void;
scrollToQuotedMessage: (options: { scrollToQuotedMessage: (options: {
@ -847,7 +843,7 @@ export class Message extends React.PureComponent<Props, State> {
renderAudioAttachment, renderAudioAttachment,
renderingContext, renderingContext,
showMessageDetail, showMessageDetail,
showVisualAttachment, showLightbox,
shouldCollapseAbove, shouldCollapseAbove,
shouldCollapseBelow, shouldCollapseBelow,
status, status,
@ -898,7 +894,7 @@ export class Message extends React.PureComponent<Props, State> {
reducedMotion={reducedMotion} reducedMotion={reducedMotion}
onError={this.handleImageError} onError={this.handleImageError}
showVisualAttachment={() => { showVisualAttachment={() => {
showVisualAttachment({ showLightbox({
attachment: firstAttachment, attachment: firstAttachment,
messageId: id, messageId: id,
}); });
@ -945,7 +941,7 @@ export class Message extends React.PureComponent<Props, State> {
if (!isDownloaded(attachment)) { if (!isDownloaded(attachment)) {
kickOffAttachmentDownload({ attachment, messageId: id }); kickOffAttachmentDownload({ attachment, messageId: id });
} else { } else {
showVisualAttachment({ attachment, messageId: id }); showLightbox({ attachment, messageId: id });
} }
}} }}
/> />
@ -2240,7 +2236,7 @@ export class Message extends React.PureComponent<Props, State> {
const { const {
attachments, attachments,
contact, contact,
displayTapToViewMessage, showLightboxForViewOnceMedia,
direction, direction,
giftBadge, giftBadge,
id, id,
@ -2250,7 +2246,7 @@ export class Message extends React.PureComponent<Props, State> {
startConversation, startConversation,
openGiftBadge, openGiftBadge,
showContactDetail, showContactDetail,
showVisualAttachment, showLightbox,
showExpiredIncomingTapToViewToast, showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
} = this.props; } = this.props;
@ -2291,7 +2287,7 @@ export class Message extends React.PureComponent<Props, State> {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
displayTapToViewMessage(id); showLightboxForViewOnceMedia(id);
} }
return; return;
@ -2328,7 +2324,7 @@ export class Message extends React.PureComponent<Props, State> {
const attachment = attachments[0]; const attachment = attachments[0];
showVisualAttachment({ attachment, messageId: id }); showLightbox({ attachment, messageId: id });
return; return;
} }
@ -2384,13 +2380,8 @@ export class Message extends React.PureComponent<Props, State> {
}; };
public openGenericAttachment = (event?: React.MouseEvent): void => { public openGenericAttachment = (event?: React.MouseEvent): void => {
const { const { id, attachments, timestamp, kickOffAttachmentDownload } =
id, this.props;
attachments,
downloadAttachment,
timestamp,
kickOffAttachmentDownload,
} = this.props;
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
@ -2410,14 +2401,7 @@ export class Message extends React.PureComponent<Props, State> {
return; return;
} }
const { fileName } = attachment; saveAttachment(attachment, timestamp);
const isDangerous = isFileDangerous(fileName || '');
downloadAttachment({
isDangerous,
attachment,
timestamp,
});
}; };
public handleClick = (event: React.MouseEvent): void => { public handleClick = (event: React.MouseEvent): void => {

View file

@ -73,7 +73,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
checkForAccount: action('checkForAccount'), checkForAccount: action('checkForAccount'),
clearSelectedMessage: action('clearSelectedMessage'), clearSelectedMessage: action('clearSelectedMessage'),
displayTapToViewMessage: action('displayTapToViewMessage'), showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'), doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
kickOffAttachmentDownload: action('kickOffAttachmentDownload'), kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'), markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
@ -90,7 +90,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
showExpiredOutgoingTapToViewToast: action( showExpiredOutgoingTapToViewToast: action(
'showExpiredOutgoingTapToViewToast' 'showExpiredOutgoingTapToViewToast'
), ),
showVisualAttachment: action('showVisualAttachment'), showLightbox: action('showLightbox'),
startConversation: action('startConversation'), startConversation: action('startConversation'),
viewStory: action('viewStory'), viewStory: action('viewStory'),
}); });

View file

@ -79,7 +79,6 @@ export type PropsData = {
export type PropsBackboneActions = Pick< export type PropsBackboneActions = Pick<
MessagePropsType, MessagePropsType,
| 'displayTapToViewMessage'
| 'kickOffAttachmentDownload' | 'kickOffAttachmentDownload'
| 'markAttachmentAsCorrupted' | 'markAttachmentAsCorrupted'
| 'openConversation' | 'openConversation'
@ -89,16 +88,17 @@ export type PropsBackboneActions = Pick<
| 'showContactDetail' | 'showContactDetail'
| 'showExpiredIncomingTapToViewToast' | 'showExpiredIncomingTapToViewToast'
| 'showExpiredOutgoingTapToViewToast' | 'showExpiredOutgoingTapToViewToast'
| 'showVisualAttachment'
| 'startConversation' | 'startConversation'
>; >;
export type PropsReduxActions = Pick< export type PropsReduxActions = Pick<
MessagePropsType, MessagePropsType,
| 'checkForAccount'
| 'clearSelectedMessage' | 'clearSelectedMessage'
| 'doubleCheckMissingQuoteReference' | 'doubleCheckMissingQuoteReference'
| 'checkForAccount'
| 'showContactModal' | 'showContactModal'
| 'showLightbox'
| 'showLightboxForViewOnceMedia'
| 'viewStory' | 'viewStory'
> & { > & {
toggleSafetyNumberModal: (contactId: string) => void; toggleSafetyNumberModal: (contactId: string) => void;
@ -280,7 +280,7 @@ export class MessageDetail extends React.Component<Props> {
checkForAccount, checkForAccount,
clearSelectedMessage, clearSelectedMessage,
contactNameColor, contactNameColor,
displayTapToViewMessage, showLightboxForViewOnceMedia,
doubleCheckMissingQuoteReference, doubleCheckMissingQuoteReference,
expirationTimestamp, expirationTimestamp,
getPreferredBadge, getPreferredBadge,
@ -297,7 +297,7 @@ export class MessageDetail extends React.Component<Props> {
showContactModal, showContactModal,
showExpiredIncomingTapToViewToast, showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
showVisualAttachment, showLightbox,
startConversation, startConversation,
theme, theme,
viewStory, viewStory,
@ -325,10 +325,7 @@ export class MessageDetail extends React.Component<Props> {
menu={undefined} menu={undefined}
disableScroll disableScroll
displayLimit={Number.MAX_SAFE_INTEGER} displayLimit={Number.MAX_SAFE_INTEGER}
displayTapToViewMessage={displayTapToViewMessage} showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
downloadAttachment={() =>
log.warn('MessageDetail: downloadAttachment called!')
}
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference} doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
i18n={i18n} i18n={i18n}
@ -358,7 +355,7 @@ export class MessageDetail extends React.Component<Props> {
showMessageDetail={() => { showMessageDetail={() => {
log.warn('MessageDetail: showMessageDetail called!'); log.warn('MessageDetail: showMessageDetail called!');
}} }}
showVisualAttachment={showVisualAttachment} showLightbox={showLightbox}
startConversation={startConversation} startConversation={startConversation}
theme={theme} theme={theme}
viewStory={viewStory} viewStory={viewStory}

View file

@ -97,8 +97,7 @@ const defaultMessageProps: TimelineMessagesProps = {
deleteMessage: action('default--deleteMessage'), deleteMessage: action('default--deleteMessage'),
deleteMessageForEveryone: action('default--deleteMessageForEveryone'), deleteMessageForEveryone: action('default--deleteMessageForEveryone'),
direction: 'incoming', direction: 'incoming',
displayTapToViewMessage: action('default--displayTapToViewMessage'), showLightboxForViewOnceMedia: action('default--showLightboxForViewOnceMedia'),
downloadAttachment: action('default--downloadAttachment'),
doubleCheckMissingQuoteReference: action( doubleCheckMissingQuoteReference: action(
'default--doubleCheckMissingQuoteReference' 'default--doubleCheckMissingQuoteReference'
), ),
@ -140,7 +139,7 @@ const defaultMessageProps: TimelineMessagesProps = {
), ),
toggleForwardMessageModal: action('default--toggleForwardMessageModal'), toggleForwardMessageModal: action('default--toggleForwardMessageModal'),
showMessageDetail: action('default--showMessageDetail'), showMessageDetail: action('default--showMessageDetail'),
showVisualAttachment: action('default--showVisualAttachment'), showLightbox: action('default--showLightbox'),
startConversation: action('default--startConversation'), startConversation: action('default--startConversation'),
status: 'sent', status: 'sent',
text: 'This is really interesting.', text: 'This is really interesting.',

View file

@ -289,9 +289,8 @@ const actions = () => ({
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'), markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
markViewed: action('markViewed'), markViewed: action('markViewed'),
messageExpanded: action('messageExpanded'), messageExpanded: action('messageExpanded'),
showVisualAttachment: action('showVisualAttachment'), showLightbox: action('showLightbox'),
downloadAttachment: action('downloadAttachment'), showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
displayTapToViewMessage: action('displayTapToViewMessage'),
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'), doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
openLink: action('openLink'), openLink: action('openLink'),

View file

@ -253,9 +253,8 @@ const getActions = createSelector(
'kickOffAttachmentDownload', 'kickOffAttachmentDownload',
'markAttachmentAsCorrupted', 'markAttachmentAsCorrupted',
'messageExpanded', 'messageExpanded',
'showVisualAttachment', 'showLightbox',
'downloadAttachment', 'showLightboxForViewOnceMedia',
'displayTapToViewMessage',
'openLink', 'openLink',
'scrollToQuotedMessage', 'scrollToQuotedMessage',
'showExpiredIncomingTapToViewToast', 'showExpiredIncomingTapToViewToast',

View file

@ -82,10 +82,9 @@ const getDefaultProps = () => ({
openGiftBadge: action('openGiftBadge'), openGiftBadge: action('openGiftBadge'),
showContactDetail: action('showContactDetail'), showContactDetail: action('showContactDetail'),
showContactModal: action('showContactModal'), showContactModal: action('showContactModal'),
showLightbox: action('showLightbox'),
toggleForwardMessageModal: action('toggleForwardMessageModal'), toggleForwardMessageModal: action('toggleForwardMessageModal'),
showVisualAttachment: action('showVisualAttachment'), showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
downloadAttachment: action('downloadAttachment'),
displayTapToViewMessage: action('displayTapToViewMessage'),
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'), doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
showExpiredIncomingTapToViewToast: action( showExpiredIncomingTapToViewToast: action(
'showExpiredIncomingTapToViewToast' 'showExpiredIncomingTapToViewToast'

View file

@ -249,9 +249,8 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
// disableMenu: overrideProps.disableMenu, // disableMenu: overrideProps.disableMenu,
disableScroll: overrideProps.disableScroll, disableScroll: overrideProps.disableScroll,
direction: overrideProps.direction || 'incoming', direction: overrideProps.direction || 'incoming',
displayTapToViewMessage: action('displayTapToViewMessage'), showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'), doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
downloadAttachment: action('downloadAttachment'),
expirationLength: expirationLength:
number('expirationLength', overrideProps.expirationLength || 0) || number('expirationLength', overrideProps.expirationLength || 0) ||
undefined, undefined,
@ -318,7 +317,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
), ),
toggleForwardMessageModal: action('toggleForwardMessageModal'), toggleForwardMessageModal: action('toggleForwardMessageModal'),
showMessageDetail: action('showMessageDetail'), showMessageDetail: action('showMessageDetail'),
showVisualAttachment: action('showVisualAttachment'), showLightbox: action('showLightbox'),
startConversation: action('startConversation'), startConversation: action('startConversation'),
status: overrideProps.status || 'sent', status: overrideProps.status || 'sent',
text: overrideProps.text || text('text', ''), text: overrideProps.text || text('text', ''),

View file

@ -12,7 +12,6 @@ import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preve
import { isDownloaded } from '../../types/Attachment'; import { isDownloaded } from '../../types/Attachment';
import type { LocalizerType } from '../../types/I18N'; import type { LocalizerType } from '../../types/I18N';
import { handleOutsideClick } from '../../util/handleOutsideClick'; import { handleOutsideClick } from '../../util/handleOutsideClick';
import { isFileDangerous } from '../../util/isFileDangerous';
import { offsetDistanceModifier } from '../../util/popperUtil'; import { offsetDistanceModifier } from '../../util/popperUtil';
import { StopPropagation } from '../StopPropagation'; import { StopPropagation } from '../StopPropagation';
import { WidthBreakpoint } from '../_util'; import { WidthBreakpoint } from '../_util';
@ -28,6 +27,7 @@ import { doesMessageBodyOverflow } from './MessageBodyReadMore';
import type { Props as ReactionPickerProps } from './ReactionPicker'; import type { Props as ReactionPickerProps } from './ReactionPicker';
import { ConfirmationDialog } from '../ConfirmationDialog'; import { ConfirmationDialog } from '../ConfirmationDialog';
import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts'; import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts';
import { saveAttachment } from '../../util/saveAttachment';
export type PropsData = { export type PropsData = {
canDownload: boolean; canDownload: boolean;
@ -172,7 +172,7 @@ export function TimelineMessage(props: Props): JSX.Element {
}); });
const openGenericAttachment = (event?: React.MouseEvent): void => { const openGenericAttachment = (event?: React.MouseEvent): void => {
const { downloadAttachment, kickOffAttachmentDownload } = props; const { kickOffAttachmentDownload } = props;
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
@ -192,14 +192,7 @@ export function TimelineMessage(props: Props): JSX.Element {
return; return;
} }
const { fileName } = attachment; saveAttachment(attachment, timestamp);
const isDangerous = isFileDangerous(fileName || '');
downloadAttachment({
isDangerous,
attachment,
timestamp,
});
}; };
const handleContextMenu = (event: React.MouseEvent<HTMLDivElement>): void => { const handleContextMenu = (event: React.MouseEvent<HTMLDivElement>): void => {

View file

@ -88,7 +88,7 @@ const createProps = (
), ),
showConversation: action('showConversation'), showConversation: action('showConversation'),
showPendingInvites: action('showPendingInvites'), showPendingInvites: action('showPendingInvites'),
showLightboxForMedia: action('showLightboxForMedia'), showLightboxWithMedia: action('showLightboxWithMedia'),
updateGroupAttributes: async () => { updateGroupAttributes: async () => {
action('updateGroupAttributes')(); action('updateGroupAttributes')();
}, },

View file

@ -17,7 +17,6 @@ import { assertDev } from '../../../util/assert';
import { getMutedUntilText } from '../../../util/getMutedUntilText'; import { getMutedUntilText } from '../../../util/getMutedUntilText';
import type { LocalizerType, ThemeType } from '../../../types/Util'; import type { LocalizerType, ThemeType } from '../../../types/Util';
import type { MediaItemType } from '../../../types/MediaItem';
import type { BadgeType } from '../../../badges/types'; import type { BadgeType } from '../../../badges/types';
import { missingCaseError } from '../../../util/missingCaseError'; import { missingCaseError } from '../../../util/missingCaseError';
import { DurationInSeconds } from '../../../util/durations'; import { DurationInSeconds } from '../../../util/durations';
@ -30,6 +29,7 @@ import { AddGroupMembersModal } from './AddGroupMembersModal';
import { ConversationDetailsActions } from './ConversationDetailsActions'; import { ConversationDetailsActions } from './ConversationDetailsActions';
import { ConversationDetailsHeader } from './ConversationDetailsHeader'; import { ConversationDetailsHeader } from './ConversationDetailsHeader';
import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon'; import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
import type { Props as ConversationDetailsMediaListPropsType } from './ConversationDetailsMediaList';
import { ConversationDetailsMediaList } from './ConversationDetailsMediaList'; import { ConversationDetailsMediaList } from './ConversationDetailsMediaList';
import type { GroupV2Membership } from './ConversationDetailsMembershipList'; import type { GroupV2Membership } from './ConversationDetailsMembershipList';
import { ConversationDetailsMembershipList } from './ConversationDetailsMembershipList'; import { ConversationDetailsMembershipList } from './ConversationDetailsMembershipList';
@ -84,10 +84,6 @@ export type StateProps = {
showGroupLinkManagement: () => void; showGroupLinkManagement: () => void;
showGroupV2Permissions: () => void; showGroupV2Permissions: () => void;
showPendingInvites: () => void; showPendingInvites: () => void;
showLightboxForMedia: (
selectedMediaItem: MediaItemType,
media: Array<MediaItemType>
) => void;
showConversationNotificationsSettings: () => void; showConversationNotificationsSettings: () => void;
updateGroupAttributes: ( updateGroupAttributes: (
_: Readonly<{ _: Readonly<{
@ -123,7 +119,7 @@ type ActionProps = {
showConversation: ShowConversationType; showConversation: ShowConversationType;
toggleAddUserToAnotherGroupModal: (contactId?: string) => void; toggleAddUserToAnotherGroupModal: (contactId?: string) => void;
toggleSafetyNumberModal: (conversationId: string) => unknown; toggleSafetyNumberModal: (conversationId: string) => unknown;
}; } & Pick<ConversationDetailsMediaListPropsType, 'showLightboxWithMedia'>;
export type Props = StateProps & ActionProps; export type Props = StateProps & ActionProps;
@ -167,7 +163,7 @@ export function ConversationDetails({
showConversation, showConversation,
showGroupLinkManagement, showGroupLinkManagement,
showGroupV2Permissions, showGroupV2Permissions,
showLightboxForMedia, showLightboxWithMedia,
showPendingInvites, showPendingInvites,
theme, theme,
toggleSafetyNumberModal, toggleSafetyNumberModal,
@ -536,7 +532,7 @@ export function ConversationDetails({
i18n={i18n} i18n={i18n}
loadRecentMediaItems={loadRecentMediaItems} loadRecentMediaItems={loadRecentMediaItems}
showAllMedia={showAllMedia} showAllMedia={showAllMedia}
showLightboxForMedia={showLightboxForMedia} showLightboxWithMedia={showLightboxWithMedia}
/> />
{!isGroup && !conversation.isMe && ( {!isGroup && !conversation.isMe && (

View file

@ -30,7 +30,7 @@ const createProps = (mediaItems?: Array<MediaItemType>): Props => ({
i18n, i18n,
loadRecentMediaItems: action('loadRecentMediaItems'), loadRecentMediaItems: action('loadRecentMediaItems'),
showAllMedia: action('showAllMedia'), showAllMedia: action('showAllMedia'),
showLightboxForMedia: action('showLightboxForMedia'), showLightboxWithMedia: action('showLightboxWithMedia'),
}); });
export function Basic(): JSX.Element { export function Basic(): JSX.Element {

View file

@ -17,8 +17,8 @@ export type Props = {
i18n: LocalizerType; i18n: LocalizerType;
loadRecentMediaItems: (id: string, limit: number) => void; loadRecentMediaItems: (id: string, limit: number) => void;
showAllMedia: () => void; showAllMedia: () => void;
showLightboxForMedia: ( showLightboxWithMedia: (
selectedMediaItem: MediaItemType, selectedAttachmentPath: string | undefined,
media: Array<MediaItemType> media: Array<MediaItemType>
) => void; ) => void;
}; };
@ -32,7 +32,7 @@ export function ConversationDetailsMediaList({
i18n, i18n,
loadRecentMediaItems, loadRecentMediaItems,
showAllMedia, showAllMedia,
showLightboxForMedia, showLightboxWithMedia,
}: Props): JSX.Element | null { }: Props): JSX.Element | null {
const mediaItems = conversation.recentMediaItems || []; const mediaItems = conversation.recentMediaItems || [];
@ -65,7 +65,9 @@ export function ConversationDetailsMediaList({
key={`${mediaItem.message.id}-${mediaItem.index}`} key={`${mediaItem.message.id}-${mediaItem.index}`}
mediaItem={mediaItem} mediaItem={mediaItem}
i18n={i18n} i18n={i18n}
onClick={() => showLightboxForMedia(mediaItem, mediaItems)} onClick={() =>
showLightboxWithMedia(mediaItem.attachment.path, mediaItems)
}
/> />
))} ))}
</div> </div>

View file

@ -56,6 +56,9 @@ class ExpiringMessagesDeletionService {
// We do this to update the UI, if this message is being displayed somewhere // We do this to update the UI, if this message is being displayed somewhere
message.trigger('expired'); message.trigger('expired');
window.reduxActions.lightbox.closeLightboxIfViewingExpiredMessage(
message.id
);
if (conversation) { if (conversation) {
// An expired message only counts as decrementing the message count, not // An expired message only counts as decrementing the message count, not

View file

@ -24,6 +24,9 @@ async function eraseTapToViewMessages() {
// We do this to update the UI, if this message is being displayed somewhere // We do this to update the UI, if this message is being displayed somewhere
message.trigger('expired'); message.trigger('expired');
window.reduxActions.lightbox.closeLightboxIfViewingExpiredMessage(
message.id
);
await message.eraseContents(); await message.eraseContents();
}) })

View file

@ -14,6 +14,7 @@ import { actions as emojis } from './ducks/emojis';
import { actions as expiration } from './ducks/expiration'; import { actions as expiration } from './ducks/expiration';
import { actions as globalModals } from './ducks/globalModals'; import { actions as globalModals } from './ducks/globalModals';
import { actions as items } from './ducks/items'; import { actions as items } from './ducks/items';
import { actions as lightbox } from './ducks/lightbox';
import { actions as linkPreviews } from './ducks/linkPreviews'; import { actions as linkPreviews } from './ducks/linkPreviews';
import { actions as network } from './ducks/network'; import { actions as network } from './ducks/network';
import { actions as safetyNumber } from './ducks/safetyNumber'; import { actions as safetyNumber } from './ducks/safetyNumber';
@ -41,6 +42,7 @@ export const actionCreators: ReduxActions = {
expiration, expiration,
globalModals, globalModals,
items, items,
lightbox,
linkPreviews, linkPreviews,
network, network,
safetyNumber, safetyNumber,
@ -68,6 +70,7 @@ export const mapDispatchToProps = {
...expiration, ...expiration,
...globalModals, ...globalModals,
...items, ...items,
...lightbox,
...linkPreviews, ...linkPreviews,
...network, ...network,
...safetyNumber, ...safetyNumber,

View file

@ -177,7 +177,7 @@ type HideSendAnywayDialogActiontype = {
type: typeof HIDE_SEND_ANYWAY_DIALOG; type: typeof HIDE_SEND_ANYWAY_DIALOG;
}; };
type ShowStickerPackPreviewActionType = { export type ShowStickerPackPreviewActionType = {
type: typeof SHOW_STICKER_PACK_PREVIEW; type: typeof SHOW_STICKER_PACK_PREVIEW;
payload: string; payload: string;
}; };
@ -454,7 +454,7 @@ function closeStickerPackPreview(): ThunkAction<
}; };
} }
function showStickerPackPreview( export function showStickerPackPreview(
packId: string, packId: string,
packKey: string packKey: string
): ShowStickerPackPreviewActionType { ): ShowStickerPackPreviewActionType {

339
ts/state/ducks/lightbox.ts Normal file
View file

@ -0,0 +1,339 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ThunkAction } from 'redux-thunk';
import type { AttachmentType } from '../../types/Attachment';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import type { MediaItemType } from '../../types/MediaItem';
import type { StateType as RootStateType } from '../reducer';
import type { ShowStickerPackPreviewActionType } from './globalModals';
import type { ShowToastActionType } from './toast';
import * as log from '../../logging/log';
import { getMessageById } from '../../messages/getMessageById';
import { isGIF } from '../../types/Attachment';
import {
isImageTypeSupported,
isVideoTypeSupported,
} from '../../util/GoogleChrome';
import { isTapToView } from '../selectors/message';
import { SHOW_TOAST, ToastType } from './toast';
import { saveAttachmentFromMessage } from '../../util/saveAttachment';
import { showStickerPackPreview } from './globalModals';
import { useBoundActions } from '../../hooks/useBoundActions';
export type LightboxStateType =
| {
isShowingLightbox: false;
}
| {
isShowingLightbox: true;
isViewOnce: boolean;
media: Array<MediaItemType>;
selectedAttachmentPath: string | undefined;
};
const CLOSE_LIGHTBOX = 'lightbox/CLOSE';
const SHOW_LIGHTBOX = 'lightbox/SHOW';
type CloseLightboxActionType = {
type: typeof CLOSE_LIGHTBOX;
};
type ShowLightboxActionType = {
type: typeof SHOW_LIGHTBOX;
payload: {
isViewOnce: boolean;
media: Array<MediaItemType>;
selectedAttachmentPath: string | undefined;
};
};
type LightboxActionType = CloseLightboxActionType | ShowLightboxActionType;
function closeLightbox(): ThunkAction<
void,
RootStateType,
unknown,
CloseLightboxActionType
> {
return (dispatch, getState) => {
const { lightbox } = getState();
if (!lightbox.isShowingLightbox) {
return;
}
const { isViewOnce, media } = lightbox;
if (isViewOnce) {
media.forEach(item => {
if (!item.attachment.path) {
return;
}
window.Signal.Migrations.deleteTempFile(item.attachment.path);
});
}
dispatch({
type: CLOSE_LIGHTBOX,
});
};
}
function closeLightboxIfViewingExpiredMessage(
messageId: string
): ThunkAction<void, RootStateType, unknown, CloseLightboxActionType> {
return (dispatch, getState) => {
const { lightbox } = getState();
if (!lightbox.isShowingLightbox) {
return;
}
const { isViewOnce, media } = lightbox;
if (!isViewOnce) {
return;
}
const hasExpiredMedia = media.some(item => item.message.id === messageId);
if (!hasExpiredMedia) {
return;
}
dispatch({
type: CLOSE_LIGHTBOX,
});
};
}
function showLightboxWithMedia(
selectedAttachmentPath: string | undefined,
media: Array<MediaItemType>
): ShowLightboxActionType {
return {
type: SHOW_LIGHTBOX,
payload: {
isViewOnce: false,
media,
selectedAttachmentPath,
},
};
}
function showLightboxForViewOnceMedia(
messageId: string
): ThunkAction<void, RootStateType, unknown, ShowLightboxActionType> {
return async dispatch => {
log.info('showLightboxForViewOnceMedia: attempting to display message');
const message = await getMessageById(messageId);
if (!message) {
throw new Error(
`showLightboxForViewOnceMedia: Message ${messageId} missing!`
);
}
if (!isTapToView(message.attributes)) {
throw new Error(
`showLightboxForViewOnceMedia: Message ${message.idForLogging()} is not a tap to view message`
);
}
if (message.isErased()) {
throw new Error(
`showLightboxForViewOnceMedia: Message ${message.idForLogging()} is already erased`
);
}
const firstAttachment = (message.get('attachments') || [])[0];
if (!firstAttachment || !firstAttachment.path) {
throw new Error(
`showLightboxForViewOnceMedia: Message ${message.idForLogging()} had no first attachment with path`
);
}
const {
copyIntoTempDirectory,
getAbsoluteAttachmentPath,
getAbsoluteTempPath,
} = window.Signal.Migrations;
const absolutePath = getAbsoluteAttachmentPath(firstAttachment.path);
const { path: tempPath } = await copyIntoTempDirectory(absolutePath);
const tempAttachment = {
...firstAttachment,
path: tempPath,
};
await message.markViewOnceMessageViewed();
const { path, contentType } = tempAttachment;
const media = [
{
attachment: tempAttachment,
objectURL: getAbsoluteTempPath(path),
contentType,
index: 0,
// TODO maybe we need to listen for message change?
message: {
attachments: message.get('attachments') || [],
id: message.get('id'),
conversationId: message.get('conversationId'),
received_at: message.get('received_at'),
received_at_ms: Number(message.get('received_at_ms')),
sent_at: message.get('sent_at'),
},
},
];
dispatch({
type: SHOW_LIGHTBOX,
payload: {
isViewOnce: true,
media,
selectedAttachmentPath: undefined,
},
});
};
}
function showLightbox(opts: {
attachment: AttachmentType;
messageId: string;
}): ThunkAction<
void,
RootStateType,
unknown,
| ShowLightboxActionType
| ShowStickerPackPreviewActionType
| ShowToastActionType
> {
return async dispatch => {
const { attachment, messageId } = opts;
const message = await getMessageById(messageId);
if (!message) {
throw new Error(`showLightbox: Message ${messageId} missing!`);
}
const sticker = message.get('sticker');
if (sticker) {
const { packId, packKey } = sticker;
dispatch(showStickerPackPreview(packId, packKey));
return;
}
const { contentType } = attachment;
if (
!isImageTypeSupported(contentType) &&
!isVideoTypeSupported(contentType)
) {
await saveAttachmentFromMessage(messageId, attachment);
return;
}
const attachments: Array<AttachmentType> = message.get('attachments') || [];
const loop = isGIF(attachments);
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
const media = attachments
.filter(item => item.thumbnail && !item.pending && !item.error)
.map((item, index) => ({
objectURL: getAbsoluteAttachmentPath(item.path ?? ''),
path: item.path,
contentType: item.contentType,
loop,
index,
message: {
attachments: message.get('attachments') || [],
id: message.get('id'),
conversationId:
window.ConversationController.lookupOrCreate({
uuid: message.get('sourceUuid'),
e164: message.get('source'),
reason: 'conversation_view.showLightBox',
})?.id || message.get('conversationId'),
received_at: message.get('received_at'),
received_at_ms: Number(message.get('received_at_ms')),
sent_at: message.get('sent_at'),
},
attachment: item,
thumbnailObjectUrl:
item.thumbnail?.objectUrl ||
getAbsoluteAttachmentPath(item.thumbnail?.path ?? ''),
}));
if (!media.length) {
log.error(
'showLightbox: unable to load attachment',
attachments.map(x => ({
contentType: x.contentType,
error: x.error,
flags: x.flags,
path: x.path,
size: x.size,
}))
);
dispatch({
type: SHOW_TOAST,
payload: {
toastType: ToastType.UnableToLoadAttachment,
},
});
return;
}
dispatch({
type: SHOW_LIGHTBOX,
payload: {
isViewOnce: false,
media,
selectedAttachmentPath: attachment.path,
},
});
};
}
export const actions = {
closeLightbox,
closeLightboxIfViewingExpiredMessage,
showLightbox,
showLightboxForViewOnceMedia,
showLightboxWithMedia,
};
export const useLightboxActions = (): BoundActionCreatorsMapObject<
typeof actions
> => useBoundActions(actions);
export function getEmptyState(): LightboxStateType {
return {
isShowingLightbox: false,
};
}
export function reducer(
state: Readonly<LightboxStateType> = getEmptyState(),
action: Readonly<LightboxActionType>
): LightboxStateType {
if (action.type === CLOSE_LIGHTBOX) {
return getEmptyState();
}
if (action.type === SHOW_LIGHTBOX) {
return {
...action.payload,
isShowingLightbox: true,
};
}
return state;
}

View file

@ -11,6 +11,7 @@ import { getEmptyState as conversations } from './ducks/conversations';
import { getEmptyState as crashReports } from './ducks/crashReports'; import { getEmptyState as crashReports } from './ducks/crashReports';
import { getEmptyState as expiration } from './ducks/expiration'; import { getEmptyState as expiration } from './ducks/expiration';
import { getEmptyState as globalModals } from './ducks/globalModals'; import { getEmptyState as globalModals } from './ducks/globalModals';
import { getEmptyState as lightbox } from './ducks/lightbox';
import { getEmptyState as linkPreviews } from './ducks/linkPreviews'; import { getEmptyState as linkPreviews } from './ducks/linkPreviews';
import { getEmptyState as network } from './ducks/network'; import { getEmptyState as network } from './ducks/network';
import { getEmptyState as preferredReactions } from './ducks/preferredReactions'; import { getEmptyState as preferredReactions } from './ducks/preferredReactions';
@ -103,6 +104,7 @@ export function getInitialState({
expiration: expiration(), expiration: expiration(),
globalModals: globalModals(), globalModals: globalModals(),
items, items,
lightbox: lightbox(),
linkPreviews: linkPreviews(), linkPreviews: linkPreviews(),
network: network(), network: network(),
preferredReactions: preferredReactions(), preferredReactions: preferredReactions(),

View file

@ -16,6 +16,7 @@ import { reducer as emojis } from './ducks/emojis';
import { reducer as expiration } from './ducks/expiration'; import { reducer as expiration } from './ducks/expiration';
import { reducer as globalModals } from './ducks/globalModals'; import { reducer as globalModals } from './ducks/globalModals';
import { reducer as items } from './ducks/items'; import { reducer as items } from './ducks/items';
import { reducer as lightbox } from './ducks/lightbox';
import { reducer as linkPreviews } from './ducks/linkPreviews'; import { reducer as linkPreviews } from './ducks/linkPreviews';
import { reducer as network } from './ducks/network'; import { reducer as network } from './ducks/network';
import { reducer as preferredReactions } from './ducks/preferredReactions'; import { reducer as preferredReactions } from './ducks/preferredReactions';
@ -43,6 +44,7 @@ export const reducer = combineReducers({
expiration, expiration,
globalModals, globalModals,
items, items,
lightbox,
linkPreviews, linkPreviews,
network, network,
preferredReactions, preferredReactions,

View file

@ -0,0 +1,35 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { createSelector } from 'reselect';
import type { MediaItemType } from '../../types/MediaItem';
import type { StateType } from '../reducer';
import type { LightboxStateType } from '../ducks/lightbox';
export const getLightboxState = (state: StateType): LightboxStateType =>
state.lightbox;
export const shouldShowLightbox = createSelector(
getLightboxState,
({ isShowingLightbox }): boolean => isShowingLightbox
);
export const getIsViewOnce = createSelector(
getLightboxState,
(state): boolean => (state.isShowingLightbox ? state.isViewOnce : false)
);
export const getSelectedIndex = createSelector(
getLightboxState,
(state): number =>
state.isShowingLightbox
? state.media.findIndex(
item => item.attachment.path === state.selectedAttachmentPath
) || 0
: 0
);
export const getMedia = createSelector(
getLightboxState,
(state): Array<MediaItemType> => (state.isShowingLightbox ? state.media : [])
);

View file

@ -11,6 +11,7 @@ import { SmartCallManager } from './CallManager';
import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal'; import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal';
import { SmartGlobalModalContainer } from './GlobalModalContainer'; import { SmartGlobalModalContainer } from './GlobalModalContainer';
import { SmartLeftPane } from './LeftPane'; import { SmartLeftPane } from './LeftPane';
import { SmartLightbox } from './Lightbox';
import { SmartStories } from './Stories'; import { SmartStories } from './Stories';
import { SmartStoryViewer } from './StoryViewer'; import { SmartStoryViewer } from './StoryViewer';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
@ -55,6 +56,7 @@ const mapStateToProps = (state: StateType) => {
), ),
renderGlobalModalContainer: () => <SmartGlobalModalContainer />, renderGlobalModalContainer: () => <SmartGlobalModalContainer />,
renderLeftPane: () => <SmartLeftPane />, renderLeftPane: () => <SmartLeftPane />,
renderLightbox: () => <SmartLightbox />,
isShowingStoriesView: shouldShowStoriesView(state), isShowingStoriesView: shouldShowStoriesView(state),
renderStories: (closeView: () => unknown) => ( renderStories: (closeView: () => unknown) => (
<ErrorBoundary name="App/renderStories" closeView={closeView}> <ErrorBoundary name="App/renderStories" closeView={closeView}>

View file

@ -18,7 +18,6 @@ import { getGroupMemberships } from '../../util/getGroupMemberships';
import { getActiveCallState } from '../selectors/calling'; import { getActiveCallState } from '../selectors/calling';
import { getAreWeASubscriber } from '../selectors/items'; import { getAreWeASubscriber } from '../selectors/items';
import { getIntl, getTheme } from '../selectors/user'; import { getIntl, getTheme } from '../selectors/user';
import type { MediaItemType } from '../../types/MediaItem';
import { import {
getBadgesSelector, getBadgesSelector,
getPreferredBadgeSelector, getPreferredBadgeSelector,
@ -44,10 +43,6 @@ export type SmartConversationDetailsProps = {
showGroupV2Permissions: () => void; showGroupV2Permissions: () => void;
showConversationNotificationsSettings: () => void; showConversationNotificationsSettings: () => void;
showPendingInvites: () => void; showPendingInvites: () => void;
showLightboxForMedia: (
selectedMediaItem: MediaItemType,
media: Array<MediaItemType>
) => void;
updateGroupAttributes: ( updateGroupAttributes: (
_: Readonly<{ _: Readonly<{
avatar?: undefined | Uint8Array; avatar?: undefined | Uint8Array;

View file

@ -0,0 +1,52 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { useSelector } from 'react-redux';
import type { GetConversationByIdType } from '../selectors/conversations';
import type { LocalizerType } from '../../types/Util';
import type { MediaItemType } from '../../types/MediaItem';
import type { StateType } from '../reducer';
import { Lightbox } from '../../components/Lightbox';
import { getConversationSelector } from '../selectors/conversations';
import { getIntl } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
import { useLightboxActions } from '../ducks/lightbox';
import {
getIsViewOnce,
getMedia,
getSelectedIndex,
shouldShowLightbox,
} from '../selectors/lightbox';
export function SmartLightbox(): JSX.Element | null {
const i18n = useSelector<StateType, LocalizerType>(getIntl);
const { closeLightbox } = useLightboxActions();
const { toggleForwardMessageModal } = useGlobalModalActions();
const conversationSelector = useSelector<StateType, GetConversationByIdType>(
getConversationSelector
);
const isShowingLightbox = useSelector<StateType, boolean>(shouldShowLightbox);
const isViewOnce = useSelector<StateType, boolean>(getIsViewOnce);
const media = useSelector<StateType, Array<MediaItemType>>(getMedia);
const selectedIndex = useSelector<StateType, number>(getSelectedIndex);
if (!isShowingLightbox) {
return null;
}
return (
<Lightbox
closeLightbox={closeLightbox}
getConversation={conversationSelector}
i18n={i18n}
isViewOnce={isViewOnce}
media={media}
selectedIndex={selectedIndex || 0}
toggleForwardMessageModal={toggleForwardMessageModal}
/>
);
}

View file

@ -39,7 +39,6 @@ const mapStateToProps = (
receivedAt, receivedAt,
sentAt, sentAt,
displayTapToViewMessage,
kickOffAttachmentDownload, kickOffAttachmentDownload,
markAttachmentAsCorrupted, markAttachmentAsCorrupted,
openConversation, openConversation,
@ -48,7 +47,6 @@ const mapStateToProps = (
showContactDetail, showContactDetail,
showExpiredIncomingTapToViewToast, showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
showVisualAttachment,
startConversation, startConversation,
} = props; } = props;
@ -75,7 +73,6 @@ const mapStateToProps = (
interactionMode: getInteractionMode(state), interactionMode: getInteractionMode(state),
theme: getTheme(state), theme: getTheme(state),
displayTapToViewMessage,
kickOffAttachmentDownload, kickOffAttachmentDownload,
markAttachmentAsCorrupted, markAttachmentAsCorrupted,
markViewed, markViewed,
@ -86,7 +83,6 @@ const mapStateToProps = (
showContactDetail, showContactDetail,
showExpiredIncomingTapToViewToast, showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
showVisualAttachment,
startConversation, startConversation,
}; };
}; };

View file

@ -66,8 +66,6 @@ export type TimelinePropsType = ExternalProps &
| 'contactSupport' | 'contactSupport'
| 'blockGroupLinkRequests' | 'blockGroupLinkRequests'
| 'deleteMessage' | 'deleteMessage'
| 'displayTapToViewMessage'
| 'downloadAttachment'
| 'downloadNewVersion' | 'downloadNewVersion'
| 'kickOffAttachmentDownload' | 'kickOffAttachmentDownload'
| 'learnMoreAboutDeliveryIssue' | 'learnMoreAboutDeliveryIssue'
@ -88,7 +86,6 @@ export type TimelinePropsType = ExternalProps &
| 'showExpiredIncomingTapToViewToast' | 'showExpiredIncomingTapToViewToast'
| 'showExpiredOutgoingTapToViewToast' | 'showExpiredOutgoingTapToViewToast'
| 'showMessageDetail' | 'showMessageDetail'
| 'showVisualAttachment'
| 'startConversation' | 'startConversation'
| 'unblurAvatar' | 'unblurAvatar'
| 'updateSharedGroups' | 'updateSharedGroups'

View file

@ -14,6 +14,7 @@ import type { actions as emojis } from './ducks/emojis';
import type { actions as expiration } from './ducks/expiration'; import type { actions as expiration } from './ducks/expiration';
import type { actions as globalModals } from './ducks/globalModals'; import type { actions as globalModals } from './ducks/globalModals';
import type { actions as items } from './ducks/items'; import type { actions as items } from './ducks/items';
import type { actions as lightbox } from './ducks/lightbox';
import type { actions as linkPreviews } from './ducks/linkPreviews'; import type { actions as linkPreviews } from './ducks/linkPreviews';
import type { actions as network } from './ducks/network'; import type { actions as network } from './ducks/network';
import type { actions as safetyNumber } from './ducks/safetyNumber'; import type { actions as safetyNumber } from './ducks/safetyNumber';
@ -40,6 +41,7 @@ export type ReduxActions = {
expiration: typeof expiration; expiration: typeof expiration;
globalModals: typeof globalModals; globalModals: typeof globalModals;
items: typeof items; items: typeof items;
lightbox: typeof lightbox;
linkPreviews: typeof linkPreviews; linkPreviews: typeof linkPreviews;
network: typeof network; network: typeof network;
safetyNumber: typeof safetyNumber; safetyNumber: typeof safetyNumber;

View file

@ -3,14 +3,26 @@
import type { AttachmentType } from '../types/Attachment'; import type { AttachmentType } from '../types/Attachment';
import * as Attachment from '../types/Attachment'; import * as Attachment from '../types/Attachment';
import { showToast } from './showToast'; import { ToastDangerousFileType } from '../components/ToastDangerousFileType';
import { ToastFileSaved } from '../components/ToastFileSaved'; import { ToastFileSaved } from '../components/ToastFileSaved';
import { isFileDangerous } from './isFileDangerous';
import { showToast } from './showToast';
import { getMessageById } from '../messages/getMessageById';
export async function saveAttachment( export async function saveAttachment(
attachment: AttachmentType, attachment: AttachmentType,
timestamp = Date.now(), timestamp = Date.now(),
index = 0 index = 0
): Promise<void> { ): Promise<void> {
const { fileName = '' } = attachment;
const isDangerous = isFileDangerous(fileName);
if (isDangerous) {
showToast(ToastDangerousFileType);
return;
}
const { openFileInFolder, readAttachmentData, saveAttachmentToDisk } = const { openFileInFolder, readAttachmentData, saveAttachmentToDisk } =
window.Signal.Migrations; window.Signal.Migrations;
@ -30,3 +42,25 @@ export async function saveAttachment(
}); });
} }
} }
export async function saveAttachmentFromMessage(
messageId: string,
providedAttachment?: AttachmentType
): Promise<void> {
const message = await getMessageById(messageId);
if (!message) {
throw new Error(`saveAttachmentFromMessage: Message ${messageId} missing!`);
}
const { attachments, sent_at: timestamp } = message.attributes;
if (!attachments || attachments.length < 1) {
return;
}
const attachment =
providedAttachment && attachments.includes(providedAttachment)
? providedAttachment
: attachments[0];
return saveAttachment(attachment, timestamp);
}

View file

@ -1,35 +0,0 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { render } from 'react-dom';
import type { PropsType } from '../components/Lightbox';
import { Lightbox } from '../components/Lightbox';
// NOTE: This file is temporarily here for convenicence of use by
// conversation_view while it is transitioning from Backbone into pure React.
// Please use <Lightbox /> directly and DO NOT USE THESE FUNCTIONS.
let lightboxMountNode: HTMLElement | undefined;
export function closeLightbox(): void {
if (!lightboxMountNode) {
return;
}
window.ReactDOM.unmountComponentAtNode(lightboxMountNode);
document.body.removeChild(lightboxMountNode);
lightboxMountNode = undefined;
}
export function showLightbox(props: PropsType): void {
if (lightboxMountNode) {
closeLightbox();
}
lightboxMountNode = document.createElement('div');
lightboxMountNode.setAttribute('data-id', 'lightbox');
document.body.appendChild(lightboxMountNode);
render(<Lightbox {...props} />, lightboxMountNode);
}

View file

@ -40,7 +40,6 @@ import type { ToastReactionFailed } from '../components/ToastReactionFailed';
import type { ToastStickerPackInstallFailed } from '../components/ToastStickerPackInstallFailed'; import type { ToastStickerPackInstallFailed } from '../components/ToastStickerPackInstallFailed';
import type { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming'; import type { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming';
import type { ToastTapToViewExpiredOutgoing } from '../components/ToastTapToViewExpiredOutgoing'; import type { ToastTapToViewExpiredOutgoing } from '../components/ToastTapToViewExpiredOutgoing';
import type { ToastUnableToLoadAttachment } from '../components/ToastUnableToLoadAttachment';
import type { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit'; import type { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit';
import type { ToastVoiceNoteMustBeOnlyAttachment } from '../components/ToastVoiceNoteMustBeOnlyAttachment'; import type { ToastVoiceNoteMustBeOnlyAttachment } from '../components/ToastVoiceNoteMustBeOnlyAttachment';
@ -79,7 +78,6 @@ export function showToast(Toast: typeof ToastReactionFailed): void;
export function showToast(Toast: typeof ToastStickerPackInstallFailed): void; export function showToast(Toast: typeof ToastStickerPackInstallFailed): void;
export function showToast(Toast: typeof ToastTapToViewExpiredIncoming): void; export function showToast(Toast: typeof ToastTapToViewExpiredIncoming): void;
export function showToast(Toast: typeof ToastTapToViewExpiredOutgoing): void; export function showToast(Toast: typeof ToastTapToViewExpiredOutgoing): void;
export function showToast(Toast: typeof ToastUnableToLoadAttachment): void;
export function showToast(Toast: typeof ToastVoiceNoteLimit): void; export function showToast(Toast: typeof ToastVoiceNoteLimit): void;
export function showToast( export function showToast(
Toast: typeof ToastVoiceNoteMustBeOnlyAttachment Toast: typeof ToastVoiceNoteMustBeOnlyAttachment

View file

@ -4,17 +4,15 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import type * as Backbone from 'backbone'; import type * as Backbone from 'backbone';
import type { ComponentProps } from 'react';
import * as React from 'react'; import * as React from 'react';
import { flatten } from 'lodash'; import { flatten } from 'lodash';
import { render } from 'mustache'; import { render } from 'mustache';
import type { AttachmentType } from '../types/Attachment'; import type { AttachmentType } from '../types/Attachment';
import { isGIF } from '../types/Attachment';
import type { MIMEType } from '../types/MIME'; import type { MIMEType } from '../types/MIME';
import type { ConversationModel } from '../models/conversations'; import type { ConversationModel } from '../models/conversations';
import type { MessageAttributesType } from '../model-types.d'; import type { MessageAttributesType } from '../model-types.d';
import type { MediaItemType, MediaItemMessageType } from '../types/MediaItem'; import type { MediaItemType } from '../types/MediaItem';
import { getMessageById } from '../messages/getMessageById'; import { getMessageById } from '../messages/getMessageById';
import { getContactId } from '../messages/helpers'; import { getContactId } from '../messages/helpers';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
@ -22,16 +20,10 @@ import { enqueueReactionForSend } from '../reactions/enqueueReactionForSend';
import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions'; import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions';
import { isGroup } from '../util/whatTypeOfConversation'; import { isGroup } from '../util/whatTypeOfConversation';
import { getPreferredBadgeSelector } from '../state/selectors/badges'; import { getPreferredBadgeSelector } from '../state/selectors/badges';
import { import { isIncoming, isOutgoing } from '../state/selectors/message';
isIncoming,
isOutgoing,
isTapToView,
} from '../state/selectors/message';
import { getConversationSelector } from '../state/selectors/conversations';
import { getActiveCallState } from '../state/selectors/calling'; import { getActiveCallState } from '../state/selectors/calling';
import { getTheme } from '../state/selectors/user'; import { getTheme } from '../state/selectors/user';
import { ReactWrapperView } from './ReactWrapperView'; import { ReactWrapperView } from './ReactWrapperView';
import type { Lightbox } from '../components/Lightbox';
import { ConversationDetailsMembershipList } from '../components/conversation/conversation-details/ConversationDetailsMembershipList'; import { ConversationDetailsMembershipList } from '../components/conversation/conversation-details/ConversationDetailsMembershipList';
import * as log from '../logging/log'; import * as log from '../logging/log';
import type { EmbeddedContactType } from '../types/EmbeddedContact'; import type { EmbeddedContactType } from '../types/EmbeddedContact';
@ -39,13 +31,11 @@ import { createConversationView } from '../state/roots/createConversationView';
import { ToastConversationArchived } from '../components/ToastConversationArchived'; import { ToastConversationArchived } from '../components/ToastConversationArchived';
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread'; import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived'; import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
import { ToastDangerousFileType } from '../components/ToastDangerousFileType';
import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong'; import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound'; import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
import { ToastReactionFailed } from '../components/ToastReactionFailed'; import { ToastReactionFailed } from '../components/ToastReactionFailed';
import { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming'; import { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming';
import { ToastTapToViewExpiredOutgoing } from '../components/ToastTapToViewExpiredOutgoing'; import { ToastTapToViewExpiredOutgoing } from '../components/ToastTapToViewExpiredOutgoing';
import { ToastUnableToLoadAttachment } from '../components/ToastUnableToLoadAttachment';
import { ToastCannotOpenGiftBadge } from '../components/ToastCannotOpenGiftBadge'; import { ToastCannotOpenGiftBadge } from '../components/ToastCannotOpenGiftBadge';
import { deleteDraftAttachment } from '../util/deleteDraftAttachment'; import { deleteDraftAttachment } from '../util/deleteDraftAttachment';
import { retryMessageSend } from '../util/retryMessageSend'; import { retryMessageSend } from '../util/retryMessageSend';
@ -62,7 +52,6 @@ import {
removeLinkPreview, removeLinkPreview,
suspendLinkPreviews, suspendLinkPreviews,
} from '../services/LinkPreview'; } from '../services/LinkPreview';
import { closeLightbox, showLightbox } from '../util/showLightbox';
import { saveAttachment } from '../util/saveAttachment'; import { saveAttachment } from '../util/saveAttachment';
import { SECOND } from '../util/durations'; import { SECOND } from '../util/durations';
import { startConversation } from '../util/startConversation'; import { startConversation } from '../util/startConversation';
@ -78,24 +67,13 @@ type PanelType = { view: Backbone.View; headerTitle?: string };
const { Message } = window.Signal.Types; const { Message } = window.Signal.Types;
const { const { getAbsoluteAttachmentPath, upgradeMessageSchema } =
copyIntoTempDirectory, window.Signal.Migrations;
deleteTempFile,
getAbsoluteAttachmentPath,
getAbsoluteTempPath,
upgradeMessageSchema,
} = window.Signal.Migrations;
const { getMessagesBySentAt } = window.Signal.Data; const { getMessagesBySentAt } = window.Signal.Data;
type MessageActionsType = { type MessageActionsType = {
deleteMessage: (messageId: string) => unknown; deleteMessage: (messageId: string) => unknown;
displayTapToViewMessage: (messageId: string) => unknown;
downloadAttachment: (options: {
attachment: AttachmentType;
timestamp: number;
isDangerous: boolean;
}) => unknown;
downloadNewVersion: () => unknown; downloadNewVersion: () => unknown;
kickOffAttachmentDownload: ( kickOffAttachmentDownload: (
options: Readonly<{ messageId: string }> options: Readonly<{ messageId: string }>
@ -120,11 +98,6 @@ type MessageActionsType = {
showExpiredIncomingTapToViewToast: () => unknown; showExpiredIncomingTapToViewToast: () => unknown;
showExpiredOutgoingTapToViewToast: () => unknown; showExpiredOutgoingTapToViewToast: () => unknown;
showMessageDetail: (messageId: string) => unknown; showMessageDetail: (messageId: string) => unknown;
showVisualAttachment: (options: {
attachment: AttachmentType;
messageId: string;
showSingle?: boolean;
}) => unknown;
startConversation: (e164: string, uuid: UUIDStringType) => unknown; startConversation: (e164: string, uuid: UUIDStringType) => unknown;
}; };
@ -173,11 +146,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.listenTo(this.model, 'open-all-media', this.showAllMedia); this.listenTo(this.model, 'open-all-media', this.showAllMedia);
this.listenTo(this.model, 'escape-pressed', this.resetPanel); this.listenTo(this.model, 'escape-pressed', this.resetPanel);
this.listenTo(this.model, 'show-message-details', this.showMessageDetail); this.listenTo(this.model, 'show-message-details', this.showMessageDetail);
this.listenTo(
this.model,
'save-attachment',
this.downloadAttachmentWrapper
);
this.listenTo(this.model, 'delete-message', this.deleteMessage); this.listenTo(this.model, 'delete-message', this.deleteMessage);
this.listenTo(this.model, 'remove-link-review', removeLinkPreview); this.listenTo(this.model, 'remove-link-review', removeLinkPreview);
this.listenTo( this.listenTo(
@ -481,22 +449,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
message.markAttachmentAsCorrupted(options.attachment); message.markAttachmentAsCorrupted(options.attachment);
}; };
const showVisualAttachment = (options: {
attachment: AttachmentType;
messageId: string;
showSingle?: boolean;
}) => {
this.showLightbox(options);
};
const downloadAttachment = (options: {
attachment: AttachmentType;
timestamp: number;
isDangerous: boolean;
}) => {
this.downloadAttachment(options);
};
const displayTapToViewMessage = (messageId: string) =>
this.displayTapToViewMessage(messageId);
const openGiftBadge = (messageId: string): void => { const openGiftBadge = (messageId: string): void => {
const message = window.MessageController.getById(messageId); const message = window.MessageController.getById(messageId);
if (!message) { if (!message) {
@ -523,8 +475,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
return { return {
deleteMessage, deleteMessage,
displayTapToViewMessage,
downloadAttachment,
downloadNewVersion, downloadNewVersion,
kickOffAttachmentDownload, kickOffAttachmentDownload,
markAttachmentAsCorrupted, markAttachmentAsCorrupted,
@ -538,7 +488,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
showExpiredIncomingTapToViewToast, showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
showMessageDetail, showMessageDetail,
showVisualAttachment,
startConversation, startConversation,
}; };
} }
@ -805,9 +754,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
} }
case 'media': { case 'media': {
const selectedMedia = window.reduxActions.lightbox.showLightboxWithMedia(
media.find(item => attachment.path === item.path) || media[0]; attachment.path,
this.showLightboxForMedia(selectedMedia, media); media
);
break; break;
} }
@ -907,131 +857,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
view.render(); view.render();
} }
downloadAttachmentWrapper(
messageId: string,
providedAttachment?: AttachmentType
): void {
const message = window.MessageController.getById(messageId);
if (!message) {
throw new Error(
`downloadAttachmentWrapper: Message ${messageId} missing!`
);
}
const { attachments, sent_at: timestamp } = message.attributes;
if (!attachments || attachments.length < 1) {
return;
}
const attachment =
providedAttachment && attachments.includes(providedAttachment)
? providedAttachment
: attachments[0];
const { fileName } = attachment;
const isDangerous = window.Signal.Util.isFileDangerous(fileName || '');
this.downloadAttachment({ attachment, timestamp, isDangerous });
}
async downloadAttachment({
attachment,
timestamp,
isDangerous,
}: {
attachment: AttachmentType;
timestamp: number;
isDangerous: boolean;
}): Promise<void> {
if (isDangerous) {
showToast(ToastDangerousFileType);
return;
}
return saveAttachment(attachment, timestamp);
}
async displayTapToViewMessage(messageId: string): Promise<void> {
log.info('displayTapToViewMessage: attempting to display message');
const message = window.MessageController.getById(messageId);
if (!message) {
throw new Error(`displayTapToViewMessage: Message ${messageId} missing!`);
}
if (!isTapToView(message.attributes)) {
throw new Error(
`displayTapToViewMessage: Message ${message.idForLogging()} is not a tap to view message`
);
}
if (message.isErased()) {
throw new Error(
`displayTapToViewMessage: Message ${message.idForLogging()} is already erased`
);
}
const firstAttachment = (message.get('attachments') || [])[0];
if (!firstAttachment || !firstAttachment.path) {
throw new Error(
`displayTapToViewMessage: Message ${message.idForLogging()} had no first attachment with path`
);
}
const absolutePath = getAbsoluteAttachmentPath(firstAttachment.path);
const { path: tempPath } = await copyIntoTempDirectory(absolutePath);
const tempAttachment = {
...firstAttachment,
path: tempPath,
};
await message.markViewOnceMessageViewed();
const close = (): void => {
try {
this.stopListening(message);
closeLightbox();
} finally {
deleteTempFile(tempPath);
}
};
this.listenTo(message, 'expired', close);
this.listenTo(message, 'change', () => {
showLightbox(getProps());
});
const getProps = (): ComponentProps<typeof Lightbox> => {
const { path, contentType } = tempAttachment;
return {
close,
i18n: window.i18n,
media: [
{
attachment: tempAttachment,
objectURL: getAbsoluteTempPath(path),
contentType,
index: 0,
message: {
attachments: message.get('attachments') || [],
id: message.get('id'),
conversationId: message.get('conversationId'),
received_at: message.get('received_at'),
received_at_ms: Number(message.get('received_at_ms')),
sent_at: message.get('sent_at'),
},
},
],
isViewOnce: true,
};
};
showLightbox(getProps());
log.info('displayTapToViewMessage: showed lightbox');
}
deleteMessage(messageId: string): void { deleteMessage(messageId: string): void {
const message = window.MessageController.getById(messageId); const message = window.MessageController.getById(messageId);
if (!message) { if (!message) {
@ -1055,136 +880,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}); });
} }
showLightboxForMedia(
selectedMediaItem: MediaItemType,
media: Array<MediaItemType> = []
): void {
const onSave = async ({
attachment,
message,
index,
}: {
attachment: AttachmentType;
message: MediaItemMessageType;
index: number;
}) => {
return saveAttachment(attachment, message.sent_at, index + 1);
};
const selectedIndex = media.findIndex(
mediaItem =>
mediaItem.attachment.path === selectedMediaItem.attachment.path
);
const mediaMessage = selectedMediaItem.message;
const message = window.MessageController.getById(mediaMessage.id);
if (!message) {
throw new Error(
`showLightboxForMedia: Message ${mediaMessage.id} missing!`
);
}
const close = () => {
closeLightbox();
this.stopListening(message, 'expired', closeLightbox);
};
showLightbox({
close,
i18n: window.i18n,
getConversation: getConversationSelector(window.reduxStore.getState()),
media,
onForward: messageId => {
window.reduxActions.globalModals.toggleForwardMessageModal(messageId);
},
onSave,
selectedIndex: selectedIndex >= 0 ? selectedIndex : 0,
});
this.listenTo(message, 'expired', close);
}
showLightbox({
attachment,
messageId,
}: {
attachment: AttachmentType;
messageId: string;
showSingle?: boolean;
}): void {
const message = window.MessageController.getById(messageId);
if (!message) {
throw new Error(`showLightbox: Message ${messageId} missing!`);
}
const sticker = message.get('sticker');
if (sticker) {
const { packId, packKey } = sticker;
window.reduxActions.globalModals.showStickerPackPreview(packId, packKey);
return;
}
const { contentType } = attachment;
if (
!window.Signal.Util.GoogleChrome.isImageTypeSupported(contentType) &&
!window.Signal.Util.GoogleChrome.isVideoTypeSupported(contentType)
) {
this.downloadAttachmentWrapper(messageId, attachment);
return;
}
const attachments: Array<AttachmentType> = message.get('attachments') || [];
const loop = isGIF(attachments);
const media = attachments
.filter(item => item.thumbnail && !item.pending && !item.error)
.map((item, index) => ({
objectURL: getAbsoluteAttachmentPath(item.path ?? ''),
path: item.path,
contentType: item.contentType,
loop,
index,
message: {
attachments: message.get('attachments') || [],
id: message.get('id'),
conversationId:
window.ConversationController.lookupOrCreate({
uuid: message.get('sourceUuid'),
e164: message.get('source'),
reason: 'conversation_view.showLightBox',
})?.id || message.get('conversationId'),
received_at: message.get('received_at'),
received_at_ms: Number(message.get('received_at_ms')),
sent_at: message.get('sent_at'),
},
attachment: item,
thumbnailObjectUrl:
item.thumbnail?.objectUrl ||
getAbsoluteAttachmentPath(item.thumbnail?.path ?? ''),
}));
if (!media.length) {
log.error(
'showLightbox: unable to load attachment',
attachments.map(x => ({
contentType: x.contentType,
error: x.error,
flags: x.flags,
path: x.path,
size: x.size,
}))
);
showToast(ToastUnableToLoadAttachment);
return;
}
const selectedMedia =
media.find(item => attachment.path === item.path) || media[0];
this.showLightboxForMedia(selectedMedia, media);
}
showGroupLinkManagement(): void { showGroupLinkManagement(): void {
const view = new ReactWrapperView({ const view = new ReactWrapperView({
className: 'panel', className: 'panel',
@ -1290,7 +985,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
showConversationNotificationsSettings: showConversationNotificationsSettings:
this.showConversationNotificationsSettings.bind(this), this.showConversationNotificationsSettings.bind(this),
showPendingInvites: this.showPendingInvites.bind(this), showPendingInvites: this.showPendingInvites.bind(this),
showLightboxForMedia: this.showLightboxForMedia.bind(this),
updateGroupAttributes: this.model.updateGroupAttributesV2.bind( updateGroupAttributes: this.model.updateGroupAttributesV2.bind(
this.model this.model
), ),