diff --git a/examples/vite/src/stream-imports-layout.scss b/examples/vite/src/stream-imports-layout.scss index dbb5dcb0f4..b3bbaebc36 100644 --- a/examples/vite/src/stream-imports-layout.scss +++ b/examples/vite/src/stream-imports-layout.scss @@ -30,8 +30,8 @@ //@use 'stream-chat-react/dist/scss/v2/MessageActionsBox/MessageActionsBox-layout'; //@use 'stream-chat-react/dist/scss/v2/MessageBouncePrompt/MessageBouncePrompt-layout'; //@use 'stream-chat-react/dist/scss/v2/MessageInput/MessageInput-layout'; // X -@use 'stream-chat-react/dist/scss/v2/MessageList/MessageList-layout'; -@use 'stream-chat-react/dist/scss/v2/MessageList/VirtualizedMessageList-layout'; +//@use 'stream-chat-react/dist/scss/v2/MessageList/MessageList-layout'; +//@use 'stream-chat-react/dist/scss/v2/MessageList/VirtualizedMessageList-layout'; // @use 'stream-chat-react/dist/scss/v2/MessageReactions/MessageReactions-layout'; @use 'stream-chat-react/dist/scss/v2/MessageReactions/MessageReactionsSelector-layout'; //@use 'stream-chat-react/dist/scss/v2/Modal/Modal-layout'; diff --git a/examples/vite/src/stream-imports-theme.scss b/examples/vite/src/stream-imports-theme.scss index 9213f0ea43..3d9efd72dc 100644 --- a/examples/vite/src/stream-imports-theme.scss +++ b/examples/vite/src/stream-imports-theme.scss @@ -24,8 +24,8 @@ //@use 'stream-chat-react/dist/scss/v2/Message/Message-theme'; //@use 'stream-chat-react/dist/scss/v2/MessageActionsBox/MessageActionsBox-theme'; //@use 'stream-chat-react/dist/scss/v2/MessageBouncePrompt/MessageBouncePrompt-theme'; -@use 'stream-chat-react/dist/scss/v2/MessageList/MessageList-theme'; -@use 'stream-chat-react/dist/scss/v2/MessageList/VirtualizedMessageList-theme'; +//@use 'stream-chat-react/dist/scss/v2/MessageList/MessageList-theme'; +//@use 'stream-chat-react/dist/scss/v2/MessageList/VirtualizedMessageList-theme'; // @use 'stream-chat-react/dist/scss/v2/MessageReactions/MessageReactions-theme'; @use 'stream-chat-react/dist/scss/v2/MessageReactions/MessageReactionsSelector-theme'; //@use 'stream-chat-react/dist/scss/v2/Modal/Modal-theme'; diff --git a/package.json b/package.json index ad1be162af..deedd6691a 100644 --- a/package.json +++ b/package.json @@ -232,7 +232,7 @@ "prettier-fix": "yarn prettier --write", "fix-staged": "lint-staged --config .lintstagedrc.fix.json --concurrent 1", "start": "tsc -p tsconfig.lib.json -w", - "start:css": "sass --watch src/styling:dist/css src/plugins/Emojis/styling:dist/css", + "start:css": "sass --watch src/styling:dist/css", "prepare": "husky install", "preversion": "yarn install", "test": "jest", diff --git a/src/components/MediaRecorder/AudioRecorder/styling/AudioRecorder.scss b/src/components/MediaRecorder/AudioRecorder/styling/AudioRecorder.scss index 7f24d7cfb7..97ea579de6 100644 --- a/src/components/MediaRecorder/AudioRecorder/styling/AudioRecorder.scss +++ b/src/components/MediaRecorder/AudioRecorder/styling/AudioRecorder.scss @@ -4,7 +4,7 @@ display: flex; align-items: center; justify-content: center; - max-width: 768px; + max-width: var(--str-chat__message-composer-max-width); width: 100%; border-radius: var(--radius-3xl); border: 1px solid var(--border-core-default); diff --git a/src/components/Message/Message.tsx b/src/components/Message/Message.tsx index 7a96db8e54..826f4d0265 100644 --- a/src/components/Message/Message.tsx +++ b/src/components/Message/Message.tsx @@ -63,7 +63,6 @@ type MessageWithContextProps = Omit & const MessageWithContext = (props: MessageWithContextProps) => { const { canPin, - groupedByUser, Message: propMessage, message, messageActions = Object.keys(MESSAGE_ACTIONS), @@ -166,8 +165,7 @@ const MessageWithContext = (props: MessageWithContextProps) => { return ( - - {/* TODO - remove prop in next major release, maintains VML backwards compatibility */} + ); }; @@ -263,10 +261,7 @@ export const Message = (props: MessageProps) => { closeReactionSelectorOnClick={closeReactionSelectorOnClick} deliveredTo={props.deliveredTo} disableQuotedMessages={props.disableQuotedMessages} - endOfGroup={props.endOfGroup} - firstOfGroup={props.firstOfGroup} formatDate={props.formatDate} - groupedByUser={props.groupedByUser} groupStyles={props.groupStyles} handleAction={handleAction} handleDelete={handleDelete} diff --git a/src/components/Message/MessageSimple.tsx b/src/components/Message/MessageSimple.tsx index 5d2b9615b9..4fcdbeb3d7 100644 --- a/src/components/Message/MessageSimple.tsx +++ b/src/components/Message/MessageSimple.tsx @@ -61,6 +61,7 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => { onUserClick, onUserHover, renderText, + showAvatar = 'incoming', threadList, } = props; const { client } = useChatContext('MessageSimple'); @@ -152,6 +153,12 @@ const MessageSimpleWithContext = (props: MessageSimpleWithContextProps) => { 'str-chat__message--highlighted': highlighted, 'str-chat__message--is-emoji-only': textHasEmojisOnly, 'str-chat__message--pinned': message.pinned, + 'str-chat__message--with-avatar': (() => { + if (!message.user) return false; + if (showAvatar === 'incoming') return !isMyMessage(); + if (showAvatar === 'outgoing') return isMyMessage(); + return showAvatar; + })(), 'str-chat__message--with-reactions': hasReactions, 'str-chat__message-send-can-be-retried': message?.status === 'failed' && message?.error?.status !== 403, diff --git a/src/components/Message/styling/Message.scss b/src/components/Message/styling/Message.scss index 2854cd4ff2..02298302f6 100644 --- a/src/components/Message/styling/Message.scss +++ b/src/components/Message/styling/Message.scss @@ -87,8 +87,28 @@ --str-chat__message-with-attachment-max-width: calc(var(--str-chat__spacing-px) * 300); } +/* Make spaces between message groups */ +.str-chat__li--top { + padding-block-start: var(--spacing-xs); + padding-block-end: var(--spacing-xxxs); +} + +.str-chat__li--bottom { + padding-block-start: var(--spacing-xxxs); + padding-block-end: var(--spacing-xs); +} + +.str-chat__li--middle { + padding-block: var(--spacing-xxxs); +} + +.str-chat__li--single { + padding-block: var(--spacing-xs); +} + .str-chat__message { --str-chat-message-options-size: calc(3 * var(--str-chat__message-options-button-size)); + padding-inline: var(--str-chat__message-composer-padding); &.str-chat__message-without-touch-support { --str-chat-message-options-size: calc( @@ -214,12 +234,45 @@ .str-chat__message-reminder { grid-area: message-reminder; - padding-block: var(--str-chat__spacing-2) var(--str-chat__spacing-0_5); + padding-block: var(--spacing-xxs); margin: 0; @include utils.component-layer-overrides('message-reminder'); font: var(--str-chat__caption-medium-text); } + @mixin message-grid-no-avatar { + grid-template-areas: + 'pin-indicator' + 'message-reminder' + 'message' + 'translation-notice' + 'custom-metadata' + 'metadata'; + grid-template-columns: 1fr; + } + + @mixin message-grid-other-with-avatar { + grid-template-areas: + '. pin-indicator' + '. message-reminder' + 'avatar message' + 'avatar translation-notice' + 'avatar custom-metadata' + 'avatar metadata'; + grid-template-columns: auto 1fr; + } + + @mixin message-grid-me-with-avatar { + grid-template-areas: + 'pin-indicator .' + 'message-reminder .' + 'message avatar' + 'translation-notice avatar' + 'custom-metadata avatar' + 'metadata avatar'; + grid-template-columns: 1fr auto; + } + .str-chat__message-pin-indicator { grid-area: pin-indicator; padding-block: var(--spacing-xxs); @@ -238,16 +291,17 @@ } &.str-chat__message--other { - grid-template-areas: - '. message-reminder' - 'avatar message' - 'avatar translation-notice' - 'avatar custom-metadata' - 'avatar metadata'; column-gap: var(--str-chat__spacing-2); - grid-template-columns: auto 1fr; justify-items: flex-start; + &.str-chat__message--with-avatar { + @include message-grid-other-with-avatar; + } + + &:not(.str-chat__message--with-avatar) { + @include message-grid-no-avatar; + } + .str-chat__message-inner { .str-chat__message-reactions-host { justify-self: flex-start; @@ -268,36 +322,25 @@ } &.str-chat__message--pinned.str-chat__message--other { - grid-template-areas: - '. pin-indicator' - '. message-reminder' - 'avatar message' - 'avatar translation-notice' - 'avatar custom-metadata' - 'avatar metadata'; + @include message-grid-other-with-avatar; } &.str-chat__message--pinned.str-chat__message--me { - grid-template-areas: - 'pin-indicator .' - 'message-reminder .' - 'message avatar' - 'translation-notice avatar' - 'custom-metadata avatar' - 'metadata avatar'; + @include message-grid-me-with-avatar; } &.str-chat__message--me { - grid-template-areas: - 'message-reminder .' - 'message avatar' - 'translation-notice avatar' - 'custom-metadata avatar' - 'metadata avatar'; column-gap: var(--str-chat__spacing-2); - grid-template-columns: 1fr auto; justify-items: flex-end; + &.str-chat__message--with-avatar { + @include message-grid-me-with-avatar; + } + + &:not(.str-chat__message--with-avatar) { + @include message-grid-no-avatar; + } + .str-chat__message-inner { .str-chat__message-reactions-host { justify-self: flex-end; @@ -315,6 +358,7 @@ } } } + } &.str-chat__message--deleted { @@ -335,7 +379,7 @@ align-self: end; } - &.str-chat__message--me .str-chat__avatar:has(~ .str-chat__message-inner) { + &:not(.str-chat__message--with-avatar) .str-chat__avatar { display: none; } @@ -660,36 +704,17 @@ .str-chat__li--middle, .str-chat__li--top { .str-chat__message { - // space below the message within a group - padding-block-end: var(--spacing-xxs); - .str-chat__message-metadata { display: none; } - // > selector added to not hide sender of inline replies - > .str-chat__message-sender-avatar { + // Hide sender avatar in middle/top of group (only show on last message in group) + > .str-chat__avatar { visibility: hidden; } } } -.str-chat__li--top, -.str-chat__li--single { - .str-chat__message { - // space above the message group - padding-block-start: var(--spacing-xs); - } -} - -.str-chat__li--bottom, -.str-chat__li--single { - .str-chat__message { - // space below the message group - padding-block-end: var(--spacing-xs); - } -} - .str-chat__li--single, .str-chat__li--bottom { .str-chat__message--other { @@ -729,7 +754,7 @@ // Avatar display in message list .str-chat__li--top, .str-chat__li--middle { - .str-chat__message > .str-chat__avatar { + .str-chat__message.str-chat__message--with-avatar > .str-chat__avatar { visibility: hidden; pointer-events: none; } diff --git a/src/components/Message/types.ts b/src/components/Message/types.ts index 93e651aeaf..18b81c3ce6 100644 --- a/src/components/Message/types.ts +++ b/src/components/Message/types.ts @@ -26,10 +26,6 @@ export type MessageProps = { deliveredTo?: UserResponse[]; /** If true, disables the ability for users to quote messages, defaults to false */ disableQuotedMessages?: boolean; - /** When true, the message is the last one in a group sent by a specific user (only used in the `VirtualizedMessageList`) */ - endOfGroup?: boolean; - /** When true, the message is the first one in a group sent by a specific user (only used in the `VirtualizedMessageList`) */ - firstOfGroup?: boolean; /** Override the default formatting of the date. This is a function that has access to the original date object, returns a string */ formatDate?: (date: Date) => string; /** Function that returns the notification text to be displayed when a delete message request fails */ @@ -50,8 +46,6 @@ export type MessageProps = { getMuteUserSuccessNotification?: (user: UserResponse) => string; /** Function that returns the notification text to be displayed when a pin message request fails */ getPinMessageErrorNotification?: (message: LocalMessage) => string; - /** If true, group messages sent by each user (only used in the `VirtualizedMessageList`) */ - groupedByUser?: boolean; /** A list of styles to apply to this message, i.e. top, bottom, single */ groupStyles?: GroupStyle[]; /** Whether to highlight and focus the message on load */ @@ -88,6 +82,14 @@ export type MessageProps = { reactionDetailsSort?: ReactionSort; /** A list of users that have read this Message if the message is the last one and was posted by my user */ readBy?: UserResponse[]; + /** + * When set, the message uses the grid layout with an avatar column and shows the sender avatar. + * - `true`: show for incoming and outgoing messages + * - `'incoming'`: show only for incoming (other users') messages + * - `'outgoing'`: show only for own (outgoing) messages + * - `false` or omitted: no avatar column + */ + showAvatar?: boolean | 'incoming' | 'outgoing'; /** Custom function to render message text content, defaults to the renderText function: [utils](https://github.com/GetStream/stream-chat-react/blob/master/src/utils.ts) */ renderText?: ( text?: string, diff --git a/src/components/Message/utils.tsx b/src/components/Message/utils.tsx index 2727845fe3..df8ac1bab0 100644 --- a/src/components/Message/utils.tsx +++ b/src/components/Message/utils.tsx @@ -272,7 +272,6 @@ export const areMessagePropsEqual = ( const { message: nextMessage, Message: nextMessageUI } = nextProps; if (prevMessageUI !== nextMessageUI) return false; - if (prevProps.endOfGroup !== nextProps.endOfGroup) return false; if (nextProps.showDetailedReactions !== prevProps.showDetailedReactions) { return false; diff --git a/src/components/MessageInput/styling/MessageComposer.scss b/src/components/MessageInput/styling/MessageComposer.scss index d14d56879f..e2e7caf963 100644 --- a/src/components/MessageInput/styling/MessageComposer.scss +++ b/src/components/MessageInput/styling/MessageComposer.scss @@ -36,6 +36,7 @@ --str-chat__cooldown-border-inline-end: 0; --str-chat__cooldown-box-shadow: none; --str-chat__message-composer-max-width: 768px; + --str-chat__message-composer-padding: var(--spacing-md); .str-chat__message-composer-container { width: 100%; @@ -44,7 +45,7 @@ align-items: center; gap: var(--spacing-xs); justify-content: center; - padding: var(--spacing-xs); + padding: var(--str-chat__message-composer-padding); min-width: 0; } diff --git a/src/components/MessageList/MessageList.tsx b/src/components/MessageList/MessageList.tsx index 0f0b996ee0..121d07391c 100644 --- a/src/components/MessageList/MessageList.tsx +++ b/src/components/MessageList/MessageList.tsx @@ -176,6 +176,7 @@ const MessageListWithContext = (props: MessageListWithContextProps) => { reactionDetailsSort, renderText: props.renderText, retrySendMessage: props.retrySendMessage, + showAvatar: props.showAvatar, sortReactionDetails, sortReactions, unsafeHTML, @@ -188,7 +189,7 @@ const MessageListWithContext = (props: MessageListWithContextProps) => { threadList, }); - const messageListClass = customClasses?.messageList || 'str-chat__list'; + const messageListClass = customClasses?.messageList || 'str-chat__message-list'; const loadMore = React.useCallback(() => { if (loadMoreCallback) { @@ -319,6 +320,7 @@ type PropsDrilledToMessage = | 'reactionDetailsSort' | 'renderText' | 'retrySendMessage' + | 'showAvatar' | 'sortReactions' | 'sortReactionDetails' | 'unsafeHTML'; diff --git a/src/components/MessageList/VirtualizedMessageList.tsx b/src/components/MessageList/VirtualizedMessageList.tsx index 679087ae46..e85041363d 100644 --- a/src/components/MessageList/VirtualizedMessageList.tsx +++ b/src/components/MessageList/VirtualizedMessageList.tsx @@ -77,6 +77,7 @@ type PropsDrilledToMessage = | 'openThread' | 'reactionDetailsSort' | 'renderText' + | 'showAvatar' | 'sortReactions' | 'sortReactionDetails'; @@ -220,6 +221,7 @@ const VirtualizedMessageListWithContext = ( scrollToLatestMessageOnFocus = false, separateGiphyPreview = false, shouldGroupByUser = false, + showAvatar, showUnreadNotificationAlways, sortReactionDetails, sortReactions, @@ -513,6 +515,7 @@ const VirtualizedMessageListWithContext = ( renderText, returnAllReadData, shouldGroupByUser, + showAvatar, sortReactionDetails, sortReactions, threadList, diff --git a/src/components/MessageList/VirtualizedMessageListComponents.tsx b/src/components/MessageList/VirtualizedMessageListComponents.tsx index efa948f17d..5c98f4cda7 100644 --- a/src/components/MessageList/VirtualizedMessageListComponents.tsx +++ b/src/components/MessageList/VirtualizedMessageListComponents.tsx @@ -5,13 +5,11 @@ import type { ItemProps, ListItem } from 'react-virtuoso'; import { EmptyStateIndicator as DefaultEmptyStateIndicator } from '../EmptyStateIndicator'; import { LoadingIndicator as DefaultLoadingIndicator } from '../Loading'; -import { isMessageEdited, Message } from '../Message'; +import { Message } from '../Message'; import { useComponentContext } from '../../context'; -import { getIsFirstUnreadMessage, isDateSeparatorMessage, isIntroMessage } from './utils'; - -import type { LocalMessage } from 'stream-chat'; import type { GroupStyle, RenderedMessage } from './utils'; +import { getIsFirstUnreadMessage, isDateSeparatorMessage, isIntroMessage } from './utils'; import type { VirtuosoContext } from './VirtualizedMessageList'; import type { UnknownType } from '../../types/types'; @@ -131,7 +129,7 @@ export const messageRenderer = ( reactionDetailsSort, renderText, returnAllReadData, - shouldGroupByUser, + showAvatar, sortReactionDetails, sortReactions, threadList, @@ -160,27 +158,6 @@ export const messageRenderer = ( return MessageSystem ? : null; } - const maybePrevMessage = messageList[streamMessageIndex - 1] as - | LocalMessage - | undefined; - const maybeNextMessage = messageList[streamMessageIndex + 1] as - | LocalMessage - | undefined; - const groupedByUser = - shouldGroupByUser && - streamMessageIndex > 0 && - message.user?.id === maybePrevMessage?.user?.id; - - // FIXME: firstOfGroup & endOfGroup should be derived from groupStyles which apply a more complex logic - const firstOfGroup = - shouldGroupByUser && - (message.user?.id !== maybePrevMessage?.user?.id || - (maybePrevMessage && isMessageEdited(maybePrevMessage))); - - const endOfGroup = - shouldGroupByUser && - (message.user?.id !== maybeNextMessage?.user?.id || isMessageEdited(message)); - const isFirstUnreadMessage = getIsFirstUnreadMessage({ firstUnreadMessageId, isFirstMessage: streamMessageIndex === 0, @@ -203,10 +180,7 @@ export const messageRenderer = ( autoscrollToBottom={virtuosoRef.current?.autoscrollToBottom} closeReactionSelectorOnClick={closeReactionSelectorOnClick} deliveredTo={ownMessagesDeliveredToOthers[message.id] || []} - endOfGroup={endOfGroup} - firstOfGroup={firstOfGroup} formatDate={formatDate} - groupedByUser={groupedByUser} groupStyles={[messageGroupStyles[message.id] ?? '']} lastOwnMessage={lastOwnMessage} lastReceivedId={lastReceivedMessageId} @@ -218,6 +192,7 @@ export const messageRenderer = ( readBy={ownMessagesReadByOthers[message.id] || []} renderText={renderText} returnAllReadData={returnAllReadData} + showAvatar={showAvatar} sortReactionDetails={sortReactionDetails} sortReactions={sortReactions} threadList={threadList} diff --git a/src/components/MessageList/styling/MessageList.scss b/src/components/MessageList/styling/MessageList.scss new file mode 100644 index 0000000000..91596687de --- /dev/null +++ b/src/components/MessageList/styling/MessageList.scss @@ -0,0 +1,169 @@ +@use '../../../styling/utils'; + +// Layout +.str-chat__main-panel-inner { + height: 100%; + display: flex; + flex-direction: column; + min-height: 0; + position: relative; + align-items: center; +} + +.str-chat__message-list { + @include utils.scrollable; + overscroll-behavior: none; + width: 100%; + /* Max container 800px, 16px padding → 768px readable content; matches composer width + padding */ + max-width: calc(var(--str-chat__message-composer-max-width) + var(--str-chat__message-composer-padding)); + height: 100%; + max-height: 100%; + @include utils.component-layer-overrides('message-list'); + + .str-chat__message-list-scroll { + @include utils.message-list-spacing; + + .str-chat__ul { + list-style: none; + padding: 0; + margin: 0; + } + } + + .str-chat__parent-message-li { + $spacing: var(--str-chat__spacing-4); + padding-block-end: $spacing; + margin-block-end: $spacing; + border-block-end: 1px solid var(--str-chat__thread-head-start-border-block-end-color); + + .str-chat__thread-start { + color: var(--str-chat__thread-head-start-color); + font: var(--str-chat__subtitle-text); + } + + .str-chat__thread-start { + text-align: start; + padding-top: var(--str-chat__spacing-3); + } + } +} + +.str-chat__jump-to-latest-message { + position: absolute; + inset-block-end: var(--str-chat__spacing-4); + inset-inline-end: var(--str-chat__spacing-2); + z-index: 2; + + .str-chat__jump-to-latest-unread-count { + position: absolute; + padding: var(--str-chat__spacing-0_5) var(--str-chat__spacing-2); + left: 50%; + transform: translateX(-50%) translateY(-100%); + } +} + +.str-chat__main-panel { + .str-chat__ul { + .str-chat__li:first-of-type { + padding-top: 4.5rem; + } + + .str-chat__date-separator + .str-chat__li:first-of-type { + padding-top: inherit; + } + } +} + +// Theme +.str-chat { + /* The border radius used for the borders of the component */ + --str-chat__message-list-border-radius: 0; + + /* The text/icon color of the component */ + --str-chat__message-list-color: var(--str-chat__text-color); + + /* The background color of the component */ + --str-chat__message-list-background-color: var(--str-chat__background-color); + + /* Box shadow applied to the component */ + --str-chat__message-list-box-shadow: none; + + /* Top border of the component */ + --str-chat__message-list-border-block-start: none; + + /* Bottom border of the component */ + --str-chat__message-list-border-block-end: none; + + /* Left (right in RTL layout) border of the component */ + --str-chat__message-list-border-inline-start: none; + + /* Right (left in RTL layout) border of the component */ + --str-chat__message-list-border-inline-end: none; + + /* The border radius used for the borders of the jump to latest message button */ + --str-chat__jump-to-latest-message-border-radius: var(--str-chat__circle-fab-border-radius); + + /* The text/icon color of the jump to latest message button */ + --str-chat__jump-to-latest-message-color: var(--str-chat__circle-fab-color); + + /* The background color of the jump to latest message button */ + --str-chat__jump-to-latest-message-background-color: var(--str-chat__circle-fab-background-color); + + /* The background color of the jump to latest message button in pressed state */ + --str-chat__jump-to-latest-message-pressed-background-color: var( + --str-chat__circle-fab-pressed-background-color + ); + + /* Box shadow applied to the jump to latest message button */ + --str-chat__jump-to-latest-message-box-shadow: var(--str-chat__circle-fab-box-shadow); + + /* Top border of the jump to latest message button */ + --str-chat__jump-to-latest-message-border-block-start: var( + --str-chat__circle-fab-border-block-start + ); + + /* Bottom border of the jump to latest message button */ + --str-chat__jump-to-latest-message-border-block-end: var(--str-chat__circle-fab-border-block-end); + + /* Left (right in RTL layout) border of the jump to latest message button */ + --str-chat__jump-to-latest-message-border-inline-start: var( + --str-chat__circle-fab-border-inline-start + ); + + /* Right (left in RTL layout) border of the jump to latest message button */ + --str-chat__jump-to-latest-message-border-inline-end: var( + --str-chat__circle-fab-border-inline-end + ); + + /* The background color of the unread messages count on the jump to latest message button */ + --str-chat__jump-to-latest-message-unread-count-background-color: var( + --str-chat__jump-to-latest-message-color + ); + + /* The text/icon color of the unread messages count on the jump to latest message button */ + --str-chat__jump-to-latest-message-unread-count-color: var( + --str-chat__jump-to-latest-message-background-color + ); + + /* The color used for displaying thread replies and thread separator at the start of a thread */ + --str-chat__thread-head-start-color: var(--str-chat__text-low-emphasis-color); + + /* The color used for the separator below the first message in a thread */ + --str-chat__thread-head-start-border-block-end-color: var(--str-chat__surface-color); +} + +.str-chat__jump-to-latest-message { + --str-chat-icon-color: var(--str-chat__jump-to-latest-message-unread-count-background-color); + + .str-chat__circle-fab { + @include utils.component-layer-overrides('jump-to-latest-message'); + @include utils.circle-fab-overrides('jump-to-latest-message'); + + .str-chat__jump-to-latest-unread-count { + background-color: var(--str-chat__jump-to-latest-message-unread-count-background-color); + color: var(--str-chat__jump-to-latest-message-unread-count-color); + border-radius: var(--str-chat__jump-to-latest-message-border-radius); + font: var(--str-chat__caption-text); + } + } +} diff --git a/src/components/MessageList/styling/VirtualizedMessageList.scss b/src/components/MessageList/styling/VirtualizedMessageList.scss new file mode 100644 index 0000000000..bc50fa0fe6 --- /dev/null +++ b/src/components/MessageList/styling/VirtualizedMessageList.scss @@ -0,0 +1,97 @@ +@use '../../../styling/utils'; + +// todo: React SDK specific. Copied from V1. Deprecate after merging with MessageList + +// Layout +.str-chat__virtual-list { + @include utils.scrollable; + position: relative; + flex: 1; + -webkit-overflow-scrolling: touch; /* enable smooth scrolling on ios */ + margin: 0; + width: 100%; + /* Max container = composer width + padding; matches message list */ + max-width: calc(var(--str-chat__message-composer-max-width) + var(--str-chat__message-composer-padding)); + height: 100%; + + .str-chat__message-list-scroll { + overscroll-behavior: none; + } + + .str-chat__message-list-scroll > div { + @include utils.message-list-spacing; + } + + .str-chat__parent-message-li { + padding-block-end: var(--str-chat__spacing-4); + + .str-chat__thread-start { + text-align: start; + padding-top: var(--str-chat__spacing-3); + } + } + + // conditionally showing the header displaces items when prepending. + // a simple workaround is to make the loading indicator an overlay. + &__loading { + display: flex; + padding-top: var(--str-chat__spacing-2); + justify-content: center; + width: 100%; + position: absolute; + } + + p { + margin: 0 !important; // always use padding, margin mess up the height calculations + + a { + white-space: pre-line; + overflow: hidden; + word-wrap: break-word; + } + } + + .str-chat__message { + margin-block-end: 0 !important; + } +} + +// Theme (only available in React SDK) +.str-chat { + /* The border radius used for the borders of the component */ + --str-chat__virtual-list-border-radius: 0; + + /* The text/icon color of the component */ + --str-chat__virtual-list-color: var(--str-chat__text-color); + + /* The background color of the component */ + --str-chat__virtual-list-background-color: var(--str-chat__background-color); + + /* Box shadow applied to the component */ + --str-chat__virtual-list-box-shadow: none; + + /* Top border of the component */ + --str-chat__virtual-list-border-block-start: none; + + /* Bottom border of the component */ + --str-chat__virtual-list-border-block-end: none; + + /* Left (right in RTL layout) border of the component */ + --str-chat__virtual-list-border-inline-start: none; + + /* Right (left in RTL layout) border of the component */ + --str-chat__virtual-list-border-inline-end: none; +} + +.str-chat__virtual-list { + @include utils.component-layer-overrides('virtual-list'); + + .str-chat__parent-message-li { + border-block-end: 1px solid var(--str-chat__thread-head-start-border-block-end-color); + + .str-chat__thread-start { + color: var(--str-chat__thread-head-start-color); + font: var(--str-chat__subtitle-text); + } + } +} diff --git a/src/components/MessageList/styling/index.scss b/src/components/MessageList/styling/index.scss new file mode 100644 index 0000000000..71982d251d --- /dev/null +++ b/src/components/MessageList/styling/index.scss @@ -0,0 +1,2 @@ +@use 'MessageList'; +@use 'VirtualizedMessageList'; diff --git a/src/context/MessageContext.tsx b/src/context/MessageContext.tsx index f10aae8850..a028c8f96d 100644 --- a/src/context/MessageContext.tsx +++ b/src/context/MessageContext.tsx @@ -115,6 +115,8 @@ export type MessageContextValue = { reactionDetailsSort?: ReactionSort; /** A list of users that have read this Message */ readBy?: UserResponse[]; + /** When set, shows the sender avatar in a grid layout. Values: true | 'incoming' | 'outgoing'. */ + showAvatar?: boolean | 'incoming' | 'outgoing'; /** Custom function to render message text content, defaults to the renderText function: [utils](https://github.com/GetStream/stream-chat-react/blob/master/src/utils.tsx) */ renderText?: ( text?: string, diff --git a/src/styling/_global-theme-variables.scss b/src/styling/_global-theme-variables.scss index 4664ac581f..bfc6b09982 100644 --- a/src/styling/_global-theme-variables.scss +++ b/src/styling/_global-theme-variables.scss @@ -42,8 +42,8 @@ var(--str-chat__font-family); --str-chat__caption-default-text: normal var(--typography-font-weight-regular) - var(--typography-font-size-sm) / var(--typography-line-height-normal) - var(--str-chat__font-family); + var(--typography-font-size-sm) / var(--typography-line-height-normal) + var(--str-chat__font-family); --str-chat__caption-emphasis-text: normal var(--typography-font-weight-semi-bold) var(--typography-font-size-sm) / var(--typography-line-height-tight) diff --git a/src/styling/_utils.scss b/src/styling/_utils.scss index 07e390237d..298868634e 100644 --- a/src/styling/_utils.scss +++ b/src/styling/_utils.scss @@ -73,6 +73,11 @@ overflow-y: hidden; } +@mixin scrollable { + overflow-x: hidden; + overflow-y: auto; +} + @mixin clamped-height-from-original-image-dimensions( $max-height-css-var, $max-width-css-var @@ -84,36 +89,6 @@ ); } -@mixin message-list-spacing { - $spacing: var(--str-chat__spacing-2); - padding: 0 $spacing; - - // Need this trick to be able to apply full-width background color on hover to messages / full-width separator to thread header - .str-chat__li { - margin-inline: calc(-1 * #{$spacing}); - padding-inline: $spacing; - } - - .str-chat__parent-message-li { - margin-inline: calc(-1 * #{$spacing}); - } - - @media only screen and (min-device-width: 768px) { - $spacing: min(var(--str-chat__spacing-10), 4%); - - padding: 0 $spacing; - - .str-chat__li { - margin-inline: calc(-1 * #{$spacing}); - padding-inline: $spacing; - } - - .str-chat__parent-message-li { - margin-inline: calc(-1 * #{$spacing} - 2px); - } - } -} - @mixin header-layout { display: flex; padding: var(--str-chat__spacing-2); @@ -154,33 +129,19 @@ } @mixin message-list-spacing { - $spacing: var(--str-chat__spacing-2); - padding: 0 $spacing; + // 800px container, 16px padding, 768px content — align with composer + $spacing: var(--str-chat__message-composer-padding, 16px); + padding: 0; + padding-inline: $spacing; // Need this trick to be able to apply full-width background color on hover to messages / full-width separator to thread header .str-chat__li { margin-inline: calc(-1 * #{$spacing}); - padding-inline: $spacing; } .str-chat__parent-message-li { margin-inline: calc(-1 * #{$spacing}); } - - @media only screen and (min-device-width: 768px) { - $spacing: min(var(--str-chat__spacing-10), 4%); - - padding: 0 $spacing; - - .str-chat__li { - margin-inline: calc(-1 * #{$spacing}); - padding-inline: $spacing; - } - - .str-chat__parent-message-li { - margin-inline: calc(-1 * #{$spacing} - 2px); - } - } } @mixin loading-item-background($component-name) { diff --git a/src/styling/index.scss b/src/styling/index.scss index 0eb12d9cc8..a16cb65962 100644 --- a/src/styling/index.scss +++ b/src/styling/index.scss @@ -35,6 +35,7 @@ @use '../components/MessageActions/styling' as MessageActions; @use '../components/MessageBounce/styling' as MessageBounce; @use '../components/MessageInput/styling' as MessageComposer; +@use '../components/MessageList/styling' as MessageList; @use '../components/Poll/styling' as Poll; @use '../components/Reactions/styling/ReactionList' as ReactionList; @use '../components/Reactions/styling/ReactionSelector' as ReactionSelector;