Moves showLightbox to redux

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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