feat: updated MessageContent with nested components

pull/3017/head
William Grant 2 years ago
parent 22b0ab5f2f
commit 3bf3bf73b1

@ -95,25 +95,32 @@ export const MessageContentWithStatuses = (props: Props) => {
} }
}; };
const { messageId, ctxMenuID, isDetailView, dataTestId, enableReactions } = props; const { messageId, ctxMenuID, isDetailView = false, dataTestId, enableReactions } = props;
const [popupReaction, setPopupReaction] = useState(''); const [popupReaction, setPopupReaction] = useState('');
if (!contentProps) { if (!contentProps) {
return null; return null;
} }
const { conversationType, direction, isDeleted, isGroup } = contentProps; const { conversationType, direction: _direction, isDeleted, isGroup } = contentProps;
// NOTE we want messages on the left in the message detail view regardless of direction
const direction = isDetailView ? 'incoming' : _direction;
const isIncoming = direction === 'incoming'; const isIncoming = direction === 'incoming';
const isPrivate = conversationType === 'private'; const isPrivate = conversationType === 'private';
const hideAvatar = isPrivate || direction === 'outgoing'; const hideAvatar = isPrivate || direction === 'outgoing' || isDetailView;
const handleMessageReaction = async (emoji: string) => { const handleMessageReaction = async (emoji: string) => {
await Reactions.sendMessageReaction(messageId, emoji); await Reactions.sendMessageReaction(messageId, emoji);
}; };
const handlePopupClick = () => { const handlePopupClick = () => {
dispatch(updateReactListModal({ reaction: popupReaction, messageId })); dispatch(
updateReactListModal({
reaction: popupReaction,
messageId,
})
);
}; };
return ( return (
@ -138,15 +145,17 @@ export const MessageContentWithStatuses = (props: Props) => {
dataTestId="msg-status-incoming" dataTestId="msg-status-incoming"
messageId={messageId} messageId={messageId}
isCorrectSide={isIncoming} isCorrectSide={isIncoming}
isDetailView={isDetailView}
/> />
<StyledMessageWithAuthor isIncoming={isIncoming}> <StyledMessageWithAuthor isIncoming={isIncoming}>
<MessageAuthorText messageId={messageId} /> {!isDetailView && <MessageAuthorText messageId={messageId} />}
<MessageContent messageId={messageId} isDetailView={isDetailView} /> <MessageContent messageId={messageId} isDetailView={isDetailView} />
</StyledMessageWithAuthor> </StyledMessageWithAuthor>
<MessageStatus <MessageStatus
dataTestId="msg-status-outgoing" dataTestId="msg-status-outgoing"
messageId={messageId} messageId={messageId}
isCorrectSide={!isIncoming} isCorrectSide={!isIncoming}
isDetailView={isDetailView}
/> />
{!isDeleted && ( {!isDeleted && (
<MessageContextMenu <MessageContextMenu
@ -165,6 +174,7 @@ export const MessageContentWithStatuses = (props: Props) => {
setPopupReaction={setPopupReaction} setPopupReaction={setPopupReaction}
onPopupClick={handlePopupClick} onPopupClick={handlePopupClick}
noAvatar={hideAvatar} noAvatar={hideAvatar}
isDetailView={isDetailView}
/> />
)} )}
</StyledMessageContentContainer> </StyledMessageContentContainer>

@ -3,6 +3,7 @@ import React, { ReactElement, useEffect, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { useMessageReactsPropsById } from '../../../../hooks/useParamSelector'; import { useMessageReactsPropsById } from '../../../../hooks/useParamSelector';
import { MessageRenderingProps } from '../../../../models/messageType'; import { MessageRenderingProps } from '../../../../models/messageType';
import { REACT_LIMIT } from '../../../../session/constants';
import { useSelectedIsGroup } from '../../../../state/selectors/selectedConversation'; import { useSelectedIsGroup } from '../../../../state/selectors/selectedConversation';
import { SortedReactionList } from '../../../../types/Reaction'; import { SortedReactionList } from '../../../../types/Reaction';
import { nativeEmojiData } from '../../../../util/emoji'; import { nativeEmojiData } from '../../../../util/emoji';
@ -10,7 +11,6 @@ import { Flex } from '../../../basic/Flex';
import { SessionIcon } from '../../../icon'; import { SessionIcon } from '../../../icon';
import { Reaction, ReactionProps } from '../reactions/Reaction'; import { Reaction, ReactionProps } from '../reactions/Reaction';
import { StyledPopupContainer } from '../reactions/ReactionPopup'; import { StyledPopupContainer } from '../reactions/ReactionPopup';
import { REACT_LIMIT } from '../../../../session/constants';
export const popupXDefault = -81; export const popupXDefault = -81;
export const popupYDefault = -90; export const popupYDefault = -90;
@ -147,6 +147,7 @@ type Props = {
inModal?: boolean; inModal?: boolean;
onSelected?: (emoji: string) => boolean; onSelected?: (emoji: string) => boolean;
noAvatar: boolean; noAvatar: boolean;
isDetailView?: boolean;
}; };
export const MessageReactions = (props: Props) => { export const MessageReactions = (props: Props) => {
@ -160,6 +161,7 @@ export const MessageReactions = (props: Props) => {
inModal = false, inModal = false,
onSelected, onSelected,
noAvatar, noAvatar,
isDetailView,
} = props; } = props;
const [reactions, setReactions] = useState<SortedReactionList>([]); const [reactions, setReactions] = useState<SortedReactionList>([]);
@ -202,10 +204,10 @@ export const MessageReactions = (props: Props) => {
inGroup, inGroup,
handlePopupX: setPopupX, handlePopupX: setPopupX,
handlePopupY: setPopupY, handlePopupY: setPopupY,
onClick, onClick: !isDetailView ? onClick : undefined,
popupReaction, popupReaction,
onSelected, onSelected,
handlePopupReaction: setPopupReaction, handlePopupReaction: !isDetailView ? setPopupReaction : undefined,
handlePopupClick: onPopupClick, handlePopupClick: onPopupClick,
}; };

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { MessageRenderingProps } from '../../../../models/messageType'; import { MessageRenderingProps } from '../../../../models/messageType';
import { OutgoingMessageStatus } from './OutgoingMessageStatus';
import { useMessageDirection, useMessageStatus } from '../../../../state/selectors'; import { useMessageDirection, useMessageStatus } from '../../../../state/selectors';
import { OutgoingMessageStatus } from './OutgoingMessageStatus';
type Props = { type Props = {
isCorrectSide: boolean; isCorrectSide: boolean;
isDetailView: boolean;
messageId: string; messageId: string;
dataTestId?: string; dataTestId?: string;
}; };
@ -12,15 +13,15 @@ type Props = {
export type MessageStatusSelectorProps = Pick<MessageRenderingProps, 'direction' | 'status'>; export type MessageStatusSelectorProps = Pick<MessageRenderingProps, 'direction' | 'status'>;
export const MessageStatus = (props: Props) => { export const MessageStatus = (props: Props) => {
const { isCorrectSide, dataTestId } = props; const { messageId, isCorrectSide, isDetailView, dataTestId } = props;
const direction = useMessageDirection(props.messageId); const direction = useMessageDirection(props.messageId);
const status = useMessageStatus(props.messageId); const status = useMessageStatus(props.messageId);
if (!props.messageId) { if (!messageId) {
return null; return null;
} }
if (!isCorrectSide) { if (!isCorrectSide || !isDetailView) {
return null; return null;
} }
const isIncoming = direction === 'incoming'; const isIncoming = direction === 'incoming';
@ -30,5 +31,5 @@ export const MessageStatus = (props: Props) => {
return null; return null;
} }
return <OutgoingMessageStatus dataTestId={dataTestId} status={status} />; return <OutgoingMessageStatus messageId={messageId} dataTestId={dataTestId} status={status} />;
}; };

@ -1,8 +1,9 @@
import { ipcRenderer } from 'electron';
import React from 'react'; import React from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { LastMessageStatusType } from '../../../../state/ducks/conversations'; import { LastMessageStatusType } from '../../../../state/ducks/conversations';
import { SessionIcon } from '../../../icon'; import { SessionIcon } from '../../../icon';
import { showMessageInfoOverlay } from './MessageContextMenu';
const MessageStatusSendingContainer = styled.div` const MessageStatusSendingContainer = styled.div`
display: inline-block; display: inline-block;
@ -38,16 +39,24 @@ const MessageStatusRead = ({ dataTestId }: { dataTestId?: string }) => {
); );
}; };
const MessageStatusError = ({ dataTestId }: { dataTestId?: string }) => { const MessageStatusError = ({
const showDebugLog = () => { messageId,
ipcRenderer.send('show-debug-log'); dataTestId,
}; }: {
messageId?: string;
dataTestId?: string;
}) => {
const dispatch = useDispatch();
return ( return (
<MessageStatusSendingContainer <MessageStatusSendingContainer
data-testid={dataTestId} data-testid={dataTestId}
data-testtype="failed" data-testtype="failed"
onClick={showDebugLog} onClick={() => {
if (messageId) {
void showMessageInfoOverlay({ messageId, dispatch });
}
}}
title={window.i18n('sendFailed')} title={window.i18n('sendFailed')}
> >
<SessionIcon iconColor={'var(--danger-color'} iconType="error" iconSize="tiny" /> <SessionIcon iconColor={'var(--danger-color'} iconType="error" iconSize="tiny" />
@ -57,9 +66,10 @@ const MessageStatusError = ({ dataTestId }: { dataTestId?: string }) => {
export const OutgoingMessageStatus = (props: { export const OutgoingMessageStatus = (props: {
status: LastMessageStatusType | null; status: LastMessageStatusType | null;
messageId?: string;
dataTestId?: string; dataTestId?: string;
}) => { }) => {
const { status, dataTestId } = props; const { status, messageId, dataTestId } = props;
switch (status) { switch (status) {
case 'sending': case 'sending':
return <MessageStatusSending dataTestId={dataTestId} />; return <MessageStatusSending dataTestId={dataTestId} />;
@ -68,7 +78,7 @@ export const OutgoingMessageStatus = (props: {
case 'read': case 'read':
return <MessageStatusRead dataTestId={dataTestId} />; return <MessageStatusRead dataTestId={dataTestId} />;
case 'error': case 'error':
return <MessageStatusError dataTestId={dataTestId} />; return <MessageStatusError messageId={messageId} dataTestId={dataTestId} />;
default: default:
return null; return null;
} }

@ -41,11 +41,13 @@ const highlightedMessageAnimation = keyframes`
const StyledReadableMessage = styled.div<{ const StyledReadableMessage = styled.div<{
selected: boolean; selected: boolean;
isRightClicked: boolean; isRightClicked: boolean;
isDetailView?: boolean;
}>` }>`
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
letter-spacing: 0.03rem; letter-spacing: 0.03rem;
padding: ${props => (props.isDetailView ? '0' : 'var(--margins-xs) var(--margins-lg) 0')};
&.message-highlighted { &.message-highlighted {
animation: ${highlightedMessageAnimation} 1s ease-in-out; animation: ${highlightedMessageAnimation} 1s ease-in-out;
@ -151,6 +153,7 @@ export const GenericReadableMessage = (props: Props) => {
return ( return (
<StyledReadableMessage <StyledReadableMessage
selected={selected} selected={selected}
isDetailView={isDetailView}
isRightClicked={isRightClicked} isRightClicked={isRightClicked}
className={classNames(selected && 'message-selected')} className={classNames(selected && 'message-selected')}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}

@ -1,16 +1,24 @@
import React, { ReactElement, useRef, useState } from 'react'; import React, { ReactElement, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useMouse } from 'react-use'; import { useMouse } from 'react-use';
import styled from 'styled-components'; import styled from 'styled-components';
import { isUsAnySogsFromCache } from '../../../../session/apis/open_group_api/sogsv3/knownBlindedkeys'; import { isUsAnySogsFromCache } from '../../../../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { UserUtils } from '../../../../session/utils'; import { UserUtils } from '../../../../session/utils';
import { getRightOverlayMode } from '../../../../state/selectors/section';
import { useIsMessageSelectionMode } from '../../../../state/selectors/selectedConversation'; import { useIsMessageSelectionMode } from '../../../../state/selectors/selectedConversation';
import { THEME_GLOBALS } from '../../../../themes/globals';
import { SortedReactionList } from '../../../../types/Reaction'; import { SortedReactionList } from '../../../../types/Reaction';
import { abbreviateNumber } from '../../../../util/abbreviateNumber'; import { abbreviateNumber } from '../../../../util/abbreviateNumber';
import { nativeEmojiData } from '../../../../util/emoji'; import { nativeEmojiData } from '../../../../util/emoji';
import { popupXDefault, popupYDefault } from '../message-content/MessageReactions'; import { popupXDefault, popupYDefault } from '../message-content/MessageReactions';
import { POPUP_WIDTH, ReactionPopup, TipPosition } from './ReactionPopup'; import { POPUP_WIDTH, ReactionPopup, TipPosition } from './ReactionPopup';
const StyledReaction = styled.button<{ selected: boolean; inModal: boolean; showCount: boolean }>` const StyledReaction = styled.button<{
selected: boolean;
inModal: boolean;
showCount: boolean;
hasOnClick?: boolean;
}>`
display: flex; display: flex;
justify-content: ${props => (props.showCount ? 'flex-start' : 'center')}; justify-content: ${props => (props.showCount ? 'flex-start' : 'center')};
align-items: center; align-items: center;
@ -29,6 +37,8 @@ const StyledReaction = styled.button<{ selected: boolean; inModal: boolean; show
span { span {
width: 100%; width: 100%;
} }
${props => !props.hasOnClick && 'cursor: not-allowed;'}
`; `;
const StyledReactionContainer = styled.div<{ const StyledReactionContainer = styled.div<{
@ -46,7 +56,7 @@ export type ReactionProps = {
inGroup: boolean; inGroup: boolean;
handlePopupX: (x: number) => void; handlePopupX: (x: number) => void;
handlePopupY: (y: number) => void; handlePopupY: (y: number) => void;
onClick: (emoji: string) => void; onClick?: (emoji: string) => void;
popupReaction?: string; popupReaction?: string;
onSelected?: (emoji: string) => boolean; onSelected?: (emoji: string) => boolean;
handlePopupReaction?: (emoji: string) => void; handlePopupReaction?: (emoji: string) => void;
@ -69,6 +79,7 @@ export const Reaction = (props: ReactionProps): ReactElement => {
handlePopupClick, handlePopupClick,
} = props; } = props;
const rightOverlayMode = useSelector(getRightOverlayMode);
const isMessageSelection = useIsMessageSelectionMode(); const isMessageSelection = useIsMessageSelectionMode();
const reactionsMap = (reactions && Object.fromEntries(reactions)) || {}; const reactionsMap = (reactions && Object.fromEntries(reactions)) || {};
const senders = reactionsMap[emoji]?.senders || []; const senders = reactionsMap[emoji]?.senders || [];
@ -76,7 +87,7 @@ export const Reaction = (props: ReactionProps): ReactElement => {
const showCount = count !== undefined && (count > 1 || inGroup); const showCount = count !== undefined && (count > 1 || inGroup);
const reactionRef = useRef<HTMLDivElement>(null); const reactionRef = useRef<HTMLDivElement>(null);
const { docX, elW } = useMouse(reactionRef); const { docX: _docX, elW } = useMouse(reactionRef);
const gutterWidth = 380; // TODOLATER make this a variable which can be shared in CSS and JS const gutterWidth = 380; // TODOLATER make this a variable which can be shared in CSS and JS
const tooltipMidPoint = POPUP_WIDTH / 2; // px const tooltipMidPoint = POPUP_WIDTH / 2; // px
@ -96,7 +107,9 @@ export const Reaction = (props: ReactionProps): ReactElement => {
const handleReactionClick = () => { const handleReactionClick = () => {
if (!isMessageSelection) { if (!isMessageSelection) {
onClick(emoji); if (onClick) {
onClick(emoji);
}
} }
}; };
@ -107,12 +120,28 @@ export const Reaction = (props: ReactionProps): ReactElement => {
selected={selected()} selected={selected()}
inModal={inModal} inModal={inModal}
onClick={handleReactionClick} onClick={handleReactionClick}
hasOnClick={Boolean(onClick)}
onMouseEnter={() => { onMouseEnter={() => {
if (inGroup && !isMessageSelection) { if (inGroup && !isMessageSelection) {
const { innerWidth: windowWidth } = window; const { innerWidth } = window;
let windowWidth = innerWidth;
let docX = _docX;
// if the right panel is open we may need to show a reaction tooltip relative to it
if (rightOverlayMode && rightOverlayMode.type === 'message_info') {
const rightPanelWidth = Number(THEME_GLOBALS['--right-panel-width'].split('px')[0]);
// we need to check that the reaction we are hovering over is inside of the right panel and not in the messages list
if (docX > windowWidth - rightPanelWidth) {
// make the values relative to the right panel
docX = docX - windowWidth + rightPanelWidth;
windowWidth = rightPanelWidth;
}
}
if (handlePopupReaction) { if (handlePopupReaction) {
// overflow on far right means we shift left // overflow on far right means we shift left
if (docX + elW + tooltipMidPoint > windowWidth) { if (docX + elW + tooltipMidPoint > innerWidth) {
handlePopupX(Math.abs(popupXDefault) * 1.5 * -1); handlePopupX(Math.abs(popupXDefault) * 1.5 * -1);
setTooltipPosition('right'); setTooltipPosition('right');
// overflow onto conversations means we lock to the right // overflow onto conversations means we lock to the right

@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { SessionSettingCategory } from '../../components/settings/SessionSettings'; import { SessionSettingCategory } from '../../components/settings/SessionSettings';
import { OverlayMode, SectionStateType, SectionType } from '../ducks/section'; import { OverlayMode, RightOverlayMode, SectionStateType, SectionType } from '../ducks/section';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
export const getSection = (state: StateType): SectionStateType => state.section; export const getSection = (state: StateType): SectionStateType => state.section;
@ -31,6 +31,10 @@ export const getOverlayMode = createSelector(
(state: SectionStateType): OverlayMode | undefined => state.overlayMode (state: SectionStateType): OverlayMode | undefined => state.overlayMode
); );
export const getRightOverlayMode = (state: StateType): RightOverlayMode | undefined => {
return state.section.rightOverlayMode;
};
export const getIsMessageRequestOverlayShown = (state: StateType) => { export const getIsMessageRequestOverlayShown = (state: StateType) => {
const focusedSection = getFocusedSection(state); const focusedSection = getFocusedSection(state);
const overlayMode = getOverlayMode(state); const overlayMode = getOverlayMode(state);

@ -36,6 +36,7 @@ export type ThemeGlobals = {
'--main-view-header-height': string; '--main-view-header-height': string;
'--composition-container-height': string; '--composition-container-height': string;
'--search-input-height': string; '--search-input-height': string;
'--toggle-width': string;
/* Animations */ /* Animations */
'--default-duration': string; '--default-duration': string;
@ -83,6 +84,12 @@ export type ThemeGlobals = {
/* Also used for FileDropZone */ /* Also used for FileDropZone */
/* Used for Quote References Not Found */ /* Used for Quote References Not Found */
'--message-link-preview-background-color': string; '--message-link-preview-background-color': string;
/* Right Panel */
'--right-panel-width': string;
'--right-panel-height': string;
'--right-panel-attachment-width': string;
'--right-panel-attachment-height': string;
}; };
// These are only set once in the global style (at root). // These are only set once in the global style (at root).
@ -115,6 +122,7 @@ export const THEME_GLOBALS: ThemeGlobals = {
'--main-view-header-height': '68px', '--main-view-header-height': '68px',
'--composition-container-height': '60px', '--composition-container-height': '60px',
'--search-input-height': '34px', '--search-input-height': '34px',
'--toggle-width': '51px',
'--default-duration': '0.25s', '--default-duration': '0.25s',
@ -149,6 +157,11 @@ export const THEME_GLOBALS: ThemeGlobals = {
'--avatar-border-color': 'var(--transparent-color)', '--avatar-border-color': 'var(--transparent-color)',
'--message-link-preview-background-color': `rgba(${hexColorToRGB(COLORS.BLACK)}, 0.06)`, '--message-link-preview-background-color': `rgba(${hexColorToRGB(COLORS.BLACK)}, 0.06)`,
'--right-panel-width': '420px',
'--right-panel-height': '100%',
'--right-panel-attachment-width': '350px',
'--right-panel-attachment-height': '350px',
}; };
// These should only be needed for the global style (at root). // These should only be needed for the global style (at root).

Loading…
Cancel
Save