diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx index 128c58d9f..3f1cd2b6e 100644 --- a/examples/vite/src/App.tsx +++ b/examples/vite/src/App.tsx @@ -1,4 +1,12 @@ -import { type PropsWithChildren, useCallback, useEffect, useMemo } from 'react'; +import { + createContext, + type PropsWithChildren, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; import { ChannelFilters, ChannelOptions, @@ -27,6 +35,9 @@ import { MessageList, Window, WithComponents, + GroupAvatar, + ReactionsList, + useMessageContext, } from 'stream-chat-react'; import { createTextComposerEmojiMiddleware, EmojiPicker } from 'stream-chat-react/emojis'; import { init, SearchIndex } from 'emoji-mart'; @@ -79,9 +90,39 @@ const useUser = () => { return { userId: userId, tokenProvider }; }; +const CustomMessageReactions = (props: React.ComponentProps) => { + const { visualStyle, verticalPosition, flipHorizontalPosition } = useContext(TempCtx); + + return ( + + ); +}; + +type TempCtxValue = { + visualStyle: 'clustered' | 'segmented'; + verticalPosition: 'top' | 'bottom'; + flipHorizontalPosition: boolean; +}; +const TempCtx = createContext({ + visualStyle: 'clustered', + verticalPosition: 'top', + flipHorizontalPosition: false, +}); + const App = () => { const { userId, tokenProvider } = useUser(); + const [tempCtxValue, setTempCtxValue] = useState({ + visualStyle: 'clustered', + verticalPosition: 'top', + flipHorizontalPosition: false, + }); + const chatClient = useCreateChatClient({ apiKey, tokenOrProvider: tokenProvider, @@ -141,43 +182,85 @@ const App = () => { if (!chatClient) return <>Loading...; return ( - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + +
+ + + +
+ + + + +
+ +
+
+ + + + + + +
+
+
+
); }; diff --git a/examples/vite/src/stream-imports-layout.scss b/examples/vite/src/stream-imports-layout.scss index 9b13ffc4f..ce93ed832 100644 --- a/examples/vite/src/stream-imports-layout.scss +++ b/examples/vite/src/stream-imports-layout.scss @@ -32,7 +32,7 @@ //@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/MessageReactions/MessageReactions-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'; @use 'stream-chat-react/dist/scss/v2/Notification/MessageNotification-layout'; diff --git a/examples/vite/src/stream-imports-theme.scss b/examples/vite/src/stream-imports-theme.scss index a41570fe1..ff14d30dc 100644 --- a/examples/vite/src/stream-imports-theme.scss +++ b/examples/vite/src/stream-imports-theme.scss @@ -28,7 +28,7 @@ @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/MessageReactions/MessageReactions-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'; @use 'stream-chat-react/dist/scss/v2/Notification/MessageNotification-theme'; diff --git a/src/components/Message/styling/Message.scss b/src/components/Message/styling/Message.scss index 46501179f..992855ec7 100644 --- a/src/components/Message/styling/Message.scss +++ b/src/components/Message/styling/Message.scss @@ -17,7 +17,9 @@ --str-chat__message-status-color: var(--str-chat__primary-color); --str-chat__message-replies-count-color: var(--str-chat__primary-color); --str-chat__message-background-color: transparent; - --str-chat__message-highlighted-background-color: var(--str-chat__message-highlight-color); + --str-chat__message-highlighted-background-color: var( + --str-chat__message-highlight-color + ); --str-chat__message-border-block-start: none; --str-chat__message-border-block-end: none; --str-chat__message-border-inline-start: none; @@ -26,7 +28,9 @@ --str-chat__message-active-background-color: transparent; --str-chat__message-options-color: var(--str-chat__text-low-emphasis-color); - --str-chat__message-options-hover-background-color: var(--str-chat__tertiary-surface-color); + --str-chat__message-options-hover-background-color: var( + --str-chat__tertiary-surface-color + ); --str-chat__message-options-border-radius: var(--str-chat__border-radius-circle); --str-chat__message-options-active-color: var(--str-chat__primary-color); @@ -35,7 +39,9 @@ --str-chat__message-bubble-background-color: var(--chat-bg-incoming); --str-chat__own-message-bubble-color: var(--chat-text-message); --str-chat__own-message-bubble-background-color: var(--chat-bg-outgoing); - --str-chat__quoted-message-bubble-background-color: var(--str-chat__secondary-background-color); + --str-chat__quoted-message-bubble-background-color: var( + --str-chat__secondary-background-color + ); --str-chat__message-bubble-border-block-start: none; --str-chat__message-bubble-border-block-end: none; --str-chat__message-bubble-border-inline-start: none; @@ -80,7 +86,9 @@ --str-chat__date-separator-box-shadow: none; --str-chat__translation-notice-color: var(--str-chat__text-low-emphasis-color); - --str-chat__translation-notice-active-background-color: var(--str-chat__tertiary-surface-color); + --str-chat__translation-notice-active-background-color: var( + --str-chat__tertiary-surface-color + ); --str-chat__message-reminder-color: var(--str-chat__primary-color); --str-chat__message-reminder-background-color: var(--str-chat__grey50); @@ -103,8 +111,8 @@ .str-chat__message-bubble { background-color: transparent; overflow: visible; - font-size: 64px; - line-height: 64px; + font-size: var(--size-64); + line-height: 1; } } } @@ -113,7 +121,9 @@ --str-chat-message-options-size: calc(3 * var(--str-chat__message-options-button-size)); &.str-chat__message-without-touch-support { - --str-chat-message-options-size: calc(1 * var(--str-chat__message-options-button-size)); + --str-chat-message-options-size: calc( + 1 * var(--str-chat__message-options-button-size) + ); } .str-chat__message-bubble { @@ -130,7 +140,6 @@ } } - .str-chat__message.str-chat__message--has-attachment { --str-chat__message-max-width: var(--str-chat__message-with-attachment-max-width); @@ -183,17 +192,27 @@ font: var(--str-chat__caption-medium-text); } - &.str-chat__message--other { + &.str-chat__message--other { grid-template-areas: '. message-reminder' 'avatar message' - '. replies' - '. translation-notice' - '. custom-metadata' - '. metadata'; + 'avatar replies' + '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-inner { + .str-chat__message-reactions-host { + justify-content: flex-start; + + &:has(.str-chat__message-reactions--flipped-horizontally) { + justify-content: flex-end; + } + } + } } &.str-chat__message--me { @@ -206,9 +225,14 @@ 'metadata'; justify-items: end; - // > selector added to not hide sender of inline replies - > .str-chat__message-sender-avatar { - display: none; + .str-chat__message-inner { + .str-chat__message-reactions-host { + justify-content: flex-end; + + &:has(.str-chat__message-reactions--flipped-horizontally) { + justify-content: flex-start; + } + } } } @@ -225,7 +249,7 @@ grid-template-columns: auto; } - .str-chat__message-sender-avatar { + .str-chat__avatar:has(~ .str-chat__message-inner) { grid-area: avatar; align-self: end; } @@ -234,7 +258,7 @@ grid-area: message; display: grid; grid-template-areas: - 'reactions reactions' + 'reactions .' 'message-bubble options'; grid-template-columns: auto 1fr; column-gap: var(--str-chat__spacing-2); @@ -286,10 +310,33 @@ } .str-chat__message-reactions-host { + display: flex; grid-area: reactions; + min-width: 100%; + z-index: 1; + + &:has(.str-chat__message-reactions--top) { + margin-bottom: calc(var(--spacing-xxs) * -1); + } + + &:has(.str-chat__message-reactions--bottom) { + padding-block: var(--spacing-xxs); + } + } + + &:has(.str-chat__message-reactions--top) { + // maybe remove later + } + + &:has(.str-chat__message-reactions--bottom) { + grid-template-areas: + 'message-bubble options' + 'reactions .'; } .str-chat__message-bubble { + display: flex; + justify-self: flex-start; grid-area: message-bubble; position: relative; min-width: 0; @@ -308,19 +355,27 @@ } &.str-chat__message--me .str-chat__message-bubble { + justify-self: flex-end; background-color: var(--str-chat__own-message-bubble-background-color); } &.str-chat__message--me .str-chat__message-inner { grid-template-areas: 'reminder reminder' - 'reactions reactions' + '. reactions' 'options message-bubble'; grid-template-columns: 1fr auto; .str-chat__message-options { flex-direction: row; } + + &:has(.str-chat__message-reactions--bottom) { + grid-template-areas: + 'reminder reminder' + 'options message-bubble' + '. reactions'; + } } .str-chat__translation-notice { @@ -517,11 +572,19 @@ } // Message options display - default mode: they appear when .str-chat__li is hovered -.str-chat__ul:not(.str-chat__message-options-in-bubble, .str-chat__message-with-touch-support), -.str-chat__virtual-list:not(.str-chat__message-options-in-bubble, .str-chat__message-with-touch-support) { +.str-chat__ul:not( + .str-chat__message-options-in-bubble, + .str-chat__message-with-touch-support + ), +.str-chat__virtual-list:not( + .str-chat__message-options-in-bubble, + .str-chat__message-with-touch-support + ) { /* This rule won't be applied in browsers that don't support :has() */ .str-chat__li:hover:not(:has(.str-chat__reaction-list:hover, .str-chat__modal--open)), - .str-chat__li:focus-within:not(:has(.str-chat__reaction-list:focus-within, .str-chat__modal--open)), + .str-chat__li:focus-within:not( + :has(.str-chat__reaction-list:focus-within, .str-chat__modal--open) + ), .str-chat__li:has(.str-chat__message-options--active) { .str-chat__message-options { display: flex; @@ -598,7 +661,8 @@ } } -.str-chat__message--other .str-chat__message-inner:not(:has(.str-chat__message-options--active)) { +.str-chat__message--other + .str-chat__message-inner:not(:has(.str-chat__message-options--active)) { margin-inline-end: var(--str-chat-message-options-size); } diff --git a/src/components/Reactions/ReactionsList.tsx b/src/components/Reactions/ReactionsList.tsx index cb122c85a..9944e6d5f 100644 --- a/src/components/Reactions/ReactionsList.tsx +++ b/src/components/Reactions/ReactionsList.tsx @@ -44,17 +44,27 @@ export type ReactionsListProps = Partial< sortReactionDetails?: ReactionDetailsComparator; /** Comparator function to sort reactions, defaults to chronological order */ sortReactions?: ReactionsComparator; + + /** + * Positioning of the reactions list relative to the message. Position is flipped by default for the messages of other users. + */ + flipHorizontalPosition?: boolean; + verticalPosition?: 'top' | 'bottom' | null; + visualStyle?: 'clustered' | 'segmented' | null; }; const UnMemoizedReactionsList = (props: ReactionsListProps) => { const { + flipHorizontalPosition = false, handleFetchReactions, // eslint-disable-next-line @typescript-eslint/no-unused-vars reactionDetailsSort, - reverse = false, sortReactionDetails, + verticalPosition = 'top', + visualStyle = 'clustered', ...rest } = props; + const { existingReactions, hasReactions, totalReactionCount } = useProcessReactions(rest); const [selectedReactionType, setSelectedReactionType] = useState( @@ -63,12 +73,12 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => { const { t } = useTranslationContext('ReactionsList'); const { ReactionsListModal = DefaultReactionsListModal } = useComponentContext(); - const handleReactionButtonClick = (reactionType: string) => { + const handleReactionButtonClick = (reactionType: ReactionType) => { if (totalReactionCount > MAX_MESSAGE_REACTIONS_TO_FETCH) { return; } - setSelectedReactionType(reactionType as ReactionType); + setSelectedReactionType(reactionType); }; if (!hasReactions) return null; @@ -77,47 +87,45 @@ const UnMemoizedReactionsList = (props: ReactionsListProps) => { <>
-
    +
      {existingReactions.map( - ({ EmojiComponent, isOwnReaction, reactionCount, reactionType }) => + ({ EmojiComponent, reactionCount, reactionType }) => EmojiComponent && ( -
    • +
    • ), )} -
    • - {totalReactionCount} -
    + {visualStyle === 'clustered' && ( + + {totalReactionCount} + + )}
{selectedReactionType !== null && (