diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ee95562c0..025dcbf8b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -527,5 +527,13 @@ "reactionListCountPlural": "And $otherPlural$ have reacted $emoji$ to this message", "setDisplayPicture": "Set Display Picture", "settingAppliesToEveryone": "This setting applies to everyone in this conversation.", - "onlyGroupAdminsCanChange": "Only group admins can change this setting." + "onlyGroupAdminsCanChange": "Only group admins can change this setting.", + "messageInfo": "Message Info", + "fileId": "File ID", + "fileSize": "File Size", + "fileType": "File Type", + "resolution": "Resolution", + "duration": "Duration", + "notApplicable": "N/A", + "unknownError": "Unknown Error" } diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss deleted file mode 100644 index 4d5406753..000000000 --- a/stylesheets/_conversation.scss +++ /dev/null @@ -1,6 +0,0 @@ -.message-detail-wrapper { - height: calc(100% - 48px); - width: 100%; - overflow-y: auto; - z-index: 2; -} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 671c514b5..10dcef5d8 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -20,7 +20,6 @@ // Build the main view @import 'index'; -@import 'conversation'; // /////////////////// // // ///// Session ///// // diff --git a/ts/components/conversation/SessionConversation.tsx b/ts/components/conversation/SessionConversation.tsx index 1626979e4..b81fc485a 100644 --- a/ts/components/conversation/SessionConversation.tsx +++ b/ts/components/conversation/SessionConversation.tsx @@ -52,7 +52,6 @@ import { InConversationCallContainer } from '../calling/InConversationCallContai import { LightboxGallery, MediaItemType } from '../lightbox/LightboxGallery'; import { NoMessageInConversation } from './SubtleNotification'; import { ConversationHeaderWithDetails } from './header/ConversationHeader'; -import { MessageDetail } from './message/message-item/MessageDetail'; import { isAudio } from '../../types/MIME'; import { HTMLDirection } from '../../util/i18n'; @@ -241,7 +240,6 @@ export class SessionConversation extends React.Component { ourDisplayNameInProfile, selectedConversation, messagesProps, - showMessageDetails, selectedMessages, isRightPanelShowing, lightBoxOptions, @@ -287,9 +285,6 @@ export class SessionConversation extends React.Component { onKeyDown={this.onKeyDown} role="navigation" > -
- -
{lightBoxOptions?.media && this.renderLightBox(lightBoxOptions)}
diff --git a/ts/components/conversation/message/message-item/MessageDetail.tsx b/ts/components/conversation/message/message-item/MessageDetail.tsx deleted file mode 100644 index 24653ca38..000000000 --- a/ts/components/conversation/message/message-item/MessageDetail.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import classNames from 'classnames'; -import moment from 'moment'; -import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import useKey from 'react-use/lib/useKey'; - -import { Message } from './Message'; - -import { deleteMessagesById } from '../../../../interactions/conversations/unsendingInteractions'; -import { - ContactPropsMessageDetail, - closeMessageDetailsView, -} from '../../../../state/ducks/conversations'; -import { getMessageDetailsViewProps } from '../../../../state/selectors/conversations'; -import { Avatar, AvatarSize } from '../../../avatar/Avatar'; -import { ContactName } from '../../ContactName'; - -import { useMessageIsDeletable } from '../../../../state/selectors'; -import { SessionButton, SessionButtonColor, SessionButtonType } from '../../../basic/SessionButton'; - -const AvatarItem = (props: { pubkey: string }) => { - const { pubkey } = props; - - return ; -}; - -const DeleteButtonItem = (props: { messageId: string; convoId: string; isDeletable: boolean }) => { - const { i18n } = window; - - return props.isDeletable ? ( -
- { - await deleteMessagesById([props.messageId], props.convoId); - }} - /> -
- ) : null; -}; - -const ContactsItem = (props: { contacts: Array }) => { - const { contacts } = props; - - if (!contacts || !contacts.length) { - return null; - } - - return ( -
- {contacts.map(contact => ( - - ))} -
- ); -}; - -const ContactItem = (props: { contact: ContactPropsMessageDetail }) => { - const { contact } = props; - const errors = contact.errors || []; - - const statusComponent = ( -
- ); - - return ( -
- -
-
- -
- {errors.map((error, index) => ( -
- {error.message} -
- ))} -
- {statusComponent} -
- ); -}; - -export const MessageDetail = () => { - const { i18n } = window; - - const messageDetailProps = useSelector(getMessageDetailsViewProps); - const isDeletable = useMessageIsDeletable(messageDetailProps?.messageId); - - const dispatch = useDispatch(); - - useKey('Escape', () => { - dispatch(closeMessageDetailsView()); - }); - - if (!messageDetailProps) { - return null; - } - - const { errors, receivedAt, sentAt, convoId, direction, messageId } = messageDetailProps; - - return ( -
-
-
- -
- - - {(errors || []).map((error, index) => ( - - - - - ))} - - - - - {receivedAt ? ( - - - - - ) : null} - - - - -
{i18n('error')} - {' '} - {error.message}{' '} -
{i18n('sent')} - {moment(sentAt).format('LLLL')} ({sentAt}) -
{i18n('received')} - {moment(receivedAt).format('LLLL')} ({receivedAt}) -
- {direction === 'incoming' ? i18n('from') : i18n('to')} -
- - -
-
- ); -}; diff --git a/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx b/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx index 8f6d5cdf6..7d15923a6 100644 --- a/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx +++ b/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx @@ -19,7 +19,7 @@ import { } from '../../../../interactions/conversationInteractions'; import { Constants } from '../../../../session'; import { closeRightPanel } from '../../../../state/ducks/conversations'; -import { setRightOverlayMode } from '../../../../state/ducks/section'; +import { resetRightOverlayMode, setRightOverlayMode } from '../../../../state/ducks/section'; import { useSelectedConversationKey, useSelectedDisplayNameInProfile, @@ -35,11 +35,13 @@ import { import { AttachmentTypeWithPath } from '../../../../types/Attachment'; import { getAbsoluteAttachmentPath } from '../../../../types/MessageAttachment'; import { Avatar, AvatarSize } from '../../../avatar/Avatar'; +import { Flex } from '../../../basic/Flex'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../../../basic/SessionButton'; -import { SpacerLG } from '../../../basic/Text'; +import { SpacerMD } from '../../../basic/Text'; import { PanelButtonGroup, PanelIconButton } from '../../../buttons'; import { MediaItemType } from '../../../lightbox/LightboxGallery'; import { MediaGallery } from '../../media-gallery/MediaGallery'; +import { Header } from './components'; async function getMediaGalleryProps( conversationId: string @@ -116,44 +118,62 @@ async function getMediaGalleryProps( const HeaderItem = () => { const selectedConvoKey = useSelectedConversationKey(); + const displayNameInProfile = useSelectedDisplayNameInProfile(); const dispatch = useDispatch(); const isBlocked = useSelectedIsBlocked(); const isKickedFromGroup = useSelectedIsKickedFromGroup(); const left = useSelectedIsLeft(); const isGroup = useSelectedIsGroup(); + const subscriberCount = useSelectedSubscriberCount(); if (!selectedConvoKey) { return null; } const showInviteContacts = isGroup && !isKickedFromGroup && !isBlocked && !left; + const showMemberCount = !!(subscriberCount && subscriberCount > 0); return ( -
- { - dispatch(closeRightPanel()); - }} - style={{ position: 'absolute' }} - dataTestId="back-button-conversation-options" - /> - - {showInviteContacts && ( - { - if (selectedConvoKey) { - showInviteContactByConvoId(selectedConvoKey); - } - }} - dataTestId="add-user-button" - /> +
{ + dispatch(closeRightPanel()); + dispatch(resetRightOverlayMode()); + }} + hideCloseButton={true} + > + + + {showInviteContacts && ( + { + if (selectedConvoKey) { + showInviteContactByConvoId(selectedConvoKey); + } + }} + style={{ position: 'absolute', right: '0px', top: '4px' }} + dataTestId="add-user-button" + /> + )} + + {displayNameInProfile} + {showMemberCount && ( + +
+ {window.i18n('members', [`${subscriberCount}`])} +
+ +
)} -
+ ); }; @@ -176,26 +196,6 @@ const StyledLeaveButton = styled.div` } `; -const StyledGroupSettingsItem = styled.div` - display: flex; - align-items: center; - min-height: 3rem; - font-size: var(--font-size-md); - color: var(--right-panel-item-text-color); - background-color: var(--right-panel-item-background-color); - border-top: 1px solid var(--border-color); - border-bottom: 1px solid var(--border-color); - - width: -webkit-fill-available; - padding: 0 var(--margins-md); - transition: var(--default-duration); - cursor: pointer; - - &:hover { - background-color: var(--right-panel-item-background-hover-color); - } -`; - const StyledName = styled.h4` padding-inline: var(--margins-md); font-size: var(--font-size-md); @@ -208,10 +208,8 @@ export const OverlayRightPanelSettings = () => { const selectedConvoKey = useSelectedConversationKey(); const dispatch = useDispatch(); const isShowing = useIsRightPanelShowing(); - const subscriberCount = useSelectedSubscriberCount(); const isActive = useSelectedIsActive(); - const displayNameInProfile = useSelectedDisplayNameInProfile(); const isBlocked = useSelectedIsBlocked(); const isKickedFromGroup = useSelectedIsKickedFromGroup(); const left = useSelectedIsLeft(); @@ -265,7 +263,6 @@ export const OverlayRightPanelSettings = () => { return null; } - const showMemberCount = !!(subscriberCount && subscriberCount > 0); const commonNoShow = isKickedFromGroup || left || isBlocked || !isActive; const hasDisappearingMessages = !isPublic && !commonNoShow; const leaveGroupString = isPublic @@ -292,65 +289,48 @@ export const OverlayRightPanelSettings = () => { return ( <> - {displayNameInProfile} - {showMemberCount && ( - <> - -
- {window.i18n('members', [`${subscriberCount}`])} -
- - - )} - {showUpdateGroupNameButton && ( - { - void showUpdateGroupNameByConvoId(selectedConvoKey); - }} - > - {isPublic ? window.i18n('editGroup') : window.i18n('editGroupName')} - - )} - {showAddRemoveModeratorsButton && ( - <> - + {showUpdateGroupNameButton && ( + { - showAddModeratorsByConvoId(selectedConvoKey); + void showUpdateGroupNameByConvoId(selectedConvoKey); }} - > - {window.i18n('addModerators')} - - + )} + + {showAddRemoveModeratorsButton && ( + <> + { + showAddModeratorsByConvoId(selectedConvoKey); + }} + /> + + { + showRemoveModeratorsByConvoId(selectedConvoKey); + }} + /> + + )} + + {showUpdateGroupMembersButton && ( + { - showRemoveModeratorsByConvoId(selectedConvoKey); + void showUpdateGroupMembersByConvoId(selectedConvoKey); }} - > - {window.i18n('removeModerators')} - - - )} - - {showUpdateGroupMembersButton && ( - { - void showUpdateGroupMembersByConvoId(selectedConvoKey); - }} - > - {window.i18n('groupMembers')} - - )} + /> + )} - {hasDisappearingMessages && ( - /* TODO Move ButtonGroup around all settings items */ - + {hasDisappearingMessages && ( { dispatch(setRightOverlayMode({ type: 'disappearing_messages', params: null })); }} /> - - )} - - - {isGroup && ( - - - - )} + )} + + + {isGroup && ( + + + + )} + ); }; diff --git a/ts/components/conversation/right-panel/overlay/components/Containers.tsx b/ts/components/conversation/right-panel/overlay/components/Containers.tsx new file mode 100644 index 000000000..7489477f1 --- /dev/null +++ b/ts/components/conversation/right-panel/overlay/components/Containers.tsx @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +export const StyledScrollContainer = styled.div` + width: 100%; + height: 100%; + overflow: hidden auto; +`; diff --git a/ts/components/conversation/right-panel/overlay/components/Header.tsx b/ts/components/conversation/right-panel/overlay/components/Header.tsx new file mode 100644 index 000000000..b2a84712f --- /dev/null +++ b/ts/components/conversation/right-panel/overlay/components/Header.tsx @@ -0,0 +1,89 @@ +import React, { ReactNode } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; +import { closeRightPanel } from '../../../../../state/ducks/conversations'; +import { resetRightOverlayMode } from '../../../../../state/ducks/section'; +import { Flex } from '../../../../basic/Flex'; +import { SessionIconButton } from '../../../../icon'; + +export const HeaderTitle = styled.h2` + font-family: var(--font-default); + font-size: var(--font-size-h2); + text-align: center; + margin-top: 0px; + margin-bottom: 0px; +`; + +export const HeaderSubtitle = styled.h3` + font-family: var(--font-default); + font-size: 11px; + font-weight: 400; + text-align: center; + padding-top: 0px; + margin-top: 0; +`; + +type HeaderProps = { + hideBackButton?: boolean; + backButtonDirection?: 'left' | 'right'; + backButtonOnClick?: () => void; + hideCloseButton?: boolean; + closeButtonOnClick?: () => void; + children?: ReactNode; +}; + +export const Header = (props: HeaderProps) => { + const { + children, + hideBackButton = false, + backButtonDirection = 'left', + backButtonOnClick, + hideCloseButton = false, + closeButtonOnClick, + } = props; + const dispatch = useDispatch(); + + return ( + + {!hideBackButton && ( + { + if (backButtonOnClick) { + backButtonOnClick(); + } else { + dispatch(resetRightOverlayMode()); + } + }} + dataTestId="back-button-conversation-options" + /> + )} + + {children} + + {!hideCloseButton && ( + { + if (closeButtonOnClick) { + closeButtonOnClick(); + } else { + dispatch(closeRightPanel()); + dispatch(resetRightOverlayMode()); + } + }} + /> + )} + + ); +}; diff --git a/ts/components/conversation/right-panel/overlay/components/index.tsx b/ts/components/conversation/right-panel/overlay/components/index.tsx new file mode 100644 index 000000000..970fd27ed --- /dev/null +++ b/ts/components/conversation/right-panel/overlay/components/index.tsx @@ -0,0 +1,4 @@ +import { StyledScrollContainer } from './Containers'; +import { Header, HeaderSubtitle, HeaderTitle } from './Header'; + +export { Header, HeaderSubtitle, HeaderTitle, StyledScrollContainer }; diff --git a/ts/components/conversation/right-panel/overlay/message-info/components/MessageInfo.tsx b/ts/components/conversation/right-panel/overlay/message-info/components/MessageInfo.tsx new file mode 100644 index 000000000..f4688d6c1 --- /dev/null +++ b/ts/components/conversation/right-panel/overlay/message-info/components/MessageInfo.tsx @@ -0,0 +1,97 @@ +import { ipcRenderer } from 'electron'; +import { isEmpty } from 'lodash'; +import moment from 'moment'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; +import { MessageFrom } from '.'; +import { getMessageDetailsViewProps } from '../../../../../../state/selectors/conversations'; +import { Flex } from '../../../../../basic/Flex'; +import { SpacerSM } from '../../../../../basic/Text'; + +export const MessageInfoLabel = styled.label<{ color?: string }>` + font-size: var(--font-size-lg); + font-weight: bold; + ${props => props.color && `color: ${props.color};`} +`; + +const MessageInfoData = styled.div<{ color?: string }>` + font-size: var(--font-size-md); + user-select: text; + ${props => props.color && `color: ${props.color};`} +`; + +const LabelWithInfoContainer = styled.div` + margin-bottom: var(--margins-md); + ${props => props.onClick && 'cursor: pointer;'} +`; + +type LabelWithInfoProps = { + label: string; + info: string; + labelColor?: string; + dataColor?: string; + title?: string; + onClick?: () => void; +}; + +export const LabelWithInfo = (props: LabelWithInfoProps) => { + return ( + + {props.label} + {props.info} + + ); +}; + +// Message timestamp format: "06:02 PM Tue, 15/11/2022" +const formatTimestamps = 'hh:mm A ddd, D/M/Y'; + +const showDebugLog = () => { + ipcRenderer.send('show-debug-log'); +}; + +export const MessageInfo = () => { + const messageDetailProps = useSelector(getMessageDetailsViewProps); + + if (!messageDetailProps) { + return null; + } + + const { errors, receivedAt, sentAt, direction, sender } = messageDetailProps; + + const sentAtStr = `${moment(sentAt).format(formatTimestamps)}`; + const receivedAtStr = `${moment(receivedAt).format(formatTimestamps)}`; + + const hasError = !isEmpty(errors); + const errorString = hasError + ? errors?.reduce((previous, current, currentIndex) => { + return `${previous}${current.message}${ + errors.length > 1 && currentIndex < errors.length - 1 ? ', ' : '' + }`; + }, '') + : null; + + return ( + + + {direction === 'incoming' ? ( + + ) : null} + + + {hasError && ( + <> + + + + )} + + ); +}; diff --git a/ts/components/icon/Icons.tsx b/ts/components/icon/Icons.tsx index 7965c6bd7..86b2d917d 100644 --- a/ts/components/icon/Icons.tsx +++ b/ts/components/icon/Icons.tsx @@ -2,6 +2,7 @@ /* eslint-disable no-multi-str */ export type SessionIconType = | 'addUser' + | 'addModerator' | 'arrow' | 'bell' | 'brand' @@ -22,6 +23,7 @@ export type SessionIconType = | 'crown' | 'communities' | 'delete' + | 'deleteModerator' | 'ellipses' | 'emoji' | 'error' @@ -31,6 +33,7 @@ export type SessionIconType = | 'fullscreen' | 'gear' | 'group' + | 'groupMembers' | 'hangup' | 'image' | 'info' @@ -52,6 +55,8 @@ export type SessionIconType = | 'plusThin' | 'plusFat' | 'reply' + | 'resend' + | 'saveToDisk' | 'save' | 'send' | 'search' @@ -91,6 +96,12 @@ export const icons: Record; + sender: string; contacts: Array; convoId: string; messageId: string; direction: MessageModelType; + attachments: Array; + timestamp?: number; + serverTimestamp?: number; }; export type LastMessageStatusType = 'sending' | 'sent' | 'read' | 'error' | undefined; @@ -148,6 +152,7 @@ export type PropsForAttachment = { size: number; width?: number; height?: number; + duration?: string; url: string; path: string; fileSize: string | null; diff --git a/ts/state/selectors/messages.ts b/ts/state/selectors/messages.ts index c23e9c41f..9acf29166 100644 --- a/ts/state/selectors/messages.ts +++ b/ts/state/selectors/messages.ts @@ -4,6 +4,7 @@ import { LastMessageStatusType, MessageModelPropsWithConvoProps, PropsForAttachment, + PropsForQuote, ReduxConversationType, } from '../ducks/conversations'; import { StateType } from '../reducer'; @@ -110,7 +111,7 @@ export function useMessageSender(messageId: string) { return useMessagePropsByMessageId(messageId)?.propsForMessage.sender; } -export function useMessageIsDeletableForEveryone(messageId: string) { +export function useMessageIsDeletableForEveryone(messageId: string | undefined) { return useMessagePropsByMessageId(messageId)?.propsForMessage.isDeletableForEveryone; } @@ -125,3 +126,11 @@ export function useMessageTimestamp(messageId: string) { export function useMessageBody(messageId: string) { return useMessagePropsByMessageId(messageId)?.propsForMessage.text; } + +export const useMessageQuote = (messageId: string | undefined): PropsForQuote | undefined => { + return useMessagePropsByMessageId(messageId)?.propsForMessage.quote; +}; + +export const useMessageText = (messageId: string | undefined): string | undefined => { + return useMessagePropsByMessageId(messageId)?.propsForMessage.text; +}; diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 89f6d81c9..ba389ddad 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -162,6 +162,7 @@ export type LocalizerKeys = | 'documentsEmptyState' | 'done' | 'downloadAttachment' + | 'duration' | 'editGroup' | 'editGroupName' | 'editMenuCopy' @@ -192,7 +193,10 @@ export type LocalizerKeys = | 'failedToAddAsModerator' | 'failedToRemoveFromModerator' | 'faq' + | 'fileId' + | 'fileSize' | 'fileSizeWarning' + | 'fileType' | 'from' | 'getStarted' | 'goToReleaseNotes' @@ -263,6 +267,7 @@ export type LocalizerKeys = | 'messageBodyMissing' | 'messageDeletedPlaceholder' | 'messageDeletionForbidden' + | 'messageInfo' | 'messageRequestAccepted' | 'messageRequestAcceptedOurs' | 'messageRequestAcceptedOursNoName' @@ -297,6 +302,7 @@ export type LocalizerKeys = | 'noModeratorsToRemove' | 'noNameOrMessage' | 'noSearchResults' + | 'notApplicable' | 'noteToSelf' | 'notificationForConvo' | 'notificationForConvo_all' @@ -389,6 +395,7 @@ export type LocalizerKeys = | 'requestsPlaceholder' | 'requestsSubtitle' | 'resend' + | 'resolution' | 'respondingToRequestWarning' | 'restoreUsingRecoveryPhrase' | 'ringing' @@ -495,6 +502,7 @@ export type LocalizerKeys = | 'unblocked' | 'unknown' | 'unknownCountry' + | 'unknownError' | 'unpinConversation' | 'unreadMessages' | 'updateGroupDialogTitle'