diff --git a/stylesheets/_modal.scss b/stylesheets/_modal.scss index 8446da1a30..43c27d01da 100644 --- a/stylesheets/_modal.scss +++ b/stylesheets/_modal.scss @@ -210,7 +210,6 @@ display: flex; align-items: center; justify-content: center; - margin-right: -20px; // offsets the edit icon button so it's centered p { font-size: $session-font-md; diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 0bd68a2f9c..3a7e348faf 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -609,6 +609,9 @@ flex-direction: column; align-items: stretch; overflow: hidden; + max-height: 100%; + display: flex; + gap: 5px; .session-icon-button:first-child { margin-right: var(--margins-sm); diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index 718467b78a..5216371f3d 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -1,12 +1,11 @@ -import React from 'react'; import classNames from 'classnames'; -import { CSSProperties } from 'styled-components'; +import React from 'react'; -import { Emojify } from './Emojify'; import { - useNicknameOrProfileNameOrShortenedPubkey, useIsPrivate, + useNicknameOrProfileNameOrShortenedPubkey, } from '../../hooks/useParamSelector'; +import { Emojify } from './Emojify'; type Props = { pubkey: string; @@ -25,12 +24,20 @@ export const ContactName = (props: Props) => { const convoName = useNicknameOrProfileNameOrShortenedPubkey(pubkey); const isPrivate = useIsPrivate(pubkey); const shouldShowProfile = Boolean(convoName || profileName || name); + + const commonStyles = { + 'min-width': 0, + 'text-overflow': 'ellipsis', + overflow: 'hidden', + } as React.CSSProperties; + const styles = ( boldProfileName ? { fontWeight: 'bold', + ...commonStyles, } - : {} + : commonStyles ) as React.CSSProperties; const textProfile = profileName || name || convoName || window.i18n('anonymous'); @@ -39,15 +46,19 @@ export const ContactName = (props: Props) => { className={classNames(prefix, compact && 'compact')} dir="auto" data-testid={`${prefix}__profile-name`} - style={{ textOverflow: 'inherit' }} + style={{ + textOverflow: 'inherit', + display: 'flex', + flexDirection: 'row', + gap: 'var(--margins-xs)', + }} > {shouldShowProfile ? ( - +
- +
) : null} - {shouldShowProfile ? ' ' : null} - {shouldShowPubkey ? {pubkey} : null} + {shouldShowPubkey ?
{pubkey}
: null}
); }; diff --git a/ts/components/conversation/ImageGrid.tsx b/ts/components/conversation/ImageGrid.tsx index 6d47e9b446..d4f9c3ce7c 100644 --- a/ts/components/conversation/ImageGrid.tsx +++ b/ts/components/conversation/ImageGrid.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { @@ -10,10 +10,10 @@ import { isVideoAttachment, } from '../../types/Attachment'; +import { useIsMessageVisible } from '../../contexts/isMessageVisibleContext'; import { useMessageSelected } from '../../state/selectors'; import { THUMBNAIL_SIDE } from '../../types/attachments/VisualAttachment'; import { Image } from './Image'; -import { IsMessageVisibleContext } from './message/message-content/MessageContent'; type Props = { attachments: Array; @@ -46,7 +46,7 @@ const Row = ( totalAttachmentsCount, selected, } = props; - const isMessageVisible = useContext(IsMessageVisibleContext); + const isMessageVisible = useIsMessageVisible(); const moreMessagesOverlay = totalAttachmentsCount > 3; const moreMessagesOverlayText = moreMessagesOverlay ? `+${totalAttachmentsCount - 3}` : undefined; diff --git a/ts/components/conversation/SessionLastSeenIndicator.tsx b/ts/components/conversation/SessionLastSeenIndicator.tsx index 9b65f07cd0..31ff6458af 100644 --- a/ts/components/conversation/SessionLastSeenIndicator.tsx +++ b/ts/components/conversation/SessionLastSeenIndicator.tsx @@ -1,9 +1,9 @@ -import React, { useContext, useLayoutEffect } from 'react'; +import React, { useLayoutEffect } from 'react'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; +import { useScrollToLoadedMessage } from '../../contexts/ScrollToLoadedMessage'; import { getQuotedMessageToAnimate } from '../../state/selectors/conversations'; import { isDarkTheme } from '../../state/selectors/theme'; -import { ScrollToLoadedMessageContext } from './SessionMessagesListContainer'; const LastSeenBar = styled.div` height: 2px; @@ -52,7 +52,7 @@ export const SessionLastSeenIndicator = (props: { const darkMode = useSelector(isDarkTheme); // if this unread-indicator is not unique it's going to cause issues const quotedMessageToAnimate = useSelector(getQuotedMessageToAnimate); - const scrollToLoadedMessage = useContext(ScrollToLoadedMessageContext); + const scrollToLoadedMessage = useScrollToLoadedMessage(); const { messageId, didScroll, setDidScroll } = props; diff --git a/ts/components/conversation/SessionMessagesList.tsx b/ts/components/conversation/SessionMessagesList.tsx index 30a0a04bf8..3f7f4d42aa 100644 --- a/ts/components/conversation/SessionMessagesList.tsx +++ b/ts/components/conversation/SessionMessagesList.tsx @@ -26,9 +26,10 @@ import { Message } from './message/message-item/Message'; import { MessageRequestResponse } from './message/message-item/MessageRequestResponse'; import { CallNotification } from './message/message-item/notification-bubble/CallNotification'; -import { DataExtractionNotification } from './message/message-item/DataExtractionNotification'; +import { IsDetailMessageViewContext } from '../../contexts/isDetailViewContext'; import { SessionLastSeenIndicator } from './SessionLastSeenIndicator'; import { TimerNotification } from './TimerNotification'; +import { DataExtractionNotification } from './message/message-item/DataExtractionNotification'; import { InteractionNotification } from './message/message-item/InteractionNotification'; function isNotTextboxEvent(e: KeyboardEvent) { @@ -98,7 +99,7 @@ export const SessionMessagesList = (props: { } return ( - <> + {messagesProps.map(messageProps => { const messageId = messageProps.message.props.messageId; const unreadIndicator = messageProps.showUnreadIndicator ? ( @@ -170,6 +171,6 @@ export const SessionMessagesList = (props: { return [, ...componentToMerge]; })} - + ); }; diff --git a/ts/components/conversation/SessionMessagesListContainer.tsx b/ts/components/conversation/SessionMessagesListContainer.tsx index 042b84a414..5da6eccfca 100644 --- a/ts/components/conversation/SessionMessagesListContainer.tsx +++ b/ts/components/conversation/SessionMessagesListContainer.tsx @@ -15,6 +15,10 @@ import { } from '../../state/ducks/conversations'; import { SessionScrollButton } from '../SessionScrollButton'; +import { + ScrollToLoadedMessageContext, + ScrollToLoadedReasons, +} from '../../contexts/ScrollToLoadedMessage'; import { StateType } from '../../state/reducer'; import { getQuotedMessageToAnimate, @@ -31,17 +35,6 @@ export type SessionMessageListProps = { }; export const messageContainerDomID = 'messages-container'; -export type ScrollToLoadedReasons = - | 'quote-or-search-result' - | 'go-to-bottom' - | 'unread-indicator' - | 'load-more-top' - | 'load-more-bottom'; - -export const ScrollToLoadedMessageContext = React.createContext( - (_loadedMessageIdToScrollTo: string, _reason: ScrollToLoadedReasons) => {} -); - type Props = SessionMessageListProps & { conversationKey?: string; messagesProps: Array; diff --git a/ts/components/conversation/message/message-content/MessageAvatar.tsx b/ts/components/conversation/message/message-content/MessageAvatar.tsx index 1cbe2dde80..e9b0dd9ff8 100644 --- a/ts/components/conversation/message/message-content/MessageAvatar.tsx +++ b/ts/components/conversation/message/message-content/MessageAvatar.tsx @@ -38,10 +38,10 @@ export type MessageAvatarSelectorProps = Pick< 'sender' | 'isSenderAdmin' | 'lastMessageOfSeries' >; -type Props = { messageId: string; hideAvatar: boolean; isPrivate: boolean; isDetailView?: boolean }; +type Props = { messageId: string; isPrivate: boolean }; export const MessageAvatar = (props: Props) => { - const { messageId, hideAvatar, isPrivate, isDetailView } = props; + const { messageId, isPrivate } = props; const dispatch = useDispatch(); const selectedConvoKey = useSelectedConversationKey(); @@ -137,13 +137,9 @@ export const MessageAvatar = (props: Props) => { // The styledAvatar, when rendered needs to have a width with margins included of var(--width-avatar-group-msg-list). // This is so that the other message is still aligned when the avatar is not rendered (we need to make up for the space used by the avatar, and we use a margin of width-avatar-group-msg-list) return ( - + - {!isDetailView && isSenderAdmin ? : null} + {isSenderAdmin ? : null} ); }; diff --git a/ts/components/conversation/message/message-content/MessageContent.tsx b/ts/components/conversation/message/message-content/MessageContent.tsx index 24a0619b23..1431855771 100644 --- a/ts/components/conversation/message/message-content/MessageContent.tsx +++ b/ts/components/conversation/message/message-content/MessageContent.tsx @@ -1,10 +1,13 @@ import classNames from 'classnames'; import { isEmpty } from 'lodash'; import moment from 'moment'; -import React, { createContext, useCallback, useContext, useLayoutEffect, useState } from 'react'; +import React, { useCallback, useLayoutEffect, useState } from 'react'; import { InView } from 'react-intersection-observer'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; +import { useScrollToLoadedMessage } from '../../../../contexts/ScrollToLoadedMessage'; +import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext'; +import { IsMessageVisibleContext } from '../../../../contexts/isMessageVisibleContext'; import { MessageModelType, MessageRenderingProps } from '../../../../models/messageType'; import { StateType } from '../../../../state/reducer'; import { @@ -19,7 +22,6 @@ import { } from '../../../../state/selectors/conversations'; import { useSelectedIsPrivate } from '../../../../state/selectors/selectedConversation'; import { canDisplayImage } from '../../../../types/Attachment'; -import { ScrollToLoadedMessageContext } from '../../SessionMessagesListContainer'; import { MessageAttachment } from './MessageAttachment'; import { MessageAvatar } from './MessageAvatar'; import { MessageHighlighter } from './MessageHighlighter'; @@ -34,7 +36,6 @@ export type MessageContentSelectorProps = Pick< type Props = { messageId: string; - isDetailView?: boolean; }; // TODO not too sure what is this doing? It is not preventDefault() @@ -76,13 +77,13 @@ const StyledMessageOpaqueContent = styled(MessageHighlighter)<{ ${props => props.selected && `box-shadow: var(--drop-shadow);`} `; -export const IsMessageVisibleContext = createContext(false); - const StyledAvatarContainer = styled.div` align-self: flex-end; `; export const MessageContent = (props: Props) => { + const isDetailView = useIsDetailMessageView(); + const [highlight, setHighlight] = useState(false); const [didScroll, setDidScroll] = useState(false); const contentProps = useSelector((state: StateType) => @@ -91,9 +92,9 @@ export const MessageContent = (props: Props) => { const isDeleted = useMessageIsDeleted(props.messageId); const [isMessageVisible, setMessageIsVisible] = useState(false); - const scrollToLoadedMessage = useContext(ScrollToLoadedMessageContext); + const scrollToLoadedMessage = useScrollToLoadedMessage(); const selectedIsPrivate = useSelectedIsPrivate(); - const hideAvatar = useHideAvatarInMsgList(props.messageId); + const hideAvatar = useHideAvatarInMsgList(props.messageId, isDetailView); const [imageBroken, setImageBroken] = useState(false); @@ -153,8 +154,7 @@ export const MessageContent = (props: Props) => { const toolTipTitle = moment(serverTimestamp || timestamp).format('llll'); - const isDetailViewAndSupportsAttachmentCarousel = - props.isDetailView && canDisplayImage(attachments); + const isDetailViewAndSupportsAttachmentCarousel = isDetailView && canDisplayImage(attachments); return ( { title={toolTipTitle} msgDirection={direction} > - - - + {hideAvatar ? null : ( + + + + )} { display: 'flex', flexDirection: 'column', gap: 'var(--margins-xs)', + maxWidth: '100%', }} > diff --git a/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx b/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx index 9e4a23d637..a49f084f1f 100644 --- a/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx +++ b/ts/components/conversation/message/message-content/MessageContentWithStatus.tsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import React, { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; +import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext'; import { replyToMessage } from '../../../../interactions/conversationInteractions'; import { MessageRenderingProps } from '../../../../models/messageType'; import { toggleSelectedMessageId } from '../../../../state/ducks/conversations'; @@ -29,30 +30,33 @@ export type MessageContentWithStatusSelectorProps = { isGroup: boolean } & Pick< type Props = { messageId: string; ctxMenuID: string; - isDetailView?: boolean; dataTestId: string; enableReactions: boolean; }; -const StyledMessageContentContainer = styled.div<{ isIncoming: boolean }>` +const StyledMessageContentContainer = styled.div<{ isIncoming: boolean; isDetailView: boolean }>` display: flex; flex-direction: column; justify-content: flex-start; align-items: ${props => (props.isIncoming ? 'flex-start' : 'flex-end')}; - padding-left: ${props => (props.isIncoming ? 0 : '25%')}; - padding-right: ${props => (props.isIncoming ? '25%' : 0)}; + padding-left: ${props => (props.isDetailView || props.isIncoming ? 0 : '25%')}; + padding-right: ${props => (props.isDetailView || !props.isIncoming ? 0 : '25%')}; width: 100%; + max-width: '100%'; margin-right: var(--margins-md); `; const StyledMessageWithAuthor = styled.div` - max-width: '100%'; + max-width: 100%; display: flex; flex-direction: column; min-width: 0; + gap: var(--margins-xs); `; export const MessageContentWithStatuses = (props: Props) => { + const isDetailView = useIsDetailMessageView(); + const contentProps = useSelector((state: StateType) => getMessageContentWithStatusesSelectorProps(state, props.messageId) ); @@ -91,7 +95,7 @@ export const MessageContentWithStatuses = (props: Props) => { } }; - const { messageId, ctxMenuID, isDetailView = false, dataTestId, enableReactions } = props; + const { messageId, ctxMenuID, dataTestId, enableReactions } = props; const [popupReaction, setPopupReaction] = useState(''); if (!contentProps) { @@ -119,6 +123,7 @@ export const MessageContentWithStatuses = (props: Props) => { return ( { setPopupReaction(''); }} @@ -127,21 +132,22 @@ export const MessageContentWithStatuses = (props: Props) => { messageId={messageId} className={classNames('module-message', `module-message--${direction}`)} role={'button'} - isDetailView={isDetailView} onClick={onClickOnMessageOuterContainer} onDoubleClickCapture={onDoubleClickReplyToMessage} dataTestId={dataTestId} > - + {!isDetailView && } - + - + {!isDeleted && ( { setPopupReaction={setPopupReaction} onPopupClick={handlePopupClick} noAvatar={hideAvatar} - isDetailView={isDetailView} /> ) : null} diff --git a/ts/components/conversation/message/message-content/MessageQuote.tsx b/ts/components/conversation/message/message-content/MessageQuote.tsx index 9a57d7a640..12e20a289c 100644 --- a/ts/components/conversation/message/message-content/MessageQuote.tsx +++ b/ts/components/conversation/message/message-content/MessageQuote.tsx @@ -1,6 +1,7 @@ import { isEmpty, toNumber } from 'lodash'; import React from 'react'; import { useSelector } from 'react-redux'; +import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext'; import { Data } from '../../../../data/data'; import { MessageRenderingProps } from '../../../../models/messageType'; import { ToastUtils } from '../../../../session/utils'; @@ -19,6 +20,7 @@ export type MessageQuoteSelectorProps = Pick { const selected = useSelector((state: StateType) => getMessageQuoteProps(state, props.messageId)); const direction = useMessageDirection(props.messageId); + const isMessageDetailView = useIsDetailMessageView(); if (!selected || isEmpty(selected)) { return null; @@ -38,6 +40,10 @@ export const MessageQuote = (props: Props) => { event.preventDefault(); event.stopPropagation(); + if (isMessageDetailView) { + return; + } + if (!quote) { ToastUtils.pushOriginalNotFound(); window.log.warn('onQuoteClick: quote not valid'); diff --git a/ts/components/conversation/message/message-content/MessageReactions.tsx b/ts/components/conversation/message/message-content/MessageReactions.tsx index bd2741e2e6..80e571ab77 100644 --- a/ts/components/conversation/message/message-content/MessageReactions.tsx +++ b/ts/components/conversation/message/message-content/MessageReactions.tsx @@ -1,6 +1,7 @@ import { isEmpty, isEqual } from 'lodash'; import React, { ReactElement, useEffect, useState } from 'react'; import styled from 'styled-components'; +import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext'; import { useMessageReactsPropsById } from '../../../../hooks/useParamSelector'; import { MessageRenderingProps } from '../../../../models/messageType'; import { REACT_LIMIT } from '../../../../session/constants'; @@ -147,10 +148,11 @@ type Props = { inModal?: boolean; onSelected?: (emoji: string) => boolean; noAvatar: boolean; - isDetailView?: boolean; }; export const MessageReactions = (props: Props) => { + const isDetailView = useIsDetailMessageView(); + const { messageId, hasReactLimit = true, @@ -161,7 +163,6 @@ export const MessageReactions = (props: Props) => { inModal = false, onSelected, noAvatar, - isDetailView, } = props; const [reactions, setReactions] = useState([]); diff --git a/ts/components/conversation/message/message-content/MessageStatus.tsx b/ts/components/conversation/message/message-content/MessageStatus.tsx index d28a41ef70..41bcdb42c3 100644 --- a/ts/components/conversation/message/message-content/MessageStatus.tsx +++ b/ts/components/conversation/message/message-content/MessageStatus.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; import { useMessageExpirationPropsById } from '../../../../hooks/useParamSelector'; import { useMessageStatus } from '../../../../state/selectors'; +import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext'; import { getMostRecentMessageId } from '../../../../state/selectors/conversations'; import { useSelectedIsGroupOrCommunity } from '../../../../state/selectors/selectedConversation'; import { SpacerXS } from '../../../basic/Text'; @@ -12,7 +13,6 @@ import { SessionIcon, SessionIconType } from '../../../icon'; import { ExpireTimer } from '../../ExpireTimer'; type Props = { - isDetailView: boolean; messageId: string; dataTestId?: string | undefined; }; @@ -30,7 +30,9 @@ type Props = { * - if the message is incoming: do not show anything (3) * - if the message is outgoing: show the text for the last message, or a message sending, or in the error state. (4) */ -export const MessageStatus = ({ isDetailView, messageId, dataTestId }: Props) => { +export const MessageStatus = ({ messageId, dataTestId }: Props) => { + const isDetailView = useIsDetailMessageView(); + const status = useMessageStatus(messageId); const selected = useMessageExpirationPropsById(messageId); diff --git a/ts/components/conversation/message/message-content/quote/Quote.tsx b/ts/components/conversation/message/message-content/quote/Quote.tsx index 342ee705c0..3c42b14ca7 100644 --- a/ts/components/conversation/message/message-content/quote/Quote.tsx +++ b/ts/components/conversation/message/message-content/quote/Quote.tsx @@ -3,10 +3,10 @@ import React, { MouseEvent, useState } from 'react'; import { isEmpty } from 'lodash'; import styled from 'styled-components'; import { useIsMessageSelectionMode } from '../../../../../state/selectors/selectedConversation'; +import * as MIME from '../../../../../types/MIME'; import { QuoteAuthor } from './QuoteAuthor'; import { QuoteIconContainer } from './QuoteIconContainer'; import { QuoteText } from './QuoteText'; -import * as MIME from '../../../../../types/MIME'; const StyledQuoteContainer = styled.div` min-width: 300px; // if the quoted content is small it doesn't look very good so we set a minimum diff --git a/ts/components/conversation/message/message-content/quote/QuoteAuthor.tsx b/ts/components/conversation/message/message-content/quote/QuoteAuthor.tsx index 3bb91ecd89..01763fa5a9 100644 --- a/ts/components/conversation/message/message-content/quote/QuoteAuthor.tsx +++ b/ts/components/conversation/message/message-content/quote/QuoteAuthor.tsx @@ -2,9 +2,9 @@ import React from 'react'; import styled from 'styled-components'; import { useQuoteAuthorName } from '../../../../../hooks/useParamSelector'; import { PubKey } from '../../../../../session/types'; +import { useSelectedIsPublic } from '../../../../../state/selectors/selectedConversation'; import { ContactName } from '../../../ContactName'; import { QuoteProps } from './Quote'; -import { useSelectedIsPublic } from '../../../../../state/selectors/selectedConversation'; const StyledQuoteAuthor = styled.div<{ isIncoming: boolean }>` color: ${props => @@ -18,6 +18,7 @@ const StyledQuoteAuthor = styled.div<{ isIncoming: boolean }>` overflow-x: hidden; white-space: nowrap; text-overflow: ellipsis; + .module-contact-name { font-weight: bold; } diff --git a/ts/components/conversation/message/message-item/ExpirableReadableMessage.tsx b/ts/components/conversation/message/message-item/ExpirableReadableMessage.tsx index d6e5c7c124..3fc0a194d9 100644 --- a/ts/components/conversation/message/message-item/ExpirableReadableMessage.tsx +++ b/ts/components/conversation/message/message-item/ExpirableReadableMessage.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react'; import { useDispatch } from 'react-redux'; import { useInterval, useMount } from 'react-use'; import styled from 'styled-components'; +import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext'; import { Data } from '../../../../data/data'; import { useMessageExpirationPropsById } from '../../../../hooks/useParamSelector'; import { MessageModelType } from '../../../../models/messageType'; @@ -84,7 +85,6 @@ export interface ExpirableReadableMessageProps extends Omit { messageId: string; isControlMessage?: boolean; - isDetailView?: boolean; } function ExpireTimerControlMessage({ @@ -109,6 +109,7 @@ function ExpireTimerControlMessage({ export const ExpirableReadableMessage = (props: ExpirableReadableMessageProps) => { const selected = useMessageExpirationPropsById(props.messageId); + const isDetailView = useIsDetailMessageView(); const { isControlMessage, onClick, onDoubleClickCapture, role, dataTestId } = props; @@ -135,7 +136,7 @@ export const ExpirableReadableMessage = (props: ExpirableReadableMessageProps) = } = selected; // NOTE we want messages on the left in the message detail view regardless of direction - const direction = props.isDetailView ? 'incoming' : _direction; + const direction = isDetailView ? 'incoming' : _direction; const isIncoming = direction === 'incoming'; return ( diff --git a/ts/components/conversation/message/message-item/GenericReadableMessage.tsx b/ts/components/conversation/message/message-item/GenericReadableMessage.tsx index 2d91f3871e..ab1254b970 100644 --- a/ts/components/conversation/message/message-item/GenericReadableMessage.tsx +++ b/ts/components/conversation/message/message-item/GenericReadableMessage.tsx @@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { contextMenu } from 'react-contexify'; import { useSelector } from 'react-redux'; import styled, { keyframes } from 'styled-components'; +import { useIsDetailMessageView } from '../../../../contexts/isDetailViewContext'; import { MessageRenderingProps } from '../../../../models/messageType'; import { getConversationController } from '../../../../session/conversations'; import { StateType } from '../../../../state/reducer'; @@ -29,7 +30,6 @@ export type GenericReadableMessageSelectorProps = Pick< type Props = { messageId: string; ctxMenuID: string; - isDetailView?: boolean; }; const highlightedMessageAnimation = keyframes` @@ -40,8 +40,8 @@ const highlightedMessageAnimation = keyframes` const StyledReadableMessage = styled.div<{ selected: boolean; + isDetailView: boolean; isRightClicked: boolean; - isDetailView?: boolean; }>` display: flex; align-items: center; @@ -64,7 +64,9 @@ const StyledReadableMessage = styled.div<{ `; export const GenericReadableMessage = (props: Props) => { - const { ctxMenuID, messageId, isDetailView } = props; + const isDetailView = useIsDetailMessageView(); + + const { ctxMenuID, messageId } = props; const [enableReactions, setEnableReactions] = useState(true); @@ -148,7 +150,6 @@ export const GenericReadableMessage = (props: Props) => { diff --git a/ts/components/conversation/message/message-item/Message.tsx b/ts/components/conversation/message/message-item/Message.tsx index 5d44a40ee2..b038a3de8a 100644 --- a/ts/components/conversation/message/message-item/Message.tsx +++ b/ts/components/conversation/message/message-item/Message.tsx @@ -12,7 +12,6 @@ export const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = THUMBNAIL_SIDE; type Props = { messageId: string; - isDetailView?: boolean; // when the detail is shown for a message, we disable click and some other stuff }; export const Message = (props: Props) => { @@ -26,11 +25,5 @@ export const Message = (props: Props) => { return null; } - return ( - - ); + return ; }; diff --git a/ts/components/conversation/message/message-item/MessageRequestResponse.tsx b/ts/components/conversation/message/message-item/MessageRequestResponse.tsx index 4e84b1d7bc..23076369c4 100644 --- a/ts/components/conversation/message/message-item/MessageRequestResponse.tsx +++ b/ts/components/conversation/message/message-item/MessageRequestResponse.tsx @@ -39,7 +39,7 @@ export const MessageRequestResponse = (props: PropsForMessageRequestResponse) => id={`msg-${messageId}`} > - + ); diff --git a/ts/components/conversation/message/message-item/ReadableMessage.tsx b/ts/components/conversation/message/message-item/ReadableMessage.tsx index 6195637324..0d436d9d86 100644 --- a/ts/components/conversation/message/message-item/ReadableMessage.tsx +++ b/ts/components/conversation/message/message-item/ReadableMessage.tsx @@ -1,14 +1,8 @@ import { debounce, noop } from 'lodash'; -import React, { - AriaRole, - MouseEventHandler, - useCallback, - useContext, - useLayoutEffect, - useState, -} from 'react'; +import React, { AriaRole, MouseEventHandler, useCallback, useLayoutEffect, useState } from 'react'; import { InView } from 'react-intersection-observer'; import { useDispatch, useSelector } from 'react-redux'; +import { useScrollToLoadedMessage } from '../../../../contexts/ScrollToLoadedMessage'; import { Data } from '../../../../data/data'; import { useHasUnread } from '../../../../hooks/useParamSelector'; import { getConversationController } from '../../../../session/conversations'; @@ -28,7 +22,6 @@ import { } from '../../../../state/selectors/conversations'; import { getIsAppFocused } from '../../../../state/selectors/section'; import { useSelectedConversationKey } from '../../../../state/selectors/selectedConversation'; -import { ScrollToLoadedMessageContext } from '../../SessionMessagesListContainer'; export type ReadableMessageProps = { children: React.ReactNode; @@ -95,7 +88,7 @@ export const ReadableMessage = (props: ReadableMessageProps) => { const [didScroll, setDidScroll] = useState(false); const quotedMessageToAnimate = useSelector(getQuotedMessageToAnimate); - const scrollToLoadedMessage = useContext(ScrollToLoadedMessageContext); + const scrollToLoadedMessage = useScrollToLoadedMessage(); // if this unread-indicator is rendered, // we want to scroll here only if the conversation was not opened to a specific message diff --git a/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx b/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx index 496babbebc..5e271d7961 100644 --- a/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx +++ b/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx @@ -10,6 +10,7 @@ import { getMessageInfoId } from '../../../../../state/selectors/conversations'; import { Flex } from '../../../../basic/Flex'; import { Header, HeaderTitle, StyledScrollContainer } from '../components'; +import { IsDetailMessageViewContext } from '../../../../../contexts/isDetailViewContext'; import { Data } from '../../../../../data/data'; import { useRightOverlayMode } from '../../../../../hooks/useUI'; import { @@ -71,9 +72,11 @@ const MessageBody = ({ } return ( - - - + + + + + ); }; diff --git a/ts/components/conversation/right-panel/overlay/message-info/components/MessageFrom.tsx b/ts/components/conversation/right-panel/overlay/message-info/components/MessageFrom.tsx index 61db093c5a..e7c90df471 100644 --- a/ts/components/conversation/right-panel/overlay/message-info/components/MessageFrom.tsx +++ b/ts/components/conversation/right-panel/overlay/message-info/components/MessageFrom.tsx @@ -2,7 +2,7 @@ import React from 'react'; import styled from 'styled-components'; import { MessageInfoLabel } from '.'; import { useConversationUsername } from '../../../../../../hooks/useParamSelector'; -import { Avatar, AvatarSize } from '../../../../../avatar/Avatar'; +import { Avatar, AvatarSize, CrownIcon } from '../../../../../avatar/Avatar'; const StyledFromContainer = styled.div` display: flex; @@ -30,8 +30,12 @@ const StyledMessageInfoAuthor = styled.div` font-size: var(--font-size-lg); `; -export const MessageFrom = (props: { sender: string }) => { - const { sender } = props; +const StyledAvatar = styled.div` + position: relative; +`; + +export const MessageFrom = (props: { sender: string; isSenderAdmin: boolean }) => { + const { sender, isSenderAdmin } = props; const profileName = useConversationUsername(sender); const from = window.i18n('from'); @@ -39,7 +43,10 @@ export const MessageFrom = (props: { sender: string }) => { {from} - + + + {isSenderAdmin ? : null} + {!!profileName && {profileName}} {sender} 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 index 1875298f0d..4bda46496e 100644 --- a/ts/components/conversation/right-panel/overlay/message-info/components/MessageInfo.tsx +++ b/ts/components/conversation/right-panel/overlay/message-info/components/MessageInfo.tsx @@ -14,6 +14,7 @@ import { useMessageHash, useMessageReceivedAt, useMessageSender, + useMessageSenderIsAdmin, useMessageServerId, useMessageServerTimestamp, useMessageTimestamp, @@ -111,6 +112,7 @@ export const MessageInfo = ({ messageId, errors }: { messageId: string; errors: const sentAt = useMessageTimestamp(messageId); const serverTimestamp = useMessageServerTimestamp(messageId); const receivedAt = useMessageReceivedAt(messageId); + const isSenderAdmin = useMessageSenderIsAdmin(messageId); if (!messageId || !sender) { return null; @@ -137,7 +139,7 @@ export const MessageInfo = ({ messageId, errors }: { messageId: string; errors: ) : null} - + {hasError && ( <> diff --git a/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx b/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx index f032d36ca4..d1ce3e5f71 100644 --- a/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx +++ b/ts/components/leftpane/conversation-list-item/ConversationListItem.tsx @@ -10,6 +10,10 @@ import { Avatar, AvatarSize } from '../../avatar/Avatar'; import { openConversationWithMessages } from '../../../state/ducks/conversations'; import { updateUserDetailsModal } from '../../../state/ducks/modalDialog'; +import { + ContextConversationProvider, + useConvoIdFromContext, +} from '../../../contexts/ConvoIdContext'; import { useAvatarPath, useConversationUsername, @@ -21,7 +25,6 @@ import { import { isSearching } from '../../../state/selectors/search'; import { useSelectedConversationKey } from '../../../state/selectors/selectedConversation'; import { MemoConversationListItemContextMenu } from '../../menu/ConversationListItemContextMenu'; -import { ContextConversationProvider, useConvoIdFromContext } from './ConvoIdContext'; import { ConversationListItemHeaderItem } from './HeaderItem'; import { MessageItem } from './MessageItem'; diff --git a/ts/components/leftpane/conversation-list-item/HeaderItem.tsx b/ts/components/leftpane/conversation-list-item/HeaderItem.tsx index 1f435bfb1f..81d37e82f7 100644 --- a/ts/components/leftpane/conversation-list-item/HeaderItem.tsx +++ b/ts/components/leftpane/conversation-list-item/HeaderItem.tsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import React from 'react'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; +import { useConvoIdFromContext } from '../../../contexts/ConvoIdContext'; import { Data } from '../../../data/data'; import { useActiveAt, @@ -20,7 +21,6 @@ import { isSearching } from '../../../state/selectors/search'; import { getIsMessageSection } from '../../../state/selectors/section'; import { Timestamp } from '../../conversation/Timestamp'; import { SessionIcon } from '../../icon'; -import { useConvoIdFromContext } from './ConvoIdContext'; import { UserItem } from './UserItem'; const NotificationSettingIcon = () => { diff --git a/ts/components/leftpane/conversation-list-item/MessageItem.tsx b/ts/components/leftpane/conversation-list-item/MessageItem.tsx index e4fc216fb7..5b0d8bf1ae 100644 --- a/ts/components/leftpane/conversation-list-item/MessageItem.tsx +++ b/ts/components/leftpane/conversation-list-item/MessageItem.tsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import { isEmpty } from 'lodash'; import React from 'react'; import { useSelector } from 'react-redux'; +import { useConvoIdFromContext } from '../../../contexts/ConvoIdContext'; import { useHasUnread, useIsPrivate, @@ -15,7 +16,6 @@ import { assertUnreachable } from '../../../types/sqlSharedTypes'; import { TypingAnimation } from '../../conversation/TypingAnimation'; import { MessageBody } from '../../conversation/message/message-content/MessageBody'; import { SessionIcon } from '../../icon'; -import { useConvoIdFromContext } from './ConvoIdContext'; import { InteractionItem } from './InteractionItem'; export const MessageItem = () => { diff --git a/ts/components/leftpane/conversation-list-item/UserItem.tsx b/ts/components/leftpane/conversation-list-item/UserItem.tsx index ec293ae3c9..fc6ad9028b 100644 --- a/ts/components/leftpane/conversation-list-item/UserItem.tsx +++ b/ts/components/leftpane/conversation-list-item/UserItem.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useSelector } from 'react-redux'; +import { useConvoIdFromContext } from '../../../contexts/ConvoIdContext'; import { useConversationRealName, useConversationUsername, @@ -9,7 +10,6 @@ import { import { PubKey } from '../../../session/types'; import { isSearching } from '../../../state/selectors/search'; import { ContactName } from '../../conversation/ContactName'; -import { useConvoIdFromContext } from './ConvoIdContext'; export const UserItem = () => { const conversationId = useConvoIdFromContext(); @@ -36,15 +36,13 @@ export const UserItem = () => { } return ( -
- -
+ ); }; diff --git a/ts/components/menu/ConversationListItemContextMenu.tsx b/ts/components/menu/ConversationListItemContextMenu.tsx index 9e3e0680b0..d0e2808e94 100644 --- a/ts/components/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/menu/ConversationListItemContextMenu.tsx @@ -2,10 +2,11 @@ import React from 'react'; import { Item, Menu } from 'react-contexify'; import { useSelector } from 'react-redux'; +import { useConvoIdFromContext } from '../../contexts/ConvoIdContext'; import { useIsPinned, useIsPrivate, useIsPrivateAndFriend } from '../../hooks/useParamSelector'; import { getConversationController } from '../../session/conversations'; +import { isSearching } from '../../state/selectors/search'; import { getIsMessageSection } from '../../state/selectors/section'; -import { useConvoIdFromContext } from '../leftpane/conversation-list-item/ConvoIdContext'; import { SessionContextMenuContainer } from '../SessionContextMenuContainer'; import { AcceptMsgRequestMenuItem, @@ -17,16 +18,15 @@ import { DeclineAndBlockMsgRequestMenuItem, DeclineMsgRequestMenuItem, DeleteMessagesMenuItem, + DeletePrivateConversationMenuItem, InviteContactMenuItem, LeaveGroupOrCommunityMenuItem, MarkAllReadMenuItem, MarkConversationUnreadMenuItem, + NotificationForConvoMenuItem, ShowUserDetailsMenuItem, UnbanMenuItem, - DeletePrivateConversationMenuItem, - NotificationForConvoMenuItem, } from './Menu'; -import { isSearching } from '../../state/selectors/search'; export type PropsContextConversationItem = { triggerId: string; diff --git a/ts/components/menu/Menu.tsx b/ts/components/menu/Menu.tsx index 69cb071188..407dfe289d 100644 --- a/ts/components/menu/Menu.tsx +++ b/ts/components/menu/Menu.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Item, Submenu } from 'react-contexify'; import { useDispatch, useSelector } from 'react-redux'; +import { useConvoIdFromContext } from '../../contexts/ConvoIdContext'; import { useAvatarPath, useConversationUsername, @@ -56,7 +57,6 @@ import { getIsMessageSection } from '../../state/selectors/section'; import { useSelectedConversationKey } from '../../state/selectors/selectedConversation'; import { LocalizerKeys } from '../../types/LocalizerKeys'; import { SessionButtonColor } from '../basic/SessionButton'; -import { useConvoIdFromContext } from '../leftpane/conversation-list-item/ConvoIdContext'; /** Menu items standardized */ diff --git a/ts/components/search/MessageSearchResults.tsx b/ts/components/search/MessageSearchResults.tsx index 13ac830988..e452d194c3 100644 --- a/ts/components/search/MessageSearchResults.tsx +++ b/ts/components/search/MessageSearchResults.tsx @@ -1,15 +1,15 @@ import React from 'react'; import styled, { CSSProperties } from 'styled-components'; +import { useConversationUsername, useIsPrivate } from '../../hooks/useParamSelector'; +import { MessageAttributes } from '../../models/messageType'; +import { UserUtils } from '../../session/utils'; import { getOurPubKeyStrFromCache } from '../../session/utils/User'; import { openConversationToSpecificMessage } from '../../state/ducks/conversations'; -import { ContactName } from '../conversation/ContactName'; import { Avatar, AvatarSize } from '../avatar/Avatar'; -import { Timestamp } from '../conversation/Timestamp'; import { MessageBodyHighlight } from '../basic/MessageBodyHighlight'; -import { MessageAttributes } from '../../models/messageType'; -import { useConversationUsername, useIsPrivate } from '../../hooks/useParamSelector'; -import { UserUtils } from '../../session/utils'; +import { ContactName } from '../conversation/ContactName'; +import { Timestamp } from '../conversation/Timestamp'; export type MessageResultProps = MessageAttributes & { snippet: string }; @@ -58,6 +58,7 @@ const StyledResultText = styled.div` display: inline-flex; flex-direction: column; align-items: stretch; + min-width: 0; `; const ResultsHeader = styled.div` diff --git a/ts/components/leftpane/conversation-list-item/ConvoIdContext.tsx b/ts/contexts/ConvoIdContext.tsx similarity index 100% rename from ts/components/leftpane/conversation-list-item/ConvoIdContext.tsx rename to ts/contexts/ConvoIdContext.tsx diff --git a/ts/contexts/ScrollToLoadedMessage.tsx b/ts/contexts/ScrollToLoadedMessage.tsx new file mode 100644 index 0000000000..3f1e9b80d7 --- /dev/null +++ b/ts/contexts/ScrollToLoadedMessage.tsx @@ -0,0 +1,16 @@ +import { createContext, useContext } from 'react'; + +export type ScrollToLoadedReasons = + | 'quote-or-search-result' + | 'go-to-bottom' + | 'unread-indicator' + | 'load-more-top' + | 'load-more-bottom'; + +export const ScrollToLoadedMessageContext = createContext( + (_loadedMessageIdToScrollTo: string, _reason: ScrollToLoadedReasons) => {} +); + +export function useScrollToLoadedMessage() { + return useContext(ScrollToLoadedMessageContext); +} diff --git a/ts/contexts/isDetailViewContext.tsx b/ts/contexts/isDetailViewContext.tsx new file mode 100644 index 0000000000..e2f31cf84a --- /dev/null +++ b/ts/contexts/isDetailViewContext.tsx @@ -0,0 +1,10 @@ +import { createContext, useContext } from 'react'; + +/** + * When the message is rendered as part of the detailView (right panel) we disable onClick and make some other minor UI changes + */ +export const IsDetailMessageViewContext = createContext(false); + +export function useIsDetailMessageView() { + return useContext(IsDetailMessageViewContext); +} diff --git a/ts/contexts/isMessageVisibleContext.tsx b/ts/contexts/isMessageVisibleContext.tsx new file mode 100644 index 0000000000..814f101676 --- /dev/null +++ b/ts/contexts/isMessageVisibleContext.tsx @@ -0,0 +1,7 @@ +import { createContext, useContext } from 'react'; + +export const IsMessageVisibleContext = createContext(false); + +export function useIsMessageVisible() { + return useContext(IsMessageVisibleContext); +} diff --git a/ts/state/selectors/messages.ts b/ts/state/selectors/messages.ts index b998bf7f70..6c87ca3e64 100644 --- a/ts/state/selectors/messages.ts +++ b/ts/state/selectors/messages.ts @@ -160,10 +160,10 @@ export const useMessageText = (messageId: string | undefined): string | undefine return useMessagePropsByMessageId(messageId)?.propsForMessage.text; }; -export function useHideAvatarInMsgList(messageId?: string) { +export function useHideAvatarInMsgList(messageId?: string, isDetailView?: boolean) { const msgProps = useMessagePropsByMessageId(messageId); const selectedIsPrivate = useSelectedIsPrivate(); - return msgProps?.propsForMessage.direction === 'outgoing' || selectedIsPrivate; + return isDetailView || msgProps?.propsForMessage.direction === 'outgoing' || selectedIsPrivate; } export function useMessageSelected(messageId?: string) {