diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 8d9c4933c..02ede7a9a 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -200,6 +200,7 @@ .module-message__generic-attachment__icon-container { position: relative; + cursor: pointer; } .module-message__generic-attachment__spinner-container { padding-inline-start: 4px; diff --git a/stylesheets/_session_conversation.scss b/stylesheets/_session_conversation.scss index 62c67bb66..75011e6c3 100644 --- a/stylesheets/_session_conversation.scss +++ b/stylesheets/_session_conversation.scss @@ -32,14 +32,15 @@ height: 100%; right: 0vw; - transition: transform 1.5 * $session-transition-duration ease-in-out; + transition: transform 0.3s ease-in-out; transform: translateX(100%); will-change: transform; width: 25vw; + z-index: 1; &.show { transform: none; - transition: transform $session-transition-duration ease-in-out; + transition: transform 0.3s ease-in-out; z-index: 2; } } diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx index 872601435..a60127d3a 100644 --- a/ts/components/Lightbox.tsx +++ b/ts/components/Lightbox.tsx @@ -14,6 +14,8 @@ import { DefaultTheme } from 'styled-components'; import useUnmount from 'react-use/lib/useUnmount'; import { useEncryptedFileFetch } from '../hooks/useEncryptedFileFetch'; import { darkTheme } from '../state/ducks/SessionTheme'; +import { useDispatch } from 'react-redux'; +import { showLightBox } from '../state/ducks/conversations'; const Colors = { TEXT_SECONDARY: '#bbb', @@ -29,7 +31,6 @@ const colorSVG = (url: string, color: string) => { }; type Props = { - close: () => void; contentType: MIME.MIMEType | undefined; objectURL: string; caption?: string; @@ -285,18 +286,19 @@ export const Lightbox = (props: Props) => { const containerRef = useRef(null); // there is no theme in use on the lightbox const theme = darkTheme; + const dispatch = useDispatch(); const { caption, contentType, objectURL, onNext, onPrevious, onSave } = props; const onObjectClick = (event: any) => { event.stopPropagation(); - props.close?.(); + dispatch(showLightBox(undefined)); }; const onContainerClick = (event: React.MouseEvent) => { if (containerRef && event.target !== containerRef.current) { return; } - props.close?.(); + dispatch(showLightBox(undefined)); }; return ( @@ -326,7 +328,7 @@ export const Lightbox = (props: Props) => { { - props.close?.(); + dispatch(showLightBox(undefined)); }} theme={theme} /> diff --git a/ts/components/LightboxGallery.tsx b/ts/components/LightboxGallery.tsx index 8f08361c3..81c16bb7a 100644 --- a/ts/components/LightboxGallery.tsx +++ b/ts/components/LightboxGallery.tsx @@ -9,6 +9,8 @@ import { Lightbox } from './Lightbox'; import { AttachmentTypeWithPath } from '../types/Attachment'; // tslint:disable-next-line: no-submodule-imports import useKey from 'react-use/lib/useKey'; +import { showLightBox } from '../state/ducks/conversations'; +import { useDispatch } from 'react-redux'; export interface MediaItemType { objectURL?: string; @@ -22,16 +24,17 @@ export interface MediaItemType { } type Props = { - close: () => void; media: Array; onSave?: (saveData: MediaItemType) => void; selectedIndex: number; }; export const LightboxGallery = (props: Props) => { - const { close, media, onSave } = props; + const { media, onSave } = props; const [currentIndex, setCurrentIndex] = useState(0); + const dispatch = useDispatch(); + // just run once, when the component is mounted. It's to show the lightbox on the specified index at start. useEffect(() => { setCurrentIndex(props.selectedIndex); @@ -86,12 +89,11 @@ export const LightboxGallery = (props: Props) => { ); useKey('Escape', () => { - props.close?.(); + dispatch(showLightBox(undefined)); }); return ( ; - onItemClick?: (event: ItemClickEvent) => void; -} +}; -export class AttachmentSection extends React.Component { - public render() { - const { type } = this.props; +const Items = (props: Props): JSX.Element => { + const { mediaItems, type } = props; + const selectedConversationKey = useSelector(getSelectedConversationKey); - return ( -
-
-
{this.renderItems()}
-
-
- ); - } - - private renderItems() { - const { mediaItems, type } = this.props; - - return mediaItems.map((mediaItem, position, array) => { - const shouldShowSeparator = position < array.length - 1; - const { index, attachment, messageTimestamp, messageId } = mediaItem; + return ( + <> + {mediaItems.map((mediaItem, position, array) => { + const shouldShowSeparator = position < array.length - 1; + const { index, attachment, messageTimestamp, messageId } = mediaItem; - const onClick = this.createClickHandler(mediaItem); - switch (type) { - case 'media': - return ( - - ); - case 'documents': - return ( - - ); - default: - return missingCaseError(type); - } - }); - } + switch (type) { + case 'media': + return ( + + ); + case 'documents': + return ( + + ); + default: + return missingCaseError(type); + } + })} + + ); +}; - private readonly createClickHandler = (mediaItem: MediaItemType) => () => { - const { onItemClick, type } = this.props; +export const AttachmentSection = (props: Props) => { + const { type } = props; - if (!onItemClick) { - return; - } - - onItemClick({ mediaItem, type }); - }; -} + return ( +
+
+
+ +
+
+
+ ); +}; diff --git a/ts/components/conversation/media-gallery/DocumentListItem.tsx b/ts/components/conversation/media-gallery/DocumentListItem.tsx index 1a765f29c..123073c37 100644 --- a/ts/components/conversation/media-gallery/DocumentListItem.tsx +++ b/ts/components/conversation/media-gallery/DocumentListItem.tsx @@ -4,6 +4,10 @@ import classNames from 'classnames'; import moment from 'moment'; // tslint:disable-next-line:match-default-export-name import formatFileSize from 'filesize'; +import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmentsManager'; +import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage'; +import { AttachmentTypeWithPath, save } from '../../../types/Attachment'; +import { MediaItemType } from '../../LightboxGallery'; type Props = { // Required @@ -12,8 +16,31 @@ type Props = { // Optional fileName?: string; fileSize?: number | null; - onClick?: () => void; shouldShowSeparator?: boolean; + mediaItem: MediaItemType; + conversationId: string; +}; + +const saveAttachment = async ({ + attachment, + messageTimestamp, + messageSender, + conversationId, +}: { + attachment: AttachmentTypeWithPath; + messageTimestamp: number; + messageSender: string; + conversationId: string; +}) => { + const timestamp = messageTimestamp; + attachment.url = await getDecryptedMediaUrl(attachment.url, attachment.contentType); + save({ + attachment, + document, + getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath, + timestamp, + }); + await sendDataExtractionNotification(conversationId, messageSender, timestamp); }; export const DocumentListItem = (props: Props) => { @@ -28,7 +55,18 @@ export const DocumentListItem = (props: Props) => { defaultShowSeparator ? 'module-document-list-item--with-separator' : null )} > -
+
{ + void saveAttachment({ + messageSender: props.mediaItem.messageSender, + messageTimestamp: props.mediaItem.messageTimestamp, + attachment: props.mediaItem.attachment, + conversationId: props.conversationId, + }); + }} + >
{fileName} diff --git a/ts/components/conversation/media-gallery/MediaGallery.tsx b/ts/components/conversation/media-gallery/MediaGallery.tsx index 2518327d4..847cd2558 100644 --- a/ts/components/conversation/media-gallery/MediaGallery.tsx +++ b/ts/components/conversation/media-gallery/MediaGallery.tsx @@ -1,26 +1,17 @@ -import React from 'react'; +import React, { useState } from 'react'; import classNames from 'classnames'; import { AttachmentSection } from './AttachmentSection'; import { EmptyState } from './EmptyState'; -import { ItemClickEvent } from './types/ItemClickEvent'; import { missingCaseError } from '../../../util/missingCaseError'; import { MediaItemType } from '../../LightboxGallery'; - -interface Props { +type Props = { documents: Array; media: Array; - onItemClick?: (event: ItemClickEvent) => void; -} - -interface State { - selectedTab: 'media' | 'documents'; -} +}; -interface TabSelectEvent { - type: 'media' | 'documents'; -} +type TabType = 'media' | 'documents'; const Tab = ({ isSelected, @@ -30,22 +21,16 @@ const Tab = ({ }: { isSelected: boolean; label: string; - onSelect?: (event: TabSelectEvent) => void; - type: 'media' | 'documents'; + onSelect: () => void; + type: TabType; }) => { - const handleClick = onSelect - ? () => { - onSelect({ type }); - } - : undefined; - return (
{label} @@ -53,72 +38,65 @@ const Tab = ({ ); }; -export class MediaGallery extends React.Component { - public state: State = { - selectedTab: 'media', - }; +const Sections = (props: Props & { selectedTab: TabType }) => { + const { media, documents, selectedTab } = props; - public render() { - const { selectedTab } = this.state; + const mediaItems = selectedTab === 'media' ? media : documents; + const type = selectedTab; - return ( -
-
- - -
-
{this.renderSections()}
-
- ); - } - - private readonly handleTabSelect = (event: TabSelectEvent): void => { - this.setState({ selectedTab: event.type }); - }; + if (!mediaItems || mediaItems.length === 0) { + const label = (() => { + switch (type) { + case 'media': + return window.i18n('mediaEmptyState'); - private renderSections() { - const { media, documents, onItemClick } = this.props; - const { selectedTab } = this.state; + case 'documents': + return window.i18n('documentsEmptyState'); - const mediaItems = selectedTab === 'media' ? media : documents; - const type = selectedTab; + default: + throw missingCaseError(type); + } + })(); - if (!mediaItems || mediaItems.length === 0) { - const label = (() => { - switch (type) { - case 'media': - return window.i18n('mediaEmptyState'); + return ; + } - case 'documents': - return window.i18n('documentsEmptyState'); + return ( +
+ +
+ ); +}; - default: - throw missingCaseError(type); - } - })(); +export const MediaGallery = (props: Props) => { + const [selectedTab, setSelectedTab] = useState('media'); - return ; - } + const isDocumentSelected = selectedTab === 'documents'; + const isMediaSelected = selectedTab === 'media'; - return ( -
- +
+ { + setSelectedTab('media'); + }} + /> + { + setSelectedTab('documents'); + }} />
- ); - } -} +
+ +
+
+ ); +}; diff --git a/ts/components/conversation/media-gallery/MediaGridItem.tsx b/ts/components/conversation/media-gallery/MediaGridItem.tsx index 79ec9e1b2..49f722445 100644 --- a/ts/components/conversation/media-gallery/MediaGridItem.tsx +++ b/ts/components/conversation/media-gallery/MediaGridItem.tsx @@ -4,10 +4,12 @@ import classNames from 'classnames'; import { isImageTypeSupported, isVideoTypeSupported } from '../../../util/GoogleChrome'; import { MediaItemType } from '../../LightboxGallery'; import { useEncryptedFileFetch } from '../../../hooks/useEncryptedFileFetch'; +import { showLightBox } from '../../../state/ducks/conversations'; +import { LightBoxOptions } from '../../session/conversation/SessionConversation'; type Props = { mediaItem: MediaItemType; - onClick?: () => void; + mediaItems: Array; }; const MediaGridItemContent = (props: Props) => { @@ -88,7 +90,18 @@ const MediaGridItemContent = (props: Props) => { export const MediaGridItem = (props: Props) => { return ( -
+
{ + const lightBoxOptions: LightBoxOptions = { + media: props.mediaItems, + attachment: props.mediaItem.attachment, + }; + + window.inboxStore?.dispatch(showLightBox(lightBoxOptions)); + }} + >
); diff --git a/ts/components/conversation/media-gallery/types/ItemClickEvent.ts b/ts/components/conversation/media-gallery/types/ItemClickEvent.ts deleted file mode 100644 index 841d4ae3d..000000000 --- a/ts/components/conversation/media-gallery/types/ItemClickEvent.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { MediaItemType } from '../../../LightboxGallery'; - -export interface ItemClickEvent { - mediaItem: MediaItemType; - type: 'media' | 'documents'; -} diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 5644e0ffd..daafe49be 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -23,6 +23,7 @@ import { PropsForMessage, ReduxConversationType, resetSelectedMessageIds, + showLightBox, SortedMessageModelProps, } from '../../../state/ducks/conversations'; import { MessageView } from '../../MainViewController'; @@ -47,9 +48,6 @@ interface State { // quoted message quotedMessageTimestamp?: number; quotedMessageProps?: any; - - // lightbox options - lightBoxOptions?: LightBoxOptions; } export interface LightBoxOptions { @@ -66,6 +64,9 @@ interface Props { selectedMessages: Array; showMessageDetails: boolean; isRightPanelShowing: boolean; + + // lightbox options + lightBoxOptions?: LightBoxOptions; } export class SessionConversation extends React.Component { @@ -175,13 +176,7 @@ export class SessionConversation extends React.Component { // ~~~~~~~~~~~~~~ RENDER METHODS ~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ public render() { - const { - showRecordingView, - quotedMessageProps, - lightBoxOptions, - isDraggingFile, - stagedAttachments, - } = this.state; + const { showRecordingView, quotedMessageProps, isDraggingFile, stagedAttachments } = this.state; const { selectedConversation, @@ -190,6 +185,7 @@ export class SessionConversation extends React.Component { showMessageDetails, selectedMessages, isRightPanelShowing, + lightBoxOptions, } = this.props; if (!selectedConversation || !messagesProps) { @@ -269,7 +265,7 @@ export class SessionConversation extends React.Component {
- +
); @@ -311,15 +307,6 @@ export class SessionConversation extends React.Component { }; } - // tslint:disable-next-line: max-func-body-length - public getRightPanelProps() { - return { - onShowLightBox: (lightBoxOptions?: LightBoxOptions) => { - this.setState({ lightBoxOptions }); - }, - }; - } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~ MICROPHONE METHODS ~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -390,7 +377,7 @@ export class SessionConversation extends React.Component { media: media as any, attachment, }; - this.setState({ lightBoxOptions }); + window.inboxStore?.dispatch(showLightBox(lightBoxOptions)); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -484,14 +471,7 @@ export class SessionConversation extends React.Component { : 0; console.warn('renderLightBox', { media, attachment }); return ( - { - this.setState({ lightBoxOptions: undefined }); - }} - selectedIndex={selectedIndex} - onSave={this.saveAttachment} - /> + ); } diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index 38f0e6a1b..2daf87eb4 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -4,23 +4,15 @@ import { Avatar, AvatarSize } from '../../Avatar'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../SessionButton'; import { SessionDropdown } from '../SessionDropdown'; import { MediaGallery } from '../../conversation/media-gallery/MediaGallery'; -import _, { noop } from 'lodash'; -import { TimerOption } from '../../conversation/ConversationHeader'; +import _ from 'lodash'; import { Constants } from '../../../session'; -import { - ConversationAvatar, - usingClosedConversationDetails, -} from '../usingClosedConversationDetails'; -import { AttachmentTypeWithPath, save } from '../../../types/Attachment'; -import { DefaultTheme, useTheme, withTheme } from 'styled-components'; +import { ConversationAvatar } from '../usingClosedConversationDetails'; +import { AttachmentTypeWithPath } from '../../../types/Attachment'; +import { useTheme } from 'styled-components'; import { getMessagesWithFileAttachments, getMessagesWithVisualMediaAttachments, } from '../../../data/data'; -import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmentsManager'; -import { LightBoxOptions } from './SessionConversation'; -import { UserUtils } from '../../../session/utils'; -import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage'; import { SpacerLG } from '../../basic/Text'; import { deleteMessagesByConvoIdWithConfirmation, @@ -32,30 +24,23 @@ import { showUpdateGroupMembersByConvoId, showUpdateGroupNameByConvoId, } from '../../../interactions/conversationInteractions'; -import { ItemClickEvent } from '../../conversation/media-gallery/types/ItemClickEvent'; import { MediaItemType } from '../../LightboxGallery'; // tslint:disable-next-line: no-submodule-imports import useInterval from 'react-use/lib/useInterval'; import { useDispatch, useSelector } from 'react-redux'; import { getTimerOptions } from '../../../state/selectors/timerOptions'; -import { getSelectedConversation, isRightPanelShowing } from '../../../state/selectors/conversations'; +import { + getSelectedConversation, + isRightPanelShowing, +} from '../../../state/selectors/conversations'; import { useMembersAvatars } from '../../../hooks/useMembersAvatar'; import { closeRightPanel } from '../../../state/ducks/conversations'; -type Props = { - memberAvatars?: Array; // this is added by usingClosedConversationDetails - - onShowLightBox: (lightboxOptions?: LightBoxOptions) => void; -}; - async function getMediaGalleryProps( - conversationId: string, - medias: Array, - onShowLightBox: (lightboxOptions?: LightBoxOptions) => void + conversationId: string ): Promise<{ documents: Array; media: Array; - onItemClick: any; }> { // We fetch more documents than media as they don’t require to be loaded // into memory right away. Revisit this once we have infinite scrolling: @@ -118,61 +103,9 @@ async function getMediaGalleryProps( }; }); - const saveAttachment = async ({ - attachment, - messageTimestamp, - messageSender, - }: { - attachment: AttachmentTypeWithPath; - messageTimestamp: number; - messageSender: string; - }) => { - const timestamp = messageTimestamp; - attachment.url = await getDecryptedMediaUrl(attachment.url, attachment.contentType); - save({ - attachment, - document, - getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath, - timestamp, - }); - await sendDataExtractionNotification(conversationId, messageSender, timestamp); - }; - - const onItemClick = (event: ItemClickEvent) => { - if (!event) { - console.warn('no event'); - return; - } - const { mediaItem, type } = event; - switch (type) { - case 'documents': { - void saveAttachment({ - messageSender: mediaItem.messageSender, - messageTimestamp: mediaItem.messageTimestamp, - attachment: mediaItem.attachment, - }); - break; - } - - case 'media': { - const lightBoxOptions: LightBoxOptions = { - media: medias, - attachment: mediaItem.attachment, - }; - - onShowLightBox(lightBoxOptions); - break; - } - - default: - throw new TypeError(`Unknown attachment type: '${type}'`); - } - }; - return { media, documents: _.compact(documents), // remove null - onItemClick, }; } @@ -239,31 +172,30 @@ const HeaderItem = () => { // tslint:disable: cyclomatic-complexity // tslint:disable: max-func-body-length -export const SessionRightPanelWithDetails = (props: Props) => { +export const SessionRightPanelWithDetails = () => { const [documents, setDocuments] = useState>([]); const [media, setMedia] = useState>([]); - const [onItemClick, setOnItemClick] = useState(undefined); const selectedConversation = useSelector(getSelectedConversation); const isShowing = useSelector(isRightPanelShowing); - console.warn('props', props); - useEffect(() => { let isRunning = true; if (isShowing && selectedConversation) { - void getMediaGalleryProps(selectedConversation.id, media, props.onShowLightBox).then( - results => { - console.warn('results2', results); + void getMediaGalleryProps(selectedConversation.id).then(results => { + console.warn('results2', results); - if (isRunning) { + if (isRunning) { + if (!_.isEqual(documents, results.documents)) { setDocuments(results.documents); + } + + if (!_.isEqual(media, results.media)) { setMedia(results.media); - setOnItemClick(results.onItemClick); } } - ); + }); } return () => { @@ -274,16 +206,10 @@ export const SessionRightPanelWithDetails = (props: Props) => { useInterval(async () => { if (isShowing && selectedConversation) { - const results = await getMediaGalleryProps( - selectedConversation.id, - media, - props.onShowLightBox - ); - console.warn('results', results); + const results = await getMediaGalleryProps(selectedConversation.id); if (results.documents.length !== documents.length || results.media.length !== media.length) { setDocuments(results.documents); setMedia(results.media); - setOnItemClick(results.onItemClick); } } }, 10000); @@ -319,8 +245,8 @@ export const SessionRightPanelWithDetails = (props: Props) => { const disappearingMessagesOptions = timerOptions.map(option => { return { content: option.name, - onClick: async () => { - await setDisappearingMessagesByConvoId(id, option.value); + onClick: () => { + void setDisappearingMessagesByConvoId(id, option.value); }, }; }); @@ -337,7 +263,6 @@ export const SessionRightPanelWithDetails = (props: Props) => { : () => { showLeaveGroupByConvoId(id); }; - console.warn('onItemClick', onItemClick); return (
@@ -404,7 +329,7 @@ export const SessionRightPanelWithDetails = (props: Props) => { /> )} - + {isGroup && ( // tslint:disable-next-line: use-simple-attributes ; + lightBox?: LightBoxOptions; }; async function getMessages( @@ -708,6 +710,13 @@ const conversationsSlice = createSlice({ state.messages = []; return state; }, + showLightBox( + state: ConversationsStateType, + action: PayloadAction + ) { + state.lightBox = action.payload; + return state; + }, }, extraReducers: (builder: any) => { // Add reducers for additional action types here, and handle loading state as needed @@ -752,4 +761,5 @@ export const { addMessageIdToSelection, resetSelectedMessageIds, toggleSelectedMessageId, + showLightBox, } = actions; diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 17717f69a..073f7585d 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -17,6 +17,7 @@ import { ConversationHeaderProps, ConversationHeaderTitleProps, } from '../../components/conversation/ConversationHeader'; +import { LightBoxOptions } from '../../components/session/conversation/SessionConversation'; export const getConversations = (state: StateType): ConversationsStateType => state.conversations; @@ -283,3 +284,8 @@ export const getSelectedMessageIds = createSelector( getConversations, (state: ConversationsStateType): Array => state.selectedMessageIds ); + +export const getLightBoxOptions = createSelector( + getConversations, + (state: ConversationsStateType): LightBoxOptions | undefined => state.lightBox +); diff --git a/ts/state/smart/SessionConversation.tsx b/ts/state/smart/SessionConversation.tsx index 50eebefdd..ec3ae9637 100644 --- a/ts/state/smart/SessionConversation.tsx +++ b/ts/state/smart/SessionConversation.tsx @@ -4,6 +4,7 @@ import { SessionConversation } from '../../components/session/conversation/Sessi import { StateType } from '../reducer'; import { getTheme } from '../selectors/theme'; import { + getLightBoxOptions, getMessagesOfSelectedConversation, getSelectedConversation, getSelectedConversationKey, @@ -23,6 +24,7 @@ const mapStateToProps = (state: StateType) => { showMessageDetails: isMessageDetailView(state), isRightPanelShowing: isRightPanelShowing(state), selectedMessages: getSelectedMessageIds(state), + lightBoxOptions: getLightBoxOptions(state), }; };