diff --git a/stylesheets/_modal.scss b/stylesheets/_modal.scss index 8446da1a3..43c27d01d 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 0bd68a2f9..3a7e348fa 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 718467b78..5216371f3 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 6d47e9b44..d4f9c3ce7 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 9b65f07cd..31ff6458a 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 30a0a04bf..3f7f4d42a 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 042b84a41..5da6eccfc 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 1cbe2dde8..e9b0dd9ff 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 24a0619b2..143185577 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 9e4a23d63..a49f084f1 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 9a57d7a64..12e20a289 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 bd2741e2e..80e571ab7 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 d28a41ef7..41bcdb42c 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 342ee705c..3c42b14ca 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 3bb91ecd8..01763fa5a 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 d6e5c7c12..3fc0a194d 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 2d91f3871..ab1254b97 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 5d44a40ee..b038a3de8 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 4e84b1d7b..23076369c 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 619563732..0d436d9d8 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 496babbeb..5e271d796 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 61db093c5..e7c90df47 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 1875298f0..4bda46496 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 f032d36ca..d1ce3e5f7 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 1f435bfb1..81d37e82f 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 e4fc216fb..5b0d8bf1a 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 ec293ae3c..fc6ad9028 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 9e3e0680b..d0e2808e9 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 69cb07118..407dfe289 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 13ac83098..e452d194c 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 000000000..3f1e9b80d --- /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 000000000..e2f31cf84 --- /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 000000000..814f10167 --- /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 b998bf7f7..6c87ca3e6 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) {