Moves showLightbox to redux
This commit is contained in:
parent
3a246656e3
commit
635a59a473
38 changed files with 584 additions and 504 deletions
|
@ -158,6 +158,7 @@ import type AccountManager from './textsecure/AccountManager';
|
|||
import { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate';
|
||||
import { StoryViewModeType, StoryViewTargetType } from './types/Stories';
|
||||
import { downloadOnboardingStory } from './util/downloadOnboardingStory';
|
||||
import { saveAttachmentFromMessage } from './util/saveAttachment';
|
||||
|
||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||
|
||||
|
@ -1114,6 +1115,7 @@ export async function startApp(): Promise<void> {
|
|||
store.dispatch
|
||||
),
|
||||
items: bindActionCreators(actionCreators.items, store.dispatch),
|
||||
lightbox: bindActionCreators(actionCreators.lightbox, store.dispatch),
|
||||
linkPreviews: bindActionCreators(
|
||||
actionCreators.linkPreviews,
|
||||
store.dispatch
|
||||
|
@ -1656,10 +1658,10 @@ export async function startApp(): Promise<void> {
|
|||
const { selectedMessage } = state.conversations;
|
||||
|
||||
if (selectedMessage) {
|
||||
conversation.trigger('save-attachment', selectedMessage);
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
saveAttachmentFromMessage(selectedMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ type PropsType = {
|
|||
renderStories: (closeView: () => unknown) => JSX.Element;
|
||||
hasSelectedStoryData: boolean;
|
||||
renderStoryViewer: (closeView: () => unknown) => JSX.Element;
|
||||
renderLightbox: () => JSX.Element | null;
|
||||
requestVerification: (
|
||||
type: 'sms' | 'voice',
|
||||
number: string,
|
||||
|
@ -77,6 +78,7 @@ export function App({
|
|||
renderCustomizingPreferredReactionsModal,
|
||||
renderGlobalModalContainer,
|
||||
renderLeftPane,
|
||||
renderLightbox,
|
||||
renderStories,
|
||||
renderStoryViewer,
|
||||
requestVerification,
|
||||
|
@ -179,6 +181,7 @@ export function App({
|
|||
<ToastManager hideToast={hideToast} i18n={i18n} toast={toast} />
|
||||
{renderGlobalModalContainer()}
|
||||
{renderCallManager()}
|
||||
{renderLightbox()}
|
||||
{isShowingStoriesView && renderStories(toggleStoriesView)}
|
||||
{hasSelectedStoryData &&
|
||||
renderStoryViewer(() => viewStory({ closeViewer: true }))}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import type { AvatarColorType } from '../types/Colors';
|
||||
import { AvatarPreview } from './AvatarPreview';
|
||||
|
@ -26,7 +27,13 @@ export function AvatarLightbox({
|
|||
onClose,
|
||||
}: PropsType): JSX.Element {
|
||||
return (
|
||||
<Lightbox close={onClose} i18n={i18n} media={[]}>
|
||||
<Lightbox
|
||||
closeLightbox={onClose}
|
||||
i18n={i18n}
|
||||
media={[]}
|
||||
isViewOnce
|
||||
toggleForwardMessageModal={noop}
|
||||
>
|
||||
<AvatarPreview
|
||||
avatarColor={avatarColor}
|
||||
avatarPath={avatarPath}
|
||||
|
|
|
@ -55,12 +55,12 @@ function createMediaItem(
|
|||
}
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
close: action('close'),
|
||||
closeLightbox: action('closeLightbox'),
|
||||
i18n,
|
||||
isViewOnce: Boolean(overrideProps.isViewOnce),
|
||||
media: overrideProps.media || [],
|
||||
onSave: action('onSave'),
|
||||
selectedIndex: number('selectedIndex', overrideProps.selectedIndex || 0),
|
||||
toggleForwardMessageModal: action('toggleForwardMessageModal'),
|
||||
});
|
||||
|
||||
export function Multimedia(): JSX.Element {
|
||||
|
@ -305,10 +305,6 @@ CustomChildren.story = {
|
|||
name: 'Custom children',
|
||||
};
|
||||
|
||||
export function Forwarding(): JSX.Element {
|
||||
return <Lightbox {...createProps({})} onForward={action('onForward')} />;
|
||||
}
|
||||
|
||||
export function ConversationHeader(): JSX.Element {
|
||||
return (
|
||||
<Lightbox
|
||||
|
|
|
@ -9,32 +9,27 @@ import { createPortal } from 'react-dom';
|
|||
import { noop } from 'lodash';
|
||||
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 { IMAGE_PNG, isImage, isVideo } from '../types/MIME';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { MediaItemType, MediaItemMessageType } from '../types/MediaItem';
|
||||
import { formatDuration } from '../util/formatDuration';
|
||||
import { useRestoreFocus } from '../hooks/useRestoreFocus';
|
||||
import * as GoogleChrome from '../util/GoogleChrome';
|
||||
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 = {
|
||||
children?: ReactNode;
|
||||
close: () => void;
|
||||
closeLightbox: () => unknown;
|
||||
getConversation?: (id: string) => ConversationType;
|
||||
i18n: LocalizerType;
|
||||
isViewOnce?: boolean;
|
||||
media: Array<MediaItemType>;
|
||||
onForward?: (messageId: string) => void;
|
||||
onSave?: (options: {
|
||||
attachment: AttachmentType;
|
||||
message: MediaItemMessageType;
|
||||
index: number;
|
||||
}) => void;
|
||||
selectedIndex?: number;
|
||||
toggleForwardMessageModal: (messageId: string) => unknown;
|
||||
};
|
||||
|
||||
const ZOOM_SCALE = 3;
|
||||
|
@ -53,14 +48,13 @@ const INITIAL_IMAGE_TRANSFORM = {
|
|||
|
||||
export function Lightbox({
|
||||
children,
|
||||
close,
|
||||
closeLightbox,
|
||||
getConversation,
|
||||
media,
|
||||
i18n,
|
||||
isViewOnce = false,
|
||||
onForward,
|
||||
onSave,
|
||||
selectedIndex: initialSelectedIndex = 0,
|
||||
toggleForwardMessageModal,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const [root, setRoot] = React.useState<HTMLElement | undefined>();
|
||||
const [selectedIndex, setSelectedIndex] =
|
||||
|
@ -138,31 +132,39 @@ export function Lightbox({
|
|||
const handleSave = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
if (isViewOnce) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
const mediaItem = media[selectedIndex];
|
||||
const { attachment, message, index } = mediaItem;
|
||||
|
||||
onSave?.({ attachment, message, index });
|
||||
saveAttachment(attachment, message.sent_at, index + 1);
|
||||
};
|
||||
|
||||
const handleForward = (
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
if (isViewOnce) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
close();
|
||||
closeLightbox();
|
||||
const mediaItem = media[selectedIndex];
|
||||
onForward?.(mediaItem.message.id);
|
||||
toggleForwardMessageModal(mediaItem.message.id);
|
||||
};
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case 'Escape': {
|
||||
close();
|
||||
closeLightbox();
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -181,14 +183,14 @@ export function Lightbox({
|
|||
default:
|
||||
}
|
||||
},
|
||||
[close, onNext, onPrevious]
|
||||
[closeLightbox, onNext, onPrevious]
|
||||
);
|
||||
|
||||
const onClose = (event: React.MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
close();
|
||||
closeLightbox();
|
||||
};
|
||||
|
||||
const playVideo = useCallback(() => {
|
||||
|
@ -521,7 +523,7 @@ export function Lightbox({
|
|||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
close();
|
||||
closeLightbox();
|
||||
}}
|
||||
onKeyUp={(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (
|
||||
|
@ -531,7 +533,7 @@ export function Lightbox({
|
|||
return;
|
||||
}
|
||||
|
||||
close();
|
||||
closeLightbox();
|
||||
}}
|
||||
ref={containerRef}
|
||||
role="presentation"
|
||||
|
@ -553,7 +555,7 @@ export function Lightbox({
|
|||
<div />
|
||||
)}
|
||||
<div className="Lightbox__controls">
|
||||
{onForward ? (
|
||||
{!isViewOnce ? (
|
||||
<button
|
||||
aria-label={i18n('forwardMessage')}
|
||||
className="Lightbox__button Lightbox__button--forward"
|
||||
|
@ -561,7 +563,7 @@ export function Lightbox({
|
|||
type="button"
|
||||
/>
|
||||
) : null}
|
||||
{onSave ? (
|
||||
{!isViewOnce ? (
|
||||
<button
|
||||
aria-label={i18n('save')}
|
||||
className="Lightbox__button Lightbox__button--save"
|
||||
|
@ -572,7 +574,7 @@ export function Lightbox({
|
|||
<button
|
||||
aria-label={i18n('close')}
|
||||
className="Lightbox__button Lightbox__button--close"
|
||||
onClick={close}
|
||||
onClick={closeLightbox}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -49,9 +49,7 @@ const MESSAGE_DEFAULT_PROPS = {
|
|||
checkForAccount: shouldNeverBeCalled,
|
||||
clearSelectedMessage: shouldNeverBeCalled,
|
||||
containerWidthBreakpoint: WidthBreakpoint.Medium,
|
||||
displayTapToViewMessage: shouldNeverBeCalled,
|
||||
doubleCheckMissingQuoteReference: shouldNeverBeCalled,
|
||||
downloadAttachment: shouldNeverBeCalled,
|
||||
isBlocked: false,
|
||||
isMessageRequestAccepted: true,
|
||||
kickOffAttachmentDownload: shouldNeverBeCalled,
|
||||
|
@ -69,8 +67,9 @@ const MESSAGE_DEFAULT_PROPS = {
|
|||
showContactModal: shouldNeverBeCalled,
|
||||
showExpiredIncomingTapToViewToast: shouldNeverBeCalled,
|
||||
showExpiredOutgoingTapToViewToast: shouldNeverBeCalled,
|
||||
showLightbox: shouldNeverBeCalled,
|
||||
showLightboxForViewOnceMedia: shouldNeverBeCalled,
|
||||
showMessageDetail: shouldNeverBeCalled,
|
||||
showVisualAttachment: shouldNeverBeCalled,
|
||||
startConversation: shouldNeverBeCalled,
|
||||
theme: ThemeType.dark,
|
||||
viewStory: shouldNeverBeCalled,
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -87,6 +87,7 @@ import { PaymentEventKind } from '../../types/Payment';
|
|||
import type { AnyPaymentEvent } from '../../types/Payment';
|
||||
import { Emojify } from './Emojify';
|
||||
import { getPaymentEventDescription } from '../../messages/helpers';
|
||||
import { saveAttachment } from '../../util/saveAttachment';
|
||||
|
||||
const GUESS_METADATA_WIDTH_TIMESTAMP_SIZE = 10;
|
||||
const GUESS_METADATA_WIDTH_EXPIRE_TIMER_SIZE = 18;
|
||||
|
@ -318,16 +319,11 @@ export type PropsActions = {
|
|||
messageId: string;
|
||||
}) => void;
|
||||
markViewed(messageId: string): void;
|
||||
showVisualAttachment: (options: {
|
||||
showLightbox: (options: {
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
}) => void;
|
||||
downloadAttachment: (options: {
|
||||
attachment: AttachmentType;
|
||||
timestamp: number;
|
||||
isDangerous: boolean;
|
||||
}) => void;
|
||||
displayTapToViewMessage: (messageId: string) => unknown;
|
||||
showLightboxForViewOnceMedia: (messageId: string) => unknown;
|
||||
|
||||
openLink: (url: string) => void;
|
||||
scrollToQuotedMessage: (options: {
|
||||
|
@ -847,7 +843,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
renderAudioAttachment,
|
||||
renderingContext,
|
||||
showMessageDetail,
|
||||
showVisualAttachment,
|
||||
showLightbox,
|
||||
shouldCollapseAbove,
|
||||
shouldCollapseBelow,
|
||||
status,
|
||||
|
@ -898,7 +894,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
reducedMotion={reducedMotion}
|
||||
onError={this.handleImageError}
|
||||
showVisualAttachment={() => {
|
||||
showVisualAttachment({
|
||||
showLightbox({
|
||||
attachment: firstAttachment,
|
||||
messageId: id,
|
||||
});
|
||||
|
@ -945,7 +941,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
if (!isDownloaded(attachment)) {
|
||||
kickOffAttachmentDownload({ attachment, messageId: id });
|
||||
} else {
|
||||
showVisualAttachment({ attachment, messageId: id });
|
||||
showLightbox({ attachment, messageId: id });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -2240,7 +2236,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
const {
|
||||
attachments,
|
||||
contact,
|
||||
displayTapToViewMessage,
|
||||
showLightboxForViewOnceMedia,
|
||||
direction,
|
||||
giftBadge,
|
||||
id,
|
||||
|
@ -2250,7 +2246,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
startConversation,
|
||||
openGiftBadge,
|
||||
showContactDetail,
|
||||
showVisualAttachment,
|
||||
showLightbox,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
} = this.props;
|
||||
|
@ -2291,7 +2287,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
displayTapToViewMessage(id);
|
||||
showLightboxForViewOnceMedia(id);
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -2328,7 +2324,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
|
||||
const attachment = attachments[0];
|
||||
|
||||
showVisualAttachment({ attachment, messageId: id });
|
||||
showLightbox({ attachment, messageId: id });
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -2384,13 +2380,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
};
|
||||
|
||||
public openGenericAttachment = (event?: React.MouseEvent): void => {
|
||||
const {
|
||||
id,
|
||||
attachments,
|
||||
downloadAttachment,
|
||||
timestamp,
|
||||
kickOffAttachmentDownload,
|
||||
} = this.props;
|
||||
const { id, attachments, timestamp, kickOffAttachmentDownload } =
|
||||
this.props;
|
||||
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
|
@ -2410,14 +2401,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
const { fileName } = attachment;
|
||||
const isDangerous = isFileDangerous(fileName || '');
|
||||
|
||||
downloadAttachment({
|
||||
isDangerous,
|
||||
attachment,
|
||||
timestamp,
|
||||
});
|
||||
saveAttachment(attachment, timestamp);
|
||||
};
|
||||
|
||||
public handleClick = (event: React.MouseEvent): void => {
|
||||
|
|
|
@ -73,7 +73,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
|
||||
checkForAccount: action('checkForAccount'),
|
||||
clearSelectedMessage: action('clearSelectedMessage'),
|
||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||
showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
|
||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
|
@ -90,7 +90,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
showExpiredOutgoingTapToViewToast: action(
|
||||
'showExpiredOutgoingTapToViewToast'
|
||||
),
|
||||
showVisualAttachment: action('showVisualAttachment'),
|
||||
showLightbox: action('showLightbox'),
|
||||
startConversation: action('startConversation'),
|
||||
viewStory: action('viewStory'),
|
||||
});
|
||||
|
|
|
@ -79,7 +79,6 @@ export type PropsData = {
|
|||
|
||||
export type PropsBackboneActions = Pick<
|
||||
MessagePropsType,
|
||||
| 'displayTapToViewMessage'
|
||||
| 'kickOffAttachmentDownload'
|
||||
| 'markAttachmentAsCorrupted'
|
||||
| 'openConversation'
|
||||
|
@ -89,16 +88,17 @@ export type PropsBackboneActions = Pick<
|
|||
| 'showContactDetail'
|
||||
| 'showExpiredIncomingTapToViewToast'
|
||||
| 'showExpiredOutgoingTapToViewToast'
|
||||
| 'showVisualAttachment'
|
||||
| 'startConversation'
|
||||
>;
|
||||
|
||||
export type PropsReduxActions = Pick<
|
||||
MessagePropsType,
|
||||
| 'checkForAccount'
|
||||
| 'clearSelectedMessage'
|
||||
| 'doubleCheckMissingQuoteReference'
|
||||
| 'checkForAccount'
|
||||
| 'showContactModal'
|
||||
| 'showLightbox'
|
||||
| 'showLightboxForViewOnceMedia'
|
||||
| 'viewStory'
|
||||
> & {
|
||||
toggleSafetyNumberModal: (contactId: string) => void;
|
||||
|
@ -280,7 +280,7 @@ export class MessageDetail extends React.Component<Props> {
|
|||
checkForAccount,
|
||||
clearSelectedMessage,
|
||||
contactNameColor,
|
||||
displayTapToViewMessage,
|
||||
showLightboxForViewOnceMedia,
|
||||
doubleCheckMissingQuoteReference,
|
||||
expirationTimestamp,
|
||||
getPreferredBadge,
|
||||
|
@ -297,7 +297,7 @@ export class MessageDetail extends React.Component<Props> {
|
|||
showContactModal,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showVisualAttachment,
|
||||
showLightbox,
|
||||
startConversation,
|
||||
theme,
|
||||
viewStory,
|
||||
|
@ -325,10 +325,7 @@ export class MessageDetail extends React.Component<Props> {
|
|||
menu={undefined}
|
||||
disableScroll
|
||||
displayLimit={Number.MAX_SAFE_INTEGER}
|
||||
displayTapToViewMessage={displayTapToViewMessage}
|
||||
downloadAttachment={() =>
|
||||
log.warn('MessageDetail: downloadAttachment called!')
|
||||
}
|
||||
showLightboxForViewOnceMedia={showLightboxForViewOnceMedia}
|
||||
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
|
@ -358,7 +355,7 @@ export class MessageDetail extends React.Component<Props> {
|
|||
showMessageDetail={() => {
|
||||
log.warn('MessageDetail: showMessageDetail called!');
|
||||
}}
|
||||
showVisualAttachment={showVisualAttachment}
|
||||
showLightbox={showLightbox}
|
||||
startConversation={startConversation}
|
||||
theme={theme}
|
||||
viewStory={viewStory}
|
||||
|
|
|
@ -97,8 +97,7 @@ const defaultMessageProps: TimelineMessagesProps = {
|
|||
deleteMessage: action('default--deleteMessage'),
|
||||
deleteMessageForEveryone: action('default--deleteMessageForEveryone'),
|
||||
direction: 'incoming',
|
||||
displayTapToViewMessage: action('default--displayTapToViewMessage'),
|
||||
downloadAttachment: action('default--downloadAttachment'),
|
||||
showLightboxForViewOnceMedia: action('default--showLightboxForViewOnceMedia'),
|
||||
doubleCheckMissingQuoteReference: action(
|
||||
'default--doubleCheckMissingQuoteReference'
|
||||
),
|
||||
|
@ -140,7 +139,7 @@ const defaultMessageProps: TimelineMessagesProps = {
|
|||
),
|
||||
toggleForwardMessageModal: action('default--toggleForwardMessageModal'),
|
||||
showMessageDetail: action('default--showMessageDetail'),
|
||||
showVisualAttachment: action('default--showVisualAttachment'),
|
||||
showLightbox: action('default--showLightbox'),
|
||||
startConversation: action('default--startConversation'),
|
||||
status: 'sent',
|
||||
text: 'This is really interesting.',
|
||||
|
|
|
@ -289,9 +289,8 @@ const actions = () => ({
|
|||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
markViewed: action('markViewed'),
|
||||
messageExpanded: action('messageExpanded'),
|
||||
showVisualAttachment: action('showVisualAttachment'),
|
||||
downloadAttachment: action('downloadAttachment'),
|
||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||
showLightbox: action('showLightbox'),
|
||||
showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
|
||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||
|
||||
openLink: action('openLink'),
|
||||
|
|
|
@ -253,9 +253,8 @@ const getActions = createSelector(
|
|||
'kickOffAttachmentDownload',
|
||||
'markAttachmentAsCorrupted',
|
||||
'messageExpanded',
|
||||
'showVisualAttachment',
|
||||
'downloadAttachment',
|
||||
'displayTapToViewMessage',
|
||||
'showLightbox',
|
||||
'showLightboxForViewOnceMedia',
|
||||
'openLink',
|
||||
'scrollToQuotedMessage',
|
||||
'showExpiredIncomingTapToViewToast',
|
||||
|
|
|
@ -82,10 +82,9 @@ const getDefaultProps = () => ({
|
|||
openGiftBadge: action('openGiftBadge'),
|
||||
showContactDetail: action('showContactDetail'),
|
||||
showContactModal: action('showContactModal'),
|
||||
showLightbox: action('showLightbox'),
|
||||
toggleForwardMessageModal: action('toggleForwardMessageModal'),
|
||||
showVisualAttachment: action('showVisualAttachment'),
|
||||
downloadAttachment: action('downloadAttachment'),
|
||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||
showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
|
||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||
showExpiredIncomingTapToViewToast: action(
|
||||
'showExpiredIncomingTapToViewToast'
|
||||
|
|
|
@ -249,9 +249,8 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
// disableMenu: overrideProps.disableMenu,
|
||||
disableScroll: overrideProps.disableScroll,
|
||||
direction: overrideProps.direction || 'incoming',
|
||||
displayTapToViewMessage: action('displayTapToViewMessage'),
|
||||
showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
|
||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||
downloadAttachment: action('downloadAttachment'),
|
||||
expirationLength:
|
||||
number('expirationLength', overrideProps.expirationLength || 0) ||
|
||||
undefined,
|
||||
|
@ -318,7 +317,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
),
|
||||
toggleForwardMessageModal: action('toggleForwardMessageModal'),
|
||||
showMessageDetail: action('showMessageDetail'),
|
||||
showVisualAttachment: action('showVisualAttachment'),
|
||||
showLightbox: action('showLightbox'),
|
||||
startConversation: action('startConversation'),
|
||||
status: overrideProps.status || 'sent',
|
||||
text: overrideProps.text || text('text', ''),
|
||||
|
|
|
@ -12,7 +12,6 @@ import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preve
|
|||
import { isDownloaded } from '../../types/Attachment';
|
||||
import type { LocalizerType } from '../../types/I18N';
|
||||
import { handleOutsideClick } from '../../util/handleOutsideClick';
|
||||
import { isFileDangerous } from '../../util/isFileDangerous';
|
||||
import { offsetDistanceModifier } from '../../util/popperUtil';
|
||||
import { StopPropagation } from '../StopPropagation';
|
||||
import { WidthBreakpoint } from '../_util';
|
||||
|
@ -28,6 +27,7 @@ import { doesMessageBodyOverflow } from './MessageBodyReadMore';
|
|||
import type { Props as ReactionPickerProps } from './ReactionPicker';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts';
|
||||
import { saveAttachment } from '../../util/saveAttachment';
|
||||
|
||||
export type PropsData = {
|
||||
canDownload: boolean;
|
||||
|
@ -172,7 +172,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
});
|
||||
|
||||
const openGenericAttachment = (event?: React.MouseEvent): void => {
|
||||
const { downloadAttachment, kickOffAttachmentDownload } = props;
|
||||
const { kickOffAttachmentDownload } = props;
|
||||
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
|
@ -192,14 +192,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
return;
|
||||
}
|
||||
|
||||
const { fileName } = attachment;
|
||||
const isDangerous = isFileDangerous(fileName || '');
|
||||
|
||||
downloadAttachment({
|
||||
isDangerous,
|
||||
attachment,
|
||||
timestamp,
|
||||
});
|
||||
saveAttachment(attachment, timestamp);
|
||||
};
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent<HTMLDivElement>): void => {
|
||||
|
|
|
@ -88,7 +88,7 @@ const createProps = (
|
|||
),
|
||||
showConversation: action('showConversation'),
|
||||
showPendingInvites: action('showPendingInvites'),
|
||||
showLightboxForMedia: action('showLightboxForMedia'),
|
||||
showLightboxWithMedia: action('showLightboxWithMedia'),
|
||||
updateGroupAttributes: async () => {
|
||||
action('updateGroupAttributes')();
|
||||
},
|
||||
|
|
|
@ -17,7 +17,6 @@ import { assertDev } from '../../../util/assert';
|
|||
import { getMutedUntilText } from '../../../util/getMutedUntilText';
|
||||
|
||||
import type { LocalizerType, ThemeType } from '../../../types/Util';
|
||||
import type { MediaItemType } from '../../../types/MediaItem';
|
||||
import type { BadgeType } from '../../../badges/types';
|
||||
import { missingCaseError } from '../../../util/missingCaseError';
|
||||
import { DurationInSeconds } from '../../../util/durations';
|
||||
|
@ -30,6 +29,7 @@ import { AddGroupMembersModal } from './AddGroupMembersModal';
|
|||
import { ConversationDetailsActions } from './ConversationDetailsActions';
|
||||
import { ConversationDetailsHeader } from './ConversationDetailsHeader';
|
||||
import { ConversationDetailsIcon, IconType } from './ConversationDetailsIcon';
|
||||
import type { Props as ConversationDetailsMediaListPropsType } from './ConversationDetailsMediaList';
|
||||
import { ConversationDetailsMediaList } from './ConversationDetailsMediaList';
|
||||
import type { GroupV2Membership } from './ConversationDetailsMembershipList';
|
||||
import { ConversationDetailsMembershipList } from './ConversationDetailsMembershipList';
|
||||
|
@ -84,10 +84,6 @@ export type StateProps = {
|
|||
showGroupLinkManagement: () => void;
|
||||
showGroupV2Permissions: () => void;
|
||||
showPendingInvites: () => void;
|
||||
showLightboxForMedia: (
|
||||
selectedMediaItem: MediaItemType,
|
||||
media: Array<MediaItemType>
|
||||
) => void;
|
||||
showConversationNotificationsSettings: () => void;
|
||||
updateGroupAttributes: (
|
||||
_: Readonly<{
|
||||
|
@ -123,7 +119,7 @@ type ActionProps = {
|
|||
showConversation: ShowConversationType;
|
||||
toggleAddUserToAnotherGroupModal: (contactId?: string) => void;
|
||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||
};
|
||||
} & Pick<ConversationDetailsMediaListPropsType, 'showLightboxWithMedia'>;
|
||||
|
||||
export type Props = StateProps & ActionProps;
|
||||
|
||||
|
@ -167,7 +163,7 @@ export function ConversationDetails({
|
|||
showConversation,
|
||||
showGroupLinkManagement,
|
||||
showGroupV2Permissions,
|
||||
showLightboxForMedia,
|
||||
showLightboxWithMedia,
|
||||
showPendingInvites,
|
||||
theme,
|
||||
toggleSafetyNumberModal,
|
||||
|
@ -536,7 +532,7 @@ export function ConversationDetails({
|
|||
i18n={i18n}
|
||||
loadRecentMediaItems={loadRecentMediaItems}
|
||||
showAllMedia={showAllMedia}
|
||||
showLightboxForMedia={showLightboxForMedia}
|
||||
showLightboxWithMedia={showLightboxWithMedia}
|
||||
/>
|
||||
|
||||
{!isGroup && !conversation.isMe && (
|
||||
|
|
|
@ -30,7 +30,7 @@ const createProps = (mediaItems?: Array<MediaItemType>): Props => ({
|
|||
i18n,
|
||||
loadRecentMediaItems: action('loadRecentMediaItems'),
|
||||
showAllMedia: action('showAllMedia'),
|
||||
showLightboxForMedia: action('showLightboxForMedia'),
|
||||
showLightboxWithMedia: action('showLightboxWithMedia'),
|
||||
});
|
||||
|
||||
export function Basic(): JSX.Element {
|
||||
|
|
|
@ -17,8 +17,8 @@ export type Props = {
|
|||
i18n: LocalizerType;
|
||||
loadRecentMediaItems: (id: string, limit: number) => void;
|
||||
showAllMedia: () => void;
|
||||
showLightboxForMedia: (
|
||||
selectedMediaItem: MediaItemType,
|
||||
showLightboxWithMedia: (
|
||||
selectedAttachmentPath: string | undefined,
|
||||
media: Array<MediaItemType>
|
||||
) => void;
|
||||
};
|
||||
|
@ -32,7 +32,7 @@ export function ConversationDetailsMediaList({
|
|||
i18n,
|
||||
loadRecentMediaItems,
|
||||
showAllMedia,
|
||||
showLightboxForMedia,
|
||||
showLightboxWithMedia,
|
||||
}: Props): JSX.Element | null {
|
||||
const mediaItems = conversation.recentMediaItems || [];
|
||||
|
||||
|
@ -65,7 +65,9 @@ export function ConversationDetailsMediaList({
|
|||
key={`${mediaItem.message.id}-${mediaItem.index}`}
|
||||
mediaItem={mediaItem}
|
||||
i18n={i18n}
|
||||
onClick={() => showLightboxForMedia(mediaItem, mediaItems)}
|
||||
onClick={() =>
|
||||
showLightboxWithMedia(mediaItem.attachment.path, mediaItems)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -56,6 +56,9 @@ class ExpiringMessagesDeletionService {
|
|||
|
||||
// We do this to update the UI, if this message is being displayed somewhere
|
||||
message.trigger('expired');
|
||||
window.reduxActions.lightbox.closeLightboxIfViewingExpiredMessage(
|
||||
message.id
|
||||
);
|
||||
|
||||
if (conversation) {
|
||||
// An expired message only counts as decrementing the message count, not
|
||||
|
|
|
@ -24,6 +24,9 @@ async function eraseTapToViewMessages() {
|
|||
|
||||
// We do this to update the UI, if this message is being displayed somewhere
|
||||
message.trigger('expired');
|
||||
window.reduxActions.lightbox.closeLightboxIfViewingExpiredMessage(
|
||||
message.id
|
||||
);
|
||||
|
||||
await message.eraseContents();
|
||||
})
|
||||
|
|
|
@ -14,6 +14,7 @@ import { actions as emojis } from './ducks/emojis';
|
|||
import { actions as expiration } from './ducks/expiration';
|
||||
import { actions as globalModals } from './ducks/globalModals';
|
||||
import { actions as items } from './ducks/items';
|
||||
import { actions as lightbox } from './ducks/lightbox';
|
||||
import { actions as linkPreviews } from './ducks/linkPreviews';
|
||||
import { actions as network } from './ducks/network';
|
||||
import { actions as safetyNumber } from './ducks/safetyNumber';
|
||||
|
@ -41,6 +42,7 @@ export const actionCreators: ReduxActions = {
|
|||
expiration,
|
||||
globalModals,
|
||||
items,
|
||||
lightbox,
|
||||
linkPreviews,
|
||||
network,
|
||||
safetyNumber,
|
||||
|
@ -68,6 +70,7 @@ export const mapDispatchToProps = {
|
|||
...expiration,
|
||||
...globalModals,
|
||||
...items,
|
||||
...lightbox,
|
||||
...linkPreviews,
|
||||
...network,
|
||||
...safetyNumber,
|
||||
|
|
|
@ -177,7 +177,7 @@ type HideSendAnywayDialogActiontype = {
|
|||
type: typeof HIDE_SEND_ANYWAY_DIALOG;
|
||||
};
|
||||
|
||||
type ShowStickerPackPreviewActionType = {
|
||||
export type ShowStickerPackPreviewActionType = {
|
||||
type: typeof SHOW_STICKER_PACK_PREVIEW;
|
||||
payload: string;
|
||||
};
|
||||
|
@ -454,7 +454,7 @@ function closeStickerPackPreview(): ThunkAction<
|
|||
};
|
||||
}
|
||||
|
||||
function showStickerPackPreview(
|
||||
export function showStickerPackPreview(
|
||||
packId: string,
|
||||
packKey: string
|
||||
): ShowStickerPackPreviewActionType {
|
||||
|
|
339
ts/state/ducks/lightbox.ts
Normal file
339
ts/state/ducks/lightbox.ts
Normal 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;
|
||||
}
|
|
@ -11,6 +11,7 @@ import { getEmptyState as conversations } from './ducks/conversations';
|
|||
import { getEmptyState as crashReports } from './ducks/crashReports';
|
||||
import { getEmptyState as expiration } from './ducks/expiration';
|
||||
import { getEmptyState as globalModals } from './ducks/globalModals';
|
||||
import { getEmptyState as lightbox } from './ducks/lightbox';
|
||||
import { getEmptyState as linkPreviews } from './ducks/linkPreviews';
|
||||
import { getEmptyState as network } from './ducks/network';
|
||||
import { getEmptyState as preferredReactions } from './ducks/preferredReactions';
|
||||
|
@ -103,6 +104,7 @@ export function getInitialState({
|
|||
expiration: expiration(),
|
||||
globalModals: globalModals(),
|
||||
items,
|
||||
lightbox: lightbox(),
|
||||
linkPreviews: linkPreviews(),
|
||||
network: network(),
|
||||
preferredReactions: preferredReactions(),
|
||||
|
|
|
@ -16,6 +16,7 @@ import { reducer as emojis } from './ducks/emojis';
|
|||
import { reducer as expiration } from './ducks/expiration';
|
||||
import { reducer as globalModals } from './ducks/globalModals';
|
||||
import { reducer as items } from './ducks/items';
|
||||
import { reducer as lightbox } from './ducks/lightbox';
|
||||
import { reducer as linkPreviews } from './ducks/linkPreviews';
|
||||
import { reducer as network } from './ducks/network';
|
||||
import { reducer as preferredReactions } from './ducks/preferredReactions';
|
||||
|
@ -43,6 +44,7 @@ export const reducer = combineReducers({
|
|||
expiration,
|
||||
globalModals,
|
||||
items,
|
||||
lightbox,
|
||||
linkPreviews,
|
||||
network,
|
||||
preferredReactions,
|
||||
|
|
35
ts/state/selectors/lightbox.ts
Normal file
35
ts/state/selectors/lightbox.ts
Normal 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 : [])
|
||||
);
|
|
@ -11,6 +11,7 @@ import { SmartCallManager } from './CallManager';
|
|||
import { SmartCustomizingPreferredReactionsModal } from './CustomizingPreferredReactionsModal';
|
||||
import { SmartGlobalModalContainer } from './GlobalModalContainer';
|
||||
import { SmartLeftPane } from './LeftPane';
|
||||
import { SmartLightbox } from './Lightbox';
|
||||
import { SmartStories } from './Stories';
|
||||
import { SmartStoryViewer } from './StoryViewer';
|
||||
import type { StateType } from '../reducer';
|
||||
|
@ -55,6 +56,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
),
|
||||
renderGlobalModalContainer: () => <SmartGlobalModalContainer />,
|
||||
renderLeftPane: () => <SmartLeftPane />,
|
||||
renderLightbox: () => <SmartLightbox />,
|
||||
isShowingStoriesView: shouldShowStoriesView(state),
|
||||
renderStories: (closeView: () => unknown) => (
|
||||
<ErrorBoundary name="App/renderStories" closeView={closeView}>
|
||||
|
|
|
@ -18,7 +18,6 @@ import { getGroupMemberships } from '../../util/getGroupMemberships';
|
|||
import { getActiveCallState } from '../selectors/calling';
|
||||
import { getAreWeASubscriber } from '../selectors/items';
|
||||
import { getIntl, getTheme } from '../selectors/user';
|
||||
import type { MediaItemType } from '../../types/MediaItem';
|
||||
import {
|
||||
getBadgesSelector,
|
||||
getPreferredBadgeSelector,
|
||||
|
@ -44,10 +43,6 @@ export type SmartConversationDetailsProps = {
|
|||
showGroupV2Permissions: () => void;
|
||||
showConversationNotificationsSettings: () => void;
|
||||
showPendingInvites: () => void;
|
||||
showLightboxForMedia: (
|
||||
selectedMediaItem: MediaItemType,
|
||||
media: Array<MediaItemType>
|
||||
) => void;
|
||||
updateGroupAttributes: (
|
||||
_: Readonly<{
|
||||
avatar?: undefined | Uint8Array;
|
||||
|
|
52
ts/state/smart/Lightbox.tsx
Normal file
52
ts/state/smart/Lightbox.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -39,7 +39,6 @@ const mapStateToProps = (
|
|||
receivedAt,
|
||||
sentAt,
|
||||
|
||||
displayTapToViewMessage,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
openConversation,
|
||||
|
@ -48,7 +47,6 @@ const mapStateToProps = (
|
|||
showContactDetail,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showVisualAttachment,
|
||||
startConversation,
|
||||
} = props;
|
||||
|
||||
|
@ -75,7 +73,6 @@ const mapStateToProps = (
|
|||
interactionMode: getInteractionMode(state),
|
||||
theme: getTheme(state),
|
||||
|
||||
displayTapToViewMessage,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
markViewed,
|
||||
|
@ -86,7 +83,6 @@ const mapStateToProps = (
|
|||
showContactDetail,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showVisualAttachment,
|
||||
startConversation,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -66,8 +66,6 @@ export type TimelinePropsType = ExternalProps &
|
|||
| 'contactSupport'
|
||||
| 'blockGroupLinkRequests'
|
||||
| 'deleteMessage'
|
||||
| 'displayTapToViewMessage'
|
||||
| 'downloadAttachment'
|
||||
| 'downloadNewVersion'
|
||||
| 'kickOffAttachmentDownload'
|
||||
| 'learnMoreAboutDeliveryIssue'
|
||||
|
@ -88,7 +86,6 @@ export type TimelinePropsType = ExternalProps &
|
|||
| 'showExpiredIncomingTapToViewToast'
|
||||
| 'showExpiredOutgoingTapToViewToast'
|
||||
| 'showMessageDetail'
|
||||
| 'showVisualAttachment'
|
||||
| 'startConversation'
|
||||
| 'unblurAvatar'
|
||||
| 'updateSharedGroups'
|
||||
|
|
|
@ -14,6 +14,7 @@ import type { actions as emojis } from './ducks/emojis';
|
|||
import type { actions as expiration } from './ducks/expiration';
|
||||
import type { actions as globalModals } from './ducks/globalModals';
|
||||
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 network } from './ducks/network';
|
||||
import type { actions as safetyNumber } from './ducks/safetyNumber';
|
||||
|
@ -40,6 +41,7 @@ export type ReduxActions = {
|
|||
expiration: typeof expiration;
|
||||
globalModals: typeof globalModals;
|
||||
items: typeof items;
|
||||
lightbox: typeof lightbox;
|
||||
linkPreviews: typeof linkPreviews;
|
||||
network: typeof network;
|
||||
safetyNumber: typeof safetyNumber;
|
||||
|
|
|
@ -3,14 +3,26 @@
|
|||
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import * as Attachment from '../types/Attachment';
|
||||
import { showToast } from './showToast';
|
||||
import { ToastDangerousFileType } from '../components/ToastDangerousFileType';
|
||||
import { ToastFileSaved } from '../components/ToastFileSaved';
|
||||
import { isFileDangerous } from './isFileDangerous';
|
||||
import { showToast } from './showToast';
|
||||
import { getMessageById } from '../messages/getMessageById';
|
||||
|
||||
export async function saveAttachment(
|
||||
attachment: AttachmentType,
|
||||
timestamp = Date.now(),
|
||||
index = 0
|
||||
): Promise<void> {
|
||||
const { fileName = '' } = attachment;
|
||||
|
||||
const isDangerous = isFileDangerous(fileName);
|
||||
|
||||
if (isDangerous) {
|
||||
showToast(ToastDangerousFileType);
|
||||
return;
|
||||
}
|
||||
|
||||
const { openFileInFolder, readAttachmentData, saveAttachmentToDisk } =
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -40,7 +40,6 @@ import type { ToastReactionFailed } from '../components/ToastReactionFailed';
|
|||
import type { ToastStickerPackInstallFailed } from '../components/ToastStickerPackInstallFailed';
|
||||
import type { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming';
|
||||
import type { ToastTapToViewExpiredOutgoing } from '../components/ToastTapToViewExpiredOutgoing';
|
||||
import type { ToastUnableToLoadAttachment } from '../components/ToastUnableToLoadAttachment';
|
||||
import type { ToastVoiceNoteLimit } from '../components/ToastVoiceNoteLimit';
|
||||
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 ToastTapToViewExpiredIncoming): 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 ToastVoiceNoteMustBeOnlyAttachment
|
||||
|
|
|
@ -4,17 +4,15 @@
|
|||
/* eslint-disable camelcase */
|
||||
|
||||
import type * as Backbone from 'backbone';
|
||||
import type { ComponentProps } from 'react';
|
||||
import * as React from 'react';
|
||||
import { flatten } from 'lodash';
|
||||
import { render } from 'mustache';
|
||||
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import { isGIF } from '../types/Attachment';
|
||||
import type { MIMEType } from '../types/MIME';
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
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 { getContactId } from '../messages/helpers';
|
||||
import { strictAssert } from '../util/assert';
|
||||
|
@ -22,16 +20,10 @@ import { enqueueReactionForSend } from '../reactions/enqueueReactionForSend';
|
|||
import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameCollisions';
|
||||
import { isGroup } from '../util/whatTypeOfConversation';
|
||||
import { getPreferredBadgeSelector } from '../state/selectors/badges';
|
||||
import {
|
||||
isIncoming,
|
||||
isOutgoing,
|
||||
isTapToView,
|
||||
} from '../state/selectors/message';
|
||||
import { getConversationSelector } from '../state/selectors/conversations';
|
||||
import { isIncoming, isOutgoing } from '../state/selectors/message';
|
||||
import { getActiveCallState } from '../state/selectors/calling';
|
||||
import { getTheme } from '../state/selectors/user';
|
||||
import { ReactWrapperView } from './ReactWrapperView';
|
||||
import type { Lightbox } from '../components/Lightbox';
|
||||
import { ConversationDetailsMembershipList } from '../components/conversation/conversation-details/ConversationDetailsMembershipList';
|
||||
import * as log from '../logging/log';
|
||||
import type { EmbeddedContactType } from '../types/EmbeddedContact';
|
||||
|
@ -39,13 +31,11 @@ import { createConversationView } from '../state/roots/createConversationView';
|
|||
import { ToastConversationArchived } from '../components/ToastConversationArchived';
|
||||
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
|
||||
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
|
||||
import { ToastDangerousFileType } from '../components/ToastDangerousFileType';
|
||||
import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
|
||||
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
|
||||
import { ToastReactionFailed } from '../components/ToastReactionFailed';
|
||||
import { ToastTapToViewExpiredIncoming } from '../components/ToastTapToViewExpiredIncoming';
|
||||
import { ToastTapToViewExpiredOutgoing } from '../components/ToastTapToViewExpiredOutgoing';
|
||||
import { ToastUnableToLoadAttachment } from '../components/ToastUnableToLoadAttachment';
|
||||
import { ToastCannotOpenGiftBadge } from '../components/ToastCannotOpenGiftBadge';
|
||||
import { deleteDraftAttachment } from '../util/deleteDraftAttachment';
|
||||
import { retryMessageSend } from '../util/retryMessageSend';
|
||||
|
@ -62,7 +52,6 @@ import {
|
|||
removeLinkPreview,
|
||||
suspendLinkPreviews,
|
||||
} from '../services/LinkPreview';
|
||||
import { closeLightbox, showLightbox } from '../util/showLightbox';
|
||||
import { saveAttachment } from '../util/saveAttachment';
|
||||
import { SECOND } from '../util/durations';
|
||||
import { startConversation } from '../util/startConversation';
|
||||
|
@ -78,24 +67,13 @@ type PanelType = { view: Backbone.View; headerTitle?: string };
|
|||
|
||||
const { Message } = window.Signal.Types;
|
||||
|
||||
const {
|
||||
copyIntoTempDirectory,
|
||||
deleteTempFile,
|
||||
getAbsoluteAttachmentPath,
|
||||
getAbsoluteTempPath,
|
||||
upgradeMessageSchema,
|
||||
} = window.Signal.Migrations;
|
||||
const { getAbsoluteAttachmentPath, upgradeMessageSchema } =
|
||||
window.Signal.Migrations;
|
||||
|
||||
const { getMessagesBySentAt } = window.Signal.Data;
|
||||
|
||||
type MessageActionsType = {
|
||||
deleteMessage: (messageId: string) => unknown;
|
||||
displayTapToViewMessage: (messageId: string) => unknown;
|
||||
downloadAttachment: (options: {
|
||||
attachment: AttachmentType;
|
||||
timestamp: number;
|
||||
isDangerous: boolean;
|
||||
}) => unknown;
|
||||
downloadNewVersion: () => unknown;
|
||||
kickOffAttachmentDownload: (
|
||||
options: Readonly<{ messageId: string }>
|
||||
|
@ -120,11 +98,6 @@ type MessageActionsType = {
|
|||
showExpiredIncomingTapToViewToast: () => unknown;
|
||||
showExpiredOutgoingTapToViewToast: () => unknown;
|
||||
showMessageDetail: (messageId: string) => unknown;
|
||||
showVisualAttachment: (options: {
|
||||
attachment: AttachmentType;
|
||||
messageId: string;
|
||||
showSingle?: boolean;
|
||||
}) => 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, 'escape-pressed', this.resetPanel);
|
||||
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, 'remove-link-review', removeLinkPreview);
|
||||
this.listenTo(
|
||||
|
@ -481,22 +449,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
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 message = window.MessageController.getById(messageId);
|
||||
if (!message) {
|
||||
|
@ -523,8 +475,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
|
||||
return {
|
||||
deleteMessage,
|
||||
displayTapToViewMessage,
|
||||
downloadAttachment,
|
||||
downloadNewVersion,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
|
@ -538,7 +488,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showMessageDetail,
|
||||
showVisualAttachment,
|
||||
startConversation,
|
||||
};
|
||||
}
|
||||
|
@ -805,9 +754,10 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
}
|
||||
|
||||
case 'media': {
|
||||
const selectedMedia =
|
||||
media.find(item => attachment.path === item.path) || media[0];
|
||||
this.showLightboxForMedia(selectedMedia, media);
|
||||
window.reduxActions.lightbox.showLightboxWithMedia(
|
||||
attachment.path,
|
||||
media
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -907,131 +857,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
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 {
|
||||
const message = window.MessageController.getById(messageId);
|
||||
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 {
|
||||
const view = new ReactWrapperView({
|
||||
className: 'panel',
|
||||
|
@ -1290,7 +985,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
showConversationNotificationsSettings:
|
||||
this.showConversationNotificationsSettings.bind(this),
|
||||
showPendingInvites: this.showPendingInvites.bind(this),
|
||||
showLightboxForMedia: this.showLightboxForMedia.bind(this),
|
||||
updateGroupAttributes: this.model.updateGroupAttributesV2.bind(
|
||||
this.model
|
||||
),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue