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 { 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }))}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 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 => {
|
||||||
|
|
|
@ -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'),
|
||||||
});
|
});
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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.',
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -253,9 +253,8 @@ const getActions = createSelector(
|
||||||
'kickOffAttachmentDownload',
|
'kickOffAttachmentDownload',
|
||||||
'markAttachmentAsCorrupted',
|
'markAttachmentAsCorrupted',
|
||||||
'messageExpanded',
|
'messageExpanded',
|
||||||
'showVisualAttachment',
|
'showLightbox',
|
||||||
'downloadAttachment',
|
'showLightboxForViewOnceMedia',
|
||||||
'displayTapToViewMessage',
|
|
||||||
'openLink',
|
'openLink',
|
||||||
'scrollToQuotedMessage',
|
'scrollToQuotedMessage',
|
||||||
'showExpiredIncomingTapToViewToast',
|
'showExpiredIncomingTapToViewToast',
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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', ''),
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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')();
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 && (
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
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 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(),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
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 { 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}>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
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,
|
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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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 { 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
|
||||||
|
|
|
@ -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
|
||||||
),
|
),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue