diff --git a/.styles/Vocab/Base/accept.txt b/.styles/Vocab/Base/accept.txt index 791451f920..26208f8584 100644 --- a/.styles/Vocab/Base/accept.txt +++ b/.styles/Vocab/Base/accept.txt @@ -45,6 +45,8 @@ async Podfile pickFile onChange +pressable +Pressable boolean Async subscribable diff --git a/docusaurus/docs/reactnative/assets/basics/migrating-from-5.x-to-6.x/message_reactions.png b/docusaurus/docs/reactnative/assets/basics/migrating-from-5.x-to-6.x/message_reactions.png new file mode 100644 index 0000000000..fe7a8d252a Binary files /dev/null and b/docusaurus/docs/reactnative/assets/basics/migrating-from-5.x-to-6.x/message_reactions.png differ diff --git a/docusaurus/docs/reactnative/basics/migrating-from-5.x-to-6.x.mdx b/docusaurus/docs/reactnative/basics/migrating-from-5.x-to-6.x.mdx index b046a7d818..5cb8a2c52c 100644 --- a/docusaurus/docs/reactnative/basics/migrating-from-5.x-to-6.x.mdx +++ b/docusaurus/docs/reactnative/basics/migrating-from-5.x-to-6.x.mdx @@ -90,6 +90,14 @@ The Message Menu design has been revamped to provide a better user experience. T The previous overlay design has been replaced with a bottom sheet modal design. ::: +#### Introduce new ReactionList design + +We have introduced a new ReactionList design that is more intuitive and provides a better user experience. + +![Message Menu](../assets/basics/migrating-from-5.x-to-6.x/message_reactions.png) + +You can switch to `bottom` reaction list design by setting the [`reactionListPosition`](../core-components/channel.mdx#reactionlistposition) prop to `bottom` in the `Channel` component. The Reaction list component can be completely customized by providing a custom component to the [`ReactionListTop`](../core-components/channel.mdx#reactionlisttop) prop in the `Channel` component for top reactions list and [`ReactionListBottom`](../core-components/channel.mdx#reactionlistbottom) prop for bottom reactions list. The default mode is `top` as it was in the previous version. + #### Remove `StreamChatRN` and introduce `ChatConfigContext` The `StreamChatRN` global config is removed in favour of the `ChatConfigContext`. The `ChatConfigContext` is a more versatile and feature-rich context that can be used to provide any global configuration to the chat application. @@ -153,7 +161,7 @@ The `MessageOverlay` component has been removed in favour of `MessageMenu`. The The `MessageOverlay` component is removed from top level `OverlayProvider` and is replaced with `MessageMenu` in the level of the `Message` component. -#### Remove prop from `Message` component. +#### Remove props from `Message` component. The following props have been removed from the `Message` component: @@ -165,6 +173,23 @@ The following props have been removed from the `Message` component: The later 3 props are removed in favour of similar props on MessagesContext and is therefore not needed. The `setData` prop is removed in favour of the removal of `MessageOverlayContext` and the `setOverlay` is not needed as we don't set the message overlay in `OverlayProvider`. +#### Removed props from `MessageContent` component + +The following props have been removed from the `MessageContent` component: + +- `hasReactions`, `lastGroupMessage`, `members`, `onlyEmojis`, `showMessageStatus` imported from the Message Context. +- `addtionalTouchableProps` is changed to `additionalPressableProps`. +- `MessageFooter`, `MessageHeader`, `MessageDeleted`, `MessagePinnedHeader`, `MessageReplies`, `MessageStatus`, `onPressInMessage` imported from the Messages Context. + +The props were redundant as per the new fixes in the Reaction Design and message simple component's improvements. + +#### Add props to `MessageSimple` component + +The components from the `MessageContent` components are rendered now in `MessageSimple` component so few of the props that were removed from `MessageContent` are added to `MessageSimple` component(as mentioned above). + +- `isMyMessage`, `lastGroupMessage`, `members`, `onlyEmojis`, `otherAttachments`, `showMessageStatus` imported from the Message Context. +- `MessageDeleted`, `MessageFooter`, `MessageHeader`, `MessagePinnedHeader`, `MessageReplies`, `MessageStatus`, `ReactionListBottom`, `reactionListPosition` and `ReactionListTop` imported from the Messages Context. + #### Added `BottomSheetModal` component The version introduces a very basic `BottomSheetModal` component that can be used to show a modal at the bottom of the screen. This can be used to show the message actions and reactions. @@ -186,6 +211,10 @@ registerNativeHandlers({ The type of `quotedMessage` is changed from `MessageType | boolean` to `MessageType | undefined` for better in the `MessageInputContext`. +#### Refactor of theme object + +The default theme object has been refactored to provide a better customization experience. The theme object is now more organized and provides better control over the customization. You can check the object [here](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/contexts/themeContext/utils/theme.ts). + #### Remove the deprecated code - We have removed `loadChannelAtMessage` from channel context because it was no more used. diff --git a/docusaurus/docs/reactnative/common-content/contexts/message-context/channel.mdx b/docusaurus/docs/reactnative/common-content/contexts/message-context/channel.mdx new file mode 100644 index 0000000000..5addd4bbab --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/contexts/message-context/channel.mdx @@ -0,0 +1,5 @@ +Channel instance from the StreamChat client. + +| Type | +| ------------------------------------------------------------------------------------------- | +| [Channel](https://getstream.io/chat/docs/javascript/creating_channels/?language=javascript) | diff --git a/docusaurus/docs/reactnative/common-content/contexts/message-context/go_to_message.mdx b/docusaurus/docs/reactnative/common-content/contexts/message-context/go_to_message.mdx new file mode 100644 index 0000000000..6ab1f2971f --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/contexts/message-context/go_to_message.mdx @@ -0,0 +1,5 @@ +A function that scrolls to a specific message in the chat. This function is useful when you want to scroll to a specific message in the chat. + +| Type | Default | +| ------------------------------------------- | ----------- | +| `(messageId: string) => void`\| `undefined` | `undefined` | diff --git a/docusaurus/docs/reactnative/common-content/contexts/message-context/handle_reaction.mdx b/docusaurus/docs/reactnative/common-content/contexts/message-context/handle_reaction.mdx new file mode 100644 index 0000000000..7ed6ae5cac --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/contexts/message-context/handle_reaction.mdx @@ -0,0 +1,5 @@ +Function to handle a reaction on a message. This function is called when a user reacts to a message. The function is passed the message ID, the reaction type, and the user ID of the user who reacted to the message. The function should update the message with the reaction and update the message in the message context. + +| Type | Default | +| -------------------------------------------------------- | ----------- | +| `(reactionType: string) => Promise` \| `undefined` | `undefined` | diff --git a/docusaurus/docs/reactnative/common-content/contexts/message-context/is_edited_message_open.mdx b/docusaurus/docs/reactnative/common-content/contexts/message-context/is_edited_message_open.mdx new file mode 100644 index 0000000000..54cbb0bb9c --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/contexts/message-context/is_edited_message_open.mdx @@ -0,0 +1,5 @@ +Boolean to check the message edited label is expanded or not. + +| Type | Default | +| ------- | ------- | +| Boolean | `false` | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/additional_pressable_props.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/additional_pressable_props.mdx new file mode 100644 index 0000000000..7594c59a52 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/additional_pressable_props.mdx @@ -0,0 +1,5 @@ +Additional props provided to the underlying [Pressable](https://reactnative.dev/docs/pressable#props) used by components within a message such as [`MessageContent`](../../../../ui-components/message-content.mdx). + +| Type | +| ------ | +| object | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/additional_touchable_props.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/additional_touchable_props.mdx deleted file mode 100644 index ff0262dd8f..0000000000 --- a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/additional_touchable_props.mdx +++ /dev/null @@ -1,5 +0,0 @@ -Additional props provided to the underlying [TouchableOpacity](https://reactnative.dev/docs/touchableopacity#props) used by components within a message such as [`MessageContent`](../../../../ui-components/message-content.mdx). - -| Type | -| ------ | -| object | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/reaction-list-bottom.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/reaction-list-bottom.mdx new file mode 100644 index 0000000000..055da4d7e4 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/reaction-list-bottom.mdx @@ -0,0 +1,5 @@ +Component to render list of reactions at the bottom of the message bubble. + +| Type | Default | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`ReactionListBottom`](https://github.com/GetStream/stream-chat-react-native/tree/develop/package/src/components/Message/MessageSimple/ReactionList/ReactionListBottom.tsx) | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/reaction-list-position.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/reaction-list-position.mdx new file mode 100644 index 0000000000..78891c4725 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/reaction-list-position.mdx @@ -0,0 +1,5 @@ +The position of the reaction list in the message component. By default, the reaction list is positioned on top the message content. + +| Type | Default value | +| ----------------- | ------------- | +| `top` \| `bottom` | 'top' | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/reaction-list-top.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/reaction-list-top.mdx new file mode 100644 index 0000000000..63a7b95173 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/reaction-list-top.mdx @@ -0,0 +1,5 @@ +Component to render list of reactions at top of the message bubble. + +| Type | Default | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`ReactionListTop`](https://github.com/GetStream/stream-chat-react-native/tree/develop/package/src/components/Message/MessageSimple/ReactionList/ReactionListTop.tsx) | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/reaction-list.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/reaction-list.mdx deleted file mode 100644 index 04bf00319a..0000000000 --- a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/reaction-list.mdx +++ /dev/null @@ -1,5 +0,0 @@ -Component to render list of reactions at top of message bubble. - -| Type | Default | -| ------------- | ------------------------------------------------------------- | -| ComponentType | [`ReactionList`](../../../../ui-components/reaction-list.mdx) | diff --git a/docusaurus/docs/reactnative/contexts/messages-context.mdx b/docusaurus/docs/reactnative/contexts/messages-context.mdx index a6a24251fb..a935125101 100644 --- a/docusaurus/docs/reactnative/contexts/messages-context.mdx +++ b/docusaurus/docs/reactnative/contexts/messages-context.mdx @@ -3,7 +3,7 @@ id: messages-context title: MessagesContext --- -import AdditionalTouchableProps from '../common-content/ui-components/channel/props/additional_touchable_props.mdx'; +import AdditionalPressableProps from '../common-content/ui-components/channel/props/additional_pressable_props.mdx'; import Attachment from '../common-content/ui-components/channel/props/attachment.mdx'; import AttachmentActions from '../common-content/ui-components/channel/props/attachment_actions.mdx'; import AudioAttachment from '../common-content/ui-components/channel/props/audio_attachment.mdx'; @@ -69,7 +69,9 @@ import MessageReactionPicker from '../common-content/ui-components/channel/props import MessageUserReactionsAvatar from '../common-content/ui-components/channel/props/message-user-reactions-avatar.mdx'; import MessageUserReactionsItem from '../common-content/ui-components/channel/props/message-user-reactions-item.mdx'; import MessageUserReactions from '../common-content/ui-components/channel/props/message-user-reactions.mdx'; -import ReactionList from '../common-content/ui-components/channel/props/reaction-list.mdx'; +import ReactionListBottom from '../common-content/ui-components/channel/props/reaction-list-bottom.mdx'; +import ReactionListPosition from '../common-content/ui-components/channel/props/reaction-list-position.mdx'; +import ReactionListTop from '../common-content/ui-components/channel/props/reaction-list-top.mdx'; import Reply from '../common-content/ui-components/channel/props/reply.mdx'; import ScrollToBottomButton from '../common-content/ui-components/channel/props/scroll-to-bottom-button.mdx'; import SelectReaction from '../common-content/ui-components/channel/props/select_reaction.mdx'; @@ -81,9 +83,9 @@ import VideoThumbnail from '../common-content/ui-components/channel/props/video_ ## Value -###
_forwarded from [Channel](../../core-components/channel#additionaltouchableprops)_ props
additionalTouchableProps {#additionaltouchableprops} +###
_forwarded from [Channel](../../core-components/channel#additionalpressableprops)_ props
additionalPressableProps {#additionalpressableprops} - + ### `channelId` @@ -201,6 +203,10 @@ Id of current channel. +###
_forwarded from [Channel](../../core-components/channel#reactionlistposition)_ props
reactionListPosition {#reactionlistposition} + + + ### `removeMessage` Function to remove message from local channel state. Please note that this function is only for updating the local state, it doesn't call the API for deleting message (`channel.deleteMessage`). @@ -421,9 +427,25 @@ Upserts a given message in local channel state. Please note that this function d -###
_forwarded from [Channel](../../core-components/channel#reactionlist)_ props
ReactionList {#reactionlist} +###
_forwarded from [Channel](../../core-components/channel#messageuserreactionsavatar)_ props
MessageUserReactionsAvatar {#messageuserreactionsavatar} + + + +###
_forwarded from [Channel](../../core-components/channel#messageuserreactionsitem)_ props
MessageUserReactionsItem {#messageuserreactionsitem} + + + +###
_forwarded from [Channel](../../core-components/channel#messageuserreactions)_ props
MessageUserReactions {#messageuserreactions} + + + +###
_forwarded from [Channel](../../core-components/channel#reactionlistbottom)_ props
ReactionListBottom {#reactionlistbottom} + + + +###
_forwarded from [Channel](../../core-components/channel#reactionlisttop)_ props
ReactionListTop {#reactionlisttop} - + ###
_forwarded from [Channel](../../core-components/channel#scrolltobottombutton)_ props
ScrollToBottomButton {#scrolltobottombutton} diff --git a/docusaurus/docs/reactnative/core-components/channel.mdx b/docusaurus/docs/reactnative/core-components/channel.mdx index 5cfb95cbd3..961e1708bb 100644 --- a/docusaurus/docs/reactnative/core-components/channel.mdx +++ b/docusaurus/docs/reactnative/core-components/channel.mdx @@ -25,7 +25,7 @@ import AutoCompleteSuggestionItem from '../common-content/ui-components/channel/ import AutoCompleteSuggestionList from '../common-content/ui-components/channel/props/autocomplete_suggestion_list.mdx'; import AutoCompleteSuggestionsLimit from '../common-content/ui-components/channel/props/auto_complete_suggestions_limit.mdx'; import AutoCompleteTriggerSettings from '../common-content/ui-components/channel/props/auto_complete_trigger_settings.mdx'; -import AdditionalTouchableProps from '../common-content/ui-components/channel/props/additional_touchable_props.mdx'; +import AdditionalPressableProps from '../common-content/ui-components/channel/props/additional_pressable_props.mdx'; import AttachButton from '../common-content/ui-components/channel/props/attach_button.mdx'; import Attachment from '../common-content/ui-components/channel/props/attachment.mdx'; import AttachmentActions from '../common-content/ui-components/channel/props/attachment_actions.mdx'; @@ -135,7 +135,9 @@ import MessageUserReactionsAvatar from '../common-content/ui-components/channel/ import MessageUserReactionsItem from '../common-content/ui-components/channel/props/message-user-reactions-item.mdx'; import MessageUserReactions from '../common-content/ui-components/channel/props/message-user-reactions.mdx'; import OverrideOwnCapabilities from '../common-content/ui-components/channel/props/override_own_capabilities.mdx'; -import ReactionList from '../common-content/ui-components/channel/props/reaction-list.mdx'; +import ReactionListBottom from '../common-content/ui-components/channel/props/reaction-list-bottom.mdx'; +import ReactionListPosition from '../common-content/ui-components/channel/props/reaction-list-position.mdx'; +import ReactionListTop from '../common-content/ui-components/channel/props/reaction-list-top.mdx'; import Reply from '../common-content/ui-components/channel/props/reply.mdx'; import ScrollToBottomButton from '../common-content/ui-components/channel/props/scroll-to-bottom-button.mdx'; import SelectReaction from '../common-content/ui-components/channel/props/select_reaction.mdx'; @@ -283,9 +285,9 @@ This is often the header height. -### additionalTouchableProps +### additionalPressableProps - + ### allowThreadMessagesInChannel @@ -682,6 +684,10 @@ Provide a custom array of messages to render in `MessageList`. --> +### reactionListPosition + + + ### selectReaction @@ -1009,9 +1015,13 @@ Component to render full screen error indicator, when channel fails to load. -### ReactionList +### ReactionListBottom + + + +### ReactionListTop - + ### Reply diff --git a/docusaurus/docs/reactnative/guides/custom-message-actions.mdx b/docusaurus/docs/reactnative/guides/custom-message-actions.mdx index da02d5129d..6115247e60 100644 --- a/docusaurus/docs/reactnative/guides/custom-message-actions.mdx +++ b/docusaurus/docs/reactnative/guides/custom-message-actions.mdx @@ -117,9 +117,9 @@ Styles for underlying Text component of action title. | ------------------------------------------------------------------------- | | [`Text Style Props`](https://reactnative.dev/docs/text-style-props#props) | -### MessageTouchableHandlerPayload +### MessagePressableHandlerPayload -`MessageTouchableHandlerPayload` object is provided as parameter to callback handlers such as `onLongPressMessage`, `onPressMessage` for user interaction with message. +`MessagePressableHandlerPayload` object is provided as parameter to callback handlers such as `onLongPressMessage`, `onPressMessage` for user interaction with message. #### Example @@ -144,7 +144,7 @@ Styles for underlying Text component of action title. #### `additionalInfo` -Additional message touchable handler info. +Additional message pressable handler info. | Type | | ------ | diff --git a/docusaurus/docs/reactnative/object-types/message_touchable_handler_payload.mdx b/docusaurus/docs/reactnative/object-types/message_pressable_handler_payload.mdx similarity index 88% rename from docusaurus/docs/reactnative/object-types/message_touchable_handler_payload.mdx rename to docusaurus/docs/reactnative/object-types/message_pressable_handler_payload.mdx index 42d8f3ada7..8eeae739fa 100644 --- a/docusaurus/docs/reactnative/object-types/message_touchable_handler_payload.mdx +++ b/docusaurus/docs/reactnative/object-types/message_pressable_handler_payload.mdx @@ -1,9 +1,9 @@ --- -id: message-touchable-handler-payload -title: MessageTouchableHandlerPayload +id: message-pressable-handler-payload +title: MessagePressableHandlerPayload --- -`MessageTouchableHandlerPayload` object is provided as parameter to callback handlers such as `onLongPressMessage`, `onPressMessage` +`MessagePressableHandlerPayload` object is provided as parameter to callback handlers such as `onLongPressMessage`, `onPressMessage` for user interaction with message. ## Example @@ -47,7 +47,7 @@ Location within message, where the interaction occurred. ### event -Native event object which could either be coming from TouchableOpacity or `react-native-gesture-handler` +Native event object which could either be coming from React Native's `Pressable` or `react-native-gesture-handler` | Type | | ------ | diff --git a/docusaurus/docs/reactnative/ui-components/card.mdx b/docusaurus/docs/reactnative/ui-components/card.mdx index d4ea11824a..c6b325fd35 100644 --- a/docusaurus/docs/reactnative/ui-components/card.mdx +++ b/docusaurus/docs/reactnative/ui-components/card.mdx @@ -7,7 +7,7 @@ import OnLongPress from '../common-content/contexts/message-context/on_long_pres import OnPress from '../common-content/contexts/message-context/on_press.mdx'; import OnPressIn from '../common-content/contexts/message-context/on_press_in.mdx'; -import AdditionalTouchableProps from '../common-content/ui-components/channel/props/additional_touchable_props.mdx'; +import AdditionalPressableProps from '../common-content/ui-components/channel/props/additional_pressable_props.mdx'; import CardCover from '../common-content/ui-components/channel/props/card_cover.mdx'; import CardFooter from '../common-content/ui-components/channel/props/card_footer.mdx'; import CardHeader from '../common-content/ui-components/channel/props/card_header.mdx'; @@ -19,9 +19,9 @@ Please check the guides for [Custom Attachment](../guides/custom-attachment.mdx) ## Props -###
_overrides the value from [MessagesContext](../../contexts/messages-context#additionaltouchableprops)_
`additionalTouchableProps` {#additionaltouchableprops} +###
_overrides the value from [MessagesContext](../../contexts/messages-context#additionalpressableprops)_
`additionalPressableProps` {#additionalpressableprops} - + ### `author_name` diff --git a/docusaurus/docs/reactnative/ui-components/file-attachment.mdx b/docusaurus/docs/reactnative/ui-components/file-attachment.mdx index bc914ebad7..0a095e98fc 100644 --- a/docusaurus/docs/reactnative/ui-components/file-attachment.mdx +++ b/docusaurus/docs/reactnative/ui-components/file-attachment.mdx @@ -3,7 +3,7 @@ id: file-attachment title: FileAttachment --- -import AdditionalTouchableProps from '../common-content/ui-components/channel/props/additional_touchable_props.mdx'; +import AdditionalPressableProps from '../common-content/ui-components/channel/props/additional_pressable_props.mdx'; import AttachmentActions from '../common-content/ui-components/channel/props/attachment_actions.mdx'; import FileAttachmentIcon from '../common-content/ui-components/channel/props/file_attachment_icon.mdx'; @@ -39,9 +39,9 @@ Size of an attachment icon. This value gets passed to [`FileAttachmentIcon`](#fi | ------ | | Number | -###
_overrides the value from [MessagesContext](../../contexts/messages-context#additionaltouchableprops)_
`additionalTouchableProps` {#additionaltouchableprops} +###
_overrides the value from [MessagesContext](../../contexts/messages-context#additionalpressableprops)_
`additionalPressableProps` {#additionalpressableprops} - + ## UI Component Props diff --git a/docusaurus/docs/reactnative/ui-components/gallery.mdx b/docusaurus/docs/reactnative/ui-components/gallery.mdx index 1923d22385..0068a6269d 100644 --- a/docusaurus/docs/reactnative/ui-components/gallery.mdx +++ b/docusaurus/docs/reactnative/ui-components/gallery.mdx @@ -24,7 +24,7 @@ import Videos from '../common-content/contexts/message-context/videos.mdx'; import SetOverlay from '../common-content/contexts/overlay-context/set_overlay.mdx'; -import AdditionalTouchableProps from '../common-content/ui-components/channel/props/additional_touchable_props.mdx'; +import AdditionalPressableProps from '../common-content/ui-components/channel/props/additional_pressable_props.mdx'; import LegacyImageViewerSwipeBehaviour from '../common-content/ui-components/channel/props/legacy_image_viewer_swipe_behaviour.mdx'; import VideoThumbnail from '../common-content/ui-components/channel/props/video_thumbnail.mdx'; @@ -42,9 +42,9 @@ import { Channel, Gallery } from 'stream-chat-react-native'; ## Props -###
_overrides the value from [MessagesContext](../../contexts/messages-context#additionaltouchableprops)_
`additionalTouchableProps` {#additionaltouchableprops} +###
_overrides the value from [MessagesContext](../../contexts/messages-context#additionalpressableprops)_
`additionalPressableProps` {#additionalpressableprops} - + ###
_overrides the value from [MessageContext](../../contexts/message-context#alignment)_
`alignment` {#alignment} diff --git a/docusaurus/docs/reactnative/ui-components/giphy.mdx b/docusaurus/docs/reactnative/ui-components/giphy.mdx index 4fe0161e45..7ee806002c 100644 --- a/docusaurus/docs/reactnative/ui-components/giphy.mdx +++ b/docusaurus/docs/reactnative/ui-components/giphy.mdx @@ -10,7 +10,7 @@ import OnPressIn from '../common-content/contexts/message-context/on_press_in.md import SetSelectedMessage from '../common-content/contexts/image-gallery-context/set_selected_message.mdx'; import SetMessages from '../common-content/contexts/image-gallery-context/set_messages.mdx'; -import AdditionalTouchableProps from '../common-content/ui-components/channel/props/additional_touchable_props.mdx'; +import AdditionalPressableProps from '../common-content/ui-components/channel/props/additional_pressable_props.mdx'; import SetOverlay from '../common-content/contexts/overlay-context/set_overlay.mdx'; import ImageComponent from '../common-content/ui-components/chat/props/image_component.mdx'; @@ -29,9 +29,9 @@ Attachment object for `giphy` type attachment. | ------ | | Object | -###
_overrides the value from [MessagesContext](../../contexts/messages-context#additionaltouchableprops)_
`additionalTouchableProps` {#additionaltouchableprops} +###
_overrides the value from [MessagesContext](../../contexts/messages-context#additionalpressableprops)_
`additionalPressableProps` {#additionalpressableprops} - + ###
_overrides the value from [MessageContext](../../contexts/message-context#handleaction)_
`handleAction` {#handleaction} diff --git a/docusaurus/docs/reactnative/ui-components/message-content.mdx b/docusaurus/docs/reactnative/ui-components/message-content.mdx index 413d3b578d..f30f5dd0e3 100644 --- a/docusaurus/docs/reactnative/ui-components/message-content.mdx +++ b/docusaurus/docs/reactnative/ui-components/message-content.mdx @@ -3,35 +3,29 @@ id: message-content title: MessageContent --- -import AdditionalTouchableProps from '../common-content/ui-components/channel/props/additional_touchable_props.mdx'; +import AdditionalPressableProps from '../common-content/ui-components/channel/props/additional_pressable_props.mdx'; import Attachment from '../common-content/ui-components/channel/props/attachment.mdx'; import FileAttachmentGroup from '../common-content/ui-components/channel/props/file_attachment_group.mdx'; -import FormatDate from '../common-content/ui-components/channel/props/format_date.mdx'; import Gallery from '../common-content/ui-components/channel/props/gallery.mdx'; -import MessageDeleted from '../common-content/ui-components/channel/props/message-deleted.mdx'; +import IsAttachmentEqual from '../common-content/ui-components/channel/props/is_attachment_equal.mdx'; import MessageError from '../common-content/ui-components/channel/props/message-error.mdx'; -import MessageFooter from '../common-content/ui-components/channel/props/message-footer.mdx'; -import MessageHeader from '../common-content/ui-components/channel/props/message-header.mdx'; -import MessageReplies from '../common-content/ui-components/channel/props/message-replies.mdx'; -import MessageStatus from '../common-content/ui-components/channel/props/message-status.mdx'; +import MyMessageTheme from '../common-content/ui-components/channel/props/my_message_theme.mdx'; import Reply from '../common-content/ui-components/channel/props/reply.mdx'; import Disabled from '../common-content/contexts/channel-context/disabled.mdx'; -import Members from '../common-content/contexts/channel-context/members.mdx'; import Alignment from '../common-content/contexts/message-context/alignment.mdx'; +import GoToMessage from '../common-content/contexts/message-context/go_to_message.mdx'; import GroupStyles from '../common-content/contexts/message-context/group_styles.mdx'; -import HasReactions from '../common-content/contexts/message-context/has_reactions.mdx'; +import IsEditedMessageOpen from '../common-content/contexts/message-context/is_edited_message_open.mdx'; import IsMyMessage from '../common-content/contexts/message-context/is_my_message.mdx'; -import LastGroupMessage from '../common-content/contexts/message-context/last_group_message.mdx'; -import MessageContentOrder from '../common-content/contexts/message-context/message_content_order.mdx'; import MessageProp from '../common-content/contexts/message-context/message.mdx'; +import MessageContentOrder from '../common-content/contexts/message-context/message_content_order.mdx'; import OnLongPress from '../common-content/contexts/message-context/on_long_press.mdx'; import OnPress from '../common-content/contexts/message-context/on_press.mdx'; import OnPressIn from '../common-content/contexts/message-context/on_press_in.mdx'; -import OnlyEmojis from '../common-content/contexts/message-context/only_emojis.mdx'; import OtherAttachments from '../common-content/contexts/message-context/other_attachments.mdx'; -import ShowMessageStatus from '../common-content/contexts/message-context/show_message_status.mdx'; +import PreventPress from '../common-content/contexts/message-context/prevent_press.mdx'; import ThreadList from '../common-content/contexts/message-context/thread_list.mdx'; Component to render content of a message within the [`MessageList`](./message-list.mdx). Message avatar and reactions are not part of `MessageContent`. @@ -39,43 +33,45 @@ This is the default component provided to the prop [`MessageContent`](../../core ## Props -###
_overrides the value from [MessagesContext](../../contexts/messages-context#additionaltouchableprops)_
`additionalTouchableProps` {#additionaltouchableprops} +###
_overrides the value from [MessagesContext](../../contexts/messages-context#additionalpressableprops)_
`additionalPressableProps` {#additionalpressableprops} - + ###
_overrides the value from [MessageContext](../../contexts/message-context#alignment)_
`alignment` {#alignment} +### `backgroundColor` + +Background color for the message content. + +| Type | +| ----------------------- | +| `string` \| `undefined` | + ###
_overrides the value from [MessageContext](../../contexts/message-context#disabled)_
`disabled` {#disabled} -###
_overrides the value from [MessagesContext](../../contexts/messages-context#formatdate)_
`formatDate` {#formatdate} - - +###
_overrides the value from [MessageContext](../../contexts/message-context#gotomessage)_
`goToMessage` {#gotomessage} - + ###
_overrides the value from [MessageContext](../../contexts/message-context#groupstyles)_
`groupStyles` {#groupstyles} -###
_overrides the value from [MessageContext](../../contexts/message-context#hasreactions)_
`hasReactions` {#hasreactions} +###
_overrides the value from [MessageContext](../../contexts/message-context#isattachmentequal)_
`isAttachmentEqual` {#isattachmentequal} - + -###
_overrides the value from [MessageContext](../../contexts/message-context#ismymessage)_
`isMyMessage` {#ismymessage} +###
_overrides the value from [MessageContext](../../contexts/message-context#iseditedmessageopen)_
`isEditedMessageOpen` {#iseditedmessageopen} - + -###
_overrides the value from [MessageContext](../../contexts/message-context#lastgroupmessage)_
`lastGroupMessage` {#lastgroupmessage} - - - -###
_overrides the value from [MessageContext](../../contexts/message-context#members)_
`members` {#members} +###
_overrides the value from [MessageContext](../../contexts/message-context#ismymessage)_
`isMyMessage` {#ismymessage} - + ###
_overrides the value from [MessageContext](../../contexts/message-context#message)_
`message` {#message} @@ -85,13 +81,21 @@ This is the default component provided to the prop [`MessageContent`](../../core -###
_overrides the value from [MessageContext](../../contexts/message-context#onlongpress)_
`onLongPress` {#onlongpress} +###
_overrides the value from [MessagesContext](../../contexts/messages-context#mymessagetheme)_
`MyMessageTheme` {#mymessagetheme} - + + +### `noBorder` -###
_overrides the value from [MessageContext](../../contexts/message-context#onlyemojis)_
`onlyEmojis` {#onlyemojis} +If `true`, the message content will not have a border. - +| Type | Default | +| ----------------------- | ------- | +| `Boolean`\| `undefined` | `false` | + +###
_overrides the value from [MessageContext](../../contexts/message-context#onlongpress)_
`onLongPress` {#onlongpress} + + ###
_overrides the value from [MessageContext](../../contexts/message-context#onpress)_
`onPress` {#onpress} @@ -105,11 +109,9 @@ This is the default component provided to the prop [`MessageContent`](../../core - +###
_overrides the value from [MessageContext](../../contexts/message-context#preventpress)_
`preventPress` {#preventpress} -###
_overrides the value from [MessageContext](../../contexts/message-context#showmessagestatus)_
`showMessageStatus` {#showmessagestatus} - - + ###
_overrides the value from [MessageContext](../../contexts/message-context#threadlist)_
`threadList` {#threadlist} @@ -129,30 +131,10 @@ This is the default component provided to the prop [`MessageContent`](../../core -###
_overrides the value from [MessagesContext](../../contexts/messages-context#messagedeleted)_
`MessageDeleted` {#messagedeleted} - - - ###
_overrides the value from [MessagesContext](../../contexts/messages-context#messageerror)_
`MessageError` {#messageerror} -###
_overrides the value from [MessagesContext](../../contexts/messages-context#messagefooter)_
`MessageFooter` {#messagefooter} - - - -###
_overrides the value from [MessagesContext](../../contexts/messages-context#messageheader)_
`MessageHeader` {#messageheader} - - - -###
_overrides the value from [MessagesContext](../../contexts/messages-context#messagereplies)_
`MessageReplies` {#messagereplies} - - - -###
_overrides the value from [MessagesContext](../../contexts/messages-context#messagestatus)_
`MessageStatus` {#messagestatus} - - - ###
_overrides the value from [MessagesContext](../../contexts/messages-context#reply)_
`Reply` {#reply} diff --git a/docusaurus/docs/reactnative/ui-components/message-simple.mdx b/docusaurus/docs/reactnative/ui-components/message-simple.mdx index c270149cfc..24bab5f6be 100644 --- a/docusaurus/docs/reactnative/ui-components/message-simple.mdx +++ b/docusaurus/docs/reactnative/ui-components/message-simple.mdx @@ -4,14 +4,31 @@ title: MessageSimple --- import Alignment from '../common-content/contexts/message-context/alignment.mdx'; +import Channel from '../common-content/contexts/message-context/channel.mdx'; import GroupStyles from '../common-content/contexts/message-context/group_styles.mdx'; import HasReactions from '../common-content/contexts/message-context/has_reactions.mdx'; +import IsMyMessage from '../common-content/contexts/message-context/is_my_message.mdx'; +import LastGroupMessage from '../common-content/contexts/message-context/last_group_message.mdx'; import MessageProp from '../common-content/contexts/message-context/message.mdx'; +import OnlyEmojis from '../common-content/contexts/message-context/only_emojis.mdx'; +import OtherAttachments from '../common-content/contexts/message-context/other_attachments.mdx'; +import ShowMessageStatus from '../common-content/contexts/message-context/show_message_status.mdx'; + +import Members from '../common-content/contexts/channel-context/members.mdx'; import EnableMessageGroupingByUser from '../common-content/ui-components/channel/props/enable_message_grouping_by_user.mdx'; import MessageAvatar from '../common-content/ui-components/channel/props/message-avatar.mdx'; import MessageContent from '../common-content/ui-components/channel/props/message-content.mdx'; -import ReactionList from '../common-content/ui-components/channel/props/reaction-list.mdx'; +import MessageDeleted from '../common-content/ui-components/channel/props/message-deleted.mdx'; +import MessageFooter from '../common-content/ui-components/channel/props/message-footer.mdx'; +import MessageHeader from '../common-content/ui-components/channel/props/message-header.mdx'; +import MessagePinnedHeader from '../common-content/ui-components/channel/props/message-pinned-header.mdx'; +import MessageReplies from '../common-content/ui-components/channel/props/message-replies.mdx'; +import MessageStatus from '../common-content/ui-components/channel/props/message-status.mdx'; +import MyMessageTheme from '../common-content/ui-components/channel/props/my_message_theme.mdx'; +import ReactionListBottom from '../common-content/ui-components/channel/props/reaction-list-bottom.mdx'; +import ReactionListPosition from '../common-content/ui-components/channel/props/reaction-list-position.mdx'; +import ReactionListTop from '../common-content/ui-components/channel/props/reaction-list-top.mdx'; Component to render a message within the [`MessageList`](./message-list.mdx). This component has been well optimized to save un-necessary re-renderings. This is the default component provided to the prop [`MessageSimple`](../../core-components/channel#messagesimple) on the `Channel` component. @@ -22,6 +39,10 @@ This is the default component provided to the prop [`MessageSimple`](../../core- +###
_overrides the value from [ChannelContext](../../contexts/channel-context#channel)_
`channel` {#channel} + + + ###
_overrides the value from [MessageContext](../../contexts/message-context#enablemessagegroupingbyuser)_
`enableMessageGroupingByUser` {#enablemessagegroupingbyuser} @@ -34,20 +55,80 @@ This is the default component provided to the prop [`MessageSimple`](../../core- +###
_overrides the value from [MessageContext](../../contexts/message-context#ismymessage)_
`isMyMessage` {#ismymessage} + + + +###
_overrides the value from [MessageContext](../../contexts/message-context#lastgroupmessage)_
`lastGroupMessage` {#lastgroupmessage} + + + +###
_overrides the value from [MessageContext](../../contexts/message-context#members)_
`members` {#members} + + + ###
_overrides the value from [MessageContext](../../contexts/message-context#message)_
`message` {#message} +###
_overrides the value from [MessageContext](../../contexts/message-context#onlyemojis)_
`onlyEmojis` {#onlyEmojis} + + + +###
_overrides the value from [MessageContext](../../contexts/message-context#otherattachments)_
`otherAttachments` {#otherattachments} + + + +###
_overrides the value from [MessageContext](../../contexts/message-context#showmessagestatus)_
`showMessageStatus` {#showmessagestatus} + + + +###
_overrides the value from [MessagesContext](../../contexts/messages-context#mymessagetheme)_
`MyMessageTheme` {#mymessagetheme} + + + ## UI Component Props -###
_overrides the value from [MessageContext](../../contexts/message-context#messageavatar)_
`MessageAvatar` {#messageavatar} +###
_overrides the value from [MessagesContext](../../contexts/messages-context#messageavatar)_
`MessageAvatar` {#messageavatar} -###
_overrides the value from [MessageContext](../../contexts/message-context#messagecontent)_
`MessageContent` {#messagecontent} +###
_overrides the value from [MessagesContext](../../contexts/messages-context#messagecontent)_
`MessageContent` {#messagecontent} -###
_overrides the value from [MessageContext](../../contexts/message-context#reactionlist)_
`ReactionList` {#reactionlist} +###
_overrides the value from [MessagesContext](../../contexts/messages-context#messagedeleted)_
`MessageDeleted` {#messagedeleted} + + + +###
_overrides the value from [MessagesContext](../../contexts/messages-context#messagefooter)_
`MessageFooter` {#messagefooter} + + + +###
_overrides the value from [MessagesContext](../../contexts/messages-context#messageheader)_
`MessageHeader` {#messageheader} + + + +###
_overrides the value from [MessagesContext](../../contexts/messages-context#messagepinnedheader)_
`MessagePinnedHeader` {#messagepinnedheader} + + + +###
_overrides the value from [MessagesContext](../../contexts/messages-context#messagereplies)_
`MessageReplies` {#messagereplies} + + + +###
_overrides the value from [MessagesContext](../../contexts/messages-context#messagestatus)_
`MessageStatus` {#messagestatus} + + + +###
_overrides the value from [MessagesContext](../../contexts/messages-context#reactionlistbottom)_
`ReactionListBottom` {#reactionlistbottom} + + + +###
_overrides the value from [MessagesContext](../../contexts/messages-context#reactionlistposition)_
`ReactionListPosition` {#reactionlistposition} + + + +###
_overrides the value from [MessagesContext](../../contexts/messages-context#reactionlisttop)_
`ReactionListTop` {#reactionlisttop} - + diff --git a/docusaurus/docs/reactnative/ui-components/reaction-list-bottom.mdx b/docusaurus/docs/reactnative/ui-components/reaction-list-bottom.mdx new file mode 100644 index 0000000000..47d6565e8e --- /dev/null +++ b/docusaurus/docs/reactnative/ui-components/reaction-list-bottom.mdx @@ -0,0 +1,65 @@ +--- +id: reaction-list-bottom +title: ReactionListBottom +--- + +import HandleReaction from '../common-content/contexts/message-context/handle_reaction.mdx'; +import HasReactions from '../common-content/contexts/message-context/has_reactions.mdx'; +import OnLongPress from '../common-content/contexts/message-context/on_long_press.mdx'; +import OnPress from '../common-content/contexts/message-context/on_press.mdx'; +import OnPressIn from '../common-content/contexts/message-context/on_press_in.mdx'; +import PreventPress from '../common-content/contexts/message-context/prevent_press.mdx'; +import Reactions from '../common-content/contexts/message-context/reactions.mdx'; +import ShowMessageOverlay from '../common-content/contexts/message-context/show_message_overlay.mdx'; + +import SupportedReactions from '../common-content/ui-components/channel/props/supported_reactions.mdx'; + +`ReactionList` component is used to display the reactions added to a message right on top of it. + +This is the default component provided to the prop [`ReactionList`](../../core-components/channel#reactionlist) on the `Channel` component. + +## Basic Usage + +**Use case**: Override the background color on the reaction list container. + +```tsx +import { Channel, ReactionList, ReactionListProps } from 'stream-chat-react-native'; + +const ReactionListWithCustomBackground = (props: ReactionListProps) => + + +``` + +## Props + +###
overrides the value from [MessageContext](../../contexts/message-context#handlereaction)
`handleReaction` {#handlereaction} + + + +###
overrides the value from [MessageContext](../../contexts/message-context#hasreactions)
`hasReactions` {#hasreactions} + + + +###
overrides the value from [MessageContext](../../contexts/message-context#onlongpress)
`onLongPress` {#onlongpress} + + + +###
overrides the value from [MessageContext](../../contexts/message-context#onpress)
`onPress` {#onpress} + + + +###
overrides the value from [MessageContext](../../contexts/message-context#onpressin)
`onPressIn` {#onpressin} + + + +###
overrides the value from [MessageContext](../../contexts/message-context#preventpress)
`preventPress` {#preventpress} + + + +###
overrides the value from [MessageContext](../../contexts/message-context#reactions)
`reactions` {#reactions} + + + +###
overrides the value from [MessagesContext](../../contexts/messages-context#supportedreactions)
`supportedReactions` {#supportedreactions} + + diff --git a/docusaurus/docs/reactnative/ui-components/reaction-list.mdx b/docusaurus/docs/reactnative/ui-components/reaction-list-top.mdx similarity index 51% rename from docusaurus/docs/reactnative/ui-components/reaction-list.mdx rename to docusaurus/docs/reactnative/ui-components/reaction-list-top.mdx index 78256b5e2d..9173b6ed13 100644 --- a/docusaurus/docs/reactnative/ui-components/reaction-list.mdx +++ b/docusaurus/docs/reactnative/ui-components/reaction-list-top.mdx @@ -1,12 +1,14 @@ --- -id: reaction-list -title: ReactionList +id: reaction-list-top +title: ReactionListTop --- import Alignment from '../common-content/contexts/message-context/alignment.mdx'; -import MessageProp from '../common-content/contexts/message-context/message.mdx'; +import HasReactions from '../common-content/contexts/message-context/has_reactions.mdx'; import OnLongPress from '../common-content/contexts/message-context/on_long_press.mdx'; import OnPress from '../common-content/contexts/message-context/on_press.mdx'; +import OnPressIn from '../common-content/contexts/message-context/on_press_in.mdx'; +import PreventPress from '../common-content/contexts/message-context/prevent_press.mdx'; import Reactions from '../common-content/contexts/message-context/reactions.mdx'; import ShowMessageOverlay from '../common-content/contexts/message-context/show_message_overlay.mdx'; @@ -30,16 +32,7 @@ const ReactionListWithCustomBackground = (props: ReactionListProps) => required `messageContentWidth` {#messagecontentwidth} - -Width of message content. This helps ReactionList align itself on the front-edge of the message content. -This value gets computed in [`MessageContent`](../message-content) once the content gets loaded. - -| Type | -| ------ | -| Number | - -###
overrides the value from [MessageContext](../../contexts/message-context#alignment)
`alignment` {#alignment} +###
overrides the value from [MessageContext](../../contexts/message-context#alignment)
`alignment` {#alignment} @@ -51,21 +44,36 @@ Background color for reaction list container when [`alignment`](#alignment) is ` | ------ | | String | - +###
overrides the value from [MessageContext](../../contexts/message-context#hasreactions)
`hasReactions` {#hasreactions} -###
overrides the value from [MessageContext](../../contexts/message-context#onlongpress)
`onLongPress` {#onlongpress} + - +###
required
`messageContentWidth` {#messagecontentwidth} + +Width of message content. This helps ReactionList align itself on the front-edge of the message content. +This value gets computed in [`MessageContent`](../message-content) once the content gets loaded. + +| Type | +| ------ | +| Number | -###
_overrides the value from [MessageContext](../../contexts/message-context#message)_
`message` {#message} +###
overrides the value from [MessageContext](../../contexts/message-context#onlongpress)
`onLongPress` {#onlongpress} - + -###
overrides the value from [MessageContext](../../contexts/message-context#onpress)
`onPress` {#onpress} +###
overrides the value from [MessageContext](../../contexts/message-context#onpress)
`onPress` {#onpress} -###
overrides the value from [MessageContext](../../contexts/message-context#reactions)
`reactions` {#reactions} +###
overrides the value from [MessageContext](../../contexts/message-context#onpressin)
`onPressIn` {#onpressin} + + + +###
overrides the value from [MessageContext](../../contexts/message-context#preventpress)
`preventPress` {#preventpress} + + + +###
overrides the value from [MessageContext](../../contexts/message-context#reactions)
`reactions` {#reactions} @@ -77,28 +85,10 @@ Dimensions for each reaction icon. | ------ | | Number | -###
overrides the value from [MessageContext](../../contexts/message-context#showmessageoverlay)
`showMessageOverlay` {#showmessageoverlay} +###
overrides the value from [MessageContext](../../contexts/message-context#showmessageoverlay)
`showMessageOverlay` {#showmessageoverlay} -### `stroke` - -Background color for reaction list container when [`alignment`](#alignment) is `right`. - -| Type | -| ------ | -| String | - - - -###
overrides the value from [MessagesContext](../../contexts/messages-context#supportedreactions)
`supportedReactions` {#supportedreactions} +###
overrides the value from [MessagesContext](../../contexts/messages-context#supportedreactions)
`supportedReactions` {#supportedreactions} - -###
overrides the value from [MessagesContext](../../contexts/messages-context#targetedmessage)
`targetedMessage` {#targetedmessage} - -Has the message id of the message which is to be highlighted. By default the value is undefined. As soon as the highlight time is out this is set to `undefined`. - -| Type | -| ------ | -| String | diff --git a/docusaurus/sidebars-react-native.json b/docusaurus/sidebars-react-native.json index 10e8423317..6979e66847 100644 --- a/docusaurus/sidebars-react-native.json +++ b/docusaurus/sidebars-react-native.json @@ -81,7 +81,8 @@ "ui-components/message-replies", "ui-components/message-status", "ui-components/message-system", - "ui-components/reaction-list", + "ui-components/reaction-list-bottom", + "ui-components/reaction-list-top", "ui-components/video-thumbnail", "contexts/message-context" ] diff --git a/examples/SampleApp/src/components/OverlayBackdrop.tsx b/examples/SampleApp/src/components/OverlayBackdrop.tsx new file mode 100644 index 0000000000..b05daccdcf --- /dev/null +++ b/examples/SampleApp/src/components/OverlayBackdrop.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { StyleProp, View, ViewStyle } from 'react-native'; +import { useTheme } from 'stream-chat-react-native'; + +type OverlayBackdropProps = { + style?: StyleProp; +}; + +export const OverlayBackdrop = (props: OverlayBackdropProps): JSX.Element => { + const { style = {} } = props; + const { + theme: { + colors: { overlay }, + }, + } = useTheme(); + return ; +}; diff --git a/examples/SampleApp/src/context/AppOverlayProvider.tsx b/examples/SampleApp/src/context/AppOverlayProvider.tsx index c7be2874f5..62c6fc948c 100644 --- a/examples/SampleApp/src/context/AppOverlayProvider.tsx +++ b/examples/SampleApp/src/context/AppOverlayProvider.tsx @@ -7,8 +7,6 @@ import Animated, { withTiming, } from 'react-native-reanimated'; -import { OverlayBackdrop } from 'stream-chat-react-native'; - import { AppOverlayContext, AppOverlayContextValue } from './AppOverlayContext'; import { BottomSheetOverlay } from '../components/BottomSheetOverlay'; @@ -17,6 +15,7 @@ import { UserInfoOverlay } from '../components/UserInfoOverlay'; import { BottomSheetOverlayProvider } from './BottomSheetOverlayContext'; import { ChannelInfoOverlayProvider } from './ChannelInfoOverlayContext'; import { UserInfoOverlayProvider } from './UserInfoOverlayContext'; +import { OverlayBackdrop } from '../components/OverlayBackdrop'; export const AppOverlayProvider = ( props: React.PropsWithChildren<{ diff --git a/package/src/components/Attachment/Card.tsx b/package/src/components/Attachment/Card.tsx index 99a3108e00..9857bb3f0c 100644 --- a/package/src/components/Attachment/Card.tsx +++ b/package/src/components/Attachment/Card.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { Image, ImageStyle, + Pressable, StyleProp, StyleSheet, Text, TextStyle, - TouchableOpacity, View, ViewStyle, } from 'react-native'; @@ -92,7 +92,7 @@ export type CardPropsWithContext< > & Pick< MessagesContextValue, - 'additionalTouchableProps' | 'CardCover' | 'CardFooter' | 'CardHeader' | 'myMessageTheme' + 'additionalPressableProps' | 'CardCover' | 'CardFooter' | 'CardHeader' | 'myMessageTheme' > & { channelId: string | undefined; messageId: string | undefined; @@ -116,7 +116,7 @@ const CardWithContext = < props: CardPropsWithContext, ) => { const { - additionalTouchableProps, + additionalPressableProps, author_name, CardCover, CardFooter, @@ -163,7 +163,7 @@ const CardWithContext = < const isVideoCard = type === FileTypes.Video && og_scrape_url; return ( - { if (onLongPress) { @@ -196,7 +196,7 @@ const CardWithContext = < }} style={[styles.container, container, stylesProp.container]} testID='card-attachment' - {...additionalTouchableProps} + {...additionalPressableProps} > {CardHeader && } {CardCover && } @@ -285,7 +285,7 @@ const CardWithContext = < )} - + ); }; @@ -316,7 +316,7 @@ export type CardProps< > & Pick< MessagesContextValue, - 'additionalTouchableProps' | 'CardCover' | 'CardFooter' | 'CardHeader' + 'additionalPressableProps' | 'CardCover' | 'CardFooter' | 'CardHeader' > >; @@ -331,14 +331,14 @@ export const Card = < const { ImageComponent } = useChatContext(); const { message, onLongPress, onPress, onPressIn, preventPress } = useMessageContext(); - const { additionalTouchableProps, CardCover, CardFooter, CardHeader, myMessageTheme } = + const { additionalPressableProps, CardCover, CardFooter, CardHeader, myMessageTheme } = useMessagesContext(); return ( & Pick< MessagesContextValue, - 'additionalTouchableProps' | 'AttachmentActions' | 'FileAttachmentIcon' + 'additionalPressableProps' | 'AttachmentActions' | 'FileAttachmentIcon' > & { /** The attachment to render */ attachment: Attachment; @@ -73,7 +65,7 @@ const FileAttachmentWithContext = < props: FileAttachmentPropsWithContext, ) => { const { - additionalTouchableProps, + additionalPressableProps, attachment, AttachmentActions, attachmentSize, @@ -98,7 +90,7 @@ const FileAttachmentWithContext = < const defaultOnPress = () => openUrlSafely(attachment.asset_url); return ( - { if (onLongPress) { @@ -130,7 +122,7 @@ const FileAttachmentWithContext = < } }} testID='file-attachment' - {...additionalTouchableProps} + {...additionalPressableProps} > @@ -153,7 +145,7 @@ const FileAttachmentWithContext = < {attachment.actions?.length ? : null} - + ); }; @@ -169,7 +161,7 @@ export const FileAttachment = < ) => { const { onLongPress, onPress, onPressIn, preventPress } = useMessageContext(); const { - additionalTouchableProps, + additionalPressableProps, AttachmentActions = AttachmentActionsDefault, FileAttachmentIcon = FileIconDefault, } = useMessagesContext(); @@ -177,7 +169,7 @@ export const FileAttachment = < return ( & Pick< MessagesContextValue, - | 'additionalTouchableProps' + | 'additionalPressableProps' | 'legacyImageViewerSwipeBehaviour' | 'VideoThumbnail' | 'ImageLoadingIndicator' @@ -84,7 +84,7 @@ const GalleryWithContext = < props: GalleryPropsWithContext, ) => { const { - additionalTouchableProps, + additionalPressableProps, alignment, groupStyles, hasThreadReplies, @@ -200,7 +200,7 @@ const GalleryWithContext = < return ( , - | 'additionalTouchableProps' + | 'additionalPressableProps' | 'legacyImageViewerSwipeBehaviour' | 'VideoThumbnail' | 'ImageLoadingIndicator' @@ -267,7 +267,7 @@ type GalleryThumbnailProps< const GalleryThumbnail = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ - additionalTouchableProps, + additionalPressableProps, borderRadius, colIndex, ImageLoadingFailedIndicator, @@ -326,8 +326,7 @@ const GalleryThumbnail = < }; return ( - { @@ -356,16 +355,17 @@ const GalleryThumbnail = < }); } }} - style={[ + style={({ pressed }) => [ styles.imageContainer, { height: thumbnail.height, + opacity: pressed ? 0.8 : 1, width: thumbnail.width, }, imageContainer, ]} testID={`gallery-${invertedDirections ? 'row' : 'column'}-${colIndex}-item-${rowIndex}`} - {...additionalTouchableProps} + {...additionalPressableProps} > {thumbnail.type === FileTypes.Video ? ( ) : null} - + ); }; @@ -555,7 +555,7 @@ export const Gallery = < props: GalleryProps, ) => { const { - additionalTouchableProps: propAdditionalTouchableProps, + additionalPressableProps: propAdditionalPressableProps, alignment: propAlignment, groupStyles: propGroupStyles, hasThreadReplies, @@ -590,7 +590,7 @@ export const Gallery = < videos: contextVideos, } = useMessageContext(); const { - additionalTouchableProps: contextAdditionalTouchableProps, + additionalPressableProps: contextAdditionalPressableProps, ImageLoadingFailedIndicator: ContextImageLoadingFailedIndicator, ImageLoadingIndicator: ContextImageLoadingIndicator, legacyImageViewerSwipeBehaviour, @@ -605,7 +605,7 @@ export const Gallery = < if (!images.length && !videos.length) return null; - const additionalTouchableProps = propAdditionalTouchableProps || contextAdditionalTouchableProps; + const additionalPressableProps = propAdditionalPressableProps || contextAdditionalPressableProps; const alignment = propAlignment || contextAlignment; const groupStyles = propGroupStyles || contextGroupStyles; const onLongPress = propOnLongPress || contextOnLongPress; @@ -625,7 +625,7 @@ export const Gallery = < return ( , | 'giphyVersion' - | 'additionalTouchableProps' + | 'additionalPressableProps' | 'ImageLoadingIndicator' | 'ImageLoadingFailedIndicator' > & { @@ -164,7 +164,7 @@ const GiphyWithContext = < props: GiphyPropsWithContext, ) => { const { - additionalTouchableProps, + additionalPressableProps, attachment, giphyVersion, handleAction, @@ -260,7 +260,7 @@ const GiphyWithContext = < - { if (actions?.[2].name && actions?.[2].value && handleAction) { handleAction(actions[2].name, actions[2].value); @@ -274,8 +274,8 @@ const GiphyWithContext = < testID={`${actions?.[2].value}-action-button`} > {actions?.[2].text} - - + { if (actions?.[1].name && actions?.[1].value && handleAction) { handleAction(actions[1].name, actions[1].value); @@ -289,8 +289,8 @@ const GiphyWithContext = < testID={`${actions?.[1].value}-action-button`} > {actions?.[1].text} - - + { if (actions?.[0].name && actions?.[0].value && handleAction) { handleAction(actions[0].name, actions[0].value); @@ -300,12 +300,12 @@ const GiphyWithContext = < testID={`${actions?.[0].value}-action-button`} > {actions?.[0].text} - + ) : ( - { if (onLongPress) { @@ -333,7 +333,7 @@ const GiphyWithContext = < } }} testID='giphy-attachment' - {...additionalTouchableProps} + {...additionalPressableProps} > - + ); }; @@ -451,7 +451,7 @@ export const Giphy = < const { handleAction, isMyMessage, message, onLongPress, onPress, onPressIn, preventPress } = useMessageContext(); const { ImageComponent } = useChatContext(); - const { additionalTouchableProps, giphyVersion } = useMessagesContext(); + const { additionalPressableProps, giphyVersion } = useMessagesContext(); const { setMessages, setSelectedMessage } = useImageGalleryContext(); const { setOverlay } = useOverlayContext(); @@ -466,7 +466,7 @@ export const Giphy = < return ( (array: T[], item: T): T[] { diff --git a/package/src/components/Attachment/utils/buildGallery/buildGalleryOfTwoImages.ts b/package/src/components/Attachment/utils/buildGallery/buildGalleryOfTwoImages.ts index a7c28c519a..1e30968584 100644 --- a/package/src/components/Attachment/utils/buildGallery/buildGalleryOfTwoImages.ts +++ b/package/src/components/Attachment/utils/buildGallery/buildGalleryOfTwoImages.ts @@ -4,9 +4,9 @@ import { buildThumbnailGrid } from './buildThumbnailGrid'; import type { GallerySizeAndThumbnailGrid, GallerySizeConfig } from './types'; +import { ChatConfigContextValue } from '../../../../contexts/chatConfigContext/ChatConfigContext'; import type { DefaultStreamChatGenerics } from '../../../../types/types'; import { getAspectRatio } from '../getAspectRatio'; -import { ChatConfigContextValue } from '../../../../contexts/chatConfigContext/ChatConfigContext'; export function buildGalleryOfTwoImages< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, diff --git a/package/src/components/Attachment/utils/buildGallery/buildThumbnail.ts b/package/src/components/Attachment/utils/buildGallery/buildThumbnail.ts index dd56668d5d..0dbe2dd8ab 100644 --- a/package/src/components/Attachment/utils/buildGallery/buildThumbnail.ts +++ b/package/src/components/Attachment/utils/buildGallery/buildThumbnail.ts @@ -4,11 +4,11 @@ import type { Attachment } from 'stream-chat'; import type { Thumbnail } from './types'; +import { ChatConfigContextValue } from '../../../../contexts/chatConfigContext/ChatConfigContext'; import type { DefaultStreamChatGenerics } from '../../../../types/types'; import { getResizedImageUrl } from '../../../../utils/getResizedImageUrl'; import { getUrlOfImageAttachment } from '../../../../utils/getUrlOfImageAttachment'; -import { ChatConfigContextValue } from '../../../../contexts/chatConfigContext/ChatConfigContext'; export type BuildThumbnailProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, diff --git a/package/src/components/Attachment/utils/buildGallery/buildThumbnailGrid.ts b/package/src/components/Attachment/utils/buildGallery/buildThumbnailGrid.ts index e7fed19e51..0a6f9e1e74 100644 --- a/package/src/components/Attachment/utils/buildGallery/buildThumbnailGrid.ts +++ b/package/src/components/Attachment/utils/buildGallery/buildThumbnailGrid.ts @@ -2,6 +2,7 @@ import type { Attachment } from 'stream-chat'; import { buildThumbnail } from './buildThumbnail'; import type { GallerySizeAndThumbnailGrid, GallerySizeConfig, ThumbnailGrid } from './types'; + import { ChatConfigContextValue } from '../../../../contexts/chatConfigContext/ChatConfigContext'; import type { DefaultStreamChatGenerics } from '../../../../types/types'; diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index d1e5ae2caa..9b1d8fafe7 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -144,7 +144,8 @@ import { MessageRepliesAvatars as MessageRepliesAvatarsDefault } from '../Messag import { MessageSimple as MessageSimpleDefault } from '../Message/MessageSimple/MessageSimple'; import { MessageStatus as MessageStatusDefault } from '../Message/MessageSimple/MessageStatus'; import { MessageTimestamp as MessageTimestampDefault } from '../Message/MessageSimple/MessageTimestamp'; -import { ReactionList as ReactionListDefault } from '../Message/MessageSimple/ReactionList'; +import { ReactionListBottom as ReactionListBottomDefault } from '../Message/MessageSimple/ReactionList/ReactionListBottom'; +import { ReactionListTop as ReactionListTopDefault } from '../Message/MessageSimple/ReactionList/ReactionListTop'; import { AttachButton as AttachButtonDefault } from '../MessageInput/AttachButton'; import { CommandsButton as CommandsButtonDefault } from '../MessageInput/CommandsButton'; import { AudioRecorder as AudioRecorderDefault } from '../MessageInput/components/AudioRecorder/AudioRecorder'; @@ -271,7 +272,7 @@ export type ChannelPropsWithContext< Partial< Pick< MessagesContextValue, - | 'additionalTouchableProps' + | 'additionalPressableProps' | 'Attachment' | 'AttachmentActions' | 'AudioAttachment' @@ -342,7 +343,9 @@ export type ChannelPropsWithContext< | 'MessageReactionPicker' | 'MessageUserReactionsAvatar' | 'MessageUserReactionsItem' - | 'ReactionList' + | 'ReactionListBottom' + | 'reactionListPosition' + | 'ReactionListTop' | 'Reply' | 'ScrollToBottomButton' | 'selectReaction' @@ -447,8 +450,8 @@ const ChannelWithContext = < ) => { const { additionalKeyboardAvoidingViewProps, + additionalPressableProps, additionalTextInputProps, - additionalTouchableProps, allowThreadMessagesInChannel = true, asyncMessagesLockDistance = 50, asyncMessagesMinimumPressDuration = 500, @@ -594,7 +597,9 @@ const ChannelWithContext = < onPressInMessage, onPressMessage, overrideOwnCapabilities, - ReactionList = ReactionListDefault, + ReactionListBottom = ReactionListBottomDefault, + reactionListPosition = 'top', + ReactionListTop = ReactionListTopDefault, read, Reply = ReplyDefault, ScrollToBottomButton = ScrollToBottomButtonDefault, @@ -1300,7 +1305,7 @@ const ChannelWithContext = < created_at: message.created_at.toString(), pinned_at: message.pinned_at?.toString(), updated_at: message.updated_at?.toString(), - }) as unknown as MessageResponse; + } as unknown as MessageResponse); const failedMessages = messages .filter((message) => message.status === MessageStatusTypes.FAILED) @@ -2249,7 +2254,7 @@ const ChannelWithContext = < }); const messagesContext = useCreateMessagesContext({ - additionalTouchableProps, + additionalPressableProps, Attachment, AttachmentActions, AudioAttachment, @@ -2325,7 +2330,9 @@ const ChannelWithContext = < onLongPressMessage, onPressInMessage, onPressMessage, - ReactionList, + ReactionListBottom, + reactionListPosition, + ReactionListTop, removeMessage, Reply, retrySendMessage, diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts index 8091500f17..f47b5eee45 100644 --- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts +++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts @@ -6,7 +6,7 @@ import type { DefaultStreamChatGenerics } from '../../../types/types'; export const useCreateMessagesContext = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >({ - additionalTouchableProps, + additionalPressableProps, Attachment, AttachmentActions, AudioAttachment, @@ -82,7 +82,9 @@ export const useCreateMessagesContext = < onLongPressMessage, onPressInMessage, onPressMessage, - ReactionList, + ReactionListBottom, + reactionListPosition, + ReactionListTop, removeMessage, Reply, retrySendMessage, @@ -104,14 +106,14 @@ export const useCreateMessagesContext = < */ channelId?: string; }) => { - const additionalTouchablePropsLength = Object.keys(additionalTouchableProps || {}).length; + const additionalTouchablePropsLength = Object.keys(additionalPressableProps || {}).length; const markdownRulesLength = Object.keys(markdownRules || {}).length; const messageContentOrderValue = messageContentOrder.join(); const supportedReactionsLength = supportedReactions?.length; const messagesContext: MessagesContextValue = useMemo( () => ({ - additionalTouchableProps, + additionalPressableProps, Attachment, AttachmentActions, AudioAttachment, @@ -186,7 +188,9 @@ export const useCreateMessagesContext = < onLongPressMessage, onPressInMessage, onPressMessage, - ReactionList, + ReactionListBottom, + reactionListPosition, + ReactionListTop, removeMessage, Reply, retrySendMessage, diff --git a/package/src/components/ImageGallery/ImageGallery.tsx b/package/src/components/ImageGallery/ImageGallery.tsx index 0971517746..a36b0b9f98 100644 --- a/package/src/components/ImageGallery/ImageGallery.tsx +++ b/package/src/components/ImageGallery/ImageGallery.tsx @@ -640,8 +640,8 @@ export const clamp = (value: number, lowerBound: number, upperBound: number) => const styles = StyleSheet.create({ animatedContainer: { alignItems: 'center', - flexDirection: 'row', backgroundColor: 'red', + flexDirection: 'row', }, }); diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index aaab00ca99..644af8f7ad 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -74,7 +74,7 @@ export type FileAttachmentTouchableHandlerPayload< additionalInfo?: { attachment?: Attachment }; }; -export type TouchableHandlerPayload = { +export type PressableHandlerPayload = { defaultHandler?: () => void; event?: GestureResponderEvent; } & ( @@ -86,9 +86,9 @@ export type TouchableHandlerPayload = { | FileAttachmentTouchableHandlerPayload ); -export type MessageTouchableHandlerPayload< +export type MessagePressableHandlerPayload< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = TouchableHandlerPayload & { +> = PressableHandlerPayload & { /** * Set of action handler functions for various message actions. You can use these functions to perform any action when give interaction occurs. */ @@ -124,7 +124,9 @@ export type MessagePropsWithContext< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Pick, 'channel' | 'enforceUniqueReaction' | 'members'> & Pick & - Partial, 'groupStyles' | 'message'>> & + Partial< + Omit, 'groupStyles' | 'handleReaction' | 'message'> + > & Pick, 'groupStyles' | 'message'> & Pick< MessagesContextValue, @@ -196,7 +198,6 @@ const MessageWithContext = < const [showMessageReactions, setShowMessageReactions] = useState(true); const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false); const [isEditedMessageOpen, setIsEditedMessageOpen] = useState(false); - const isMessageTypeDeleted = props.message.type === 'deleted'; const { channel, @@ -252,6 +253,7 @@ const MessageWithContext = < threadList = false, updateMessage, } = props; + const isMessageTypeDeleted = message.type === 'deleted'; const { client } = chatContext; const { theme: { @@ -331,8 +333,8 @@ const MessageWithContext = < forceAlignMessages && (forceAlignMessages === 'left' || forceAlignMessages === 'right') ? forceAlignMessages : isMyMessage - ? 'right' - : 'left'; + ? 'right' + : 'left'; /** * attachments contain files/images or other attachments @@ -561,29 +563,16 @@ const MessageWithContext = < unpinMessage: handleTogglePinMessage, }; - const onLongPressMessage = - hasAttachmentActions || isBlockedMessage(message) - ? () => null - : onLongPressMessageProp - ? (payload?: TouchableHandlerPayload) => - onLongPressMessageProp({ - actionHandlers, - defaultHandler: payload?.defaultHandler || showMessageOverlay, - emitter: payload?.emitter || 'message', - event: payload?.event, - message, - }) - : enableLongPress - ? () => { - // If a message is bounced, on long press the message bounce options modal should open. - if (isBouncedMessage(message)) { - setIsBounceDialogOpen(true); - return; - } - triggerHaptic('impactMedium'); - showMessageOverlay(); - } - : () => null; + const onLongPress = () => { + if (hasAttachmentActions || isBlockedMessage(message) || !enableLongPress) return; + // If a message is bounced, on long press the message bounce options modal should open. + if (isBouncedMessage(message)) { + setIsBounceDialogOpen(true); + return; + } + triggerHaptic('impactMedium'); + showMessageOverlay(); + }; const messageContext = useCreateMessageContext({ actionsEnabled, @@ -594,6 +583,7 @@ const MessageWithContext = < goToMessage, groupStyles, handleAction, + handleReaction, handleToggleReaction, hasReactions, images: attachments.images, @@ -605,7 +595,24 @@ const MessageWithContext = < message, messageContentOrder, myMessageTheme: messagesContext.myMessageTheme, - onLongPress: onLongPressMessage, + onLongPress: (payload) => { + const onLongPressArgs = { + actionHandlers, + defaultHandler: payload?.defaultHandler || onLongPress, + emitter: payload?.emitter || 'message', + event: payload?.event, + message, + }; + + const handleOnLongPress = () => { + if (onLongPressMessageProp) return onLongPressMessageProp(onLongPressArgs); + if (payload?.defaultHandler) return payload.defaultHandler(); + + return onLongPress(); + }; + + handleOnLongPress(); + }, onlyEmojis, onOpenThread, onPress: (payload) => { @@ -664,8 +671,8 @@ const MessageWithContext = < > = Partial, 'groupStyles' | 'message'>> & +> = Partial< + Omit, 'groupStyles' | 'handleReaction' | 'message'> +> & Pick, 'groupStyles' | 'message'>; /** diff --git a/package/src/components/Message/MessageSimple/MessageContent.tsx b/package/src/components/Message/MessageSimple/MessageContent.tsx index a2e243f79e..c69fc3c06a 100644 --- a/package/src/components/Message/MessageSimple/MessageContent.tsx +++ b/package/src/components/Message/MessageSimple/MessageContent.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { AnimatableNumericValue, + ColorValue, LayoutChangeEvent, + Pressable, StyleSheet, - TouchableOpacity, View, } from 'react-native'; @@ -25,14 +26,16 @@ import { import { useViewport } from '../../../hooks/useViewport'; import type { DefaultStreamChatGenerics } from '../../../types/types'; -import { MessageStatusTypes } from '../../../utils/utils'; +import { useMessageData } from '../hooks/useMessageData'; const styles = StyleSheet.create({ + container: { + flexShrink: 1, + }, containerInner: { borderTopLeftRadius: 16, borderTopRightRadius: 16, borderWidth: 1, - overflow: 'hidden', }, leftAlignContent: { justifyContent: 'flex-start', @@ -63,44 +66,40 @@ export type MessageContentPropsWithContext< > = Pick< MessageContextValue, | 'alignment' - | 'isEditedMessageOpen' | 'goToMessage' | 'groupStyles' - | 'hasReactions' + | 'isEditedMessageOpen' | 'isMyMessage' - | 'lastGroupMessage' - | 'members' | 'message' | 'messageContentOrder' | 'onLongPress' - | 'onlyEmojis' | 'onPress' | 'onPressIn' | 'otherAttachments' | 'preventPress' - | 'showMessageStatus' | 'threadList' > & Pick< MessagesContextValue, - | 'additionalTouchableProps' + | 'additionalPressableProps' | 'Attachment' | 'FileAttachmentGroup' | 'Gallery' | 'isAttachmentEqual' - | 'MessageFooter' - | 'MessageHeader' - | 'MessageDeleted' | 'MessageError' - | 'MessagePinnedHeader' - | 'MessageReplies' - | 'MessageStatus' | 'myMessageTheme' - | 'onPressInMessage' | 'Reply' > & Pick & { setMessageContentWidth: React.Dispatch>; + /** + * Background color for the message content + */ + backgroundColor?: ColorValue; + /** + * If the message has no border radius + */ + noBorder?: boolean; }; /** @@ -112,40 +111,31 @@ const MessageContentWithContext = < props: MessageContentPropsWithContext, ) => { const { - additionalTouchableProps, + additionalPressableProps, alignment, Attachment, + backgroundColor, FileAttachmentGroup, Gallery, groupStyles, - hasReactions, isMyMessage, - lastGroupMessage, - members, message, messageContentOrder, - MessageDeleted, MessageError, - MessageFooter, - MessageHeader, - MessagePinnedHeader, - MessageReplies, - MessageStatus, + noBorder, onLongPress, - onlyEmojis, onPress, onPressIn, otherAttachments, preventPress, Reply, setMessageContentWidth, - showMessageStatus, threadList, } = props; const { theme: { - colors: { blue_alice, grey_gainsboro, grey_whisper, transparent, white }, + colors: { grey_gainsboro, grey_whisper }, messageSimple: { content: { container: { @@ -156,17 +146,12 @@ const MessageContentWithContext = < borderRadiusS, borderTopLeftRadius, borderTopRightRadius, - ...container }, containerInner, - errorContainer, - receiverMessageBackgroundColor, replyBorder, replyContainer, - senderMessageBackgroundColor, wrapper, }, - reactionList: { radius, reactionSize }, }, }, } = useTheme(); @@ -180,48 +165,7 @@ const MessageContentWithContext = < setMessageContentWidth(width); }; - const error = message.type === 'error' || message.status === MessageStatusTypes.FAILED; - - const groupStyle = `${alignment}_${groupStyles?.[0]?.toLowerCase?.()}`; - - const hasThreadReplies = !!message?.reply_count; - - let noBorder = onlyEmojis && !message.quoted_message; - if (otherAttachments.length) { - if (otherAttachments[0].type === 'giphy' && !isMyMessage) { - noBorder = false; - } else { - noBorder = true; - } - } - - const isMessageTypeDeleted = message.type === 'deleted'; - - if (isMessageTypeDeleted) { - return ( - - ); - } - - const isMessageReceivedOrErrorType = !isMyMessage || error; - - let backgroundColor = senderMessageBackgroundColor || grey_gainsboro; - if (onlyEmojis && !message.quoted_message) { - backgroundColor = transparent; - } else if (otherAttachments.length) { - if (otherAttachments[0].type === 'giphy') { - backgroundColor = message.quoted_message ? grey_gainsboro : transparent; - } else { - backgroundColor = blue_alice; - } - } else if (isMessageReceivedOrErrorType) { - backgroundColor = receiverMessageBackgroundColor || white; - } + const { hasThreadReplies, isMessageErrorType, isMessageReceivedOrErrorType } = useMessageData({}); const repliesCurveColor = !isMessageReceivedOrErrorType ? backgroundColor : grey_gainsboro; @@ -268,8 +212,7 @@ const MessageContentWithContext = < }; return ( - { if (onLongPress) { @@ -295,33 +238,9 @@ const MessageContentWithContext = < }); } }} - testID='message-content' - {...additionalTouchableProps} - /** - * Border radii are useful for the case of error message types only. - * Otherwise background is transparent, so border radius is not really visible. - */ - style={[ - isMyMessage ? styles.rightAlignItems : styles.leftAlignItems, - { paddingTop: hasReactions ? reactionSize / 2 + radius : 2 }, - error ? errorContainer : {}, - container, - ]} + style={({ pressed }) => ({ opacity: pressed ? 0.5 : 1 })} + {...additionalPressableProps} > - {MessageHeader && ( - - )} - {message.pinned && } {hasThreadReplies && !threadList && !noBorder && ( - {error && } + {isMessageErrorType && } - - - + ); }; @@ -402,39 +319,25 @@ const areEqual = (); const { - additionalTouchableProps, + additionalPressableProps, Attachment, FileAttachmentGroup, Gallery, isAttachmentEqual, - MessageDeleted, MessageError, - MessageFooter, - MessageHeader, - MessagePinnedHeader, - MessageReplies, - MessageStatus, myMessageTheme, Reply, } = useMessagesContext(); @@ -587,38 +473,27 @@ export const MessageContent = < return ( {...{ - additionalTouchableProps, + additionalPressableProps, alignment, Attachment, FileAttachmentGroup, Gallery, goToMessage, groupStyles, - hasReactions, isAttachmentEqual, isEditedMessageOpen, isMyMessage, - lastGroupMessage, lastReceivedId, - members, message, messageContentOrder, - MessageDeleted, MessageError, - MessageFooter, - MessageHeader, - MessagePinnedHeader, - MessageReplies, - MessageStatus, myMessageTheme, onLongPress, - onlyEmojis, onPress, onPressIn, otherAttachments, preventPress, Reply, - showMessageStatus, t, threadList, }} @@ -626,5 +501,3 @@ export const MessageContent = < /> ); }; - -MessageContent.displayName = 'MessageContent{messageSimple{content}}'; diff --git a/package/src/components/Message/MessageSimple/MessageDeleted.tsx b/package/src/components/Message/MessageSimple/MessageDeleted.tsx index 330f8a9f7d..ebb6f77b6f 100644 --- a/package/src/components/Message/MessageSimple/MessageDeleted.tsx +++ b/package/src/components/Message/MessageSimple/MessageDeleted.tsx @@ -75,6 +75,7 @@ const MessageDeletedWithContext = < alignment === 'left' ? styles.leftAlignItems : styles.rightAlignItems, deletedContainer, ]} + testID='message-deleted' > markdownStyles={merge({ em: { color: grey } }, deletedText)} diff --git a/package/src/components/Message/MessageSimple/MessageFooter.tsx b/package/src/components/Message/MessageSimple/MessageFooter.tsx index b16e2dcf6f..ba07714921 100644 --- a/package/src/components/Message/MessageSimple/MessageFooter.tsx +++ b/package/src/components/Message/MessageSimple/MessageFooter.tsx @@ -116,7 +116,7 @@ const MessageFooterWithContext = < if (isDeleted) { return ( - + {deletedMessagesVisibilityType === 'sender' && ( )} diff --git a/package/src/components/Message/MessageSimple/MessagePinnedHeader.tsx b/package/src/components/Message/MessageSimple/MessagePinnedHeader.tsx index dc2dc67b54..c14b819825 100644 --- a/package/src/components/Message/MessageSimple/MessagePinnedHeader.tsx +++ b/package/src/components/Message/MessageSimple/MessagePinnedHeader.tsx @@ -10,26 +10,20 @@ import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; import { PinHeader } from '../../../icons'; -const styles = StyleSheet.create({ - container: { - display: 'flex', - flexDirection: 'row', - }, - label: {}, -}); - import type { DefaultStreamChatGenerics } from '../../../types/types'; -export type MessagePinnedHeaderPropsWithContext< +export type MessagePinnedHeaderProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'message'>; +> = Partial, 'message'>>; -const MessagePinnedHeaderWithContext = < +export const MessagePinnedHeader = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( - props: MessagePinnedHeaderPropsWithContext, + props: MessagePinnedHeaderProps, ) => { - const { message } = props; + const { message: propMessage } = props; + const { message: contextMessage } = useMessageContext(); + const message = propMessage || contextMessage; const { theme: { colors: { grey }, @@ -40,8 +34,12 @@ const MessagePinnedHeaderWithContext = < const { t } = useTranslationContext(); const { client } = useChatContext(); return ( - - + + {t('Pinned by')}{' '} {message?.pinned_by?.id === client?.user?.id ? t('You') : message?.pinned_by?.name} @@ -50,47 +48,14 @@ const MessagePinnedHeaderWithContext = < ); }; -const areEqual = ( - prevProps: MessagePinnedHeaderPropsWithContext, - nextProps: MessagePinnedHeaderPropsWithContext, -) => { - const { message: prevMessage } = prevProps; - const { message: nextMessage } = nextProps; - const messageEqual = - prevMessage.deleted_at === nextMessage.deleted_at && - prevMessage.status === nextMessage.status && - prevMessage.type === nextMessage.type && - prevMessage.text === nextMessage.text && - prevMessage.pinned === nextMessage.pinned; - if (!messageEqual) return false; - return true; -}; - -const MemoizedMessagePinnedHeader = React.memo( - MessagePinnedHeaderWithContext, - areEqual, -) as typeof MessagePinnedHeaderWithContext; - -export type MessagePinnedHeaderProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Partial>; - -export const MessagePinnedHeader = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: MessagePinnedHeaderProps, -) => { - const { lastGroupMessage, message } = useMessageContext(); - - return ( - - ); -}; - -MessagePinnedHeader.displayName = 'MessagePinnedHeader{messageSimple{pinned}}'; +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center', + }, + label: { + fontSize: 12, + marginLeft: 4, + }, +}); diff --git a/package/src/components/Message/MessageSimple/MessageSimple.tsx b/package/src/components/Message/MessageSimple/MessageSimple.tsx index e7c2abbc1d..a3f9784936 100644 --- a/package/src/components/Message/MessageSimple/MessageSimple.tsx +++ b/package/src/components/Message/MessageSimple/MessageSimple.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { StyleSheet, View } from 'react-native'; +import { LayoutChangeEvent, StyleSheet, View } from 'react-native'; import { MessageContextValue, @@ -12,26 +12,47 @@ import { import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import type { DefaultStreamChatGenerics } from '../../../types/types'; +import { useMessageData } from '../hooks/useMessageData'; const styles = StyleSheet.create({ container: { alignItems: 'flex-end', flexDirection: 'row', }, + contentContainer: {}, + contentWrapper: { + flexDirection: 'row', + }, lastMessageContainer: { marginBottom: 12, }, + leftAlignItems: { + alignItems: 'flex-start', + }, messageGroupedSingleOrBottomContainer: { marginBottom: 8, }, messageGroupedTopContainer: {}, + rightAlignItems: { + alignItems: 'flex-end', + }, }); export type MessageSimplePropsWithContext< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Pick< MessageContextValue, - 'alignment' | 'channel' | 'isEditedMessageOpen' | 'groupStyles' | 'hasReactions' | 'message' + | 'alignment' + | 'channel' + | 'groupStyles' + | 'hasReactions' + | 'isMyMessage' + | 'lastGroupMessage' + | 'members' + | 'message' + | 'onlyEmojis' + | 'otherAttachments' + | 'showMessageStatus' > & Pick< MessagesContextValue, @@ -39,7 +60,15 @@ export type MessageSimplePropsWithContext< | 'myMessageTheme' | 'MessageAvatar' | 'MessageContent' - | 'ReactionList' + | 'MessageDeleted' + | 'MessageFooter' + | 'MessageHeader' + | 'MessagePinnedHeader' + | 'MessageReplies' + | 'MessageStatus' + | 'ReactionListBottom' + | 'reactionListPosition' + | 'ReactionListTop' >; const MessageSimpleWithContext = < @@ -47,45 +76,104 @@ const MessageSimpleWithContext = < >( props: MessageSimplePropsWithContext, ) => { + const [messageContentWidth, setMessageContentWidth] = useState(0); const { alignment, - channel, enableMessageGroupingByUser, groupStyles, hasReactions, + isMyMessage, + lastGroupMessage, + members, message, MessageAvatar, MessageContent, - ReactionList, + MessageDeleted, + MessageFooter, + MessageHeader, + MessagePinnedHeader, + MessageReplies, + MessageStatus, + onlyEmojis, + otherAttachments, + ReactionListBottom, + reactionListPosition, + ReactionListTop, + showMessageStatus, } = props; const { theme: { + colors: { blue_alice, grey_gainsboro, transparent }, messageSimple: { container, + content: { + container: contentContainer, + errorContainer, + receiverMessageBackgroundColor, + senderMessageBackgroundColor, + }, + contentWrapper, + headerWrapper, lastMessageContainer, messageGroupedSingleOrBottomContainer, messageGroupedTopContainer, + reactionListTop: { position: reactionPosition }, }, }, } = useTheme(); - const [messageContentWidth, setMessageContentWidth] = useState(0); - - const isVeryLastMessage = - channel?.state.messages[channel?.state.messages.length - 1]?.id === message.id; - - const messageGroupedSingleOrBottom = - groupStyles.includes('single') || groupStyles.includes('bottom'); - - const showReactions = message.type !== 'deleted' && hasReactions && ReactionList; + const { + isMessageErrorType, + isMessageReceivedOrErrorType, + isMessageTypeDeleted, + isVeryLastMessage, + messageGroupedSingleOrBottom, + } = useMessageData({}); const lastMessageInMessageListStyles = [styles.lastMessageContainer, lastMessageContainer]; - const messageGroupedSingleOrBottomStyles = [ - styles.messageGroupedSingleOrBottomContainer, - messageGroupedSingleOrBottomContainer, - ]; - const messageGroupedTopStyles = [styles.messageGroupedTopContainer, messageGroupedTopContainer]; + const messageGroupedSingleOrBottomStyles = { + ...styles.messageGroupedSingleOrBottomContainer, + ...messageGroupedSingleOrBottomContainer, + }; + const messageGroupedTopStyles = { + ...styles.messageGroupedTopContainer, + ...messageGroupedTopContainer, + }; + + const onLayout: (event: LayoutChangeEvent) => void = ({ + nativeEvent: { + layout: { width }, + }, + }) => { + setMessageContentWidth(width); + }; + + const groupStyle = `${alignment}_${groupStyles?.[0]?.toLowerCase?.()}`; + + let noBorder = onlyEmojis && !message.quoted_message; + if (otherAttachments.length) { + if (otherAttachments[0].type === 'giphy' && !isMyMessage) { + noBorder = false; + } else { + noBorder = true; + } + } + + let backgroundColor = senderMessageBackgroundColor; + if (onlyEmojis && !message.quoted_message) { + backgroundColor = transparent; + } else if (otherAttachments.length) { + if (otherAttachments[0].type === 'giphy') { + backgroundColor = message.quoted_message ? grey_gainsboro : transparent; + } else { + backgroundColor = blue_alice; + } + } else if (isMessageReceivedOrErrorType) { + backgroundColor = receiverMessageBackgroundColor; + } + + const repliesCurveColor = isMessageReceivedOrErrorType ? grey_gainsboro : backgroundColor; return ( - {alignment === 'left' && } - - {showReactions && } + {alignment === 'left' ? : null} + {isMessageTypeDeleted ? ( + + ) : ( + + + {MessageHeader && ( + + )} + {message.pinned ? : null} + + + + + {reactionListPosition === 'top' && ReactionListTop ? ( + + ) : null} + + + {reactionListPosition === 'bottom' && ReactionListBottom ? : null} + + + + )} ); }; @@ -119,40 +262,51 @@ const areEqual = ( props: MessageSimpleProps, ) => { - const { alignment, channel, groupStyles, hasReactions, isEditedMessageOpen, message } = - useMessageContext(); + const { + alignment, + channel, + groupStyles, + hasReactions, + isMyMessage, + lastGroupMessage, + members, + message, + onlyEmojis, + otherAttachments, + showMessageStatus, + } = useMessageContext(); const { enableMessageGroupingByUser, MessageAvatar, MessageContent, + MessageDeleted, + MessageFooter, + MessageHeader, + MessagePinnedHeader, + MessageReplies, + MessageStatus, myMessageTheme, - ReactionList, + ReactionListBottom, + reactionListPosition, + ReactionListTop, } = useMessagesContext(); return ( @@ -236,12 +417,25 @@ export const MessageSimple = < enableMessageGroupingByUser, groupStyles, hasReactions, - isEditedMessageOpen, + isMyMessage, + lastGroupMessage, + members, message, MessageAvatar, MessageContent, + MessageDeleted, + MessageFooter, + MessageHeader, + MessagePinnedHeader, + MessageReplies, + MessageStatus, myMessageTheme, - ReactionList, + onlyEmojis, + otherAttachments, + ReactionListBottom, + reactionListPosition, + ReactionListTop, + showMessageStatus, }} {...props} /> diff --git a/package/src/components/Message/MessageSimple/ReactionList.tsx b/package/src/components/Message/MessageSimple/ReactionList.tsx deleted file mode 100644 index d220d244b3..0000000000 --- a/package/src/components/Message/MessageSimple/ReactionList.tsx +++ /dev/null @@ -1,398 +0,0 @@ -import React from 'react'; -import { StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native'; - -import Svg, { Circle } from 'react-native-svg'; - -import { ReactionGroupResponse, ReactionResponse } from 'stream-chat'; - -import { - MessageContextValue, - useMessageContext, -} from '../../../contexts/messageContext/MessageContext'; -import { - MessagesContextValue, - useMessagesContext, -} from '../../../contexts/messagesContext/MessagesContext'; -import { useTheme } from '../../../contexts/themeContext/ThemeContext'; - -import { Unknown } from '../../../icons/Unknown'; - -import type { IconProps } from '../../../icons/utils/base'; -import type { DefaultStreamChatGenerics } from '../../../types/types'; -import type { ReactionData } from '../../../utils/utils'; -import { ReactionSummary } from '../hooks/useProcessReactions'; - -export type MessageReactions = { - reactions: ReactionSummary[]; - supportedReactions?: ReactionData[]; -}; - -type Props = Pick & { - size: number; - type: string; - supportedReactions?: ReactionData[]; -}; - -const Icon = ({ pathFill, size, style, supportedReactions, type }: Props) => { - const ReactionIcon = - supportedReactions?.find((reaction) => reaction.type === type)?.Icon || Unknown; - - return ( - - - - ); -}; - -export type ReactionListPropsWithContext< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick< - MessageContextValue, - | 'alignment' - | 'message' - | 'onLongPress' - | 'onPress' - | 'onPressIn' - | 'preventPress' - | 'reactions' - | 'showMessageOverlay' -> & - Pick, 'targetedMessage' | 'supportedReactions'> & { - messageContentWidth: number; - fill?: string; - /** An array of the reaction objects to display in the list */ - latest_reactions?: ReactionResponse[]; - /** An array of the own reaction objects to distinguish own reactions visually */ - own_reactions?: ReactionResponse[] | null; - radius?: number; // not recommended to change this - /** An object containing summary for each reaction type on a message */ - reaction_groups?: Record | null; - reactionSize?: number; - stroke?: string; - strokeSize?: number; // not recommended to change this - }; - -const ReactionListWithContext = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: ReactionListPropsWithContext, -) => { - const { - alignment, - fill: propFill, - message, - messageContentWidth, - onLongPress, - onPress, - onPressIn, - preventPress, - radius: propRadius, - reactions, - reactionSize: propReactionSize, - showMessageOverlay, - stroke: propStroke, - strokeSize: propStrokeSize, - supportedReactions, - targetedMessage, - } = props; - - const { - theme: { - colors: { - accent_blue, - black, - grey, - grey_gainsboro, - grey_whisper, - targetedMessageBackground, - white, - white_snow, - }, - messageSimple: { - avatarWrapper: { leftAlign, spacer }, - reactionList: { - container, - iconFillColor, - middleIcon, - radius: themeRadius, - reactionBubble, - reactionContainer, - reactionCount, - reactionSize: themeReactionSize, - strokeSize: themeStrokeSize, - }, - }, - screenPadding, - }, - } = useTheme(); - - const width = useWindowDimensions().width; - - const supportedReactionTypes = supportedReactions?.map( - (supportedReaction) => supportedReaction.type, - ); - - const hasSupportedReactions = reactions.some((reaction) => - supportedReactionTypes?.includes(reaction.type), - ); - - if (!hasSupportedReactions || messageContentWidth === 0) { - return null; - } - - const alignmentLeft = alignment === 'left'; - const fill = propFill || (alignmentLeft ? grey_gainsboro : grey_whisper); - const radius = propRadius || themeRadius; - const reactionSize = propReactionSize || themeReactionSize; - const highlighted = message.pinned || targetedMessage === message.id; - const stroke = propStroke || (highlighted ? targetedMessageBackground : white_snow); - const strokeSize = propStrokeSize || themeStrokeSize; - - const x1 = alignmentLeft - ? messageContentWidth + - (Number(leftAlign.marginRight) || 0) + - (Number(spacer.width) || 0) - - radius * 0.5 - : width - screenPadding * 2 - messageContentWidth; - const x2 = x1 + radius * 2 * (alignmentLeft ? 1 : -1); - const y1 = reactionSize + radius * 2; - const y2 = reactionSize - radius; - - const insideLeftBound = x2 - (reactionSize * reactions.length) / 2 > screenPadding; - const insideRightBound = - x2 + strokeSize + (reactionSize * reactions.length) / 2 < width - screenPadding * 2; - const left = - reactions.length === 1 - ? x1 + (alignmentLeft ? -radius : radius - reactionSize) - : !insideLeftBound - ? screenPadding - : !insideRightBound - ? width - screenPadding * 2 - reactionSize * reactions.length - strokeSize - : x2 - (reactionSize * reactions.length) / 2 - strokeSize; - - return ( - - {reactions.length ? ( - - - - - - - - - - - - - - - { - if (onLongPress) { - onLongPress({ - emitter: 'reactionList', - event, - }); - } - }} - onPress={(event) => { - if (onPress) { - onPress({ - defaultHandler: () => showMessageOverlay(false), - emitter: 'reactionList', - event, - }); - } - }} - onPressIn={(event) => { - if (onPressIn) { - onPressIn({ - defaultHandler: () => showMessageOverlay(false), - emitter: 'reactionList', - event, - }); - } - }} - style={[ - styles.reactionBubble, - { - backgroundColor: alignmentLeft ? fill : white, - borderColor: fill, - borderRadius: reactionSize, - borderWidth: strokeSize, - height: reactionSize - strokeSize * 2, - left: left + strokeSize, - top: strokeSize, - }, - reactionBubble, - ]} - > - {reactions.map((reaction, index) => ( - - - - {reaction.count} - - - ))} - - - ) : null} - - ); -}; - -const areEqual = ( - prevProps: ReactionListPropsWithContext, - nextProps: ReactionListPropsWithContext, -) => { - const { - message: prevMessage, - messageContentWidth: prevMessageContentWidth, - reactions: prevReactions, - targetedMessage: prevTargetedMessage, - } = prevProps; - const { - message: nextMessage, - messageContentWidth: nextMessageContentWidth, - reactions: nextReactions, - targetedMessage: nextTargetedMessage, - } = nextProps; - - const messageContentWidthEqual = prevMessageContentWidth === nextMessageContentWidth; - if (!messageContentWidthEqual) return false; - - const messagePinnedEqual = prevMessage.pinned === nextMessage.pinned; - - if (!messagePinnedEqual) return false; - - const targetedMessageEqual = prevTargetedMessage === nextTargetedMessage; - - if (!targetedMessageEqual) return false; - - const latestReactionsEqual = - Array.isArray(prevMessage.latest_reactions) && Array.isArray(nextMessage.latest_reactions) - ? prevMessage.latest_reactions.length === nextMessage.latest_reactions.length && - prevMessage.latest_reactions.every( - ({ type }, index) => type === nextMessage.latest_reactions?.[index].type, - ) - : prevMessage.latest_reactions === nextMessage.latest_reactions; - if (!latestReactionsEqual) return false; - - const reactionsEqual = - Array.isArray(prevReactions) && Array.isArray(nextReactions) - ? prevReactions.length === nextReactions.length && - prevReactions.every( - ({ count, type }, index) => - type === nextMessage.latest_reactions?.[index].type && - count === nextMessage.latest_reactions?.[index].count, - ) - : prevReactions === nextReactions; - - if (!reactionsEqual) return false; - - return true; -}; - -const MemoizedReactionList = React.memo( - ReactionListWithContext, - areEqual, -) as typeof ReactionListWithContext; - -export type ReactionListProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Partial> & - Pick, 'messageContentWidth'>; - -/** - * ReactionList - A high level component which implements all the logic required for a message reaction list - */ -export const ReactionList = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: ReactionListProps, -) => { - const { - alignment, - message, - onLongPress, - onPress, - onPressIn, - preventPress, - reactions, - showMessageOverlay, - } = useMessageContext(); - const { supportedReactions, targetedMessage } = useMessagesContext(); - - return ( - - ); -}; - -const styles = StyleSheet.create({ - container: { - left: 0, - position: 'absolute', - top: 0, - }, - reactionBubble: { - alignItems: 'center', - flexDirection: 'row', - justifyContent: 'space-evenly', - paddingHorizontal: 5, - position: 'absolute', - }, - reactionContainer: { - alignItems: 'center', - flexDirection: 'row', - justifyContent: 'center', - }, - reactionCount: { - fontSize: 12, - fontWeight: 'bold', - marginLeft: 2, - }, -}); diff --git a/package/src/components/Message/MessageSimple/ReactionList/ReactionListBottom.tsx b/package/src/components/Message/MessageSimple/ReactionList/ReactionListBottom.tsx new file mode 100644 index 0000000000..56600dcf1c --- /dev/null +++ b/package/src/components/Message/MessageSimple/ReactionList/ReactionListBottom.tsx @@ -0,0 +1,276 @@ +import React, { useCallback, useRef } from 'react'; +import { Animated, Pressable, StyleSheet, Text, View } from 'react-native'; + +import { + MessageContextValue, + useMessageContext, +} from '../../../../contexts/messageContext/MessageContext'; +import { + MessagesContextValue, + useMessagesContext, +} from '../../../../contexts/messagesContext/MessagesContext'; +import { useTheme } from '../../../../contexts/themeContext/ThemeContext'; + +import { Unknown } from '../../../../icons/Unknown'; + +import type { IconProps } from '../../../../icons/utils/base'; +import type { DefaultStreamChatGenerics } from '../../../../types/types'; +import type { ReactionData } from '../../../../utils/utils'; +import { ReactionSummary } from '../../hooks/useProcessReactions'; + +type Props = Pick & { + size: number; + type: string; + supportedReactions?: ReactionData[]; +}; + +const Icon = ({ pathFill, size, style, supportedReactions, type }: Props) => { + const ReactionIcon = + supportedReactions?.find((reaction) => reaction.type === type)?.Icon || Unknown; + + return ; +}; + +export type ReactionListBottomItemProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Partial< + Pick< + MessageContextValue, + | 'handleReaction' + | 'onLongPress' + | 'onPress' + | 'onPressIn' + | 'preventPress' + | 'showMessageOverlay' + > +> & + Partial, 'supportedReactions'>> & { + reaction: ReactionSummary; + }; + +export const ReactionListBottomItem = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + props: ReactionListBottomItemProps, +) => { + const { + handleReaction, + onLongPress, + onPress, + onPressIn, + preventPress, + reaction, + showMessageOverlay, + supportedReactions, + } = props; + const scaleValue = useRef(new Animated.Value(1)).current; + const { + theme: { + colors: { black, grey_gainsboro, light_blue }, + messageSimple: { + reactionListBottom: { + item: { container, countText, icon, iconFillColor, iconSize, iconUnFillColor }, + }, + }, + }, + } = useTheme(); + + const onPressInAnimation = useCallback(() => { + Animated.spring(scaleValue, { + toValue: 0.8, + useNativeDriver: true, + }).start(); + }, [scaleValue]); + + const onPressOutAnimation = useCallback(() => { + Animated.spring(scaleValue, { + toValue: 1, + useNativeDriver: true, + }).start(); + }, [scaleValue]); + + return ( + { + if (onLongPress) { + onLongPress({ + defaultHandler: () => { + if (showMessageOverlay) { + showMessageOverlay(true); + } + }, + emitter: 'reactionList', + event, + }); + } + }} + onPress={(event) => { + if (onPress) { + onPress({ + defaultHandler: () => { + if (handleReaction) { + handleReaction(reaction.type); + } + }, + emitter: 'reactionList', + event, + }); + } + }} + onPressIn={(event) => { + onPressInAnimation(); + if (onPressIn) { + onPressIn({ + defaultHandler: () => { + if (handleReaction) { + handleReaction(reaction.type); + } + }, + emitter: 'reactionList', + event, + }); + } + }} + onPressOut={onPressOutAnimation} + > + + + {reaction.count} + + + ); +}; + +export type ReactionListBottomProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Partial< + Pick< + MessageContextValue, + | 'handleReaction' + | 'hasReactions' + | 'onLongPress' + | 'onPress' + | 'onPressIn' + | 'preventPress' + | 'reactions' + | 'showMessageOverlay' + > +> & + Partial, 'supportedReactions'>>; + +export const ReactionListBottom = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + props: ReactionListBottomProps, +) => { + const { + handleReaction: propHandlerReaction, + hasReactions: propHasReactions, + onLongPress: propOnLongPress, + onPress: propOnPress, + onPressIn: propOnPressIn, + preventPress: propPreventPress, + reactions: propReactions, + showMessageOverlay: propShowMessageOverlay, + supportedReactions: propSupportedReactions, + } = props; + + const { + handleReaction: contextHandleReaction, + hasReactions: contextHasReactions, + onLongPress: contextOnLongPress, + onPress: contextOnPress, + onPressIn: contextOnPressIn, + preventPress: contextPreventPress, + reactions: contextReactions, + showMessageOverlay: contextShowMessageOverlay, + } = useMessageContext(); + + const { supportedReactions: contextSupportedReactions } = + useMessagesContext(); + + const handleReaction = propHandlerReaction || contextHandleReaction; + const hasReactions = propHasReactions || contextHasReactions; + const onLongPress = propOnLongPress || contextOnLongPress; + const onPress = propOnPress || contextOnPress; + const onPressIn = propOnPressIn || contextOnPressIn; + const preventPress = propPreventPress || contextPreventPress; + const reactions = propReactions || contextReactions; + const showMessageOverlay = propShowMessageOverlay || contextShowMessageOverlay; + const supportedReactions = propSupportedReactions || contextSupportedReactions; + const { + theme: { + messageSimple: { + reactionListBottom: { container }, + }, + }, + } = useTheme(); + + const supportedReactionTypes = supportedReactions?.map( + (supportedReaction) => supportedReaction.type, + ); + + const hasSupportedReactions = reactions.some((reaction) => + supportedReactionTypes?.includes(reaction.type), + ); + + if (!hasSupportedReactions || !hasReactions) { + return null; + } + + return ( + + {reactions.map((reaction, index) => ( + + ))} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + flexWrap: 'wrap', + }, + itemContainer: { + alignItems: 'center', + borderRadius: 12, + flexDirection: 'row', + justifyContent: 'center', + margin: 2, + padding: 8, + }, + reactionCount: { + fontWeight: '600', + marginLeft: 4, + }, +}); diff --git a/package/src/components/Message/MessageSimple/ReactionList/ReactionListTop.tsx b/package/src/components/Message/MessageSimple/ReactionList/ReactionListTop.tsx new file mode 100644 index 0000000000..9344c726cf --- /dev/null +++ b/package/src/components/Message/MessageSimple/ReactionList/ReactionListTop.tsx @@ -0,0 +1,271 @@ +import React from 'react'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; + +import { + MessageContextValue, + useMessageContext, +} from '../../../../contexts/messageContext/MessageContext'; +import { + MessagesContextValue, + useMessagesContext, +} from '../../../../contexts/messagesContext/MessagesContext'; +import { useTheme } from '../../../../contexts/themeContext/ThemeContext'; + +import { Unknown } from '../../../../icons/Unknown'; + +import type { IconProps } from '../../../../icons/utils/base'; +import type { DefaultStreamChatGenerics } from '../../../../types/types'; +import type { ReactionData } from '../../../../utils/utils'; +import { ReactionSummary } from '../../hooks/useProcessReactions'; + +type Props = Pick & { + size: number; + type: string; + supportedReactions?: ReactionData[]; +}; + +const Icon = ({ pathFill, size, style, supportedReactions, type }: Props) => { + const ReactionIcon = + supportedReactions?.find((reaction) => reaction.type === type)?.Icon || Unknown; + + return ( + + + + ); +}; + +export type ReactionListTopItemProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Partial, 'reactions'>> & + Partial, 'supportedReactions'>> & { + index: number; + reaction: ReactionSummary; + }; + +export const ReactionListTopItem = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + props: ReactionListTopItemProps, +) => { + const { index, reaction, reactions, supportedReactions } = props; + const { + theme: { + messageSimple: { + reactionListTop: { + item: { container, icon, iconFillColor, iconUnFillColor, reactionSize }, + }, + }, + }, + } = useTheme(); + + const reactionsLength = reactions ? reactions.length : 0; + + return ( + + + + ); +}; + +export type ReactionListTopProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Partial< + Pick< + MessageContextValue, + | 'alignment' + | 'hasReactions' + | 'onLongPress' + | 'onPress' + | 'onPressIn' + | 'preventPress' + | 'reactions' + | 'showMessageOverlay' + > +> & + Pick, 'supportedReactions'> & { + messageContentWidth: number; + fill?: string; + reactionSize?: number; + }; + +/** + * ReactionListTop - A high level component which implements all the logic required for a message reaction list + */ +export const ReactionListTop = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + props: ReactionListTopProps, +) => { + const { + alignment: propAlignment, + fill: propFill, + hasReactions: propHasReactions, + messageContentWidth, + onLongPress: propOnLongPress, + onPress: propOnPress, + onPressIn: propOnPressIn, + preventPress: propPreventPress, + reactions: propReactions, + reactionSize: propReactionSize, + showMessageOverlay: propShowMessageOverlay, + supportedReactions: propSupportedReactions, + } = props; + + const { + alignment: contextAlignment, + hasReactions: contextHasReactions, + onLongPress: contextOnLongPress, + onPress: contextOnPress, + onPressIn: contextOnPressIn, + preventPress: contextPreventPress, + reactions: contextReactions, + showMessageOverlay: contextShowMessageOverlay, + } = useMessageContext(); + + const { supportedReactions: contextSupportedReactions } = + useMessagesContext(); + + const alignment = propAlignment || contextAlignment; + const hasReactions = propHasReactions || contextHasReactions; + const onLongPress = propOnLongPress || contextOnLongPress; + const onPress = propOnPress || contextOnPress; + const onPressIn = propOnPressIn || contextOnPressIn; + const preventPress = propPreventPress || contextPreventPress; + const reactions = propReactions || contextReactions; + const showMessageOverlay = propShowMessageOverlay || contextShowMessageOverlay; + const supportedReactions = propSupportedReactions || contextSupportedReactions; + + const { + theme: { + colors: { grey_gainsboro, grey_whisper, white }, + messageSimple: { + reactionListTop: { + container, + item: { reactionSize: themeReactionSize }, + position: reactionPosition, + }, + }, + }, + } = useTheme(); + + const supportedReactionTypes = supportedReactions?.map( + (supportedReaction) => supportedReaction.type, + ); + + const hasSupportedReactions = reactions.some((reaction) => + supportedReactionTypes?.includes(reaction.type), + ); + + if (!hasSupportedReactions || messageContentWidth === 0 || !hasReactions) { + return null; + } + + const alignmentLeft = alignment === 'left'; + const fill = propFill || (alignmentLeft ? grey_gainsboro : grey_whisper); + const reactionSize = propReactionSize || themeReactionSize; + + // This is an estimated value for the message length that is considered small + const SMALL_MESSAGE_LENGTH_THRESHOLD = 80; + + // It is the length of the reaction list + const totalReactionSize = reactionSize * reactions.length; + const halfReactionSize = totalReactionSize / 2; + + const position = + messageContentWidth < SMALL_MESSAGE_LENGTH_THRESHOLD + ? messageContentWidth - reactionPosition + : messageContentWidth - halfReactionSize; + + return ( + { + if (onLongPress) { + onLongPress({ + emitter: 'reactionList', + event, + }); + } + }} + onPress={(event) => { + if (onPress) { + onPress({ + defaultHandler: () => showMessageOverlay(true), + emitter: 'reactionList', + event, + }); + } + }} + onPressIn={(event) => { + if (onPressIn) { + onPressIn({ + defaultHandler: () => showMessageOverlay(true), + emitter: 'reactionList', + event, + }); + } + }} + style={[ + styles.container, + { + backgroundColor: fill, + borderColor: white, + borderRadius: reactionSize, + height: reactionSize, + top: -reactionPosition, + }, + alignmentLeft ? { left: position } : { right: position }, + container, + ]} + > + {reactions.map((reaction, index) => ( + + ))} + + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + borderWidth: 1, + flexDirection: 'row', + justifyContent: 'space-evenly', + paddingHorizontal: 5, + position: 'absolute', + }, + reactionContainer: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'center', + }, + reactionCount: { + fontSize: 12, + fontWeight: 'bold', + marginLeft: 2, + }, +}); diff --git a/package/src/components/Message/MessageSimple/__tests__/MessageContent.test.js b/package/src/components/Message/MessageSimple/__tests__/MessageContent.test.js index ef06ec7c46..02da976d97 100644 --- a/package/src/components/Message/MessageSimple/__tests__/MessageContent.test.js +++ b/package/src/components/Message/MessageSimple/__tests__/MessageContent.test.js @@ -102,7 +102,6 @@ describe('MessageContent', () => { }); await waitFor(() => { - expect(screen.getByTestId('message-content-wrapper')).toBeTruthy(); expect(screen.getByTestId('message-deleted')).toBeTruthy(); }); }); @@ -237,7 +236,7 @@ describe('MessageContent', () => { await waitFor(() => { expect(screen.getByTestId('message-content-wrapper')).toBeTruthy(); - expect(screen.getByTestId('reaction-list')).toBeTruthy(); + expect(screen.getByLabelText('Reaction List Top')).toBeTruthy(); }); }); }); diff --git a/package/src/components/Message/MessageSimple/__tests__/MessageSimple.test.js b/package/src/components/Message/MessageSimple/__tests__/MessageSimple.test.js new file mode 100644 index 0000000000..e6c5d4bfb1 --- /dev/null +++ b/package/src/components/Message/MessageSimple/__tests__/MessageSimple.test.js @@ -0,0 +1,208 @@ +import React from 'react'; + +import { Text } from 'react-native'; + +import { cleanup, render, screen, waitFor } from '@testing-library/react-native'; + +import { ChannelsStateProvider } from '../../../../contexts/channelsStateContext/ChannelsStateContext'; + +import { getOrCreateChannelApi } from '../../../../mock-builders/api/getOrCreateChannel'; +import { useMockedApis } from '../../../../mock-builders/api/useMockedApis'; +import { generateChannelResponse } from '../../../../mock-builders/generator/channel'; +import { generateMember } from '../../../../mock-builders/generator/member'; +import { generateMessage } from '../../../../mock-builders/generator/message'; +import { generateUser } from '../../../../mock-builders/generator/user'; +import { getTestClientWithUser } from '../../../../mock-builders/mock'; +import { Channel } from '../../../Channel/Channel'; +import { Chat } from '../../../Chat/Chat'; +import { Message } from '../../Message'; + +describe('MessageSimple', () => { + let channel; + let chatClient; + let renderMessage; + + const user = generateUser({ id: 'id', name: 'name' }); + const messages = [generateMessage({ user })]; + + beforeEach(async () => { + const members = [generateMember({ user })]; + const mockedChannel = generateChannelResponse({ + members, + messages, + }); + + chatClient = await getTestClientWithUser(user); + useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); + channel = chatClient.channel('messaging', mockedChannel.id); + + renderMessage = (options, channelProps) => + render( + + + + + + + , + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + it('renders the MessageSimple component', async () => { + const user = generateUser(); + const message = generateMessage({ user }); + + renderMessage({ message }); + + await waitFor(() => { + expect(screen.getByTestId('message-simple-wrapper')).toBeTruthy(); + }); + }); + + it('renders MessageAvatar when alignment is left', async () => { + const user = generateUser(); + const message = generateMessage({ user }); + + renderMessage({ message }); + + await waitFor(() => { + expect(screen.queryByTestId('message-avatar')).toBeDefined(); + }); + }); + + it('do not renders MessageAvatar when alignment is right', async () => { + const user = generateUser({ id: 'id', name: 'name' }); + const message = generateMessage({ user }); + renderMessage({ message }); + + await waitFor(() => { + expect(screen.queryByTestId('message-avatar')).toBeNull(); + }); + }); + + it('renders MessageDeleted when message type is deleted', async () => { + const user = generateUser(); + const message = generateMessage({ type: 'deleted', user }); + + renderMessage({ message }); + + await waitFor(() => { + expect(screen.queryByTestId('message-deleted')).toBeDefined(); + }); + }); + + it('renders MessageContent when message is not deleted', async () => { + const user = generateUser(); + const message = generateMessage({ user }); + + renderMessage({ message }); + + await waitFor(() => { + expect(screen.queryByTestId('message-components')).toBeDefined(); + }); + }); + + it('renders ReactionListTop when reactionListPosition is top', async () => { + const user = generateUser(); + const message = generateMessage({ user }); + + renderMessage({ message }, { reactionListPosition: 'top' }); + + await waitFor(() => { + expect(screen.queryByLabelText('Reaction List Top')).toBeDefined(); + expect(screen.queryByLabelText('Reaction List Bottom')).toBeNull(); + }); + }); + + it('renders ReactionListBottom when reactionListPosition is bottom', async () => { + const user = generateUser(); + const message = generateMessage({ user }); + + renderMessage({ message }, { reactionListPosition: 'bottom' }); + + await waitFor(() => { + expect(screen.queryByLabelText('Reaction List Top')).toBeNull(); + expect(screen.queryByLabelText('Reaction List Bottom')).toBeDefined(); + }); + }); + + it('renders MessagePinnedHeader when message is pinned', async () => { + const user = generateUser(); + const message = generateMessage({ pinned: true, user }); + + renderMessage({ message }); + + await waitFor(() => { + expect(screen.queryByLabelText('Message Pinned Header')).not.toBeNull(); + }); + }); + + it('renders MessageHeader component if defined', async () => { + const user = generateUser(); + const message = generateMessage({ user }); + + renderMessage({ message }, { MessageHeader: () => Message Header }); + + await waitFor(() => { + expect(screen.queryByText('Message Header')).not.toBeNull(); + }); + }); + + it('applies correct styles for when group styles are not single or bottom', async () => { + const user = generateUser(); + const message = generateMessage({ user }); + + renderMessage({ groupStyles: ['top'], message }); + + await waitFor(() => { + console.log(screen.getByTestId('message-simple-wrapper').props.style[1]); + expect(screen.getByTestId('message-simple-wrapper').props.style[1]).toMatchObject({}); + }); + }); + + it('applies correct styles for when group styles are single/bottom and not last message', async () => { + const user = generateUser(); + const message = generateMessage({ user }); + + renderMessage({ message }); + + await waitFor(() => { + expect(screen.getByTestId('message-simple-wrapper').props.style[1]).toMatchObject({ + marginBottom: 8, + }); + }); + }); + + it('noBorder true when only emojis text', async () => { + const user = generateUser(); + const message = generateMessage({ quoted_message: undefined, text: '😊', user }); + + renderMessage({ message }); + + await waitFor(() => { + console.log(screen.getByTestId('message-content-wrapper').props.style); + expect(screen.getByTestId('message-content-wrapper').props.style[2]).toMatchObject({ + borderWidth: 0, + }); + }); + }); + + it('noBorder true when only other attachments is present', async () => { + const user = generateUser(); + const message = generateMessage({ attachments: [{ title: 'other', type: 'other' }], user }); + + renderMessage({ message }); + + await waitFor(() => { + console.log(screen.getByTestId('message-content-wrapper').props.style); + expect(screen.getByTestId('message-content-wrapper').props.style[2]).toMatchObject({ + borderWidth: 0, + }); + }); + }); +}); diff --git a/package/src/components/Message/MessageSimple/__tests__/ReactionListBottom.test.js b/package/src/components/Message/MessageSimple/__tests__/ReactionListBottom.test.js new file mode 100644 index 0000000000..94a11cb19b --- /dev/null +++ b/package/src/components/Message/MessageSimple/__tests__/ReactionListBottom.test.js @@ -0,0 +1,166 @@ +import React from 'react'; + +import { Animated } from 'react-native'; + +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react-native'; + +import { ChannelsStateProvider } from '../../../../contexts/channelsStateContext/ChannelsStateContext'; + +import { getOrCreateChannelApi } from '../../../../mock-builders/api/getOrCreateChannel'; +import { useMockedApis } from '../../../../mock-builders/api/useMockedApis'; +import { generateChannelResponse } from '../../../../mock-builders/generator/channel'; +import { generateMember } from '../../../../mock-builders/generator/member'; +import { generateMessage } from '../../../../mock-builders/generator/message'; +import { generateReaction } from '../../../../mock-builders/generator/reaction'; +import { generateUser } from '../../../../mock-builders/generator/user'; +import { getTestClientWithUser } from '../../../../mock-builders/mock'; +import { Channel } from '../../../Channel/Channel'; +import { Chat } from '../../../Chat/Chat'; +import { Message } from '../../Message'; + +describe('ReactionListBottom', () => { + let channel; + let chatClient; + let renderMessage; + + const user = generateUser({ id: 'id', name: 'name' }); + const messages = [generateMessage({ user })]; + + beforeEach(async () => { + const members = [generateMember({ user })]; + const mockedChannel = generateChannelResponse({ + members, + messages, + }); + + chatClient = await getTestClientWithUser(user); + useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); + channel = chatClient.channel('messaging', mockedChannel.id); + + renderMessage = (options, channelProps) => + render( + + + + + + + , + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + it('renders the ReactionListBottom component', async () => { + const user = generateUser(); + const reaction = generateReaction(); + const message = generateMessage({ + reaction_groups: { [reaction.type]: reaction }, + user, + }); + + renderMessage({ message }, { reactionListPosition: 'bottom' }); + + await waitFor(() => { + expect(screen.getByLabelText('Reaction List Bottom')).toBeTruthy(); + }); + }); + + it('renders null when no supported reaction', async () => { + const user = generateUser(); + const reaction = generateReaction(); + const message = generateMessage({ + reaction_groups: { [reaction.type]: reaction }, + user, + }); + + renderMessage({ message }, { reactionListPosition: 'bottom', supportedReactions: [] }); + + await waitFor(() => { + expect(screen.queryByLabelText('Reaction List Bottom')).toBeNull(); + }); + }); + + it('renders null when no hasReactions false', async () => { + const user = generateUser(); + const message = generateMessage({ + reaction_groups: {}, + user, + }); + + renderMessage({ hasReactions: false, message }, { reactionListPosition: 'bottom' }); + + await waitFor(() => { + expect(screen.queryByLabelText('Reaction List Bottom')).toBeNull(); + }); + }); + + it('applies animation on press in', () => { + const animatedSpy = jest.spyOn(Animated, 'spring'); + const user = generateUser(); + const reaction = generateReaction(); + const message = generateMessage({ + reaction_groups: { [reaction.type]: reaction }, + user, + }); + + renderMessage({ message }, { reactionListPosition: 'bottom' }); + + const reactionListBottomItem = screen.getByLabelText('Reaction List Bottom Item'); + + fireEvent(reactionListBottomItem, 'onPressIn'); + + expect(animatedSpy).toHaveBeenCalledWith(expect.any(Animated.Value), { + toValue: 0.8, + useNativeDriver: true, + }); + }); + + it('applies animation on press out', () => { + const animatedSpy = jest.spyOn(Animated, 'spring'); + const user = generateUser(); + const reaction = generateReaction(); + const message = generateMessage({ + reaction_groups: { [reaction.type]: reaction }, + user, + }); + + renderMessage({ message }, { reactionListPosition: 'bottom' }); + + const reactionListBottomItem = screen.getByLabelText('Reaction List Bottom Item'); + + fireEvent(reactionListBottomItem, 'onPressOut'); + + expect(animatedSpy).toHaveBeenCalledWith(expect.any(Animated.Value), { + toValue: 1, + useNativeDriver: true, + }); + }); + + it('call handleReaction on press', () => { + const handleReactionMock = jest.fn(); + const user = generateUser(); + const reaction = generateReaction(); + const message = generateMessage({ + reaction_groups: { [reaction.type]: reaction }, + user, + }); + + renderMessage( + { + handleReaction: handleReactionMock, + message, + }, + { reactionListPosition: 'bottom' }, + ); + + const reactionListBottomItem = screen.getByLabelText('Reaction List Bottom Item'); + + fireEvent(reactionListBottomItem, 'onPress'); + + expect(handleReactionMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/package/src/components/Message/MessageSimple/__tests__/ReactionListTop.test.js b/package/src/components/Message/MessageSimple/__tests__/ReactionListTop.test.js new file mode 100644 index 0000000000..c0fe78f9a7 --- /dev/null +++ b/package/src/components/Message/MessageSimple/__tests__/ReactionListTop.test.js @@ -0,0 +1,92 @@ +import React from 'react'; + +import { cleanup, render, screen, waitFor } from '@testing-library/react-native'; + +import { ChannelsStateProvider } from '../../../../contexts/channelsStateContext/ChannelsStateContext'; + +import { getOrCreateChannelApi } from '../../../../mock-builders/api/getOrCreateChannel'; +import { useMockedApis } from '../../../../mock-builders/api/useMockedApis'; +import { generateChannelResponse } from '../../../../mock-builders/generator/channel'; +import { generateMember } from '../../../../mock-builders/generator/member'; +import { generateMessage } from '../../../../mock-builders/generator/message'; +import { generateUser } from '../../../../mock-builders/generator/user'; +import { getTestClientWithUser } from '../../../../mock-builders/mock'; +import { Channel } from '../../../Channel/Channel'; +import { Chat } from '../../../Chat/Chat'; +import { ReactionListTop } from '../ReactionList/ReactionListTop'; + +describe('ReactionListTop', () => { + let channel; + let chatClient; + let renderMessage; + + const user = generateUser({ id: 'id', name: 'name' }); + const messages = [generateMessage({ user })]; + + beforeEach(async () => { + const members = [generateMember({ user })]; + const mockedChannel = generateChannelResponse({ + members, + messages, + }); + + chatClient = await getTestClientWithUser(user); + useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); + channel = chatClient.channel('messaging', mockedChannel.id); + + renderMessage = (options, channelProps) => + render( + + + + + + + , + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + it('renders the ReactionListTop component', async () => { + renderMessage({ + hasReactions: true, + messageContentWidth: 100, + reactions: [{ count: 1, own: true, type: 'love' }], + }); + + await waitFor(() => { + expect(screen.getByLabelText('Reaction List Top')).toBeTruthy(); + }); + }); + + it('return null in ReactionListTop component when hasReactions false', async () => { + renderMessage({ + hasReactions: false, + messageContentWidth: 100, + reactions: [{ count: 1, own: true, type: 'love' }], + }); + + await waitFor(() => { + expect(screen.queryByLabelText('Reaction List Top')).toBeNull(); + }); + }); + + it('return null in ReactionListTop component when supportedReactions are not matching', async () => { + renderMessage( + { + hasReactions: false, + messageContentWidth: 100, + reactions: [{ count: 1, own: true, type: 'love' }], + }, + { supportedReactions: [] }, + ); + + await waitFor(() => { + expect(screen.queryByLabelText('Reaction List Top')).toBeNull(); + }); + }); +}); diff --git a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessagePinnedHeader.test.js.snap b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessagePinnedHeader.test.js.snap index 9e5b53bb71..05765328b6 100644 --- a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessagePinnedHeader.test.js.snap +++ b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessagePinnedHeader.test.js.snap @@ -2,11 +2,13 @@ exports[`MessagePinnedHeader should render message pinned 1`] = ` @@ -73,7 +77,10 @@ exports[`MessagePinnedHeader should render message pinned 1`] = ` { "color": "#7A7A7A", }, - {}, + { + "fontSize": 12, + "marginLeft": 4, + }, {}, ] } diff --git a/package/src/components/Message/hooks/useCreateMessageContext.ts b/package/src/components/Message/hooks/useCreateMessageContext.ts index 67e2bdfcb7..3536a2faee 100644 --- a/package/src/components/Message/hooks/useCreateMessageContext.ts +++ b/package/src/components/Message/hooks/useCreateMessageContext.ts @@ -15,6 +15,7 @@ export const useCreateMessageContext = < goToMessage, groupStyles, handleAction, + handleReaction, handleToggleReaction, hasReactions, images, @@ -60,6 +61,7 @@ export const useCreateMessageContext = < goToMessage, groupStyles, handleAction, + handleReaction, handleToggleReaction, hasReactions, images, diff --git a/package/src/components/Message/hooks/useMessageData.ts b/package/src/components/Message/hooks/useMessageData.ts new file mode 100644 index 0000000000..20cd17d923 --- /dev/null +++ b/package/src/components/Message/hooks/useMessageData.ts @@ -0,0 +1,59 @@ +import { + MessageContextValue, + useMessageContext, +} from '../../../contexts/messageContext/MessageContext'; + +import { DefaultStreamChatGenerics } from '../../../types/types'; +import { MessageStatusTypes } from '../../../utils/utils'; + +export type UseMessageDataProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Partial< + Pick< + MessageContextValue, + 'channel' | 'groupStyles' | 'isMyMessage' | 'message' + > +>; + +export const useMessageData = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>({ + channel: propChannel, + groupStyles: propGroupStyles, + isMyMessage: propIsMyMessage, + message: propMessage, +}: UseMessageDataProps) => { + const { + channel: contextChannel, + groupStyles: contextGroupStyles, + isMyMessage: contextIsMyMessage, + message: contextMessage, + } = useMessageContext(); + + const channel = propChannel || contextChannel; + const groupStyles = propGroupStyles || contextGroupStyles; + const isMyMessage = propIsMyMessage || contextIsMyMessage; + const message = propMessage || contextMessage; + + const isVeryLastMessage = + channel?.state.messages[channel?.state.messages.length - 1]?.id === message.id; + + const messageGroupedSingleOrBottom = + groupStyles.includes('single') || groupStyles.includes('bottom'); + + const isMessageErrorType = + message.type === 'error' || message.status === MessageStatusTypes.FAILED; + const isMessageTypeDeleted = message.type === 'deleted'; + const isMessageReceivedOrErrorType = !isMyMessage || isMessageErrorType; + + const hasThreadReplies = !!message?.reply_count; + + return { + hasThreadReplies, + isMessageErrorType, + isMessageReceivedOrErrorType, + isMessageTypeDeleted, + isVeryLastMessage, + messageGroupedSingleOrBottom, + }; +}; diff --git a/package/src/components/Message/hooks/useProcessReactions.ts b/package/src/components/Message/hooks/useProcessReactions.ts index 7239db8db4..f5550eb603 100644 --- a/package/src/components/Message/hooks/useProcessReactions.ts +++ b/package/src/components/Message/hooks/useProcessReactions.ts @@ -1,6 +1,6 @@ import { ComponentType, useMemo } from 'react'; -import { ReactionResponse } from 'stream-chat'; +import { ReactionGroupResponse, ReactionResponse } from 'stream-chat'; import { useChatContext } from '../../../contexts'; import { @@ -9,7 +9,6 @@ import { } from '../../../contexts/messagesContext/MessagesContext'; import { DefaultStreamChatGenerics } from '../../../types/types'; import { ReactionData } from '../../../utils/utils'; -import { ReactionListProps } from '../MessageSimple/ReactionList'; export type ReactionSummary = { own: boolean; @@ -24,12 +23,20 @@ export type ReactionSummary = { export type ReactionsComparator = (a: ReactionSummary, b: ReactionSummary) => number; +export type MessageReactionsData< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = { + /** An array of the reaction objects to display in the list */ + latest_reactions?: ReactionResponse[]; + /** An array of the own reaction objects to distinguish own reactions visually */ + own_reactions?: ReactionResponse[] | null; + /** An object containing summary for each reaction type on a message */ + reaction_groups?: Record | null; +}; + type UseProcessReactionsParams< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick< - ReactionListProps, - 'own_reactions' | 'reaction_groups' | 'latest_reactions' -> & +> = MessageReactionsData & Partial, 'supportedReactions'>> & { sortReactions?: ReactionsComparator; }; @@ -46,16 +53,8 @@ const isOwnReaction = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( reactionType: string, - ownReactions?: ReactionResponse[] | null, - latestReactions?: ReactionResponse[] | null, - userID?: string, -) => - (ownReactions ? ownReactions.some((reaction) => reaction.type === reactionType) : false) || - (latestReactions - ? latestReactions.some( - (reaction) => reaction?.user?.id === userID && reaction.type === reactionType, - ) - : false); + ownReactions?: MessageReactionsData['own_reactions'], +) => (ownReactions ? ownReactions.some((reaction) => reaction.type === reactionType) : false); const isSupportedReaction = (reactionType: string, supportedReactions?: ReactionData[]) => supportedReactions @@ -110,12 +109,7 @@ export const useProcessReactions = < Icon: getEmojiByReactionType(reactionType, supportedReactions), lastReactionAt: last_reaction_at ? new Date(last_reaction_at) : null, latestReactedUserNames, - own: isOwnReaction( - reactionType, - own_reactions, - latest_reactions, - client.userID, - ), + own: isOwnReaction(reactionType, own_reactions), type: reactionType, unlistedReactedUserCount: count - latestReactedUserNames.length, }; @@ -127,6 +121,7 @@ export const useProcessReactions = < hasReactions: unsortedReactions.length > 0, totalReactionCount: unsortedReactions.reduce((total, { count }) => total + count, 0), }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ client.userID, reaction_groups, diff --git a/package/src/components/MessageMenu/MessageReactionPicker.tsx b/package/src/components/MessageMenu/MessageReactionPicker.tsx index 67c5462fca..b66312e4c9 100644 --- a/package/src/components/MessageMenu/MessageReactionPicker.tsx +++ b/package/src/components/MessageMenu/MessageReactionPicker.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { StyleSheet, View } from 'react-native'; +import { FlatList, StyleSheet, View } from 'react-native'; import { ReactionButton } from './ReactionButton'; @@ -12,6 +12,7 @@ import { useOwnCapabilitiesContext } from '../../contexts/ownCapabilitiesContext import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { triggerHaptic } from '../../native'; import type { DefaultStreamChatGenerics } from '../../types/types'; +import { ReactionData } from '../../utils/utils'; export type MessageReactionPickerProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, @@ -33,6 +34,21 @@ export type MessageReactionPickerProps< handleReaction?: (reactionType: string) => Promise; }; +export type ReactionPickerItemType = ReactionData & { + onSelectReaction: (type: string) => void; + ownReactionTypes: string[]; +}; + +const renderItem = ({ index, item }: { index: number; item: ReactionPickerItemType }) => ( + +); + /** * MessageReactionPicker - A high level component which implements all the logic required for a message overlay reaction list */ @@ -51,7 +67,7 @@ export const MessageReactionPicker = < const { theme: { messageMenu: { - reactionPicker: { container }, + reactionPicker: { container, contentContainer }, }, }, } = useTheme(); @@ -71,29 +87,36 @@ export const MessageReactionPicker = < return null; } + const reactions: ReactionPickerItemType[] = + supportedReactions?.map((reaction) => ({ + ...reaction, + onSelectReaction, + ownReactionTypes, + })) ?? []; + return ( - {supportedReactions?.map(({ Icon, type }, index) => ( - - ))} + item.type} + renderItem={renderItem} + /> ); }; const styles = StyleSheet.create({ container: { - flexDirection: 'row', - justifyContent: 'space-between', - marginBottom: 16, - paddingHorizontal: 16, + alignSelf: 'stretch', + }, + contentContainer: { + flexGrow: 1, + justifyContent: 'space-around', + marginVertical: 16, }, }); diff --git a/package/src/components/MessageMenu/MessageUserReactions.tsx b/package/src/components/MessageMenu/MessageUserReactions.tsx index 2dcda50d4c..fa8570183c 100644 --- a/package/src/components/MessageMenu/MessageUserReactions.tsx +++ b/package/src/components/MessageMenu/MessageUserReactions.tsx @@ -38,6 +38,21 @@ const sort: ReactionSortBase = { created_at: -1, }; +export type ReactionSelectorItemType = ReactionData & { + onSelectReaction: (type: string) => void; + selectedReaction?: string; +}; + +const renderSelectorItem = ({ index, item }: { index: number; item: ReactionSelectorItemType }) => ( + +); + export const MessageUserReactions = (props: MessageUserReactionsProps) => { const { message, @@ -60,6 +75,10 @@ export const MessageUserReactions = (props: MessageUserReactionsProps) => { propMessageUserReactionsAvatar ?? contextMessageUserReactionsAvatar; const MessageUserReactionsItem = propMessageUserReactionsItem ?? contextMessageUserReactionsItem; + const onSelectReaction = (reactionType: string) => { + setSelectedReaction(reactionType); + }; + const messageReactions = useMemo( () => reactionTypes.reduce((acc, reaction) => { @@ -89,6 +108,7 @@ export const MessageUserReactions = (props: MessageUserReactionsProps) => { messageMenu: { userReactions: { container, + contentContainer, flatlistColumnContainer, flatlistContainer, reactionSelectorContainer, @@ -123,9 +143,11 @@ export const MessageUserReactions = (props: MessageUserReactionsProps) => { {t('Message Reactions')} ); - const onSelectReaction = (reactionType: string) => { - setSelectedReaction(reactionType); - }; + const selectorReactions: ReactionSelectorItemType[] = messageReactions.map((reaction) => ({ + ...reaction, + onSelectReaction, + selectedReaction, + })); return ( { style={[styles.container, container]} > - {messageReactions?.map(({ Icon, type }, index) => ( - - ))} + item.type} + renderItem={renderSelectorItem} + /> {!loading ? ( @@ -165,6 +185,11 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + contentContainer: { + flexGrow: 1, + justifyContent: 'space-around', + marginVertical: 16, + }, flatListColumnContainer: { justifyContent: 'space-evenly', }, diff --git a/package/src/components/MessageMenu/ReactionButton.tsx b/package/src/components/MessageMenu/ReactionButton.tsx index a017628849..7997814498 100644 --- a/package/src/components/MessageMenu/ReactionButton.tsx +++ b/package/src/components/MessageMenu/ReactionButton.tsx @@ -49,7 +49,7 @@ export const ReactionButton = (props: ReactionButtonProps) => { onPress={onPressHandler} style={({ pressed }) => [ styles.reactionButton, - { backgroundColor: pressed ? light_blue : white }, + { backgroundColor: pressed || selected ? light_blue : white }, buttonContainer, ]} > diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap index b06825bebd..63f4b75502 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap @@ -270,6 +270,7 @@ exports[`Thread should match thread snapshot 1`] = ` style={ [ { + "marginTop": 2, "paddingHorizontal": 8, }, {}, @@ -284,15 +285,11 @@ exports[`Thread should match thread snapshot 1`] = ` "alignItems": "flex-end", "flexDirection": "row", }, - [ - { - "marginBottom": 8, - }, - {}, - ], + { + "marginBottom": 8, + }, { "justifyContent": "flex-start", - "marginTop": 0, }, {}, ] @@ -359,109 +356,145 @@ exports[`Thread should match thread snapshot 1`] = ` + - - - Message6 - - + + + Message6 + + + + @@ -585,6 +618,7 @@ exports[`Thread should match thread snapshot 1`] = ` style={ [ { + "marginTop": 2, "paddingHorizontal": 8, }, {}, @@ -599,15 +633,11 @@ exports[`Thread should match thread snapshot 1`] = ` "alignItems": "flex-end", "flexDirection": "row", }, - [ - { - "marginBottom": 8, - }, - {}, - ], + { + "marginBottom": 8, + }, { "justifyContent": "flex-start", - "marginTop": 0, }, {}, ] @@ -674,109 +704,145 @@ exports[`Thread should match thread snapshot 1`] = ` + - - - Message5 - - + + + Message5 + + + + @@ -900,6 +966,7 @@ exports[`Thread should match thread snapshot 1`] = ` style={ [ { + "marginTop": 2, "paddingHorizontal": 8, }, {}, @@ -914,15 +981,11 @@ exports[`Thread should match thread snapshot 1`] = ` "alignItems": "flex-end", "flexDirection": "row", }, - [ - { - "marginBottom": 8, - }, - {}, - ], + { + "marginBottom": 8, + }, { "justifyContent": "flex-start", - "marginTop": 0, }, {}, ] @@ -989,109 +1052,145 @@ exports[`Thread should match thread snapshot 1`] = ` + - - - Message4 - - + + + Message4 + + + + @@ -1246,6 +1345,7 @@ exports[`Thread should match thread snapshot 1`] = ` style={ [ { + "marginTop": 2, "paddingHorizontal": 8, }, {}, @@ -1268,7 +1368,6 @@ exports[`Thread should match thread snapshot 1`] = ` ], { "justifyContent": "flex-start", - "marginTop": 0, }, {}, ] @@ -1335,109 +1434,145 @@ exports[`Thread should match thread snapshot 1`] = ` + - - - Message3 - - + + + Message3 + + + + diff --git a/package/src/components/index.ts b/package/src/components/index.ts index af72c934fd..40f5db9d46 100644 --- a/package/src/components/index.ts +++ b/package/src/components/index.ts @@ -110,7 +110,8 @@ export * from './Message/MessageSimple/MessageSimple'; export * from './Message/MessageSimple/MessageStatus'; export * from './Message/MessageSimple/MessageTextContainer'; export * from './Message/MessageSimple/MessageTimestamp'; -export * from './Message/MessageSimple/ReactionList'; +export * from './Message/MessageSimple/ReactionList/ReactionListBottom'; +export * from './Message/MessageSimple/ReactionList/ReactionListTop'; export * from './Message/MessageSimple/utils/renderText'; export * from './Message/utils/messageActions'; export * from '../utils/removeReservedFields'; diff --git a/package/src/contexts/messageContext/MessageContext.tsx b/package/src/contexts/messageContext/MessageContext.tsx index 1c852a4ab9..0dacdfa324 100644 --- a/package/src/contexts/messageContext/MessageContext.tsx +++ b/package/src/contexts/messageContext/MessageContext.tsx @@ -5,8 +5,8 @@ import type { Attachment } from 'stream-chat'; import type { ActionHandler } from '../../components/Attachment/Attachment'; import { ReactionSummary } from '../../components/Message/hooks/useProcessReactions'; import type { - MessageTouchableHandlerPayload, - TouchableHandlerPayload, + MessagePressableHandlerPayload, + PressableHandlerPayload, } from '../../components/Message/Message'; import type { GroupType, MessageType } from '../../components/MessageList/hooks/useMessageList'; import type { ChannelContextValue } from '../../contexts/channelContext/ChannelContext'; @@ -65,7 +65,7 @@ export type MessageContextValue< * * @param payload Payload object for onLongPress event */ - onLongPress: (payload: TouchableHandlerPayload) => void; + onLongPress: (payload: PressableHandlerPayload) => void; /** Whether the message is only text and the text is only emojis */ onlyEmojis: boolean; /** Handler to open a thread on a message */ @@ -80,8 +80,8 @@ export type MessageContextValue< * * @param payload Payload object for onPress event */ - onPress: (payload: MessageTouchableHandlerPayload) => void; - onPressIn: ((payload: TouchableHandlerPayload) => void) | null; + onPress: (payload: MessagePressableHandlerPayload) => void; + onPressIn: ((payload: PressableHandlerPayload) => void) | null; /** The images attached to a message */ otherAttachments: Attachment[]; reactions: ReactionSummary[]; @@ -99,6 +99,12 @@ export type MessageContextValue< /** The videos attached to a message */ videos: Attachment[]; goToMessage?: (messageId: string) => void; + /** + * Function to handle reaction on message + * @param reactionType + * @returns + */ + handleReaction?: (reactionType: string) => Promise; /** Latest message id on current channel */ lastReceivedId?: string; /** diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx index 8a678b3e89..6459bdec3c 100644 --- a/package/src/contexts/messagesContext/MessagesContext.tsx +++ b/package/src/contexts/messagesContext/MessagesContext.tsx @@ -1,6 +1,6 @@ import React, { PropsWithChildren, useContext } from 'react'; -import { FlatList, TouchableOpacityProps } from 'react-native'; +import { FlatList, PressableProps } from 'react-native'; import type { Attachment, ChannelState, MessageResponse } from 'stream-chat'; @@ -17,8 +17,8 @@ import type { ImageLoadingFailedIndicatorProps } from '../../components/Attachme import type { ImageLoadingIndicatorProps } from '../../components/Attachment/ImageLoadingIndicator'; import type { VideoThumbnailProps } from '../../components/Attachment/VideoThumbnail'; import type { + MessagePressableHandlerPayload, MessageProps, - MessageTouchableHandlerPayload, } from '../../components/Message/Message'; import type { MessageAvatarProps } from '../../components/Message/MessageSimple/MessageAvatar'; import type { MessageBounceProps } from '../../components/Message/MessageSimple/MessageBounce'; @@ -34,7 +34,8 @@ import type { MessageSimpleProps } from '../../components/Message/MessageSimple/ import type { MessageStatusProps } from '../../components/Message/MessageSimple/MessageStatus'; import type { MessageTextProps } from '../../components/Message/MessageSimple/MessageTextContainer'; import { MessageTimestampProps } from '../../components/Message/MessageSimple/MessageTimestamp'; -import type { ReactionListProps } from '../../components/Message/MessageSimple/ReactionList'; +import { ReactionListBottomProps } from '../../components/Message/MessageSimple/ReactionList/ReactionListBottom'; +import type { ReactionListTopProps } from '../../components/Message/MessageSimple/ReactionList/ReactionListTop'; import type { MarkdownRules } from '../../components/Message/MessageSimple/utils/renderText'; import type { MessageActionsParams } from '../../components/Message/utils/messageActions'; import type { DateHeaderProps } from '../../components/MessageList/DateHeader'; @@ -266,11 +267,7 @@ export type MessagesContextValue< * **Default** [MessageUserReactionsItem](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageMenu/MessageUserReactionsItem.tsx) */ MessageUserReactionsItem: React.ComponentType; - /** - * UI component for ReactionList - * Defaults to: [ReactionList](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Reaction/ReactionList.tsx) - */ - ReactionList: React.ComponentType>; + removeMessage: (message: { id: string; parent_id?: string }) => void; /** * UI component for Reply @@ -314,12 +311,12 @@ export type MessagesContextValue< UrlPreview: React.ComponentType>; VideoThumbnail: React.ComponentType; /** - * Provide any additional props for `TouchableOpacity` which wraps inner MessageContent component here. - * Please check docs for TouchableOpacity for supported props - https://reactnative.dev/docs/touchableopacity#props + * Provide any additional props for `Pressable` which wraps inner MessageContent component here. + * Please check docs for Pressable for supported props - https://reactnative.dev/docs/pressable#props * * @overrideType Object */ - additionalTouchableProps?: Omit; + additionalPressableProps?: Omit; /** * Custom UI component to override default cover (between Header and Footer) of Card component. * Accepts the same props as Card component. @@ -482,7 +479,7 @@ export type MessagesContextValue< * /> * ``` */ - onLongPressMessage?: (payload: MessageTouchableHandlerPayload) => void; + onLongPressMessage?: (payload: MessagePressableHandlerPayload) => void; /** * Add onPressIn handler for attachments. You have access to payload of that handler as param: * @@ -509,7 +506,7 @@ export type MessagesContextValue< * /> * ``` */ - onPressInMessage?: (payload: MessageTouchableHandlerPayload) => void; + onPressInMessage?: (payload: MessagePressableHandlerPayload) => void; /** * Override onPress handler for message. You have access to payload of that handler as param: * @@ -536,7 +533,23 @@ export type MessagesContextValue< * /> * ``` */ - onPressMessage?: (payload: MessageTouchableHandlerPayload) => void; + onPressMessage?: (payload: MessagePressableHandlerPayload) => void; + + /** + * UI component for ReactionListTop + * Defaults to: [ReactionList](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Reaction/ReactionList.tsx) + */ + ReactionListBottom?: React.ComponentType>; + /** + * The position of the reaction list in the message + */ + reactionListPosition?: 'top' | 'bottom'; + + /** + * UI component for ReactionListTop + * Defaults to: [ReactionList](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Reaction/ReactionList.tsx) + */ + ReactionListTop?: React.ComponentType>; /** * Full override of the reaction function on Message and Message Overlay diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts index 1cef3691ee..630a973bc7 100644 --- a/package/src/contexts/themeContext/utils/theme.ts +++ b/package/src/contexts/themeContext/utils/theme.ts @@ -4,6 +4,7 @@ import type { CircleProps, StopProps } from 'react-native-svg'; import type { IconProps } from '../../../icons/utils/base'; export const DEFAULT_STATUS_ICON_SIZE = 16; +export const BASE_AVATAR_SIZE = 32; export const Colors = { accent_blue: '#005FFF', @@ -425,6 +426,7 @@ export type Theme = { reactionPicker: { buttonContainer: ViewStyle; container: ViewStyle; + contentContainer: ViewStyle; reactionIconSize: number; }; userReactions: { @@ -432,6 +434,7 @@ export type Theme = { avatarName: TextStyle; avatarSize: number; container: ViewStyle; + contentContainer: ViewStyle; flatlistColumnContainer: ViewStyle; flatlistContainer: ViewStyle; radius: number; @@ -518,6 +521,7 @@ export type Theme = { senderMessageBackgroundColor?: ColorValue; timestampText?: TextStyle; }; + contentWrapper: ViewStyle; file: { container: ViewStyle; details: ViewStyle; @@ -566,6 +570,7 @@ export type Theme = { shuffle: TextStyle; title: TextStyle; }; + headerWrapper: ViewStyle; lastMessageContainer: ViewStyle; loadingIndicator: { container: ViewStyle; @@ -577,16 +582,28 @@ export type Theme = { container: ViewStyle; label: TextStyle; }; - reactionList: { + reactionListBottom: { container: ViewStyle; - iconFillColor: ColorValue; - middleIcon: ViewStyle; - radius: number; - reactionBubble: ViewStyle; - reactionContainer: ViewStyle; - reactionCount: TextStyle; - reactionSize: number; - strokeSize: number; + item: { + container: ViewStyle; + countText: TextStyle; + icon: ViewStyle; + iconFillColor: ColorValue; + iconSize: number; + iconUnFillColor: ColorValue; + }; + }; + reactionListTop: { + container: ViewStyle; + item: { + container: ViewStyle; + icon: ViewStyle; + iconFillColor: ColorValue; + iconSize: number; + iconUnFillColor: ColorValue; + reactionSize: number; + }; + position: number; }; replies: { avatar: ViewStyle; @@ -700,7 +717,7 @@ export const defaultTheme: Theme = { speedChangeButtonText: {}, }, avatar: { - BASE_AVATAR_SIZE: 32, + BASE_AVATAR_SIZE, container: {}, image: { borderRadius: 16, @@ -1019,6 +1036,7 @@ export const defaultTheme: Theme = { reactionPicker: { buttonContainer: {}, container: {}, + contentContainer: {}, reactionIconSize: 24, }, userReactions: { @@ -1026,6 +1044,7 @@ export const defaultTheme: Theme = { avatarName: {}, avatarSize: 64, container: {}, + contentContainer: {}, flatlistColumnContainer: {}, flatlistContainer: {}, radius: 2, @@ -1052,8 +1071,8 @@ export const defaultTheme: Theme = { marginLeft: 8, }, spacer: { - height: 28, - width: 32, // same as BASE_AVATAR_SIZE + height: BASE_AVATAR_SIZE, + width: BASE_AVATAR_SIZE, // same as BASE_AVATAR_SIZE }, }, card: { @@ -1132,14 +1151,17 @@ export const defaultTheme: Theme = { metaText: { fontSize: 12, }, + receiverMessageBackgroundColor: Colors.grey_gainsboro, replyBorder: {}, replyContainer: {}, + senderMessageBackgroundColor: Colors.light_blue, textContainer: { onlyEmojiMarkdown: { text: { fontSize: 50 } }, }, timestampText: {}, wrapper: {}, }, + contentWrapper: {}, file: { container: {}, details: {}, @@ -1183,6 +1205,7 @@ export const defaultTheme: Theme = { shuffle: {}, title: {}, }, + headerWrapper: {}, lastMessageContainer: {}, loadingIndicator: { container: {}, @@ -1194,16 +1217,28 @@ export const defaultTheme: Theme = { container: {}, label: {}, }, - reactionList: { + reactionListBottom: { container: {}, - iconFillColor: '', - middleIcon: {}, - radius: 2, // not recommended to change this - reactionBubble: {}, - reactionContainer: {}, - reactionCount: {}, - reactionSize: 24, - strokeSize: 1, // not recommended to change this + item: { + container: {}, + countText: {}, + icon: {}, + iconFillColor: Colors.accent_blue, + iconSize: 16, + iconUnFillColor: Colors.grey, + }, + }, + reactionListTop: { + container: {}, + item: { + container: {}, + icon: {}, + iconFillColor: Colors.accent_blue, + iconSize: 24, + iconUnFillColor: Colors.grey, + reactionSize: 24, + }, + position: 16, }, replies: { avatar: {}, diff --git a/package/src/icons/PinHeader.tsx b/package/src/icons/PinHeader.tsx index a7a29e4da3..e5743e1c5f 100644 --- a/package/src/icons/PinHeader.tsx +++ b/package/src/icons/PinHeader.tsx @@ -1,12 +1,18 @@ import React from 'react'; -import { IconProps, RootPath, RootSvg } from './utils/base'; +import Svg, { Path } from 'react-native-svg'; -export const PinHeader = (props: IconProps) => ( - - ( + + - + ); diff --git a/package/src/utils/getResizedImageUrl.ts b/package/src/utils/getResizedImageUrl.ts index 9c2734dc21..775e1724ba 100644 --- a/package/src/utils/getResizedImageUrl.ts +++ b/package/src/utils/getResizedImageUrl.ts @@ -1,8 +1,8 @@ import { PixelRatio } from 'react-native'; import { - ChatConfigContextValue, chatConfigContextDefaultvalue, + ChatConfigContextValue, } from '../contexts/chatConfigContext/ChatConfigContext'; export type GetResizedImageUrlParams = Pick & { @@ -26,8 +26,8 @@ export type GetResizedImageUrlParams = Pick