diff --git a/docusaurus/docs/reactnative/assets/basics/migrating-from-5.x-to-6.x/message_actions.png b/docusaurus/docs/reactnative/assets/basics/migrating-from-5.x-to-6.x/message_actions.png new file mode 100644 index 0000000000..483009ba68 Binary files /dev/null and b/docusaurus/docs/reactnative/assets/basics/migrating-from-5.x-to-6.x/message_actions.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-message-actions/message_actions.png b/docusaurus/docs/reactnative/assets/guides/custom-message-actions/message_actions.png index 5629323e2c..483009ba68 100644 Binary files a/docusaurus/docs/reactnative/assets/guides/custom-message-actions/message_actions.png and b/docusaurus/docs/reactnative/assets/guides/custom-message-actions/message_actions.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-message-actions/my_message_actions.png b/docusaurus/docs/reactnative/assets/guides/custom-message-actions/my_message_actions.png deleted file mode 100644 index b45cb0d218..0000000000 Binary files a/docusaurus/docs/reactnative/assets/guides/custom-message-actions/my_message_actions.png and /dev/null 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 98c247b0e6..fab141ed48 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 @@ -7,11 +7,11 @@ title: Migration from 5.x to 6.x v6.0 is supported on React native version **0.72** and above. ::: -## Changes +## Dependency changes The following are the crucial changes of the Stream Chat React Native SDK from version 5.x to 6.x: -### Change `react-native-fs` to `react-native-blob-util` +#### Change `react-native-fs` to `react-native-blob-util` The `react-native-fs` package has been replaced to `react-native-blob-util` in favour of the former not being actively maintained. You can replace it by running the following commands: @@ -20,7 +20,7 @@ yarn remove react-native-fs yarn add react-native-blob-util ``` -### Change `react-native-image-resizer` to `@bam.tech/react-native-image-resizer` +#### Change `react-native-image-resizer` to `@bam.tech/react-native-image-resizer` The `react-native-image-resizer` package has been replaced with `@bam.tech/react-native-image-resizer`. @@ -33,7 +33,7 @@ yarn remove react-native-image-resizer yarn add @bam.tech/react-native-image-resizer ``` -### Change `react-native-image-crop-picker` to `react-native-image-picker` +#### Change `react-native-image-crop-picker` to `react-native-image-picker` The `react-native-image-crop-picker` package has been replaced with `react-native-image-picker`. This is a better alternative, and can help us with our new architecture endeavors. @@ -46,7 +46,7 @@ yarn add react-native-image-picker Also, the dependency is made optional now, so if you don't want to use the image picker, you can remove it from your project and the camera selector icon on the Attachment picker will be simply hidden for you. -### Change `react-native-quick-sqlite` to `op-sqlite` +#### Change `react-native-quick-sqlite` to `op-sqlite` The `react-native-quick-sqlite` package has been replaced with `op-sqlite`. This is a faster alternative, and can help us with our new architecture endeavors. @@ -57,7 +57,7 @@ yarn remove react-native-quick-sqlite yarn add op-sqlite ``` -### Remove the usage of `@stream-io/flat-list-mvcp` +#### Remove the usage of `@stream-io/flat-list-mvcp` The dependency on `@stream-io/flat-list-mvcp` package has been removed in favour of React Native's [`FlatList`](https://reactnative.dev/docs/flatlist) component supporting [`maintainVisibleContentPosition`](https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition) from React Native version `>=0.72`. You can replace it by using the `FlatList` component directly. @@ -78,7 +78,68 @@ registerNativeHandlers({ This also involves not passing the `FlatList` component as a handler to [`registerNativeHandlers`](../customization/native-handlers.mdx#overriding-handlers) anymore. -### Remove `NetInfo` from the native handlers +## SDK changes + +#### Introduce new Message Menu design + +The Message Menu design has been revamped to provide a better user experience. The new design is more intuitive and provides a better user experience. + +![Message Menu](../assets/basics/migrating-from-5.x-to-6.x/message_actions.png) + +:::note +The previous overlay design has been replaced with a bottom sheet modal design. +::: + +#### Removed `MessageOverlayContext` and `MessageOverlayProvider` + +The `MessageOverlayContext` and `MessageOverlayProvider` have been removed. + +#### Removed props from `OverlayProvider` + +The following props have been removed from the `OverlayProvider`: + +- `MessageActionList` +- `MessageActionListItem` +- `OverlayReactions` +- `OverlayReactionsAvatar` +- `OverlayReactionsItem` +- `messageTextNumberOfLines` +- `error`, `isMyMessage`, `isThreadMessage`, `message` and `messageReactions` + +#### New `Channel` props + +The props from the `OverlayProvider` have been moved to the `Channel` component. The following props have been added to the `Channel` component: + +- `MessageActionList` +- `MessageActionListItem` +- `OverlayReactions` is changed to `MessageUserReactions` +- `OverlayReactionsAvatar` is changed to `MessageUserReactionsAvatar` +- `OverlayReactionsItem` is changed to `MessageUserReactionsItem` +- `messageTextNumberOfLines` + +#### Removed `MessageOverlay` in favour of `MessageMenu`. + +The `MessageOverlay` component has been removed in favour of `MessageMenu`. The `MessageMenu` component is a more versatile and feature-rich component that can be used to show more than just reactions and actions. + +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. + +The following props have been removed from the `Message` component: + +- `setData` +- `setOverlay` +- `onLongPress` +- `onPress` +- `onPressIn` + +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`. + +#### 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. + +#### Remove `NetInfo` from the native handlers The `NetInfo` package has been removed from the native handlers. This also involves not passing the `NetInfo` utility as a handler to [`registerNativeHandlers`](../customization/native-handlers.mdx#overriding-handlers) anymore. @@ -91,6 +152,14 @@ registerNativeHandlers({ }); ``` -### Change the type of `quotedMessage` in `MessageInputContext` +#### Change the type of `quotedMessage` in `MessageInputContext` The type of `quotedMessage` is changed from `MessageType | boolean` to `MessageType | undefined` for better in the `MessageInputContext`. + +## Other changes + +- The `useMessageActions` hook doesn't take `setOverlay` anymore but takes in `dismissOverlay`. +- The MessageContext has a new prop - `dismissOverlay`. The definition of `showMessageOverlay` is changed to `(showMessageReactions?: boolean) => void`. +- The `isMessageActionsVisible` is changed to `showMessageReactions` in `messageAction.ts`. +- Removed the `useMessageActionAnimation` hook. +- Removed `alignment` prop from `MessagePinnedHeader` component. diff --git a/docusaurus/docs/reactnative/basics/navigation.mdx b/docusaurus/docs/reactnative/basics/navigation.mdx index 2afba8ce58..a0f1ed0c2a 100644 --- a/docusaurus/docs/reactnative/basics/navigation.mdx +++ b/docusaurus/docs/reactnative/basics/navigation.mdx @@ -8,7 +8,7 @@ import TabItem from '@theme/TabItem'; Stream Chat for React Native provides many features out of the box that require positioning the components on the screen in a certain manner to get the desired UI. -The `AttachmentPicker`, `ImageGallery`, and `MessageOverlay`, all need to be rendered in front of other components to have the desired effect. All of these elements are controlled by the `OverlayProvider`. When used together with navigation, certain steps are needed to be taken to make these components appear fluently. +The `AttachmentPicker` and  `ImageGallery`, all need to be rendered in front of other components to have the desired effect. All of these elements are controlled by the `OverlayProvider`. When used together with navigation, certain steps are needed to be taken to make these components appear fluently. The guidance provided makes the assumption you are using [React Navigation](https://reactnavigation.org/) in your app in conjunction with [`createStackNavigator`](https://reactnavigation.org/docs/stack-navigator/). diff --git a/docusaurus/docs/reactnative/basics/troubleshooting.mdx b/docusaurus/docs/reactnative/basics/troubleshooting.mdx index 8250a3c6cb..48bcf13424 100644 --- a/docusaurus/docs/reactnative/basics/troubleshooting.mdx +++ b/docusaurus/docs/reactnative/basics/troubleshooting.mdx @@ -79,7 +79,7 @@ To do this make sure your `Channel` components are always aware of the thread st ## Image gallery not full screen -If the image viewer or message overlay is not covering the full screen, for instance it is rendering below or behind a Header, it is likely the `OverlayProvider` is not setup in the correct location within the application. +If the image viewer or message menu is not covering the full screen, for instance it is rendering below or behind a Header, it is likely the `OverlayProvider` is not setup in the correct location within the application. Please refer to the [Stream Chat with Navigation](./navigation.mdx) documentation to properly place the `OverlayProvider` in relation to your navigation solution or other components. ## Image picker incorrect height @@ -295,7 +295,7 @@ This includes ensuring you import `react-native-gesture-handler` at the top of y import 'react-native-gesture-handler'; ``` -Also do not forget to wrap your component tree(probably above `OverlayProvider`) with `` or `gestureHandlerRootHOC` as mentioned in [RNGH Installation docs](https://docs.swmansion.com/react-native-gesture-handler/docs/installation#js). Not doing so, can cause unusual behaviour with the `MessageOverlay` and `Imagegallery` gestures. +Also do not forget to wrap your component tree(probably above `OverlayProvider`) with `` or `gestureHandlerRootHOC` as mentioned in [RNGH Installation docs](https://docs.swmansion.com/react-native-gesture-handler/docs/installation#js). Not doing so, can cause unusual behaviour with the `Imagegallery` gestures. ```tsx diff --git a/docusaurus/docs/reactnative/common-content/contexts/message-context/show_message_overlay.mdx b/docusaurus/docs/reactnative/common-content/contexts/message-context/show_message_overlay.mdx index cd85f03110..98bde0d530 100644 --- a/docusaurus/docs/reactnative/common-content/contexts/message-context/show_message_overlay.mdx +++ b/docusaurus/docs/reactnative/common-content/contexts/message-context/show_message_overlay.mdx @@ -1,5 +1,5 @@ Function to open the message action overlay. This function gets called when user long presses a message. -| Type | -| -------- | -| function | +| Type | +| ------------------------------------------ | +| `(showMessageReactions?: boolean) => void` | diff --git a/docusaurus/docs/reactnative/common-content/contexts/message-overlay-context/data.mdx b/docusaurus/docs/reactnative/common-content/contexts/message-overlay-context/data.mdx deleted file mode 100644 index 9087fba502..0000000000 --- a/docusaurus/docs/reactnative/common-content/contexts/message-overlay-context/data.mdx +++ /dev/null @@ -1,83 +0,0 @@ -import MessageActions from '../../ui-components/channel/props/message_actions.mdx'; -import SupportedReactions from '../../ui-components/channel/props/supported_reactions.mdx'; -import OverlayReactionList from '../../ui-components/overlay-provider/props/overlay_reaction_list.mdx'; -import HandleReaction from '../../ui-components/channel/props/handle_reaction.mdx'; - -import Alignment from '../message-context/alignment.mdx'; -import Files from '../message-context/files.mdx'; -import GroupStyles from '../message-context/group_styles.mdx'; -import Images from '../message-context/images.mdx'; -import MessageProp from '../message-context/message.mdx'; -import OnlyEmojis from '../message-context/only_emojis.mdx'; -import OtherAttachments from '../message-context/other_attachments.mdx'; -import ThreadList from '../message-context/thread_list.mdx'; - -This is an object with following keys: - -- `alignment` - - - -- `clientId` - -Id of the current user connected to the chat. - -| Type | -| ------ | -| String | - -- `files` - - - -- `groupStyles` - - - -- `handleReaction` - - - -- `images` - - - -- `message` - - - -- `messageActions` - - - -- `messageReactionTitle` - -Title for `MessageReactions` component. - -| Type | Default | -| ------ | ------------------- | -| String | "Message Reactions" | - -- `messagesContext` - -Entire value object of [MessagesContext](../../../contexts/messages-context.mdx#value) - -- `onlyEmojis` - - - -- `otherAttachments` - - - -- `OverlayReactionList` - - - -- `supportedReactions` - - - -- `threadList` - - diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/handle_reaction.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/handle_reaction.mdx index 02f30f3c5e..6e1545a25d 100644 --- a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/handle_reaction.mdx +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/handle_reaction.mdx @@ -1,4 +1,4 @@ -Function called when a reaction is selected in the message overlay, this is called on both the add and remove action. +Function called when a reaction is selected in the message menu, this is called on both the add and remove action. This function does not override the default behavior of the reaction being selected. Please refer to [the guide on customizing message actions](../../../../guides/custom-message-actions.mdx) for details. diff --git a/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/message_action_list_item.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-action-list-item.mdx similarity index 52% rename from docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/message_action_list_item.mdx rename to docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-action-list-item.mdx index 22b1fe5171..cd253accc7 100644 --- a/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/message_action_list_item.mdx +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-action-list-item.mdx @@ -1,5 +1,5 @@ Component for rendering message action list items within a message action list. -| Type | Default | -| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ComponentType | [MessageActionListItem](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageOverlay/MessageActionListItem.tsx) | +| Type | Default | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`MessageActionListItem`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageMenu/MessageActionListItem.tsx) \| `undefined` | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/message_action_list.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-action-list.mdx similarity index 52% rename from docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/message_action_list.mdx rename to docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-action-list.mdx index 65610973a3..b8c7102854 100644 --- a/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/message_action_list.mdx +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-action-list.mdx @@ -1,5 +1,5 @@ -Component for rendering a message action list within the message overlay. +Component for rendering a message action list within the message menu. -| Type | Default | -| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| ComponentType | [[MessageActionList](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageOverlay/MessageActionList.tsx) | +| Type | Default | +| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`MessageActionList`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageMenu/MessageActionList.tsx) \| `undefined` | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-menu.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-menu.mdx new file mode 100644 index 0000000000..aa10a0e01b --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-menu.mdx @@ -0,0 +1,5 @@ +Component to customize the message contextual menu which allows users to perform actions on a message and react to messages. + +| Type | Default | +| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`MessageMenu`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageMenu/MessageMenu.tsx) \| `undefined` | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-reaction-picker.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-reaction-picker.mdx new file mode 100644 index 0000000000..7dad596910 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-reaction-picker.mdx @@ -0,0 +1,5 @@ +Reaction selector component displayed within the message menu when user long presses a message. + +| Type | Default | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`MessageReactionPicker`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/components/MessageMenu/MessageReactionPicker.tsx) \| `undefined` | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/message_text_number_of_lines.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-text-number-of-lines.mdx similarity index 100% rename from docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/message_text_number_of_lines.mdx rename to docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-text-number-of-lines.mdx diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-user-reactions-avatar.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-user-reactions-avatar.mdx new file mode 100644 index 0000000000..46d80419ef --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-user-reactions-avatar.mdx @@ -0,0 +1,5 @@ +Component for rendering an avatar in the message user reactions in the message menu. + +| Type | Default | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`MessageUserReactionsAvatar`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/components/MessageMenu/MessageUserReactionsAvatar.tsx) \| `undefined` | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-user-reactions-item.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-user-reactions-item.mdx new file mode 100644 index 0000000000..f6a06ebedb --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-user-reactions-item.mdx @@ -0,0 +1,5 @@ +Component prop to customize the individual user reaction item in the `MessageUserReactions` component of `MessageMenu`. This includes the avatar, reaction type, and the name of the person who has reacted, etc. + +| Type | Default | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`MessageUserReactionsItem`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/components/MessageMenu/MessageUserReactionsItem.tsx) \| `undefined` | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-user-reactions.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-user-reactions.mdx new file mode 100644 index 0000000000..fdb7f75788 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message-user-reactions.mdx @@ -0,0 +1,5 @@ +List of reactions component within the message menu. + +| Type | Default | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`MessageUserReactions`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/components/MessageMenu/MessageUserReactions.tsx) \| `undefined` | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message.mdx new file mode 100644 index 0000000000..f46b0dd7c5 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message.mdx @@ -0,0 +1,5 @@ +Component to render a specific message bubble, within [`MessageList`](../../../../ui-components/message-list.mdx). + +| Type | Default | +| ------------- | -------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`Message`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/components/Message/Message.tsx) | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message_actions.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message_actions.mdx index e9587fd93c..01d1de390d 100644 --- a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message_actions.mdx +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/message_actions.mdx @@ -1,4 +1,4 @@ -An array of, or function that returns and array of, actions that can be performed on a message shown in the message overlay. +An array of, or function that returns and array of, actions that can be performed on a message shown in the message menu. Please refer to [the guide on customizing message actions](../../../../guides//custom-message-actions.mdx) for details. | Type | Default | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/on_long_press_message.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/on_long_press_message.mdx index 8f20fe2362..e29b546cc3 100644 --- a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/on_long_press_message.mdx +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/on_long_press_message.mdx @@ -1,5 +1,4 @@ -Function called when a user long presses a message. -The default opens the message actions overlay. +Function called when a user long presses a message. The default opens the message menu. | Type | | -------- | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/overlay_reaction_list.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/overlay_reaction_list.mdx deleted file mode 100644 index 72efaac957..0000000000 --- a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/overlay_reaction_list.mdx +++ /dev/null @@ -1,5 +0,0 @@ -Component to render reaction list within message overlay, which shows up when user long presses a message. - -| Type | Default | -| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ComponentType | [`OverlayReactionList`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/components/MessageOverlay/OverlayReactionList.tsx) | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/override_own_capabilities.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/override_own_capabilities.mdx index 65269905c0..9e1593f700 100644 --- a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/override_own_capabilities.mdx +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/override_own_capabilities.mdx @@ -15,20 +15,20 @@ For example: Available keys for the object: -- `banChannelMembers` When false, "Block User" message action won't be available within overlay. -- `deleteAnyMessage` When false, "Delete Message" action won't be available for received message within overlay. -- `deleteOwnMessage` When false, "Delete Message" action won't be available for own message within overlay. -- `flagMessage` When false, "Flag Message" message action won't be available within overlay. -- `pinMessage` When false, "Pin Message" action won't be available within overlay. -- `quoteMessage` When false, "Reply" message action won't be available within overlay. +- `banChannelMembers` When false, "Block User" message action won't be available within message menu. +- `deleteAnyMessage` When false, "Delete Message" action won't be available for received message within message menu. +- `deleteOwnMessage` When false, "Delete Message" action won't be available for own message within message menu. +- `flagMessage` When false, "Flag Message" message action won't be available within message menu. +- `pinMessage` When false, "Pin Message" action won't be available within message menu. +- `quoteMessage` When false, "Reply" message action won't be available within message menu. - `readEvents` When false, read receipts for message won't be rendered. - `sendLinks` When false, user will not be allowed to send URLs in message. - `sendMessage` When false, message input component will render `SendMessageDisallowedIndicator` instead of input box. -- `sendReaction` When false, reaction selector (`OverlayReactionList`) component won't be available within overlay. -- `sendReply` When false, "Thread Reply" message action won't be available within overlay. +- `sendReaction` When false, reaction selector (`OverlayReactionList`) component won't be available within message menu. +- `sendReply` When false, "Thread Reply" message action won't be available within message menu. - `sendTypingEvents` When false, typing events won't be sent from current user. -- `updateAnyMessage` When false, "Edit Message" action won't be available for received messages within overlay. -- `updateOwnMessage` When false, "Edit Message" action won't be available for own messages within overlay. +- `updateAnyMessage` When false, "Edit Message" action won't be available for received messages within message menu. +- `updateOwnMessage` When false, "Edit Message" action won't be available for own messages within message menu. - `uploadFile` When false, upload file button (`AttachButton`) won't be available within `MessageInput` component. | Type | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/overlay_reaction_list.mdx b/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/overlay_reaction_list.mdx deleted file mode 100644 index 83bbb2cdfc..0000000000 --- a/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/overlay_reaction_list.mdx +++ /dev/null @@ -1,5 +0,0 @@ -Reaction selector component displayed within the message overlay when user long presses a message. - -| Type | Default | -| ------------- | -------------------------------------------------------------------------- | -| ComponentType | [OverlayReactionList](../../../../ui-components/overlay-reaction-list.mdx) | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/overlay_reactions.mdx b/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/overlay_reactions.mdx deleted file mode 100644 index 3c88e62df5..0000000000 --- a/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/overlay_reactions.mdx +++ /dev/null @@ -1,5 +0,0 @@ -List of reactions component within the message overlay. - -| Type | Default | -| ------------- | ------------------------------------------------------------------- | -| ComponentType | [OverlayReactions](../../../../ui-components/overlay-reactions.mdx) | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/overlay_reactions_avatar.mdx b/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/overlay_reactions_avatar.mdx deleted file mode 100644 index ed6109518b..0000000000 --- a/docusaurus/docs/reactnative/common-content/ui-components/overlay-provider/props/overlay_reactions_avatar.mdx +++ /dev/null @@ -1,5 +0,0 @@ -Component for rendering a avatar in the message reaction overlay. - -| Type | Default | -| ------------- | -------------------------------------------------------------------------------- | -| ComponentType | [OverlayReactionsAvatar](../../../../ui-components/overlay-reactions-avatar.mdx) | diff --git a/docusaurus/docs/reactnative/contexts/message-overlay-context.mdx b/docusaurus/docs/reactnative/contexts/message-overlay-context.mdx deleted file mode 100644 index ff9f4ad9c3..0000000000 --- a/docusaurus/docs/reactnative/contexts/message-overlay-context.mdx +++ /dev/null @@ -1,42 +0,0 @@ ---- -id: message-overlay-context -title: MessageOverlayContext ---- - -import OverlayReactionList from '../common-content/ui-components/overlay-provider/props/overlay_reaction_list.mdx'; -import OverlayReactions from '../common-content/ui-components/overlay-provider/props/overlay_reactions.mdx'; -import OverlayReactionsAvatar from '../common-content/ui-components/overlay-provider/props/overlay_reactions_avatar.mdx'; - -import MessageOverlayContextData from '../common-content/contexts/message-overlay-context/data.mdx'; - -`MessageOverlayContext` is a context provided by `OverlayProvider`, to provide values for message overlay. Message overlay shows up when user long presses a message. - -## Value - - - - - -### `setData` - -Setter function for [data](#data). - -| Type | -| -------- | -| function | - -### data - - - -###
_forwarded from [OverlayProvider](../core-components/overlay-provider.mdx#overlayreactionlist)_ props
OverlayReactionList {#overlayreactionlist} - - - -###
_forwarded from [OverlayProvider](../core-components/overlay-provider.mdx#overlayreactions)_ props
OverlayReactions {#overlayreactions} - - - -###
_forwarded from [OverlayProvider](../core-components/overlay-provider.mdx#overlayreactionsavatar)_ props
OverlayReactionsAvatar {#overlayreactionsavatar} - - diff --git a/docusaurus/docs/reactnative/contexts/messages-context.mdx b/docusaurus/docs/reactnative/contexts/messages-context.mdx index 455c1cc30b..1d660cda7c 100644 --- a/docusaurus/docs/reactnative/contexts/messages-context.mdx +++ b/docusaurus/docs/reactnative/contexts/messages-context.mdx @@ -42,6 +42,8 @@ import InlineUnreadIndicator from '../common-content/ui-components/channel/props import IsAttachmentEqual from '../common-content/ui-components/channel/props/is_attachment_equal.mdx'; import LegacyImageViewerSwipeBehaviour from '../common-content/ui-components/channel/props/legacy_image_viewer_swipe_behaviour.mdx'; import MarkdownRules from '../common-content/ui-components/channel/props/markdown_rules.mdx'; +import MessageActionList from '../common-content/ui-components/channel/props/message-action-list.mdx'; +import MessageActionListItem from '../common-content/ui-components/channel/props/message-action-list-item.mdx'; import MessageAvatar from '../common-content/ui-components/channel/props/message-avatar.mdx'; import MessageBounce from '../common-content/ui-components/channel/props/message-bounce.mdx'; import MessageContent from '../common-content/ui-components/channel/props/message-content.mdx'; @@ -52,17 +54,22 @@ import MessageEditedTimestamp from '../common-content/ui-components/channel/prop 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 MessageMenu from '../common-content/ui-components/channel/props/message-menu.mdx'; import MessageReplies from '../common-content/ui-components/channel/props/message-replies.mdx'; import MessageRepliesAvatars from '../common-content/ui-components/channel/props/message-replies-avatars.mdx'; import MessageSimple from '../common-content/ui-components/channel/props/message-simple.mdx'; import MessageStatus from '../common-content/ui-components/channel/props/message-status.mdx'; import MessageSystem from '../common-content/ui-components/channel/props/message-system.mdx'; import MessageText from '../common-content/ui-components/channel/props/message-text.mdx'; +import MessageTextNumberOfLines from '../common-content/ui-components/channel/props/message-text-number-of-lines.mdx'; import MyMessageTheme from '../common-content/ui-components/channel/props/my_message_theme.mdx'; import OnLongPressMessage from '../common-content/ui-components/channel/props/on_long_press_message.mdx'; import OnPressInMessage from '../common-content/ui-components/channel/props/on_press_in_message.mdx'; import OnPressMessage from '../common-content/ui-components/channel/props/on_press_message.mdx'; -import OverlayReactionList from '../common-content/ui-components/overlay-provider/props/overlay_reaction_list.mdx'; +import MessageReactionPicker from '../common-content/ui-components/channel/props/message-reaction-picker.mdx'; +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 Reply from '../common-content/ui-components/channel/props/reply.mdx'; import ScrollToBottomButton from '../common-content/ui-components/channel/props/scroll-to-bottom-button.mdx'; @@ -183,6 +190,10 @@ Id of current channel. +###
_forwarded from [Channel](../../core-components/channel#messagetextnumberoflines)_ props
messageTextNumberOfLines {#messagetextnumberoflines} + + + ###
_forwarded from [Channel](../../core-components/channel#onlongpressmessage)_ props
onLongPressMessage {#onlongpressmessage} @@ -327,6 +338,14 @@ Upserts a given message in local channel state. Please note that this function d --> +###
_forwarded from [Channel](../../core-components/channel#messageactionlist)_ props
MessageActionList {#messageactionlist} + + + +###
_forwarded from [Channel](../../core-components/channel#messageactionlistitem)_ props
MessageActionListItem {#messageactionlistitem} + + + ###
_forwarded from [Channel](../../core-components/channel#messageavatar)_ props
MessageAvatar {#messageavatar} @@ -359,6 +378,10 @@ Upserts a given message in local channel state. Please note that this function d +###
_forwarded from [Channel](../../core-components/channel#messagemenu)_ props
MessageMenu {#messagemenu} + + + @@ -387,9 +410,21 @@ Upserts a given message in local channel state. Please note that this function d -###
_forwarded from [Channel](../../core-components/channel#overlayreactionlist)_ props
OverlayReactionList {#overlayreactionlist} +###
_forwarded from [Channel](../../core-components/channel#messagereactionpicker)_ props
MessageReactionPicker {#messagereactionpicker} + + + +###
_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#reactionlist)_ props
ReactionList {#reactionlist} diff --git a/docusaurus/docs/reactnative/contexts/overlay-context.mdx b/docusaurus/docs/reactnative/contexts/overlay-context.mdx index 53f4deae1c..4cd4097f57 100644 --- a/docusaurus/docs/reactnative/contexts/overlay-context.mdx +++ b/docusaurus/docs/reactnative/contexts/overlay-context.mdx @@ -28,21 +28,35 @@ const { setOverlay } = useOverlayContext(); ## Value -### overlay +### `overlay` Current active overlay. Overlay gets rendered in following cases - 'alert' - For delete message confirmation alert box - 'gallery' - When image viewer/gallery is opened -- 'message' - When message overlay is opened by long pressing a message - 'none' - Default value -| Type | -| ------------------------------------------- | -| enum('alert', 'gallery', 'message', 'none') | +| Type | +| -------------------------------- | +| enum('alert', 'gallery', 'none') | ### `setOverlay` - +### `style` + +A `theme` object to customize the styles of SDK components. +Detailed information on theming can be found in the [customization documentation](../customization/theme.mdx). + +:::note + +Themes are inherited from parent providers. +A [theme provided to the `OverlayProvider`](./overlay-provider.mdx#value) will be the base theme `style` is merged into. +Themes are not hoisted though, therefore a theme provided to `Chat` will not change overlay components such as the attachment picker. + +::: + +| Type | +| ------ | +| Object | diff --git a/docusaurus/docs/reactnative/core-components/channel.mdx b/docusaurus/docs/reactnative/core-components/channel.mdx index 529e1a459f..5bb78e8465 100644 --- a/docusaurus/docs/reactnative/core-components/channel.mdx +++ b/docusaurus/docs/reactnative/core-components/channel.mdx @@ -101,16 +101,20 @@ import MaxMessageLength from '../common-content/ui-components/channel/props/max_ import MaxNumberOfFiles from '../common-content/ui-components/channel/props/max_number_of_files.mdx'; import MentionAllAppUsersEnabled from '../common-content/ui-components/channel/props/mention_all_app_users_enabled.mdx'; import MentionAllAppUsersQuery from '../common-content/ui-components/channel/props/mention_all_app_users_query.mdx'; +import Message from '../common-content/ui-components/channel/props/message.mdx'; +import MessageActionListItem from '../common-content/ui-components/channel/props/message-action-list-item.mdx'; +import MessageActionList from '../common-content/ui-components/channel/props/message-action-list.mdx'; +import MessageActions from '../common-content/ui-components/channel/props/message_actions.mdx'; import MessageAvatar from '../common-content/ui-components/channel/props/message-avatar.mdx'; import MessageBounce from '../common-content/ui-components/channel/props/message-bounce.mdx'; import MessageContent from '../common-content/ui-components/channel/props/message-content.mdx'; -import MessageActions from '../common-content/ui-components/channel/props/message_actions.mdx'; import MessageContentOrder from '../common-content/ui-components/channel/props/message_content_order.mdx'; import MessageDeleted from '../common-content/ui-components/channel/props/message-deleted.mdx'; import MessageEditedTimestamp from '../common-content/ui-components/channel/props/message-edited-timestamp.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 MessageMenu from '../common-content/ui-components/channel/props/message-menu.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 MessageRepliesAvatars from '../common-content/ui-components/channel/props/message-replies-avatars.mdx'; @@ -118,6 +122,7 @@ import MessageSimple from '../common-content/ui-components/channel/props/message import MessageStatus from '../common-content/ui-components/channel/props/message-status.mdx'; import MessageSystem from '../common-content/ui-components/channel/props/message-system.mdx'; import MessageText from '../common-content/ui-components/channel/props/message-text.mdx'; +import MessageTextNumberOfLines from '../common-content/ui-components/channel/props/message-text-number-of-lines.mdx'; import MoreOptionsButton from '../common-content/ui-components/channel/props/more-options-button.mdx'; import MyMessageTheme from '../common-content/ui-components/channel/props/my_message_theme.mdx'; import NewMessageStateUpdateThrottleInterval from '../common-content/ui-components/channel/props/new_message_state_update_throttle_interval.mdx'; @@ -126,7 +131,10 @@ import OnChangeText from '../common-content/ui-components/channel/props/on_chang import OnLongPressMessage from '../common-content/ui-components/channel/props/on_long_press_message.mdx'; import OnPressInMessage from '../common-content/ui-components/channel/props/on_press_in_message.mdx'; import OnPressMessage from '../common-content/ui-components/channel/props/on_press_message.mdx'; -import OverlayReactionList from '../common-content/ui-components/overlay-provider/props/overlay_reaction_list.mdx'; +import MessageReactionPicker from '../common-content/ui-components/channel/props/message-reaction-picker.mdx'; +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 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 Reply from '../common-content/ui-components/channel/props/reply.mdx'; @@ -639,6 +647,10 @@ Load the channel at a specified message instead of the most recent message. +### `messageTextNumberOfLines` + + + @@ -898,8 +910,20 @@ Component to render full screen error indicator, when channel fails to load. + + +### MessageActionList + + + +### MessageActionListItem + + + ### MessageAvatar @@ -932,6 +956,10 @@ Component to render full screen error indicator, when channel fails to load. +### MessageMenu + + + ### MessagePinnedHeader @@ -962,6 +990,22 @@ Component to render full screen error indicator, when channel fails to load. +### MessageReactionPicker + + + +### MessageUserReactionsAvatar + + + +### MessageUserReactionsItem + + + +### MessageUserReactions + + + ### MoreOptionsButton @@ -970,10 +1014,6 @@ Component to render full screen error indicator, when channel fails to load. -### OverlayReactionList - - - ### ReactionList diff --git a/docusaurus/docs/reactnative/core-components/overlay-provider.mdx b/docusaurus/docs/reactnative/core-components/overlay-provider.mdx index 2add3b7ff6..daee8b4981 100644 --- a/docusaurus/docs/reactnative/core-components/overlay-provider.mdx +++ b/docusaurus/docs/reactnative/core-components/overlay-provider.mdx @@ -28,13 +28,12 @@ export const App = () => ( ## Context Providers -`OverlayProvider` contains providers for the `AttachmentPickerContext`, `ImageGalleryContext`, `MessageOverlayContext`, `OverlayContext`, `ThemeContext`, and `TranslationContext`. These can be accessed using the corresponding hooks. +`OverlayProvider` contains providers for the `AttachmentPickerContext`, `ImageGalleryContext`, `OverlayContext`, `ThemeContext`, and `TranslationContext`. These can be accessed using the corresponding hooks. | Context | Hook | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------- | | [`AttachmentPickerContext`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx) | useAttachmentPickerContext | | [`ImageGalleryContext`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/contexts/imageGalleryContext/ImageGalleryContext.tsx) | useImageGalleryContext | -| [`MessageOverlayContext`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx) | useMessageOverlayContext | | [`OverlayContext`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/contexts/overlayContext/OverlayContext.tsx) | useOverlayContext | | [`ThemeContext`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/contexts/themeContext/ThemeContext.tsx) | useTheme | | [`TranslationContext`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/contexts/translationContext/TranslationContext.tsx) | useTranslationContext | @@ -150,14 +149,6 @@ The [`SnapPoints`](https://gorhom.github.io/react-native-bottom-sheet/props#snap | ----- | ------------------------------ | | Array | `[0, (screenHeight * 9) / 10]` | -### `messageTextNumberOfLines` - -Number of lines for the message text in the Message Overlay. - -| Type | Default | -| ------ | ------- | -| Number | 5 | - ### `numberOfAttachmentImagesToLoadPerCall` Number of images to load per call to [`CameraRoll.getPhotos`](https://github.com/react-native-cameraroll/react-native-cameraroll#getphotos). @@ -289,43 +280,3 @@ Image selector component displayed in the attachment selector bar. | Type | Default | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ComponentType | `undefined` \| [`ImageSelectorIcon`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/ImageSelectorIcon.tsx) | - -### `MessageActionList` - -Component for rendering a message action list within the message overlay. - -| Type | Default | -| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ComponentType | `undefined` \| [`MessageActionList`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageOverlay/MessageActionList.tsx) | - -### `MessageActionListItem` - -Component for rendering message action list items within a message action list. - -| Type | Default | -| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ComponentType | `undefined` \| [`MessageActionListItem`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageOverlay/MessageActionListItem.tsx) | - -### `OverlayReactionList` - -Reaction selector component displayed within the message overlay when user long presses a message. - -| Type | Default | -| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| ComponentType | `undefined` \| [`OverlayReactionList`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/components/MessageOverlay/OverlayReactionList.tsx) | - -### `OverlayReactions` - -List of reactions component within the message overlay. - -| Type | Default | -| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| ComponentType | `undefined` \| [`OverlayReactions`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/components/MessageOverlay/OverlayReactions.tsx) | - -### `OverlayReactionsAvatar` - -Component for rendering an avatar in the message reaction overlay. - -| Type | Default | -| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| ComponentType | `undefined` \| [`OverlayReactionsAvatar`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/components/MessageOverlay/OverlayReactionsAvatar.tsx) | diff --git a/docusaurus/docs/reactnative/customization/contexts.mdx b/docusaurus/docs/reactnative/customization/contexts.mdx index e345a8fe3b..8f3c1250f1 100644 --- a/docusaurus/docs/reactnative/customization/contexts.mdx +++ b/docusaurus/docs/reactnative/customization/contexts.mdx @@ -29,7 +29,6 @@ The majority of the contexts within the SDK are established in the higher level - `OverlayProvider` - [`AttachmentPickerContext`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx) - [`ImageGalleryContext`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/contexts/imageGalleryContext/ImageGalleryContext.tsx) - - [`MessageOverlayContext`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx) - [`OverlayContext`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/contexts/overlayContext/OverlayContext.tsx) - [`ThemeContext`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/contexts/themeContext/ThemeContext.tsx) - [`TranslationContext`](https://github.com/GetStream/stream-chat-react-native/blob/develop/package/src/contexts/translationContext/TranslationContext.tsx) @@ -58,7 +57,6 @@ To access information from these contexts we suggest using the hooks that are pr | `KeyboardContext` | `useKeyboardContext` | | `MessageContext` | `useMessageContext` | | `MessageInputContext` | `useMessageInputContext` | -| `MessageOverlayContext` | `useMessageOverlayContext` | | `MessagesContext` | `useMessagesContext` | | `OverlayContext` | `useOverlayContext` | | `OwnCapabilitiesContext` | `useOwnCapabilitiesContext` | diff --git a/docusaurus/docs/reactnative/guides/custom-message-actions.mdx b/docusaurus/docs/reactnative/guides/custom-message-actions.mdx index 7abc635a1c..8f93f2f7df 100644 --- a/docusaurus/docs/reactnative/guides/custom-message-actions.mdx +++ b/docusaurus/docs/reactnative/guides/custom-message-actions.mdx @@ -4,27 +4,14 @@ title: Custom Message Actions --- import messageActions from '../assets/guides/custom-message-actions/message_actions.png'; -import myMessageActions from '../assets/guides/custom-message-actions/my_message_actions.png'; -Message actions pop up in message overlay, when you long-press a message. We have provided a granular control over these actions. +Message actions pop up in message menu, when you long-press a message. We have provided a granular control over these actions. By default we render the following actions (as shown in screenshots) - +
- - - - - -
- - - -
- Actions on received message - - Actions on my message +
@@ -32,7 +19,7 @@ By default we render the following actions (as shown in screenshots) Every message action that you see in UI, is represented by [`MessageAction`](#messageaction) object for that action. And `MessageAction` object provides all the necessary inputs (title, icon, action handler, `actionType`) for rendering the action button. ```tsx -type MessageAction = { +type MessageActionType = { action: () => void; actionType: enum('blockUser', 'copyMessage', 'deleteMessage', 'editMessage', 'flagMessage', 'muteUser', 'pinMessage', 'selectReaction', 'reply', 'retry', 'quotedReply', 'threadReply', 'unpinMessage') title: string; @@ -45,9 +32,9 @@ You can customize each one of the default actions using props on the [`Channel c The channel component accepts a prop called `messageActions`. You can use this prop as a callback function to render message actions selectively. -The arguments to this function is an object with all the default message actions as [`MessageAction`](#messageaction) objects. The function should return an array of MessageAction objects to render in a [MessageActionList](../common-content/ui-components/overlay-provider/props/message_action_list.mdx) within the message overlay, that is shown when a user long presses a message in a MessageList. +The arguments to this function is an object with all the default message actions as [`MessageActionType`](#messageaction) objects. The function should return an array of MessageAction objects to render in a [MessageActionList](../common-content/ui-components/channel/props/message_action_list.mdx) within the message menu, that is shown when a user long presses a message in a MessageList. -You can also customize each one of the default actions using the `messageActions` prop passed to the OverlayProvider as shown in the example below. The OverlayProvider component makes these props available in the `MessageOverlayContext` context. +You can also customize each one of the default actions using the `messageActions` prop passed to the Channel component as shown in the example below. ```tsx messageActions={({ @@ -75,7 +62,7 @@ messageActions={({ ### MessageAction -When you long press a message, it opens up a message overlay and renders all the actions available on message. MessageAction is an object consisting of all the parameters required to render a single action button, in message overlay. +When you long press a message, it opens up a message menu and renders all the actions available on message. MessageAction is an object consisting of all the parameters required to render a single action button, in message menu. #### Example @@ -257,9 +244,9 @@ import { messageActions as defaultMessageActions, Mute as MuteIcon } from 'strea } } }, - actionType: 'muteUser', + actionType: 'custom-mute-user', icon: , - title: isMuted ? t('Unmute User') : t('Mute User'), + title: isMuted ? t('Custom Unmute User') : t('Custom Mute User'), }); } @@ -272,7 +259,7 @@ import { messageActions as defaultMessageActions, Mute as MuteIcon } from 'strea ## How to customize message action UI -[`OverlayProvider`](../core-components/overlay-provider.mdx) component accepts props called - `MessageActionList` and `MessageActionListItem`. They both serve a different purpose. +The [`Channel`](../core-components/channel.mdx) component accepts props called - `MessageActionList` and `MessageActionListItem`. They both serve a different purpose. - `MessageActionList` - Allows full customizability of the message action list and allows users to add/define their own message action along with the style they prefer for the application. - `MessageActionListItem` - Allows customizability of an item in a message action list. @@ -282,15 +269,16 @@ import { messageActions as defaultMessageActions, Mute as MuteIcon } from 'strea An example for the usage of `MessageActionList` component is as follows. You can obviously have your own logic in the component. ```tsx -import { MessageActionListItem, OverlayProvider, useOverlayContext } from 'stream-chat-react-native'; +import { Alert } from 'react-native'; +import { Channel, MessageActionListItem, useMessageContext } from 'stream-chat-react-native'; const CustomMessageActionList = () => { - const { setOverlay } = useOverlayContext(); + const { dismissOverlay } = useMessageContext(); const messageActions = [ { action: function () { Alert.alert('Edit Message action called.'); - setOverlay('none'); + dismissOverlay(); }, actionType: 'editMessage', title: 'Edit messagee', @@ -298,7 +286,7 @@ const CustomMessageActionList = () => { { action: function () { Alert.alert('Delete message action'); - setOverlay('none'); + dismissOverlay(); }, actionType: 'deleteMessage', title: 'Delete Message', @@ -313,12 +301,12 @@ const CustomMessageActionList = () => { ); }; - - {/* Underlying Channel, MessageList and Message components */} -; + + {/* Underlying MessageList and MessageInput components */} +; ``` -Notice that the `MessageActionList` is a simple prop which just accepts your own component for the message action overlay. The content, styles and the action logic are all defined by the user itself. +Notice that the `MessageActionList` is a simple prop which just accepts your own component for the message menu. The content, styles and the action logic are all defined by the user itself. ### Customize Message Action list item @@ -347,38 +335,32 @@ You can use these props to provide your own component. An example for the `MessageActionListItem` component customization is as following: ```tsx -import { MessageActionListItem, OverlayProvider, useMessageActionAnimation } from 'stream-chat-react-native'; +import { Pressable, Text } from 'react-native'; +import { Channel, MessageActionListItem, useMessageActionAnimation } from 'stream-chat-react-native'; -const CustomMessageActionListItem = ({ action, actionType, ...rest }) => { - const { onTap } = useMessageActionAnimation({ action: action }); +const CustomMessageActionListItem = ({ action, actionType, ...rest }: MessageActionListItemProps) => { if (actionType === 'pinMessage') { return ( - - - {actionType} - - + + {actionType} + ); } else if (actionType === 'muteUser') { return ( - - - {actionType} - - + + {actionType} + ); } else { return ; } }; - - {/* Underlying Channel, MessageList and Message components */} -; + + {/* Underlying MessageList and MessageInput components */} +; ``` -Note: The `useMessageActionAnimation` hook takes in the `action` for the message action and this would give some utilities like `ontap`, `animatedStyle` and the `opacity` value which would be helpful with the Animated View which can be used in support of the package `react-native-reanimated`. - Please continue reading further to see different use cases. ## How to intercept a message action @@ -413,7 +395,7 @@ Following example demonstrates how to add analytics tracking to "Copy Message" a To disable a particular action you can return `null` for a particular action type in the MessageActionListItem prop. An example to the situation would be as follows: ```tsx -import { MessageActionListItem, OverlayProvider, useMessageActionAnimation } from 'stream-chat-react-native'; +import { Channel, MessageActionListItem, useMessageActionAnimation } from 'stream-chat-react-native'; const CustomMessageActionListItem = ({ action, actionType, ...rest }) => { if (actionType === 'pinMessage') { @@ -423,7 +405,7 @@ const CustomMessageActionListItem = ({ action, actionType, ...rest }) => { } }; - - {/* Underlying Channel, MessageList and Message components */} -; + + {/* Underlying MessageList and MessageInput components */} +; ``` diff --git a/docusaurus/docs/reactnative/guides/live-location-sharing.mdx b/docusaurus/docs/reactnative/guides/live-location-sharing.mdx index 80b126d254..0f2c202754 100644 --- a/docusaurus/docs/reactnative/guides/live-location-sharing.mdx +++ b/docusaurus/docs/reactnative/guides/live-location-sharing.mdx @@ -341,7 +341,6 @@ import { Channel, Card as DefaultCard, useMessageContext, - useMessageOverlayContext, useOverlayContext, } from 'stream-chat-react-native'; import {useLiveLocationContext} from './LiveLocationContext'; @@ -361,9 +360,8 @@ const MapCard = ({ const {stopLiveLocation} = useLiveLocationContext(); const {isMyMessage, message} = useMessageContext(); - const {data} = useMessageOverlayContext(); const {overlay} = useOverlayContext(); - const overlayId = data?.message?.id; + const overlayId = message?.id; // is this message shown on overlay? If yes, then don't show the button const isOverlayOpen = overlay === 'message' && overlayId === message.id; const showStopSharingButton = !ended_at && isMyMessage && !isOverlayOpen; diff --git a/docusaurus/docs/reactnative/object-types/message_action.mdx b/docusaurus/docs/reactnative/object-types/message_action.mdx index 2081a80fde..0f63ed6f08 100644 --- a/docusaurus/docs/reactnative/object-types/message_action.mdx +++ b/docusaurus/docs/reactnative/object-types/message_action.mdx @@ -3,7 +3,7 @@ id: message-action title: MessageAction --- -When you long press a message, it opens up a message overlay and renders all the actions available on message. MessageAction is an object consisting of all the parameters required to render a single action button, in message overlay. +When you long press a message, it opens up a message menu and renders all the actions available on message. MessageAction is an object consisting of all the parameters required to render a single action button, in message menu. ## Example diff --git a/docusaurus/docs/reactnative/ui-components/message-pinned-header.mdx b/docusaurus/docs/reactnative/ui-components/message-pinned-header.mdx index 7e9d3878dc..cbceec4446 100644 --- a/docusaurus/docs/reactnative/ui-components/message-pinned-header.mdx +++ b/docusaurus/docs/reactnative/ui-components/message-pinned-header.mdx @@ -3,7 +3,6 @@ id: message-pinned-header title: MessagePinnedHeader --- -import Alignment from '../common-content/contexts/message-context/alignment.mdx'; import MessageProp from '../common-content/contexts/message-context/message.mdx'; import LastGroupMessage from '../common-content/contexts/message-context/last_group_message.mdx'; @@ -25,10 +24,6 @@ const MessagePinnedHeaderComponent = () => This component uses default values for all the following props, from [`MessageContext`](../../contexts/message-context) -###
_overrides the value from [MessageContext](../../contexts/message-context#alignment)_
`alignment` {#alignment} - - - ###
_overrides the value from [MessageContext](../../contexts/message-context#message)_
`message` {#message} diff --git a/docusaurus/docs/reactnative/ui-components/overlay-reaction-list.mdx b/docusaurus/docs/reactnative/ui-components/overlay-reaction-list.mdx deleted file mode 100644 index 98a7e5231f..0000000000 --- a/docusaurus/docs/reactnative/ui-components/overlay-reaction-list.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -id: overlay-reaction-list -title: OverlayReactionList ---- - -import SupportedReactions from '../common-content/ui-components/channel/props/supported_reactions.mdx'; -import SetOverlay from '../common-content/contexts/overlay-context/set_overlay.mdx'; -import MessageOverlayContextData from '../common-content/contexts/message-overlay-context/data.mdx'; - -`OverlayReactionList` component is used to display reaction selector within message overlay. - -This is the default component provided to the prop [`OverlayReactionList`](../core-components/overlay-provider.mdx#overlayreactionlist) on the `OverlayProvider` component. - -## Props - -###
_overrides the value from [MessageOverlayContext](../../contexts/message-overlay-context#data)_
`data` {#data} - - - -###
_overrides the value from [OverlayContext](../../contexts/overlay-context#setoverlay)_
`setOverlay` {#setoverlay} - - - -###
_overrides the value from [MessagesContext](../../contexts/messages-context#supportedreactions)_
`supportedReactions` {#supportedreactions} - - diff --git a/docusaurus/docs/reactnative/ui-components/overlay-reactions-avatar.mdx b/docusaurus/docs/reactnative/ui-components/overlay-reactions-avatar.mdx deleted file mode 100644 index fffd363745..0000000000 --- a/docusaurus/docs/reactnative/ui-components/overlay-reactions-avatar.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -id: overlay-reactions-avatar -title: OverlayReactionsAvatar ---- - -`OverlayReactionsAvatar` component is used to display the avatar of the users who have reacted to the message. This is displayed on the message overlay, which opens up when the user presses on the reaction above the message. - -This is the default component provided to the prop [`OverlayReactionsAvatar`](../core-components/overlay-provider.mdx#overlayreactionsavatar) on the `OverlayProvider` component. - -## Props - -###
required
`reaction` - -Reaction which can be extracted from a message. - -```tsx -{ - alignment: clientId && clientId === reaction.user?.id ? 'right' : 'left', - image: reaction?.user?.image, - name: reaction?.user?.name || reaction.user_id || '', - type: reaction.type, -}; -``` - -| Type | -| ------ | -| Object | - -### `size` - -Dimension for avatar image. - -:::tip -You can also set the size for message avatar, using our [theming system](../customization/theme.mdx). - -```tsx -const theme = { - avatar: { - BASE_AVATAR_SIZE: 30, - }, -}; -``` - -::: - -| Type | Default | -| ------ | ------- | -| Number | 32 | diff --git a/docusaurus/docs/reactnative/ui-components/overlay-reactions.mdx b/docusaurus/docs/reactnative/ui-components/overlay-reactions.mdx deleted file mode 100644 index f316ab61e6..0000000000 --- a/docusaurus/docs/reactnative/ui-components/overlay-reactions.mdx +++ /dev/null @@ -1,64 +0,0 @@ ---- -id: overlay-reactions -title: OverlayReactions ---- - -import SupportedReactions from '../common-content/ui-components/channel/props/supported_reactions.mdx'; - -import Alignment from '../common-content/contexts/message-context/alignment.mdx'; - -`OverlayReactions` component is used to display the list of existing reactions within the message overlay, which opens up when the user long presses a message. - -This is the default component provided to the prop [`OverlayReactions`](../core-components/overlay-provider.mdx#overlayreactions) on the `OverlayProvider` component. - -## Props - -### `alignment` - - - -### `messageId` - -The message ID for which the reactions are displayed. - -| Type | Default | -| ----------------------- | ----------- | -| `String` \| `undefined` | `undefined` | - -### `reactions` - -List of existing reactions which can be extracted from a message. - -```tsx -const reactions = message.latest_reactions.map(reaction => ({ - alignment: clientId && clientId === reaction.user?.id ? 'right' : 'left', - id: reaction?.user?.id || '', - image: reaction?.user?.image, - name: reaction?.user?.name || reaction.user_id || '', - type: reaction.type, -})); -``` - -| Type | Default | -| ---------------------- | ----------- | -| `Array` \| `undefined` | `undefined` | - -###
required
`showScreen` {#showscreen} - -`Shared` value from React Native Reanimated [`useSharedValue`](https://docs.swmansion.com/react-native-reanimated/docs/api/hooks/useSharedValue/) hook. - -| Type | -| ------ | -| Object | - -###
overrides the value from [MessagesContext](../../contexts/messages-context#supportedreactions)
`supportedReactions` {#supportedreactions} - - - -###
required
`title` {#title} - -Title for the component. - -| Type | -| ------ | -| String | diff --git a/docusaurus/sidebars-react-native.json b/docusaurus/sidebars-react-native.json index 0b9ad84c50..21de7f1482 100644 --- a/docusaurus/sidebars-react-native.json +++ b/docusaurus/sidebars-react-native.json @@ -5,13 +5,7 @@ "ui-components/overview", "customization/theming", { - "Overlay Provider": [ - "core-components/overlay-provider", - "ui-components/overlay-reaction-list", - "ui-components/overlay-reactions-avatar", - "ui-components/overlay-reactions", - "contexts/overlay-context" - ] + "Overlay Provider": ["core-components/overlay-provider", "contexts/overlay-context"] }, { "Chat": ["core-components/chat", "contexts/chat-context"] @@ -101,7 +95,6 @@ "contexts/attachment-picker-context", "contexts/image-gallery-context", "contexts/keyboard-context", - "contexts/message-overlay-context", "contexts/own-capabilities-context", "contexts/paginated-message-list-context", "contexts/suggestions-context", diff --git a/package/src/components/Attachment/Card.tsx b/package/src/components/Attachment/Card.tsx index b791fbb231..99a3108e00 100644 --- a/package/src/components/Attachment/Card.tsx +++ b/package/src/components/Attachment/Card.tsx @@ -29,7 +29,7 @@ import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { Play } from '../../icons/Play'; import { DefaultStreamChatGenerics, FileTypes } from '../../types/types'; import { makeImageCompatibleUrl } from '../../utils/utils'; -import { ImageBackground } from '../ImageBackground'; +import { ImageBackground } from '../UIComponents/ImageBackground'; const styles = StyleSheet.create({ authorName: { fontSize: 14.5, fontWeight: '600' }, diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 63a9ae0501..9ecb7cd1a9 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -171,7 +171,13 @@ import { ScrollToBottomButton as ScrollToBottomButtonDefault } from '../MessageL import { StickyHeader as StickyHeaderDefault } from '../MessageList/StickyHeader'; import { TypingIndicator as TypingIndicatorDefault } from '../MessageList/TypingIndicator'; import { TypingIndicatorContainer as TypingIndicatorContainerDefault } from '../MessageList/TypingIndicatorContainer'; -import { OverlayReactionList as OverlayReactionListDefault } from '../MessageOverlay/OverlayReactionList'; +import { MessageActionList as MessageActionListDefault } from '../MessageMenu/MessageActionList'; +import { MessageActionListItem as MessageActionListItemDefault } from '../MessageMenu/MessageActionListItem'; +import { MessageMenu as MessageMenuDefault } from '../MessageMenu/MessageMenu'; +import { MessageReactionPicker as MessageReactionPickerDefault } from '../MessageMenu/MessageReactionPicker'; +import { MessageUserReactions as MessageUserReactionsDefault } from '../MessageMenu/MessageUserReactions'; +import { MessageUserReactionsAvatar as MessageUserReactionsAvatarDefault } from '../MessageMenu/MessageUserReactionsAvatar'; +import { MessageUserReactionsItem as MessageUserReactionsItemDefault } from '../MessageMenu/MessageUserReactionsItem'; import { Reply as ReplyDefault } from '../Reply/Reply'; const styles = StyleSheet.create({ @@ -301,6 +307,8 @@ export type ChannelPropsWithContext< | 'ImageLoadingIndicator' | 'markdownRules' | 'Message' + | 'MessageActionList' + | 'MessageActionListItem' | 'messageActions' | 'MessageAvatar' | 'MessageBounce' @@ -312,6 +320,7 @@ export type ChannelPropsWithContext< | 'MessageFooter' | 'MessageHeader' | 'MessageList' + | 'MessageMenu' | 'MessagePinnedHeader' | 'MessageReplies' | 'MessageRepliesAvatars' @@ -319,12 +328,16 @@ export type ChannelPropsWithContext< | 'MessageStatus' | 'MessageSystem' | 'MessageText' + | 'messageTextNumberOfLines' | 'MessageTimestamp' + | 'MessageUserReactions' | 'myMessageTheme' | 'onLongPressMessage' | 'onPressInMessage' | 'onPressMessage' - | 'OverlayReactionList' + | 'MessageReactionPicker' + | 'MessageUserReactionsAvatar' + | 'MessageUserReactionsItem' | 'ReactionList' | 'Reply' | 'ScrollToBottomButton' @@ -540,6 +553,8 @@ const ChannelWithContext = < mentionAllAppUsersEnabled = false, mentionAllAppUsersQuery, Message = MessageDefault, + MessageActionList = MessageActionListDefault, + MessageActionListItem = MessageActionListItemDefault, messageActions, MessageAvatar = MessageAvatarDefault, MessageBounce = MessageBounceDefault, @@ -552,7 +567,9 @@ const ChannelWithContext = < MessageHeader, messageId, MessageList = MessageListDefault, + MessageMenu = MessageMenuDefault, MessagePinnedHeader = MessagePinnedHeaderDefault, + MessageReactionPicker = MessageReactionPickerDefault, MessageReplies = MessageRepliesDefault, MessageRepliesAvatars = MessageRepliesAvatarsDefault, messages, @@ -560,7 +577,11 @@ const ChannelWithContext = < MessageStatus = MessageStatusDefault, MessageSystem = MessageSystemDefault, MessageText, + messageTextNumberOfLines = 5, MessageTimestamp = MessageTimestampDefault, + MessageUserReactions = MessageUserReactionsDefault, + MessageUserReactionsAvatar = MessageUserReactionsAvatarDefault, + MessageUserReactionsItem = MessageUserReactionsItemDefault, MoreOptionsButton = MoreOptionsButtonDefault, myMessageTheme, NetworkDownIndicator = NetworkDownIndicatorDefault, @@ -570,7 +591,6 @@ const ChannelWithContext = < onLongPressMessage, onPressInMessage, onPressMessage, - OverlayReactionList = OverlayReactionListDefault, overrideOwnCapabilities, ReactionList = ReactionListDefault, read, @@ -2360,6 +2380,8 @@ const ChannelWithContext = < legacyImageViewerSwipeBehaviour, markdownRules, Message, + MessageActionList, + MessageActionListItem, messageActions, MessageAvatar, MessageBounce, @@ -2371,19 +2393,24 @@ const ChannelWithContext = < MessageFooter, MessageHeader, MessageList, + MessageMenu, MessagePinnedHeader, + MessageReactionPicker, MessageReplies, MessageRepliesAvatars, MessageSimple, MessageStatus, MessageSystem, MessageText, + messageTextNumberOfLines, MessageTimestamp, + MessageUserReactions, + MessageUserReactionsAvatar, + MessageUserReactionsItem, myMessageTheme, onLongPressMessage, onPressInMessage, onPressMessage, - OverlayReactionList, ReactionList, removeMessage, Reply, diff --git a/package/src/components/Channel/__tests__/ownCapabilities.test.js b/package/src/components/Channel/__tests__/ownCapabilities.test.js index 7158b30204..a879746ec7 100644 --- a/package/src/components/Channel/__tests__/ownCapabilities.test.js +++ b/package/src/components/Channel/__tests__/ownCapabilities.test.js @@ -68,249 +68,249 @@ describe('Own capabilities', () => { }; const renderChannelAndOpenMessageActionsList = async (targetMessage, props = {}) => { - const { findByTestId, queryByTestId, queryByText, unmount } = render(getComponent(props)); + const { findByTestId, queryByLabelText, queryByText, unmount } = render(getComponent(props)); await waitFor(() => queryByText(targetMessage.text)); act(() => { fireEvent(queryByText(targetMessage.text), 'onLongPress'); }); - await waitFor(() => expect(!!queryByTestId('message-action-list')).toBeTruthy()); + await waitFor(() => expect(!!queryByLabelText('Message action list')).toBeTruthy()); - return { findByTestId, queryByTestId, queryByText, unmount }; + return { findByTestId, queryByLabelText, queryByText, unmount }; }; describe(`${allOwnCapabilities.sendReply} capability`, () => { it(`should render "Thread Reply" message action when ${allOwnCapabilities.sendReply} capability is enabled`, async () => { await generateChannelWithCapabilities([allOwnCapabilities.sendReply]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(sentMessage); - expect(!!queryByTestId('threadReply-list-item')).toBeTruthy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(sentMessage); + expect(!!queryByLabelText('threadReply action list item')).toBeTruthy(); }); it('should not render "Thread Reply" message action when "send-reply" capability is disabled', async () => { await generateChannelWithCapabilities(); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(sentMessage); - expect(!!queryByTestId('threadReply-list-item')).toBeFalsy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(sentMessage); + expect(!!queryByLabelText('threadReply action list item')).toBeFalsy(); }); it('should override capability from "overrideOwnCapability.sendReply" prop', async () => { await generateChannelWithCapabilities([allOwnCapabilities.sendReply]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(sentMessage, { + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(sentMessage, { overrideOwnCapabilities: { sendReply: false, }, }); - expect(!!queryByTestId('threadReply-list-item')).toBeFalsy(); + expect(!!queryByLabelText('threadReply action list item')).toBeFalsy(); }); }); describe(`${allOwnCapabilities.banChannelMembers} capability`, () => { it(`should render "Ban User" message action when ${allOwnCapabilities.banChannelMembers} capability is enabled`, async () => { await generateChannelWithCapabilities([allOwnCapabilities.banChannelMembers]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('banUser-list-item')).toBeTruthy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('banUser action list item')).toBeTruthy(); }); it(`should not render "Ban User" message action when ${allOwnCapabilities.banChannelMembers} capability is disabled`, async () => { await generateChannelWithCapabilities(); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('banUser-list-item')).toBeFalsy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('banUser action list item')).toBeFalsy(); }); it(`should override capability from "overrideOwnCapability.banChannelMembers" prop`, async () => { await generateChannelWithCapabilities([allOwnCapabilities.banChannelMembers]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage, { + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage, { overrideOwnCapabilities: { banChannelMembers: false, }, }); - expect(!!queryByTestId('banUser-list-item')).toBeFalsy(); + expect(!!queryByLabelText('banUser action list item')).toBeFalsy(); }); }); describe(`${allOwnCapabilities.deleteAnyMessage} capability`, () => { it(`should render "Delete Message" action for received message when "${allOwnCapabilities.deleteAnyMessage}" capability is enabled`, async () => { await generateChannelWithCapabilities(['delete-any-message']); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('deleteMessage-list-item')).toBeTruthy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('deleteMessage action list item')).toBeTruthy(); }); it(`should not render "Delete Message" action for received message when "${allOwnCapabilities.deleteAnyMessage}" capability is disabled`, async () => { await generateChannelWithCapabilities(); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('deleteMessage-list-item')).toBeFalsy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('deleteMessage action list item')).toBeFalsy(); }); it('should override capability from "overrideOwnCapability.deleteAnyMessage" prop', async () => { await generateChannelWithCapabilities([allOwnCapabilities.deleteAnyMessage]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage, { + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage, { overrideOwnCapabilities: { deleteAnyMessage: false, }, }); - expect(!!queryByTestId('deleteMessage-list-item')).toBeFalsy(); + expect(!!queryByLabelText('deleteMessage action list item')).toBeFalsy(); }); }); describe(`${allOwnCapabilities.deleteOwnMessage} capability`, () => { it(`should render "Delete Message" action for sent message when "${allOwnCapabilities.deleteOwnMessage}" capability is enabled`, async () => { await generateChannelWithCapabilities([allOwnCapabilities.deleteOwnMessage]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(sentMessage); - expect(!!queryByTestId('deleteMessage-list-item')).toBeTruthy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(sentMessage); + expect(!!queryByLabelText('deleteMessage action list item')).toBeTruthy(); }); it(`should not render "Delete Message" action for sent message when "${allOwnCapabilities.deleteOwnMessage}" capability is disabled`, async () => { await generateChannelWithCapabilities(); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(sentMessage); - expect(!!queryByTestId('deleteMessage-list-item')).toBeFalsy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(sentMessage); + expect(!!queryByLabelText('deleteMessage action list item')).toBeFalsy(); }); it('should override capability from "overrideOwnCapability.deleteOwnMessage" prop', async () => { await generateChannelWithCapabilities([allOwnCapabilities.deleteOwnMessage]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(sentMessage, { + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(sentMessage, { overrideOwnCapabilities: { deleteOwnMessage: false, }, }); - expect(!!queryByTestId('deleteMessage-list-item')).toBeFalsy(); + expect(!!queryByLabelText('deleteMessage action list item')).toBeFalsy(); }); }); describe(`${allOwnCapabilities.updateAnyMessage} capability`, () => { it(`should render "Edit Message" action for received message when "${allOwnCapabilities.updateAnyMessage}" capability is enabled`, async () => { await generateChannelWithCapabilities([allOwnCapabilities.updateAnyMessage]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('editMessage-list-item')).toBeTruthy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('editMessage action list item')).toBeTruthy(); }); it(`should not render "Edit Message" action for received message when "${allOwnCapabilities.updateAnyMessage}" capability is disabled`, async () => { await generateChannelWithCapabilities(); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('editMessage-list-item')).toBeFalsy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('editMessage action list item')).toBeFalsy(); }); it('should override capability from "overrideOwnCapability.updateAnyMessage" prop', async () => { await generateChannelWithCapabilities([allOwnCapabilities.updateAnyMessage]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage, { + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage, { overrideOwnCapabilities: { updateAnyMessage: false, }, }); - expect(!!queryByTestId('editMessage-list-item')).toBeFalsy(); + expect(!!queryByLabelText('editMessage action list item')).toBeFalsy(); }); }); describe(`${allOwnCapabilities.flagMessage} capability`, () => { it(`should render "Flag Message" action for sent message when "${allOwnCapabilities.flagMessage}" capability is enabled`, async () => { await generateChannelWithCapabilities([allOwnCapabilities.flagMessage]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('flagMessage-list-item')).toBeTruthy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('flagMessage action list item')).toBeTruthy(); }); it(`should not render "Flag Message" action for sent message when "${allOwnCapabilities.flagMessage}" capability is disabled`, async () => { await generateChannelWithCapabilities(); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('flagMessage-list-item')).toBeFalsy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('flagMessage action list item')).toBeFalsy(); }); it('should override capability from "overrideOwnCapability.deleteOwnMessage" prop', async () => { await generateChannelWithCapabilities([allOwnCapabilities.flagMessage]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage, { + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage, { overrideOwnCapabilities: { flagMessage: false, }, }); - expect(!!queryByTestId('flagMessage-list-item')).toBeFalsy(); + expect(!!queryByLabelText('flagMessage action list item')).toBeFalsy(); }); }); describe(`${allOwnCapabilities.pinMessage} capability`, () => { it(`should render "Pin Message" action for sent message when "${allOwnCapabilities.pinMessage}" capability is enabled`, async () => { await generateChannelWithCapabilities([allOwnCapabilities.pinMessage]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('pinMessage-list-item')).toBeTruthy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('pinMessage action list item')).toBeTruthy(); }); it(`should not render "Pin Message" action for sent message when "${allOwnCapabilities.pinMessage}" capability is disabled`, async () => { await generateChannelWithCapabilities(); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('pinMessage-list-item')).toBeFalsy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('pinMessage action list item')).toBeFalsy(); }); it('should override capability from "overrideOwnCapability.pinMessage" prop', async () => { await generateChannelWithCapabilities([allOwnCapabilities.pinMessage]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage, { + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage, { overrideOwnCapabilities: { pinMessage: false, }, }); - expect(!!queryByTestId('pinMessage-list-item')).toBeFalsy(); + expect(!!queryByLabelText('pinMessage action list item')).toBeFalsy(); }); }); describe(`${allOwnCapabilities.quoteMessage} capability`, () => { it(`should render "Reply" action for sent message when "${allOwnCapabilities.quoteMessage}" capability is enabled`, async () => { await generateChannelWithCapabilities([allOwnCapabilities.quoteMessage]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('quotedReply-list-item')).toBeTruthy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('quotedReply action list item')).toBeTruthy(); }); it(`should not render "Reply" action for sent message when "${allOwnCapabilities.quoteMessage}" capability is disabled`, async () => { await generateChannelWithCapabilities(); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('quotedReply-list-item')).toBeFalsy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('quotedReply action list item')).toBeFalsy(); }); it('should override capability from "overrideOwnCapability.quoteMessage" prop', async () => { await generateChannelWithCapabilities([allOwnCapabilities.quoteMessage]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage, { + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage, { overrideOwnCapabilities: { quoteMessage: false, }, }); - expect(!!queryByTestId('quotedReply-list-item')).toBeFalsy(); + expect(!!queryByLabelText('quotedReply action list item')).toBeFalsy(); }); }); describe(`${allOwnCapabilities.sendReaction} capability`, () => { it(`should render reaction selector when "${allOwnCapabilities.sendReaction}" capability is enabled`, async () => { await generateChannelWithCapabilities([allOwnCapabilities.sendReaction]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('overlay-reaction-list')).toBeTruthy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('Reaction Selector on long pressing message')).toBeTruthy(); }); it(`should not render reaction selector when "${allOwnCapabilities.sendReaction}" capability is disabled`, async () => { await generateChannelWithCapabilities(); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage); - expect(!!queryByTestId('overlay-reaction-list')).toBeFalsy(); + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage); + expect(!!queryByLabelText('Reaction Selector on long pressing message')).toBeFalsy(); }); it('should override capability from "overrideOwnCapability.sendReaction" prop', async () => { await generateChannelWithCapabilities([allOwnCapabilities.sendReaction]); - const { queryByTestId } = await renderChannelAndOpenMessageActionsList(receivedMessage, { + const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage, { overrideOwnCapabilities: { sendReaction: false, }, }); - expect(!!queryByTestId('overlay-reaction-list')).toBeFalsy(); + expect(!!queryByLabelText('Reaction Selector on long pressing message')).toBeFalsy(); }); }); diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts index a0789400be..09eef6ebc6 100644 --- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts +++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts @@ -52,6 +52,8 @@ export const useCreateMessagesContext = < legacyImageViewerSwipeBehaviour, markdownRules, Message, + MessageActionList, + MessageActionListItem, messageActions, MessageAvatar, MessageBounce, @@ -63,19 +65,24 @@ export const useCreateMessagesContext = < MessageFooter, MessageHeader, MessageList, + MessageMenu, MessagePinnedHeader, + MessageReactionPicker, MessageReplies, MessageRepliesAvatars, MessageSimple, MessageStatus, MessageSystem, MessageText, + messageTextNumberOfLines, MessageTimestamp, + MessageUserReactions, + MessageUserReactionsAvatar, + MessageUserReactionsItem, myMessageTheme, onLongPressMessage, onPressInMessage, onPressMessage, - OverlayReactionList, ReactionList, removeMessage, Reply, @@ -101,7 +108,7 @@ export const useCreateMessagesContext = < const additionalTouchablePropsLength = Object.keys(additionalTouchableProps || {}).length; const markdownRulesLength = Object.keys(markdownRules || {}).length; const messageContentOrderValue = messageContentOrder.join(); - const supportedReactionsLength = supportedReactions.length; + const supportedReactionsLength = supportedReactions?.length; const messagesContext: MessagesContextValue = useMemo( () => ({ @@ -150,6 +157,8 @@ export const useCreateMessagesContext = < legacyImageViewerSwipeBehaviour, markdownRules, Message, + MessageActionList, + MessageActionListItem, messageActions, MessageAvatar, MessageBounce, @@ -161,19 +170,24 @@ export const useCreateMessagesContext = < MessageFooter, MessageHeader, MessageList, + MessageMenu, MessagePinnedHeader, + MessageReactionPicker, MessageReplies, MessageRepliesAvatars, MessageSimple, MessageStatus, MessageSystem, MessageText, + messageTextNumberOfLines, MessageTimestamp, + MessageUserReactions, + MessageUserReactionsAvatar, + MessageUserReactionsItem, myMessageTheme, onLongPressMessage, onPressInMessage, onPressMessage, - OverlayReactionList, ReactionList, removeMessage, Reply, diff --git a/package/src/components/ChannelList/ChannelListFooterLoadingIndicator.tsx b/package/src/components/ChannelList/ChannelListFooterLoadingIndicator.tsx index aaad130173..6680e948d3 100644 --- a/package/src/components/ChannelList/ChannelListFooterLoadingIndicator.tsx +++ b/package/src/components/ChannelList/ChannelListFooterLoadingIndicator.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { StyleSheet, View } from 'react-native'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; -import { Spinner } from '../Spinner/Spinner'; +import { Spinner } from '../UIComponents/Spinner'; const styles = StyleSheet.create({ container: { diff --git a/package/src/components/Chat/__tests__/Chat.test.js b/package/src/components/Chat/__tests__/Chat.test.js index 191e6eee3f..98a9e5fe0d 100644 --- a/package/src/components/Chat/__tests__/Chat.test.js +++ b/package/src/components/Chat/__tests__/Chat.test.js @@ -132,9 +132,12 @@ describe('ChatContext', () => { }); describe('TranslationContext', () => { + beforeEach(() => { + jest.spyOn(DBSyncManager, 'init'); + }); afterEach(() => { - cleanup(); jest.clearAllMocks(); + cleanup(); }); const chatClient = getTestClient(); it('exposes the translation context', async () => { @@ -229,7 +232,6 @@ describe('TranslationContext', () => { it('makes sure DBSyncManager listeners are cleaned up after Chat remount', async () => { const chatClientWithUser = await getTestClientWithUser({ id: 'testID' }); - jest.spyOn(DBSyncManager, 'init'); // initial mount and render const { rerender } = render(); @@ -252,7 +254,6 @@ describe('TranslationContext', () => { it('makes sure DBSyncManager listeners are cleaned up if the user changes', async () => { const chatClientWithUser = await getTestClientWithUser({ id: 'testID1' }); - jest.spyOn(DBSyncManager, 'init'); // initial render const { rerender } = render(); @@ -278,7 +279,6 @@ describe('TranslationContext', () => { it('makes sure DBSyncManager state stays intact during normal rerenders', async () => { const chatClientWithUser = await getTestClientWithUser({ id: 'testID' }); - jest.spyOn(DBSyncManager, 'init'); // initial render const { rerender } = render(); diff --git a/package/src/components/ImageGallery/__tests__/ImageGalleryGrid.test.tsx b/package/src/components/ImageGallery/__tests__/ImageGalleryGrid.test.tsx index 55372d38de..494f256ffc 100644 --- a/package/src/components/ImageGallery/__tests__/ImageGalleryGrid.test.tsx +++ b/package/src/components/ImageGallery/__tests__/ImageGalleryGrid.test.tsx @@ -22,7 +22,7 @@ const getComponent = (props: Partial = {}) => { return ( - + diff --git a/package/src/components/ImageGallery/__tests__/ImageGalleryGridHandle.test.tsx b/package/src/components/ImageGallery/__tests__/ImageGalleryGridHandle.test.tsx index 7d67cba54c..4ab178ed4e 100644 --- a/package/src/components/ImageGallery/__tests__/ImageGalleryGridHandle.test.tsx +++ b/package/src/components/ImageGallery/__tests__/ImageGalleryGridHandle.test.tsx @@ -24,7 +24,7 @@ const getComponent = (props: Partial = {}) => { return ( - + diff --git a/package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx b/package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx index 9a4a4bd3c3..8c0d8b157e 100644 --- a/package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx +++ b/package/src/components/ImageGallery/components/AnimatedGalleryVideo.tsx @@ -13,7 +13,7 @@ import { VideoType, } from '../../../native'; -import { Spinner } from '../../Spinner/Spinner'; +import { Spinner } from '../../UIComponents/Spinner'; const oneEighth = 1 / 8; diff --git a/package/src/components/Indicators/LoadingIndicator.tsx b/package/src/components/Indicators/LoadingIndicator.tsx index abbdbdd130..689563743c 100644 --- a/package/src/components/Indicators/LoadingIndicator.tsx +++ b/package/src/components/Indicators/LoadingIndicator.tsx @@ -3,7 +3,7 @@ import { StyleSheet, Text, View } from 'react-native'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useTranslationContext } from '../../contexts/translationContext/TranslationContext'; -import { Spinner } from '../Spinner/Spinner'; +import { Spinner } from '../UIComponents/Spinner'; type LoadingIndicatorWrapperProps = { text: string }; diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index 1e962a96d3..2b226c0311 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -19,18 +19,10 @@ import { useKeyboardContext, } from '../../contexts/keyboardContext/KeyboardContext'; import { MessageContextValue, MessageProvider } from '../../contexts/messageContext/MessageContext'; -import { - MessageOverlayContextValue, - useMessageOverlayContext, -} from '../../contexts/messageOverlayContext/MessageOverlayContext'; import { MessagesContextValue, useMessagesContext, } from '../../contexts/messagesContext/MessagesContext'; -import { - OverlayContextValue, - useOverlayContext, -} from '../../contexts/overlayContext/OverlayContext'; import { useOwnCapabilitiesContext } from '../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { ThreadContextValue, useThreadContext } from '../../contexts/threadContext/ThreadContext'; @@ -53,7 +45,6 @@ import { isMessageWithStylesReadByAndDateSeparator, MessageType, } from '../MessageList/hooks/useMessageList'; -import type { MessageActionListItemProps } from '../MessageOverlay/MessageActionListItem'; export type TouchableEmitter = | 'fileAttachment' @@ -159,6 +150,7 @@ export type MessagePropsWithContext< | 'handleRetry' | 'handleThreadReply' | 'isAttachmentEqual' + | 'MessageMenu' | 'messageActions' | 'messageContentOrder' | 'MessageBounce' @@ -166,7 +158,6 @@ export type MessagePropsWithContext< | 'onLongPressMessage' | 'onPressInMessage' | 'onPressMessage' - | 'OverlayReactionList' | 'removeMessage' | 'deleteReaction' | 'retrySendMessage' @@ -176,8 +167,6 @@ export type MessagePropsWithContext< | 'supportedReactions' | 'updateMessage' > & - Pick, 'setData'> & - Pick & Pick, 'openThread'> & Pick & { chatContext: ChatContextValue; @@ -188,36 +177,6 @@ export type MessagePropsWithContext< enableLongPress?: boolean; goToMessage?: (messageId: string) => void; isTargetedMessage?: boolean; - /** - * Array of allowed actions or null on message, this can also be a function returning the array. - * If all the actions need to be disabled an empty array should be provided as value of prop - */ - /** - * You can call methods available on the Message - * component such as handleEdit, handleDelete, handleAction etc. - * - * Source - [Message](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Message/Message.tsx) - * - * By default, we show the overlay with all the message actions on long press. - * - * @param message Message object which was long pressed - * @param event Event object for onLongPress event - **/ - onLongPress?: (payload: Partial>) => void; - - /** - * You can call methods available on the Message - * component such as handleEdit, handleDelete, handleAction etc. - * - * Source - [Message](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Message/Message.tsx) - * - * By default, we will dismiss the keyboard on press. - * - * @param message Message object which was long pressed - * @param event Event object for onLongPress event - * */ - onPress?: (payload: Partial>) => void; - onPressIn?: (payload: Partial>) => void; /** * Handler to open the thread on message. This is callback for touch event for replies button. * @@ -238,6 +197,9 @@ const MessageWithContext = < >( props: MessagePropsWithContext, ) => { + const [messageOverlayVisible, setMessageOverlayVisible] = useState(false); + const [isErrorInMessage, setIsErrorInMessage] = useState(false); + const [showMessageReactions, setShowMessageReactions] = useState(true); const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false); const [isEditedMessageOpen, setIsEditedMessageOpen] = useState(false); const isMessageTypeDeleted = props.message.type === 'deleted'; @@ -273,25 +235,20 @@ const MessageWithContext = < messageActions: messageActionsProp = defaultMessageActions, MessageBounce, messageContentOrder: messageContentOrderProp, + MessageMenu, messagesContext, MessageSimple, - onLongPress: onLongPressProp, onLongPressMessage: onLongPressMessageProp, - onPress: onPressProp, - onPressIn: onPressInProp, onPressInMessage: onPressInMessageProp, onPressMessage: onPressMessageProp, onThreadSelect, openThread, - OverlayReactionList, preventPress, removeMessage, retrySendMessage, selectReaction, sendReaction, - setData, setEditingState, - setOverlay, setQuotedMessageState, showAvatar, showMessageStatus, @@ -306,10 +263,21 @@ const MessageWithContext = < const { theme: { colors: { bg_gradient_start, targetedMessageBackground }, - messageSimple: { targetedMessageContainer, targetedMessageUnderlay }, + messageSimple: { targetedMessageContainer }, + screenPadding, }, } = useTheme(); + const showMessageOverlay = async (showMessageReactions = false) => { + await dismissKeyboard(); + setShowMessageReactions(showMessageReactions); + setMessageOverlayVisible(true); + }; + + const dismissOverlay = () => { + setMessageOverlayVisible(false); + }; + const actionsEnabled = message.type === 'regular' && message.status === MessageStatusTypes.RECEIVED; @@ -346,6 +314,7 @@ const MessageWithContext = < } const quotedMessage = message.quoted_message as MessageType; if (error) { + setIsErrorInMessage(true); /** * If its a Blocked message, we don't do anything as per specs. */ @@ -359,7 +328,7 @@ const MessageWithContext = < setIsBounceDialogOpen(true); return; } - showMessageOverlay(true, true); + showMessageOverlay(); } else if (quotedMessage) { onPressQuotedMessage(quotedMessage); } @@ -531,6 +500,7 @@ const MessageWithContext = < client, deleteMessage: deleteMessageFromContext, deleteReaction, + dismissOverlay, enforceUniqueReaction, handleBan, handleBlock, @@ -552,73 +522,39 @@ const MessageWithContext = < selectReaction, sendReaction, setEditingState, - setOverlay, setQuotedMessageState, supportedReactions, t, updateMessage, }); - const { userLanguage } = useTranslationContext(); - - const showMessageOverlay = async (isMessageActionsVisible = true, error = errorOrFailed) => { - await dismissKeyboard(); - - const isThreadMessage = threadList || !!message.parent_id; - - const dismissOverlay = () => setOverlay('none'); - - const messageActions = - typeof messageActionsProp !== 'function' - ? messageActionsProp - : messageActionsProp({ - banUser, - blockUser, - copyMessage, - deleteMessage, - dismissOverlay, - editMessage, - error, - flagMessage, - isMessageActionsVisible, - isMyMessage, - isThreadMessage, - message, - messageReactions: isMessageActionsVisible === false, - muteUser, - ownCapabilities, - pinMessage, - quotedReply, - retry, - threadReply, - unpinMessage, - }); - - setData({ - alignment, - chatContext, - clientId: client.userID, - files: attachments.files, - groupStyles, - handleReaction: ownCapabilities.sendReaction ? handleReaction : undefined, - images: attachments.images, - message, - messageActions: messageActions?.filter(Boolean) as MessageActionListItemProps[] | undefined, - messageContext: { ...messageContext, preventPress: true }, - messageReactionTitle: !error && !isMessageActionsVisible ? t('Message Reactions') : undefined, - messagesContext: { ...messagesContext, messageContentOrder }, - onlyEmojis, - otherAttachments: attachments.other, - OverlayReactionList, - ownCapabilities, - supportedReactions, - threadList, - userLanguage, - videos: attachments.videos, - }); - - setOverlay('message'); - }; + // const { userLanguage } = useTranslationContext(); + const isThreadMessage = threadList || !!message.parent_id; + + const messageActions = + typeof messageActionsProp !== 'function' + ? messageActionsProp + : messageActionsProp({ + banUser, + blockUser, + copyMessage, + deleteMessage, + dismissOverlay, + editMessage, + error: isErrorInMessage, + flagMessage, + isMyMessage, + isThreadMessage, + message, + muteUser, + ownCapabilities, + pinMessage, + quotedReply, + retry, + showMessageReactions, + threadReply, + unpinMessage, + }); const actionHandlers: MessageActionHandlers = { copyMessage: handleCopyMessage, @@ -648,14 +584,6 @@ const MessageWithContext = < event: payload?.event, message, }) - : onLongPressProp - ? (payload?: TouchableHandlerPayload) => - onLongPressProp({ - actionHandlers, - defaultHandler: payload?.defaultHandler || showMessageOverlay, - emitter: payload?.emitter || 'message', - event: payload?.event, - }) : enableLongPress ? () => { // If a message is bounced, on long press the message bounce options modal should open. @@ -664,7 +592,7 @@ const MessageWithContext = < return; } triggerHaptic('impactMedium'); - showMessageOverlay(true); + showMessageOverlay(); } : () => null; @@ -672,6 +600,7 @@ const MessageWithContext = < actionsEnabled, alignment, channel, + dismissOverlay, files: attachments.files, goToMessage, groupStyles, @@ -709,7 +638,6 @@ const MessageWithContext = < }; const handleOnPress = () => { - if (onPressProp) return onPressProp(onPressArgs); if (onPressMessageProp) return onPressMessageProp(onPressArgs); if (payload.defaultHandler) return payload.defaultHandler(); @@ -718,23 +646,18 @@ const MessageWithContext = < handleOnPress(); }, - onPressIn: - onPressInProp || onPressInMessageProp - ? (payload) => { - const onPressInArgs = { + onPressIn: onPressInMessageProp + ? (payload) => { + if (onPressInMessageProp) + return onPressInMessageProp({ actionHandlers, defaultHandler: payload.defaultHandler, emitter: payload.emitter || 'message', event: payload.event, message, - }; - const handleOnpressIn = () => { - if (onPressInProp) return onPressInProp(onPressInArgs); - if (onPressInMessageProp) return onPressInMessageProp(onPressInArgs); - }; - handleOnpressIn(); - } - : null, + }); + } + : null, otherAttachments: attachments.other, preventPress, reactions, @@ -749,15 +672,7 @@ const MessageWithContext = < if (!(isMessageTypeDeleted || messageContentOrder.length)) return null; return ( - + - - - {isBounceDialogOpen && } - + + {isBounceDialogOpen && } + {messageOverlayVisible ? ( + + ) : null} - + ); }; @@ -942,9 +866,7 @@ export const Message = < const { channel, enforceUniqueReaction, members } = useChannelContext(); const chatContext = useChatContext(); const { dismissKeyboard } = useKeyboardContext(); - const { setData } = useMessageOverlayContext(); const messagesContext = useMessagesContext(); - const { setOverlay } = useOverlayContext(); const { openThread } = useThreadContext(); const { t } = useTranslationContext(); @@ -959,8 +881,6 @@ export const Message = < members, messagesContext, openThread, - setData, - setOverlay, t, }} {...props} diff --git a/package/src/components/Message/MessageSimple/MessagePinnedHeader.tsx b/package/src/components/Message/MessageSimple/MessagePinnedHeader.tsx index 08f674e6de..dc2dc67b54 100644 --- a/package/src/components/Message/MessageSimple/MessagePinnedHeader.tsx +++ b/package/src/components/Message/MessageSimple/MessagePinnedHeader.tsx @@ -22,14 +22,14 @@ import type { DefaultStreamChatGenerics } from '../../../types/types'; export type MessagePinnedHeaderPropsWithContext< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'alignment' | 'message'>; +> = Pick, 'message'>; const MessagePinnedHeaderWithContext = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( props: MessagePinnedHeaderPropsWithContext, ) => { - const { alignment, message } = props; + const { message } = props; const { theme: { colors: { grey }, @@ -40,16 +40,7 @@ const MessagePinnedHeaderWithContext = < const { t } = useTranslationContext(); const { client } = useChatContext(); return ( - + {t('Pinned by')}{' '} @@ -89,12 +80,11 @@ export const MessagePinnedHeader = < >( props: MessagePinnedHeaderProps, ) => { - const { alignment, lastGroupMessage, message } = useMessageContext(); + const { lastGroupMessage, message } = useMessageContext(); return ( - - {alignment === 'left' && } - - {showReactions && } - - + + {alignment === 'left' && } + + {showReactions && } + ); }; diff --git a/package/src/components/Message/MessageSimple/MessageTextContainer.tsx b/package/src/components/Message/MessageSimple/MessageTextContainer.tsx index 2eadb234af..187902f9f9 100644 --- a/package/src/components/Message/MessageSimple/MessageTextContainer.tsx +++ b/package/src/components/Message/MessageSimple/MessageTextContainer.tsx @@ -37,11 +37,10 @@ export type MessageTextContainerPropsWithContext< > & Pick< MessagesContextValue, - 'markdownRules' | 'MessageText' | 'myMessageTheme' + 'markdownRules' | 'MessageText' | 'myMessageTheme' | 'messageTextNumberOfLines' > & { markdownStyles?: MarkdownStyle; messageOverlay?: boolean; - messageTextNumberOfLines?: number; styles?: Partial<{ textContainer: StyleProp; }>; @@ -183,8 +182,8 @@ export const MessageTextContainer = < ) => { const { message, onLongPress, onlyEmojis, onPress, preventPress } = useMessageContext(); - const { markdownRules, MessageText, myMessageTheme } = useMessagesContext(); - const { messageTextNumberOfLines } = props; + const { markdownRules, MessageText, messageTextNumberOfLines, myMessageTheme } = + useMessagesContext(); return ( & { size: number; - supportedReactions: ReactionData[]; type: string; + supportedReactions?: ReactionData[]; }; const Icon = ({ pathFill, size, style, supportedReactions, type }: Props) => { const ReactionIcon = - supportedReactions.find((reaction) => reaction.type === type)?.Icon || Unknown; + supportedReactions?.find((reaction) => reaction.type === type)?.Icon || Unknown; return ( @@ -57,9 +57,8 @@ export type ReactionListPropsWithContext< | 'reactions' | 'showMessageOverlay' > & - Pick, 'targetedMessage'> & { + Pick, 'targetedMessage' | 'supportedReactions'> & { messageContentWidth: number; - supportedReactions: ReactionData[]; fill?: string; /** An array of the reaction objects to display in the list */ latest_reactions?: ReactionResponse[]; @@ -129,11 +128,12 @@ const ReactionListWithContext = < const width = useWindowDimensions().width; - const supportedReactionTypes = supportedReactions.map( + const supportedReactionTypes = supportedReactions?.map( (supportedReaction) => supportedReaction.type, ); + const hasSupportedReactions = reactions.some((reaction) => - supportedReactionTypes.includes(reaction.type), + supportedReactionTypes?.includes(reaction.type), ); if (!hasSupportedReactions || messageContentWidth === 0) { @@ -211,7 +211,7 @@ const ReactionListWithContext = < onPress={(event) => { if (onPress) { onPress({ - defaultHandler: () => showMessageOverlay(false), + defaultHandler: () => showMessageOverlay(true), emitter: 'reactionList', event, }); @@ -220,7 +220,7 @@ const ReactionListWithContext = < onPressIn={(event) => { if (onPressIn) { onPressIn({ - defaultHandler: () => showMessageOverlay(false), + defaultHandler: () => showMessageOverlay(true), emitter: 'reactionList', event, }); diff --git a/package/src/components/Message/MessageSimple/__tests__/MessageTextContainer.test.tsx b/package/src/components/Message/MessageSimple/__tests__/MessageTextContainer.test.tsx index af70ee73a4..c4abae945b 100644 --- a/package/src/components/Message/MessageSimple/__tests__/MessageTextContainer.test.tsx +++ b/package/src/components/Message/MessageSimple/__tests__/MessageTextContainer.test.tsx @@ -30,7 +30,7 @@ describe('MessageTextContainer', () => { user: { ...staticUser, image: undefined }, }); const { getByTestId, getByText, rerender, toJSON } = render( - + , ); @@ -41,7 +41,7 @@ describe('MessageTextContainer', () => { }); rerender( - + {message?.text}} @@ -60,7 +60,7 @@ describe('MessageTextContainer', () => { }); rerender( - + , ); 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 ce6d2d02ee..9e5b53bb71 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 @@ -8,9 +8,6 @@ exports[`MessagePinnedHeader should render message pinned 1`] = ` "display": "flex", "flexDirection": "row", }, - { - "justifyContent": "flex-start", - }, {}, ] } diff --git a/package/src/components/Message/hooks/useCreateMessageContext.ts b/package/src/components/Message/hooks/useCreateMessageContext.ts index eedd4d561e..1fb365e0bc 100644 --- a/package/src/components/Message/hooks/useCreateMessageContext.ts +++ b/package/src/components/Message/hooks/useCreateMessageContext.ts @@ -10,6 +10,7 @@ export const useCreateMessageContext = < actionsEnabled, alignment, channel, + dismissOverlay, files, goToMessage, groupStyles, @@ -62,6 +63,7 @@ export const useCreateMessageContext = < actionsEnabled, alignment, channel, + dismissOverlay, files, goToMessage, groupStyles, diff --git a/package/src/components/Message/hooks/useMessageActions.tsx b/package/src/components/Message/hooks/useMessageActions.tsx index 3ee3ad8bf1..e11c17e36f 100644 --- a/package/src/components/Message/hooks/useMessageActions.tsx +++ b/package/src/components/Message/hooks/useMessageActions.tsx @@ -6,7 +6,6 @@ import type { ChannelContextValue } from '../../../contexts/channelContext/Chann import type { ChatContextValue } from '../../../contexts/chatContext/ChatContext'; import type { MessageContextValue } from '../../../contexts/messageContext/MessageContext'; import type { MessagesContextValue } from '../../../contexts/messagesContext/MessagesContext'; -import type { OverlayContextValue } from '../../../contexts/overlayContext/OverlayContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import type { ThreadContextValue } from '../../../contexts/threadContext/ThreadContext'; import type { TranslationContextValue } from '../../../contexts/translationContext/TranslationContext'; @@ -28,40 +27,11 @@ import { removeReservedFields } from '../../../utils/removeReservedFields'; import { MessageStatusTypes } from '../../../utils/utils'; import type { MessageType } from '../../MessageList/hooks/useMessageList'; -import type { MessageActionType } from '../../MessageOverlay/MessageActionListItem'; +import type { MessageActionType } from '../../MessageMenu/MessageActionListItem'; -export const useMessageActions = < +export type MessageActionsHookProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->({ - channel, - client, - deleteMessage: deleteMessageFromContext, - deleteReaction, - enforceUniqueReaction, - handleBan, - handleBlock, - handleCopy, - handleDelete, - handleEdit, - handleFlag, - handleMute, - handlePinMessage, - handleQuotedReply, - handleReaction: handleReactionProp, - handleRetry, - handleThreadReply, - message, - onThreadSelect, - openThread, - retrySendMessage, - selectReaction, - sendReaction, - setEditingState, - setOverlay, - setQuotedMessageState, - supportedReactions, - t, -}: Pick< +> = Pick< MessagesContextValue, | 'deleteMessage' | 'sendReaction' @@ -88,12 +58,44 @@ export const useMessageActions = < > & Pick, 'channel' | 'enforceUniqueReaction'> & Pick, 'client'> & - Pick & Pick, 'openThread'> & - Pick, 'message'> & + Pick, 'dismissOverlay' | 'message'> & Pick & { onThreadSelect?: (message: MessageType) => void; - }) => { + }; + +export const useMessageActions = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>({ + channel, + client, + deleteMessage: deleteMessageFromContext, + deleteReaction, + dismissOverlay, + enforceUniqueReaction, + handleBan, + handleBlock, + handleCopy, + handleDelete, + handleEdit, + handleFlag, + handleMute, + handlePinMessage, + handleQuotedReply, + handleReaction: handleReactionProp, + handleRetry, + handleThreadReply, + message, + onThreadSelect, + openThread, + retrySendMessage, + selectReaction, + sendReaction, + setEditingState, + setQuotedMessageState, + supportedReactions, + t, +}: MessageActionsHookProps) => { const { theme: { colors: { accent_red, grey }, @@ -141,7 +143,7 @@ export const useMessageActions = < const banUser: MessageActionType = { action: async () => { - setOverlay('none'); + dismissOverlay(); if (message.user?.id) { if (handleBan) { handleBan(message); @@ -160,7 +162,7 @@ export const useMessageActions = < */ const blockUser: MessageActionType = { action: async () => { - setOverlay('none'); + dismissOverlay(); if (message.user?.id) { if (handleBlock) { handleBlock(message); @@ -176,7 +178,7 @@ export const useMessageActions = < const copyMessage: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handleCopy) { handleCopy(message); } @@ -189,7 +191,7 @@ export const useMessageActions = < const deleteMessage: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handleDelete) { handleDelete(message); } @@ -203,7 +205,7 @@ export const useMessageActions = < const editMessage: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handleEdit) { handleEdit(message); } @@ -216,7 +218,7 @@ export const useMessageActions = < const pinMessage: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handlePinMessage) { handlePinMessage(message); } @@ -229,7 +231,7 @@ export const useMessageActions = < const unpinMessage: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handlePinMessage) { handlePinMessage(message); } @@ -242,7 +244,7 @@ export const useMessageActions = < const flagMessage: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handleFlag) { handleFlag(message); } @@ -268,7 +270,7 @@ export const useMessageActions = < const muteUser: MessageActionType = { action: async () => { - setOverlay('none'); + dismissOverlay(); if (message.user?.id) { if (handleMute) { handleMute(message); @@ -284,7 +286,7 @@ export const useMessageActions = < const quotedReply: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handleQuotedReply) { handleQuotedReply(message); } @@ -297,7 +299,7 @@ export const useMessageActions = < const retry: MessageActionType = { action: async () => { - setOverlay('none'); + dismissOverlay(); const messageWithoutReservedFields = removeReservedFields(message); if (handleRetry) { handleRetry(messageWithoutReservedFields as MessageType); @@ -312,7 +314,7 @@ export const useMessageActions = < const threadReply: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handleThreadReply) { handleThreadReply(message); } diff --git a/package/src/components/Message/hooks/useProcessReactions.ts b/package/src/components/Message/hooks/useProcessReactions.ts index 96a9cea310..69bd55d237 100644 --- a/package/src/components/Message/hooks/useProcessReactions.ts +++ b/package/src/components/Message/hooks/useProcessReactions.ts @@ -48,13 +48,13 @@ const isOwnReaction = < ownReactions?: ReactionResponse[] | null, ) => (ownReactions ? ownReactions.some((reaction) => reaction.type === reactionType) : false); -const isSupportedReaction = (reactionType: string, supportedReactions: ReactionData[]) => +const isSupportedReaction = (reactionType: string, supportedReactions?: ReactionData[]) => supportedReactions ? supportedReactions.some((reactionOption) => reactionOption.type === reactionType) : false; -const getEmojiByReactionType = (reactionType: string, supportedReactions: ReactionData[]) => - supportedReactions.find(({ type }) => type === reactionType)?.Icon ?? null; +const getEmojiByReactionType = (reactionType: string, supportedReactions?: ReactionData[]) => + supportedReactions ? supportedReactions.find(({ type }) => type === reactionType)?.Icon : null; const getLatestReactedUserNames = (reactionType: string, latestReactions?: ReactionResponse[]) => latestReactions diff --git a/package/src/components/Message/utils/messageActions.ts b/package/src/components/Message/utils/messageActions.ts index fea05791b2..c79c1736d6 100644 --- a/package/src/components/Message/utils/messageActions.ts +++ b/package/src/components/Message/utils/messageActions.ts @@ -2,7 +2,7 @@ import type { MessageContextValue } from '../../../contexts/messageContext/Messa import type { OwnCapabilitiesContextValue } from '../../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext'; import { isClipboardAvailable } from '../../../native'; import type { DefaultStreamChatGenerics } from '../../../types/types'; -import type { MessageActionType } from '../../MessageOverlay/MessageActionListItem'; +import type { MessageActionType } from '../../MessageMenu/MessageActionListItem'; export type MessageActionsParams< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, @@ -14,20 +14,16 @@ export type MessageActionsParams< editMessage: MessageActionType; error: boolean | Error; flagMessage: MessageActionType; - /** - * Determines if the message actions are visible. - */ - isMessageActionsVisible: boolean; isThreadMessage: boolean; - /** - * @deprecated use `isMessageActionsVisible` instead. - */ - messageReactions: boolean; muteUser: MessageActionType; ownCapabilities: OwnCapabilitiesContextValue; pinMessage: MessageActionType; quotedReply: MessageActionType; retry: MessageActionType; + /** + * Determines if the message actions are visible. + */ + showMessageReactions: boolean; threadReply: MessageActionType; unpinMessage: MessageActionType; /** @@ -50,19 +46,18 @@ export const messageActions = < editMessage, error, flagMessage, - isMessageActionsVisible, isMyMessage, isThreadMessage, message, - messageReactions, ownCapabilities, pinMessage, quotedReply, retry, + showMessageReactions, threadReply, unpinMessage, }: MessageActionsParams) => { - if (messageReactions || !isMessageActionsVisible) { + if (showMessageReactions) { return []; } diff --git a/package/src/components/MessageInput/MessageInput.tsx b/package/src/components/MessageInput/MessageInput.tsx index 20a9cf52fb..6fb7652679 100644 --- a/package/src/components/MessageInput/MessageInput.tsx +++ b/package/src/components/MessageInput/MessageInput.tsx @@ -693,7 +693,6 @@ const MessageInputWithContext = < micButton: useAnimatedStyle(() => ({ opacity: interpolate(micPositionX.value, [0, X_AXIS_POSITION], [1, 0], Extrapolation.CLAMP), transform: [{ translateX: micPositionX.value }, { translateY: micPositionY.value }], - zIndex: 2, })), slideToCancel: useAnimatedStyle(() => ({ opacity: interpolate(micPositionX.value, [0, X_AXIS_POSITION], [1, 0], Extrapolation.CLAMP), diff --git a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingLockIndicator.tsx b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingLockIndicator.tsx index 936fcd3381..81f934d08e 100644 --- a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingLockIndicator.tsx +++ b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingLockIndicator.tsx @@ -80,6 +80,5 @@ const styles = StyleSheet.create({ padding: 8, position: 'absolute', right: 0, - zIndex: 1, }, }); diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx index c60ee66a3f..df9010e42d 100644 --- a/package/src/components/MessageList/MessageList.tsx +++ b/package/src/components/MessageList/MessageList.tsx @@ -660,7 +660,7 @@ const MessageListWithContext = < message={message} onThreadSelect={onThreadSelect} showUnreadUnderlay={showUnreadUnderlay} - style={[{ paddingHorizontal: screenPadding }, messageContainer]} + style={[messageContainer]} threadList={threadList} /> ); diff --git a/package/src/components/MessageMenu/MessageActionList.tsx b/package/src/components/MessageMenu/MessageActionList.tsx new file mode 100644 index 0000000000..4f5901dfcc --- /dev/null +++ b/package/src/components/MessageMenu/MessageActionList.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import { MessageActionType } from './MessageActionListItem'; + +import { MessagesContextValue } from '../../contexts/messagesContext/MessagesContext'; +import { useTheme } from '../../contexts/themeContext/ThemeContext'; + +export type MessageActionListProps = Pick & { + /** + * Function to close the message actions bottom sheet + * @returns void + */ + dismissOverlay?: () => void; + /** + * An array of message actions to render + */ + messageActions?: MessageActionType[]; +}; + +export const MessageActionList = (props: MessageActionListProps) => { + const { MessageActionListItem, messageActions } = props; + const { + theme: { + messageMenu: { + actionList: { container }, + }, + }, + } = useTheme(); + + if (messageActions?.length === 0) return null; + + return ( + + {messageActions?.map((messageAction, index) => ( + + ))} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingHorizontal: 16, + }, +}); diff --git a/package/src/components/MessageMenu/MessageActionListItem.tsx b/package/src/components/MessageMenu/MessageActionListItem.tsx new file mode 100644 index 0000000000..ca5f57aa54 --- /dev/null +++ b/package/src/components/MessageMenu/MessageActionListItem.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { Pressable, StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native'; + +import { useTheme } from '../../contexts/themeContext/ThemeContext'; + +export type ActionType = + | 'banUser' + | 'blockUser' + | 'copyMessage' + | 'deleteMessage' + | 'editMessage' + | 'flagMessage' + | 'muteUser' + | 'pinMessage' + | 'selectReaction' + | 'reply' + | 'retry' + | 'quotedReply' + | 'threadReply' + | 'unpinMessage'; + +export type MessageActionType = { + /** + * Callback when user presses the action button. + */ + action: () => void; + /** + * Type of the action performed. + * Eg: 'banUser', 'blockUser', 'copyMessage', 'deleteMessage', 'editMessage', 'flagMessage', 'muteUser', 'pinMessage', 'selectReaction', 'reply', 'retry', 'quotedReply', 'threadReply', 'unpinMessage' + */ + actionType: ActionType | string; + /** + * Title for action button. + */ + title: string; + /** + * Element to render as icon for action button. + */ + icon?: React.ReactElement; + /** + * Styles for underlying Text component of action title. + */ + titleStyle?: StyleProp; +}; + +/** + * MessageActionListItem - A high-level component that implements all the logic required for a `MessageAction` in a `MessageActionList` + */ +export type MessageActionListItemProps = MessageActionType; + +export const MessageActionListItem = (props: MessageActionListItemProps) => { + const { action, actionType, icon, title, titleStyle } = props; + + const { + theme: { + colors: { black }, + messageMenu: { + actionListItem: { container, icon: iconTheme, title: titleTheme }, + }, + }, + } = useTheme(); + + return ( + [{ opacity: pressed ? 0.5 : 1 }]}> + + {icon} + {title} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'flex-start', + paddingVertical: 8, + }, + titleStyle: { + paddingLeft: 16, + }, +}); diff --git a/package/src/components/MessageMenu/MessageMenu.tsx b/package/src/components/MessageMenu/MessageMenu.tsx new file mode 100644 index 0000000000..cb3e7f4778 --- /dev/null +++ b/package/src/components/MessageMenu/MessageMenu.tsx @@ -0,0 +1,123 @@ +import React from 'react'; + +import { useWindowDimensions } from 'react-native'; + +import { MessageActionType } from './MessageActionListItem'; + +import { + MessageContextValue, + useMessageContext, +} from '../../contexts/messageContext/MessageContext'; +import { + MessagesContextValue, + useMessagesContext, +} from '../../contexts/messagesContext/MessagesContext'; +import { DefaultStreamChatGenerics } from '../../types/types'; +import { BottomSheetModal } from '../UIComponents/BottomSheetModal'; + +export type MessageMenuProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Partial< + Pick< + MessagesContextValue, + | 'MessageActionList' + | 'MessageActionListItem' + | 'MessageReactionPicker' + | 'MessageUserReactions' + | 'MessageUserReactionsAvatar' + | 'MessageUserReactionsItem' + > +> & + Partial, 'message'>> & { + /** + * Function to close the message actions bottom sheet + * @returns void + */ + dismissOverlay: () => void; + /** + * An array of message actions to render + */ + messageActions: MessageActionType[]; + /** + * Boolean to determine if there are message actions + */ + showMessageReactions: boolean; + /** + * Boolean to determine if the overlay is visible. + */ + visible: boolean; + /** + * Function to handle reaction on press + * @param reactionType + * @returns + */ + handleReaction?: (reactionType: string) => Promise; + }; + +export const MessageMenu = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + props: MessageMenuProps, +) => { + const { + dismissOverlay, + handleReaction, + message: propMessage, + MessageActionList: propMessageActionList, + MessageActionListItem: propMessageActionListItem, + messageActions, + MessageReactionPicker: propMessageReactionPicker, + MessageUserReactions: propMessageUserReactions, + MessageUserReactionsAvatar: propMessageUserReactionsAvatar, + MessageUserReactionsItem: propMessageUserReactionsItem, + showMessageReactions, + visible, + } = props; + const { height } = useWindowDimensions(); + const { + MessageActionList: contextMessageActionList, + MessageActionListItem: contextMessageActionListItem, + MessageReactionPicker: contextMessageReactionPicker, + MessageUserReactions: contextMessageUserReactions, + MessageUserReactionsAvatar: contextMessageUserReactionsAvatar, + MessageUserReactionsItem: contextMessageUserReactionsItem, + } = useMessagesContext(); + const { message: contextMessage } = useMessageContext(); + const MessageActionList = propMessageActionList ?? contextMessageActionList; + const MessageActionListItem = propMessageActionListItem ?? contextMessageActionListItem; + const MessageReactionPicker = propMessageReactionPicker ?? contextMessageReactionPicker; + const MessageUserReactions = propMessageUserReactions ?? contextMessageUserReactions; + const MessageUserReactionsAvatar = + propMessageUserReactionsAvatar ?? contextMessageUserReactionsAvatar; + const MessageUserReactionsItem = propMessageUserReactionsItem ?? contextMessageUserReactionsItem; + const message = propMessage ?? contextMessage; + + return ( + + {showMessageReactions ? ( + + ) : ( + <> + reaction.type) || []} + /> + + + )} + + ); +}; diff --git a/package/src/components/MessageMenu/MessageReactionPicker.tsx b/package/src/components/MessageMenu/MessageReactionPicker.tsx new file mode 100644 index 0000000000..67c5462fca --- /dev/null +++ b/package/src/components/MessageMenu/MessageReactionPicker.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import { ReactionButton } from './ReactionButton'; + +import { + MessagesContextValue, + useMessagesContext, +} from '../../contexts/messagesContext/MessagesContext'; + +import { useOwnCapabilitiesContext } from '../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext'; +import { useTheme } from '../../contexts/themeContext/ThemeContext'; +import { triggerHaptic } from '../../native'; +import type { DefaultStreamChatGenerics } from '../../types/types'; + +export type MessageReactionPickerProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Pick, 'supportedReactions'> & { + /** + * Function to dismiss the action bottom sheet. + * @returns void + */ + dismissOverlay: () => void; + /** + * An array of reaction types that the current user has reacted with + */ + ownReactionTypes: string[]; + /** + * Function to handle reaction on press + * @param reactionType + * @returns + */ + handleReaction?: (reactionType: string) => Promise; +}; + +/** + * MessageReactionPicker - A high level component which implements all the logic required for a message overlay reaction list + */ +export const MessageReactionPicker = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>( + props: MessageReactionPickerProps, +) => { + const { + dismissOverlay, + handleReaction, + ownReactionTypes, + supportedReactions: propSupportedReactions, + } = props; + const { supportedReactions: contextSupportedReactions } = useMessagesContext(); + const { + theme: { + messageMenu: { + reactionPicker: { container }, + }, + }, + } = useTheme(); + const own_capabilities = useOwnCapabilitiesContext(); + + const supportedReactions = propSupportedReactions || contextSupportedReactions; + + const onSelectReaction = (type: string) => { + triggerHaptic('impactLight'); + if (handleReaction) { + handleReaction(type); + } + dismissOverlay(); + }; + + if (!own_capabilities.sendReaction) { + return null; + } + + return ( + + {supportedReactions?.map(({ Icon, type }, index) => ( + + ))} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 16, + paddingHorizontal: 16, + }, +}); diff --git a/package/src/components/MessageMenu/MessageUserReactions.tsx b/package/src/components/MessageMenu/MessageUserReactions.tsx new file mode 100644 index 0000000000..2dcda50d4c --- /dev/null +++ b/package/src/components/MessageMenu/MessageUserReactions.tsx @@ -0,0 +1,184 @@ +import React, { useMemo } from 'react'; +import { FlatList, StyleSheet, Text, View } from 'react-native'; + +import { ReactionSortBase } from 'stream-chat'; + +import { useFetchReactions } from './hooks/useFetchReactions'; +import { ReactionButton } from './ReactionButton'; + +import { + MessagesContextValue, + useMessagesContext, +} from '../../contexts/messagesContext/MessagesContext'; +import { useTheme } from '../../contexts/themeContext/ThemeContext'; +import { useTranslationContext } from '../../contexts/translationContext/TranslationContext'; +import { DefaultStreamChatGenerics, Reaction } from '../../types/types'; +import { ReactionData } from '../../utils/utils'; +import { MessageType } from '../MessageList/hooks/useMessageList'; + +export type MessageUserReactionsProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Partial< + Pick< + MessagesContextValue, + 'MessageUserReactionsAvatar' | 'MessageUserReactionsItem' | 'supportedReactions' + > +> & { + /** + * The message object + */ + message?: MessageType; + /** + * An array of reactions + */ + reactions?: Reaction[]; +}; + +const sort: ReactionSortBase = { + created_at: -1, +}; + +export const MessageUserReactions = (props: MessageUserReactionsProps) => { + const { + message, + MessageUserReactionsAvatar: propMessageUserReactionsAvatar, + MessageUserReactionsItem: propMessageUserReactionsItem, + reactions: propReactions, + supportedReactions: propSupportedReactions, + } = props; + const reactionTypes = Object.keys(message?.reaction_groups ?? {}); + const [selectedReaction, setSelectedReaction] = React.useState( + reactionTypes[0], + ); + const { + MessageUserReactionsAvatar: contextMessageUserReactionsAvatar, + MessageUserReactionsItem: contextMessageUserReactionsItem, + supportedReactions: contextSupportedReactions, + } = useMessagesContext(); + const supportedReactions = propSupportedReactions ?? contextSupportedReactions; + const MessageUserReactionsAvatar = + propMessageUserReactionsAvatar ?? contextMessageUserReactionsAvatar; + const MessageUserReactionsItem = propMessageUserReactionsItem ?? contextMessageUserReactionsItem; + + const messageReactions = useMemo( + () => + reactionTypes.reduce((acc, reaction) => { + const reactionData = supportedReactions?.find( + (supportedReaction) => supportedReaction.type === reaction, + ); + if (reactionData) { + acc.push(reactionData); + } + return acc; + }, []), + [reactionTypes, supportedReactions], + ); + + const { + loading, + loadNextPage, + reactions: fetchedReactions, + } = useFetchReactions({ + messageId: message?.id, + reactionType: selectedReaction, + sort, + }); + + const { + theme: { + messageMenu: { + userReactions: { + container, + flatlistColumnContainer, + flatlistContainer, + reactionSelectorContainer, + reactionsText, + }, + }, + }, + } = useTheme(); + const { t } = useTranslationContext(); + + const reactions = useMemo( + () => + propReactions || + (fetchedReactions.map((reaction) => ({ + id: reaction.user?.id, + image: reaction.user?.image, + name: reaction.user?.name, + type: reaction.type, + })) as Reaction[]), + [propReactions, fetchedReactions], + ); + + const renderItem = ({ item }: { item: Reaction }) => ( + + ); + + const renderHeader = () => ( + {t('Message Reactions')} + ); + + const onSelectReaction = (reactionType: string) => { + setSelectedReaction(reactionType); + }; + + return ( + + + {messageReactions?.map(({ Icon, type }, index) => ( + + ))} + + + {!loading ? ( + item.id} + ListHeaderComponent={renderHeader} + numColumns={4} + onEndReached={loadNextPage} + renderItem={renderItem} + /> + ) : null} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + flatListColumnContainer: { + justifyContent: 'space-evenly', + }, + flatListContainer: { + justifyContent: 'center', + }, + reactionSelectorContainer: { + flexDirection: 'row', + justifyContent: 'space-evenly', + }, + reactionsText: { + fontSize: 16, + fontWeight: 'bold', + marginVertical: 16, + textAlign: 'center', + }, +}); diff --git a/package/src/components/MessageOverlay/OverlayReactionsAvatar.tsx b/package/src/components/MessageMenu/MessageUserReactionsAvatar.tsx similarity index 62% rename from package/src/components/MessageOverlay/OverlayReactionsAvatar.tsx rename to package/src/components/MessageMenu/MessageUserReactionsAvatar.tsx index 26e0a4ccc7..3f8f38f4d5 100644 --- a/package/src/components/MessageOverlay/OverlayReactionsAvatar.tsx +++ b/package/src/components/MessageMenu/MessageUserReactionsAvatar.tsx @@ -1,15 +1,17 @@ import React from 'react'; -import type { Reaction } from './OverlayReactions'; - import { useTheme } from '../../contexts/themeContext/ThemeContext'; +import { Reaction } from '../../types/types'; import { Avatar, AvatarProps } from '../Avatar/Avatar'; -export type OverlayReactionsAvatarProps = { +export type MessageUserReactionsAvatarProps = { + /** + * The reaction object + */ reaction: Reaction; } & Partial>; -export const OverlayReactionsAvatar = (props: OverlayReactionsAvatarProps) => { +export const MessageUserReactionsAvatar = (props: MessageUserReactionsAvatarProps) => { const { reaction: { image, name }, size, @@ -23,5 +25,3 @@ export const OverlayReactionsAvatar = (props: OverlayReactionsAvatarProps) => { return ; }; - -OverlayReactionsAvatar.displayName = 'OverlayReactionsAvatar{overlay{reactionsAvatar}}'; diff --git a/package/src/components/MessageMenu/MessageUserReactionsItem.tsx b/package/src/components/MessageMenu/MessageUserReactionsItem.tsx new file mode 100644 index 0000000000..6942e7937b --- /dev/null +++ b/package/src/components/MessageMenu/MessageUserReactionsItem.tsx @@ -0,0 +1,130 @@ +import React from 'react'; + +import { StyleSheet, Text, View } from 'react-native'; + +import { useChatContext } from '../../contexts/chatContext/ChatContext'; +import { MessagesContextValue } from '../../contexts/messagesContext/MessagesContext'; +import { useTheme } from '../../contexts/themeContext/ThemeContext'; +import { Unknown } from '../../icons'; + +import type { DefaultStreamChatGenerics, Reaction } from '../../types/types'; +import { ReactionData } from '../../utils/utils'; + +export type MessageUserReactionsItemProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Pick, 'MessageUserReactionsAvatar'> & { + /** + * The reaction object + */ + reaction: Reaction; + /** + * An array of supported reactions + */ + supportedReactions: ReactionData[]; +}; + +export const MessageUserReactionsItem = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>({ + MessageUserReactionsAvatar, + reaction, + supportedReactions, +}: MessageUserReactionsItemProps) => { + const { id, name, type } = reaction; + const { + theme: { + colors: { accent_blue, black, grey, grey_gainsboro, white }, + messageMenu: { + userReactions: { + avatarContainer, + avatarName, + avatarSize, + radius, + reactionBubbleBackground, + reactionBubbleBorderRadius, + }, + }, + }, + } = useTheme(); + const { client } = useChatContext(); + const alignment = client.userID && client.userID === id ? 'left' : 'right'; + const x = avatarSize / 2 - (avatarSize / (radius * 4)) * (alignment === 'left' ? 1 : -1); + const y = avatarSize - radius; + + const left = + alignment === 'left' + ? x - + (Number(reactionBubbleBackground.width || 0) || styles.reactionBubbleBackground.width) + + radius + : x - radius; + const top = + y - + radius - + (Number(reactionBubbleBackground.height || 0) || styles.reactionBubbleBackground.height); + + const Icon = supportedReactions.find((reaction) => reaction.type === type)?.Icon ?? Unknown; + + return ( + + + + + + + + + + {name} + + + + ); +}; + +const styles = StyleSheet.create({ + avatarContainer: { + marginBottom: 8, + }, + avatarInnerContainer: { + alignSelf: 'center', + }, + avatarName: { + flex: 1, + fontSize: 12, + fontWeight: '700', + paddingTop: 6, + textAlign: 'center', + }, + avatarNameContainer: { + alignItems: 'center', + flexDirection: 'row', + flexGrow: 1, + }, + reactionBubbleBackground: { + alignItems: 'center', + borderRadius: 24, + height: 24, + justifyContent: 'center', + position: 'absolute', + width: 24, + }, +}); diff --git a/package/src/components/MessageMenu/ReactionButton.tsx b/package/src/components/MessageMenu/ReactionButton.tsx new file mode 100644 index 0000000000..a017628849 --- /dev/null +++ b/package/src/components/MessageMenu/ReactionButton.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { Pressable, StyleSheet } from 'react-native'; + +import { useTheme } from '../../contexts/themeContext/ThemeContext'; +import { IconProps } from '../../icons'; + +type ReactionButtonProps = { + /** + * Icon to display for the reaction button + */ + Icon: React.ComponentType; + /** + * Whether the reaction button is selected + */ + selected: boolean; + /** + * The type of reaction + */ + type: string; + /** + * Function to call when the reaction button is pressed + * @param reactionType + * @returns + */ + onPress?: (reactionType: string) => void; +}; + +export const ReactionButton = (props: ReactionButtonProps) => { + const { Icon, onPress, selected, type } = props; + const { + theme: { + colors: { light_blue, white }, + messageMenu: { + reactionButton: { filledColor, unfilledColor }, + reactionPicker: { buttonContainer, reactionIconSize }, + }, + }, + } = useTheme(); + + const onPressHandler = () => { + if (onPress) { + onPress(type); + } + }; + + return ( + [ + styles.reactionButton, + { backgroundColor: pressed ? light_blue : white }, + buttonContainer, + ]} + > + + + ); +}; + +const styles = StyleSheet.create({ + reactionButton: { + alignItems: 'center', + borderRadius: 8, + justifyContent: 'center', + padding: 8, + }, +}); diff --git a/package/src/components/MessageMenu/__tests__/MessageActionList.test.tsx b/package/src/components/MessageMenu/__tests__/MessageActionList.test.tsx new file mode 100644 index 0000000000..ad5e51cccc --- /dev/null +++ b/package/src/components/MessageMenu/__tests__/MessageActionList.test.tsx @@ -0,0 +1,48 @@ +// MessageActionList.test.tsx + +import React from 'react'; + +import { Text } from 'react-native'; + +import { render } from '@testing-library/react-native'; + +import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext'; +import { defaultTheme } from '../../../contexts/themeContext/utils/theme'; +import { MessageActionList } from '../MessageActionList'; +import { MessageActionListItemProps } from '../MessageActionListItem'; + +const MockMessageActionListItem = (props: MessageActionListItemProps) => {props.title}; + +const defaultProps = { + MessageActionListItem: MockMessageActionListItem, + messageActions: [ + { action: jest.fn(), actionType: 'copyMessage', title: 'Copy Message' }, + { action: jest.fn(), actionType: 'deleteMessage', title: 'Delete Message' }, + ], +}; + +describe('MessageActionList', () => { + it('should render correctly with provided message actions', () => { + const { getByLabelText, getByText } = render( + + + , + ); + + expect(getByLabelText('Message action list')).toBeTruthy(); + + expect(getByText('Copy Message')).toBeTruthy(); + expect(getByText('Delete Message')).toBeTruthy(); + }); + + it('should pass the correct props to MessageActionListItem', () => { + const { getByText } = render( + + + , + ); + + expect(getByText('Copy Message')).toBeTruthy(); + expect(getByText('Delete Message')).toBeTruthy(); + }); +}); diff --git a/package/src/components/MessageMenu/__tests__/MessageActionListItem.test.tsx b/package/src/components/MessageMenu/__tests__/MessageActionListItem.test.tsx new file mode 100644 index 0000000000..ec5ac0f4b3 --- /dev/null +++ b/package/src/components/MessageMenu/__tests__/MessageActionListItem.test.tsx @@ -0,0 +1,48 @@ +// MessageActionListItem.test.tsx + +import React from 'react'; + +import { Text } from 'react-native'; + +import { fireEvent, render } from '@testing-library/react-native'; + +import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext'; +import { defaultTheme } from '../../../contexts/themeContext/utils/theme'; +import { MessageActionListItem } from '../MessageActionListItem'; + +describe('MessageActionListItem', () => { + const mockAction = jest.fn(); + + const defaultProps = { + action: mockAction, + actionType: 'copyMessage', + icon: Icon, + title: 'Copy Message', + }; + + it('should render correctly with given props', () => { + const { getByLabelText, getByText } = render( + + + , + ); + + expect(getByText('Copy Message')).toBeTruthy(); + + expect(getByText('Icon')).toBeTruthy(); + + expect(getByLabelText('copyMessage action list item')).toBeTruthy(); + }); + + it('should call the action callback when pressed', () => { + const { getByLabelText } = render( + + + , + ); + + fireEvent.press(getByLabelText('copyMessage action list item')); + + expect(mockAction).toHaveBeenCalled(); + }); +}); diff --git a/package/src/components/MessageMenu/__tests__/MessageReactionPicker.test.tsx b/package/src/components/MessageMenu/__tests__/MessageReactionPicker.test.tsx new file mode 100644 index 0000000000..5528c84251 --- /dev/null +++ b/package/src/components/MessageMenu/__tests__/MessageReactionPicker.test.tsx @@ -0,0 +1,101 @@ +import React from 'react'; + +import { fireEvent, render } from '@testing-library/react-native'; + +import { + MessagesContextValue, + MessagesProvider, +} from '../../../contexts/messagesContext/MessagesContext'; +import { + OwnCapabilitiesContextValue, + OwnCapabilitiesProvider, +} from '../../../contexts/ownCapabilitiesContext/OwnCapabilitiesContext'; +import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext'; +import { defaultTheme } from '../../../contexts/themeContext/utils/theme'; +import { triggerHaptic } from '../../../native'; +import { MessageReactionPicker } from '../MessageReactionPicker'; + +jest.mock('../../../native', () => ({ + triggerHaptic: jest.fn(), +})); + +const mockSupportedReactions = [ + { Icon: () => null, type: 'like' }, + { Icon: () => null, type: 'love' }, +]; + +const defaultProps = { + dismissOverlay: jest.fn(), + handleReaction: jest.fn(), + ownReactionTypes: ['like'], +}; + +const renderComponent = (props = {}, ownCapabilities = { sendReaction: true }) => + render( + + + + + + + , + ); + +describe('MessageReactionPicker', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly with supported reactions', () => { + const { getAllByLabelText, getByLabelText } = renderComponent(); + expect(getByLabelText('Reaction Selector on long pressing message')).toBeTruthy(); + expect(getAllByLabelText(/\breaction-button[^\s]+/)).toHaveLength( + mockSupportedReactions.length, + ); + }); + + it('does not render when sendReaction capability is false', () => { + const { queryByLabelText } = renderComponent({}, { sendReaction: false }); + expect(queryByLabelText('Reaction Selector on long pressing message')).toBeNull(); + }); + + it('marks own reactions as selected', () => { + const { getAllByLabelText } = renderComponent(); + const reactionButtons = getAllByLabelText(/\breaction-button[^\s]+/); + + expect(reactionButtons[0].props.accessibilityLabel).toBe('reaction-button-like-selected'); + expect(reactionButtons[1].props.accessibilityLabel).toBe('reaction-button-love-unselected'); + }); + + it('calls handleReaction and dismissOverlay when a reaction is pressed', () => { + const { getAllByLabelText } = renderComponent(); + const reactionButtons = getAllByLabelText(/\breaction-button[^\s]+/); + + fireEvent.press(reactionButtons[1]); + + expect(defaultProps.handleReaction).toHaveBeenCalledWith('love'); + expect(defaultProps.dismissOverlay).toHaveBeenCalled(); + expect(triggerHaptic).toHaveBeenCalledWith('impactLight'); + }); + + it("doesn't call handleReaction when it's not provided", () => { + const { getAllByLabelText } = renderComponent({ handleReaction: undefined }); + const reactionButtons = getAllByLabelText(/\breaction-button[^\s]+/); + + fireEvent.press(reactionButtons[1]); + + expect(defaultProps.handleReaction).not.toHaveBeenCalled(); + }); + + it('uses provided supportedReactions prop over context value', () => { + const customSupportedReactions = [ + { Icon: () => null, type: 'wow' }, + { Icon: () => null, type: 'haha' }, + ]; + const { getAllByLabelText } = renderComponent({ supportedReactions: customSupportedReactions }); + const reactionButtons = getAllByLabelText(/\breaction-button[^\s]+/); + expect(reactionButtons).toHaveLength(customSupportedReactions.length); + }); +}); diff --git a/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx b/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx new file mode 100644 index 0000000000..50db95dd39 --- /dev/null +++ b/package/src/components/MessageMenu/__tests__/MessageUserReactions.test.tsx @@ -0,0 +1,186 @@ +import React from 'react'; + +import { Text } from 'react-native'; + +import { fireEvent, render } from '@testing-library/react-native'; + +import { ReactionResponse } from 'stream-chat'; + +import { + MessagesContextValue, + MessagesProvider, +} from '../../../contexts/messagesContext/MessagesContext'; +import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext'; +import { defaultTheme } from '../../../contexts/themeContext/utils/theme'; +import { + TranslationContextValue, + TranslationProvider, +} from '../../../contexts/translationContext/TranslationContext'; +import { generateMessage } from '../../../mock-builders/generator/message'; +import { MessageType } from '../../MessageList/hooks/useMessageList'; +import * as useFetchReactionsModule from '../hooks/useFetchReactions'; +import { MessageUserReactions } from '../MessageUserReactions'; +import { MessageUserReactionsItemProps } from '../MessageUserReactionsItem'; + +const mockTranslations = { + t: jest.fn((key) => key), +}; + +const mockSupportedReactions = [ + { Icon: () => null, type: 'like' }, + { Icon: () => null, type: 'love' }, +]; + +const defaultProps = { + message: { + ...generateMessage(), + reaction_groups: { like: { count: 1, sum_scores: 1 }, love: { count: 1, sum_scores: 1 } }, + } as unknown as MessageType, + supportedReactions: mockSupportedReactions, +}; + +describe('MessageUserReactions when the supportedReactions are defined', () => { + beforeAll(() => { + const mockLoadNextPage = jest.fn(); + + const mockUseFetchReactions = jest.spyOn(useFetchReactionsModule, 'useFetchReactions'); + mockUseFetchReactions.mockReturnValue({ + loading: false, + loadNextPage: mockLoadNextPage, + reactions: [ + { + type: 'like', + user: { id: '1', image: 'user1.jpg', name: 'User 1' }, + } as unknown as ReactionResponse, + { + type: 'love', + user: { id: '2', image: 'user2.jpg', name: 'User 2' }, + } as unknown as ReactionResponse, + ], + }); + }); + const renderComponent = (props = {}) => + render( + + + null, + MessageUserReactionsItem: (props: MessageUserReactionsItemProps) => ( + {props.reaction.id + ' ' + props.reaction.type} + ), + } as unknown as MessagesContextValue + } + > + + + + , + ); + + it('renders correctly', () => { + const { getByLabelText, getByText } = renderComponent(); + expect(getByLabelText('User Reactions on long press message')).toBeTruthy(); + expect(getByText('Message Reactions')).toBeTruthy(); + }); + + it('renders reaction buttons', () => { + const { getByLabelText } = renderComponent(); + const likeReactionButton = getByLabelText('reaction-button-like-selected'); + expect(likeReactionButton).toBeDefined(); + const loveReactionButton = getByLabelText('reaction-button-love-unselected'); + expect(loveReactionButton).toBeDefined(); + }); + + it('selects the first reaction by default', () => { + const { getAllByLabelText } = renderComponent(); + const reactionButtons = getAllByLabelText(/\breaction-button[^\s]+/); + expect(reactionButtons[0].props.accessibilityLabel).toBe('reaction-button-like-selected'); + expect(reactionButtons[1].props.accessibilityLabel).toBe('reaction-button-love-unselected'); + }); + + it('changes selected reaction when a reaction button is pressed', () => { + const { getAllByLabelText } = renderComponent(); + const reactionButtons = getAllByLabelText(/\breaction-button[^\s]+/); + + fireEvent.press(reactionButtons[1]); + + expect(reactionButtons[0].props.accessibilityLabel).toBe('reaction-button-like-unselected'); + expect(reactionButtons[1].props.accessibilityLabel).toBe('reaction-button-love-selected'); + }); + + it('renders reactions list', async () => { + const { getByText } = renderComponent(); + const reactionItems = await getByText('1 like'); + expect(reactionItems).toBeDefined(); + }); + + it('uses provided reactions when passed as a prop', () => { + const propReactions = [{ id: '3', image: 'user3.jpg', name: 'User 3', type: 'wow' }]; + const { getByText } = renderComponent({ reactions: propReactions }); + const reactionItem = getByText('3 wow'); + expect(reactionItem).toBeDefined(); + }); + + it("don't render reaction buttons that is of unsupported type", () => { + const { queryAllByLabelText } = renderComponent({ + message: { ...generateMessage(), reaction_groups: { money: 1 } }, + }); + const reactionButtons = queryAllByLabelText(/\breaction-button[^\s]+/); + + expect(reactionButtons.length).toBe(0); + }); +}); + +const renderComponent = (props = {}) => + render( + + + null, + MessageUserReactionsItem: (props: MessageUserReactionsItemProps) => ( + {props.reaction.id + ' ' + props.reaction.type} + ), + } as unknown as MessagesContextValue + } + > + + + + , + ); + +describe("MessageUserReactions when the supportedReactions aren't defined", () => { + it("don't render reaction buttons that is of unsupported type", () => { + const { queryAllByLabelText } = renderComponent({ + message: { ...generateMessage(), reaction_groups: undefined }, + supportedReactions: undefined, + }); + const reactionButtons = queryAllByLabelText(/\breaction-button[^\s]+/); + + expect(reactionButtons.length).toBe(0); + }); +}); + +describe('MessageUserReactions when the reactions are in loading phase', () => { + beforeAll(() => { + const mockLoadNextPage = jest.fn(); + + const mockUseFetchReactions = jest.spyOn(useFetchReactionsModule, 'useFetchReactions'); + mockUseFetchReactions.mockReturnValue({ + loading: true, + loadNextPage: mockLoadNextPage, + reactions: [], + }); + }); + + it("don't render reactions flatlist when loading is false", () => { + const { queryByLabelText } = renderComponent(); + const reactionsFlatList = queryByLabelText('reaction-flat-list'); + + expect(reactionsFlatList).toBeNull(); + }); +}); diff --git a/package/src/components/MessageMenu/__tests__/MessageUserReactionsAvatar.test.tsx b/package/src/components/MessageMenu/__tests__/MessageUserReactionsAvatar.test.tsx new file mode 100644 index 0000000000..90b6b0848d --- /dev/null +++ b/package/src/components/MessageMenu/__tests__/MessageUserReactionsAvatar.test.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import { render } from '@testing-library/react-native'; + +import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext'; +import { defaultTheme } from '../../../contexts/themeContext/utils/theme'; +import { MessageUserReactionsAvatar } from '../MessageUserReactionsAvatar'; + +describe('MessageUserReactionsAvatar', () => { + const reaction = { id: 'test-user', image: 'image-url', name: 'Test User', type: 'like' }; // Mock reaction data + + it('should render Avatar with correct image, name, and default size', () => { + const { queryByTestId } = render( + + + , + ); + + // Check if the mocked Avatar component is rendered with correct props + expect(queryByTestId(`avatar-image`)).toBeTruthy(); + }); + + it('should render Avatar with correct image, name, and custom size', () => { + const customSize = 40; + + const { queryByTestId } = render( + + + , + ); + + // Check if the mocked Avatar component is rendered with correct custom size + expect(queryByTestId(`avatar-image`)).toBeTruthy(); + }); +}); diff --git a/package/src/components/MessageMenu/__tests__/MessageUserReactionsItem.test.tsx b/package/src/components/MessageMenu/__tests__/MessageUserReactionsItem.test.tsx new file mode 100644 index 0000000000..98dac61c42 --- /dev/null +++ b/package/src/components/MessageMenu/__tests__/MessageUserReactionsItem.test.tsx @@ -0,0 +1,81 @@ +import React from 'react'; + +import { render } from '@testing-library/react-native'; + +import { ChatContextValue, ChatProvider } from '../../../contexts/chatContext/ChatContext'; +import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext'; + +import { Colors, defaultTheme } from '../../../contexts/themeContext/utils/theme'; +import { getTestClientWithUser } from '../../../mock-builders/mock'; +import { MessageUserReactionsItem } from '../MessageUserReactionsItem'; + +jest.mock('../../../icons', () => ({ + Unknown: () => null, +})); + +const mockReaction = { + id: 'user1', + name: 'John Doe', + type: 'like', +}; + +const mockSupportedReactions = [ + { Icon: () => null, type: 'like' }, + { Icon: () => null, type: 'love' }, +]; + +const MockOverlayReactionsAvatar = () => null; + +const renderComponent = async (props = {}, clientUserID = 'user2') => + render( + + + + + , + ); + +describe('MessageUserReactionsItem', () => { + it('renders correctly', async () => { + const { getByLabelText, getByText } = await renderComponent(); + expect(getByLabelText('Individual User Reaction on long press message')).toBeTruthy(); + expect(getByText('John Doe')).toBeTruthy(); + }); + + it('aligns reaction bubble to the right for other users', async () => { + const { getByLabelText } = await renderComponent(); + const container = getByLabelText('Individual User Reaction on long press message'); + const reactionBubble = container.props.children[0].props.children[1]; + expect(reactionBubble.props.style[1].borderColor).toBe(Colors.grey_gainsboro); + }); + + it('aligns reaction bubble to the left for the current user', async () => { + const { getByLabelText } = await renderComponent({}, 'user1'); + const container = getByLabelText('Individual User Reaction on long press message'); + const reactionBubble = container.props.children[0].props.children[1]; + expect(reactionBubble.props.style[1].borderColor).toBe(Colors.white); + }); + + it('uses Unknown icon for unsupported reaction types', async () => { + const { getByLabelText } = await renderComponent({ + reaction: { ...mockReaction, type: 'unsupported' }, + }); + const container = getByLabelText('Individual User Reaction on long press message'); + const reactionIcon = container.props.children[0].props.children[1].props.children; + expect(reactionIcon.type.name).toBe('Unknown'); + }); + + it('uses correct icon for supported reaction types', async () => { + const { getByLabelText } = await renderComponent(); + const container = getByLabelText('Individual User Reaction on long press message'); + const reactionIcon = container.props.children[0].props.children[1].props.children; + expect(reactionIcon.type).not.toBe('Unknown'); + }); +}); diff --git a/package/src/components/MessageMenu/__tests__/ReactionButton.test.tsx b/package/src/components/MessageMenu/__tests__/ReactionButton.test.tsx new file mode 100644 index 0000000000..fadb515dd9 --- /dev/null +++ b/package/src/components/MessageMenu/__tests__/ReactionButton.test.tsx @@ -0,0 +1,77 @@ +import React from 'react'; + +import { Text } from 'react-native'; + +import { cleanup, fireEvent, render } from '@testing-library/react-native'; + +import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext'; +import { defaultTheme } from '../../../contexts/themeContext/utils/theme'; +import { IconProps } from '../../../icons'; +import { ReactionButton } from '../ReactionButton'; + +const MockIcon = (props: IconProps) => {props?.pathFill?.toString() || ''}; + +describe('ReactionButton', () => { + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + const mockOnPress = jest.fn(); + + const defaultProps = { + Icon: MockIcon, + onPress: mockOnPress, + selected: false, + type: 'like', + }; + + it('should render correctly with given props', () => { + const { getByText } = render( + + + , + ); + + // Check if the unselected pathFill color is rendered by the mock Icon + expect( + getByText(defaultTheme.messageMenu.reactionButton.unfilledColor.toString()), + ).toBeTruthy(); + }); + + it('should call onPress function with the correct reaction type when pressed', () => { + const { getByLabelText } = render( + + + , + ); + + // Simulate a press event + fireEvent.press(getByLabelText('reaction-button-like-unselected')); + + // Verify if the mock function has been called with the correct reaction type + expect(mockOnPress).toHaveBeenCalledWith('like'); + }); + + it('should not call onPress when the onPress prop is not provided', () => { + const { getByLabelText } = render( + + + , + ); + + fireEvent.press(getByLabelText('reaction-button-like-unselected')); + + expect(mockOnPress).not.toHaveBeenCalled(); + }); + + it('should apply selected styles correctly when selected is true', () => { + const { getByText } = render( + + + , + ); + + expect(getByText(defaultTheme.messageMenu.reactionButton.filledColor.toString())).toBeTruthy(); + }); +}); diff --git a/package/src/components/MessageOverlay/hooks/useFetchReactions.ts b/package/src/components/MessageMenu/hooks/useFetchReactions.ts similarity index 98% rename from package/src/components/MessageOverlay/hooks/useFetchReactions.ts rename to package/src/components/MessageMenu/hooks/useFetchReactions.ts index 76eb848e73..898bf90954 100644 --- a/package/src/components/MessageOverlay/hooks/useFetchReactions.ts +++ b/package/src/components/MessageMenu/hooks/useFetchReactions.ts @@ -80,6 +80,8 @@ export const useFetchReactions = < }, [fetchReactions]); useEffect(() => { + setReactions([]); + setNext(undefined); fetchReactions(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [messageId, reactionType, sortString]); diff --git a/package/src/components/MessageOverlay/MessageActionList.tsx b/package/src/components/MessageOverlay/MessageActionList.tsx deleted file mode 100644 index 72e44c2b69..0000000000 --- a/package/src/components/MessageOverlay/MessageActionList.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import React from 'react'; -import { StyleSheet, ViewStyle } from 'react-native'; -import Animated, { - interpolate, - SharedValue, - useAnimatedStyle, - useSharedValue, -} from 'react-native-reanimated'; - -import { MessageActionListItem as DefaultMessageActionListItem } from './MessageActionListItem'; - -import { - MessageOverlayData, - useMessageOverlayContext, -} from '../../contexts/messageOverlayContext/MessageOverlayContext'; -import type { OverlayProviderProps } from '../../contexts/overlayContext/OverlayContext'; -import { useTheme } from '../../contexts/themeContext/ThemeContext'; -import { useViewport } from '../../hooks/useViewport'; -import type { DefaultStreamChatGenerics } from '../../types/types'; - -export type MessageActionListPropsWithContext< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick< - OverlayProviderProps, - | 'MessageActionListItem' - | 'error' - | 'isMyMessage' - | 'isThreadMessage' - | 'message' - | 'messageReactions' -> & - Pick, 'alignment' | 'messageActions'> & { - showScreen: SharedValue; - }; - -const MessageActionListWithContext = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: MessageActionListPropsWithContext, -) => { - const { - alignment, - error, - isMyMessage, - isThreadMessage, - message, - MessageActionListItem = DefaultMessageActionListItem, - messageActions, - messageReactions, - showScreen, - } = props; - - const messageActionProps = { - error, - isMyMessage, - isThreadMessage, - message, - messageReactions, - }; - const { vw } = useViewport(); - - const { - theme: { - colors: { white_snow }, - }, - } = useTheme(); - - const height = useSharedValue(0); - const width = useSharedValue(0); - - const showScreenStyle = useAnimatedStyle( - () => ({ - transform: [ - { - translateY: interpolate(showScreen.value, [0, 1], [-height.value / 2, 0]), - }, - { - translateX: interpolate( - showScreen.value, - [0, 1], - [alignment === 'left' ? -width.value / 2 : width.value / 2, 0], - ), - }, - { - scale: showScreen.value, - }, - ], - }), - [alignment], - ); - - return ( - { - width.value = layout.width; - height.value = layout.height; - }} - style={[styles.container, { backgroundColor: white_snow, minWidth: vw(65) }, showScreenStyle]} - testID='message-action-list' - > - {messageActions?.map((messageAction, index) => ( - - ))} - - ); -}; - -const areEqual = ( - prevProps: MessageActionListPropsWithContext, - nextProps: MessageActionListPropsWithContext, -) => { - const { alignment: prevAlignment, messageActions: prevMessageActions } = prevProps; - const { alignment: nextAlignment, messageActions: nextMessageActions } = nextProps; - - const messageActionsEqual = prevMessageActions?.length === nextMessageActions?.length; - if (!messageActionsEqual) return false; - - const alignmentEqual = prevAlignment === nextAlignment; - if (!alignmentEqual) return false; - - return true; -}; - -const MemoizedMessageActionList = React.memo( - MessageActionListWithContext, - areEqual, -) as typeof MessageActionListWithContext; - -export type MessageActionListProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Partial, 'showScreen'>> & - Pick< - MessageActionListPropsWithContext, - 'showScreen' | 'message' | 'isMyMessage' | 'error' | 'isThreadMessage' | 'messageReactions' - >; - -/** - * MessageActionList - A high level component which implements all the logic required for MessageActions - */ -export const MessageActionList = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: MessageActionListProps, -) => { - const { data } = useMessageOverlayContext(); - - const { alignment, messageActions } = data || {}; - - return ; -}; - -const styles = StyleSheet.create({ - bottomBorder: { - borderBottomWidth: 1, - }, - container: { - borderRadius: 16, - marginTop: 8, - overflow: 'hidden', - }, - titleStyle: { - paddingLeft: 20, - }, -}); diff --git a/package/src/components/MessageOverlay/MessageActionListItem.tsx b/package/src/components/MessageOverlay/MessageActionListItem.tsx deleted file mode 100644 index 4ffe14ad74..0000000000 --- a/package/src/components/MessageOverlay/MessageActionListItem.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React from 'react'; -import { StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle } from 'react-native'; -import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; - -import { useTheme } from '../../contexts/themeContext/ThemeContext'; -import { useViewport } from '../../hooks/useViewport'; -import type { DefaultStreamChatGenerics } from '../../types/types'; -import type { MessageOverlayPropsWithContext } from '../MessageOverlay/MessageOverlay'; - -export type ActionType = - | 'banUser' - | 'blockUser' - | 'copyMessage' - | 'deleteMessage' - | 'editMessage' - | 'flagMessage' - | 'muteUser' - | 'pinMessage' - | 'selectReaction' - | 'reply' - | 'retry' - | 'quotedReply' - | 'threadReply' - | 'unpinMessage'; - -export type MessageActionType = { - /** - * Callback when user presses the action button. - */ - action: () => void; - /** - * Type of the action performed. - * Eg: 'banUser', 'blockUser', 'copyMessage', 'deleteMessage', 'editMessage', 'flagMessage', 'muteUser', 'pinMessage', 'selectReaction', 'reply', 'retry', 'quotedReply', 'threadReply', 'unpinMessage' - */ - actionType: ActionType | string; - /** - * Title for action button. - */ - title: string; - /** - * Element to render as icon for action button. - */ - icon?: React.ReactElement; - /** - * Styles for underlying Text component of action title. - */ - titleStyle?: StyleProp; -}; - -export type MessageActionListItemProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = MessageActionType & - Pick< - MessageOverlayPropsWithContext, - 'error' | 'isMyMessage' | 'isThreadMessage' | 'message' | 'messageReactions' - > & { - index: number; - length: number; - }; - -const MessageActionListItemWithContext = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: MessageActionListItemProps, -) => { - const { action, actionType, icon, index, length, title, titleStyle } = props; - const { vw } = useViewport(); - const opacity = useSharedValue(1); - const activeOpacity = 0.2; - - const { - theme: { - colors: { black, border }, - overlay: { messageActions }, - }, - } = useTheme(); - - const animatedStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - })); - - const tap = Gesture.Tap() - .onStart(() => { - opacity.value = activeOpacity; - }) - .onFinalize(() => { - opacity.value = 1; - }) - .onEnd(() => { - runOnJS(action)(); - }); - - return ( - - - {icon} - - {title} - - - - ); -}; - -const messageActionIsEqual = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - prevProps: MessageActionListItemProps, - nextProps: MessageActionListItemProps, -) => prevProps.length === nextProps.length; - -export const MemoizedMessageActionListItem = React.memo( - MessageActionListItemWithContext, - messageActionIsEqual, -) as typeof MessageActionListItemWithContext; - -/** - * MessageActionListItem - A high-level component that implements all the logic required for a `MessageAction` in a `MessageActionList` - */ -export const MessageActionListItem = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: MessageActionListItemProps, -) => ; - -const styles = StyleSheet.create({ - bottomBorder: { - borderBottomWidth: 1, - }, - container: { - borderRadius: 16, - marginTop: 8, - maxWidth: 275, - }, - row: { - alignItems: 'center', - flexDirection: 'row', - justifyContent: 'flex-start', - paddingHorizontal: 20, - paddingVertical: 10, - }, - titleStyle: { - paddingLeft: 20, - }, -}); diff --git a/package/src/components/MessageOverlay/MessageOverlay.tsx b/package/src/components/MessageOverlay/MessageOverlay.tsx deleted file mode 100644 index 74e5f62e72..0000000000 --- a/package/src/components/MessageOverlay/MessageOverlay.tsx +++ /dev/null @@ -1,615 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { Keyboard, Platform, SafeAreaView, StyleSheet, View, ViewStyle } from 'react-native'; -import { Gesture, GestureDetector, ScrollView } from 'react-native-gesture-handler'; -import Animated, { - cancelAnimation, - Easing, - Extrapolation, - interpolate, - runOnJS, - SharedValue, - useAnimatedStyle, - useSharedValue, - withDecay, - withSpring, - withTiming, -} from 'react-native-reanimated'; - -import { MessageActionList as DefaultMessageActionList } from './MessageActionList'; -import { OverlayReactionList as OverlayReactionListDefault } from './OverlayReactionList'; -import { OverlayReactionsAvatar as OverlayReactionsAvatarDefault } from './OverlayReactionsAvatar'; - -import { ChatProvider } from '../../contexts/chatContext/ChatContext'; -import { MessageProvider } from '../../contexts/messageContext/MessageContext'; -import { - MessageOverlayContextValue, - MessageOverlayData, - useMessageOverlayContext, -} from '../../contexts/messageOverlayContext/MessageOverlayContext'; - -import { MessagesProvider } from '../../contexts/messagesContext/MessagesContext'; -import { - OverlayContextValue, - OverlayProviderProps, - useOverlayContext, -} from '../../contexts/overlayContext/OverlayContext'; -import { mergeThemes, ThemeProvider, useTheme } from '../../contexts/themeContext/ThemeContext'; - -import { useViewport } from '../../hooks/useViewport'; -import type { DefaultStreamChatGenerics } from '../../types/types'; -import { MessageTextContainer } from '../Message/MessageSimple/MessageTextContainer'; -import { OverlayReactions as DefaultOverlayReactions } from '../MessageOverlay/OverlayReactions'; -import type { ReplyProps } from '../Reply/Reply'; - -const styles = StyleSheet.create({ - alignEnd: { alignItems: 'flex-end' }, - alignStart: { alignItems: 'flex-start' }, - center: { - flexGrow: 1, - justifyContent: 'center', - }, - containerInner: { - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - borderWidth: 1, - overflow: 'hidden', - }, - flex: { - flex: 1, - }, - overlayPadding: { - padding: 8, - }, - replyContainer: { - flexDirection: 'row', - paddingHorizontal: 8, - paddingTop: 8, - }, - row: { flexDirection: 'row' }, - scrollView: { overflow: Platform.OS === 'ios' ? 'visible' : 'scroll' }, -}); - -const DefaultMessageTextNumberOfLines = 5; - -export type MessageOverlayPropsWithContext< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick< - MessageOverlayContextValue, - | 'MessageActionList' - | 'MessageActionListItem' - | 'OverlayReactionList' - | 'OverlayReactions' - | 'OverlayReactionsAvatar' -> & - Omit, 'supportedReactions'> & - Pick & - Pick< - OverlayProviderProps, - | 'error' - | 'isMyMessage' - | 'isThreadMessage' - | 'message' - | 'messageReactions' - | 'messageTextNumberOfLines' - > & { - overlayOpacity: SharedValue; - showScreen?: SharedValue; - }; - -const MessageOverlayWithContext = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: MessageOverlayPropsWithContext, -) => { - const { - alignment, - chatContext, - clientId, - error, - files, - groupStyles, - handleReaction, - images, - isMyMessage, - isThreadMessage, - message, - MessageActionList = DefaultMessageActionList, - MessageActionListItem, - messageActions, - messageContext, - messageReactions, - messageReactionTitle, - messagesContext, - messageTextNumberOfLines = DefaultMessageTextNumberOfLines, - onlyEmojis, - otherAttachments, - overlay, - overlayOpacity, - OverlayReactionList = OverlayReactionListDefault, - OverlayReactions = DefaultOverlayReactions, - OverlayReactionsAvatar = OverlayReactionsAvatarDefault, - ownCapabilities, - setOverlay, - threadList, - videos, - } = props; - - const messageActionProps = { - error, - isMyMessage, - isThreadMessage, - message, - messageReactions, - }; - - const { theme } = useTheme(); - const { vh, vw } = useViewport(); - - const screenHeight = vh(100); - const halfScreenHeight = vh(50); - - const myMessageTheme = messagesContext?.myMessageTheme; - const wrapMessageInTheme = clientId === message?.user?.id && !!myMessageTheme; - - const [reactionListHeight, setReactionListHeight] = useState(0); - - const myMessageThemeString = useMemo(() => JSON.stringify(myMessageTheme), [myMessageTheme]); - - const modifiedTheme = useMemo( - () => mergeThemes({ style: myMessageTheme, theme }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [myMessageThemeString, theme], - ); - - const { - colors: { blue_alice, grey_gainsboro, grey_whisper, transparent, white_smoke }, - messageSimple: { - content: { - container: { borderRadiusL, borderRadiusS }, - containerInner, - replyContainer, - }, - }, - overlay: { container: containerStyle, padding: overlayPadding }, - } = wrapMessageInTheme ? modifiedTheme : theme; - - const messageHeight = useSharedValue(0); - const messageLayout = useSharedValue({ x: 0, y: 0 }); - const messageWidth = useSharedValue(0); - - const offsetY = useSharedValue(0); - const translateY = useSharedValue(0); - const scale = useSharedValue(1); - - const showScreen = useSharedValue(0); - const fadeScreen = () => { - 'worklet'; - - offsetY.value = 0; - translateY.value = 0; - scale.value = 1; - showScreen.value = withSpring(1, { - damping: 600, - mass: 0.5, - restDisplacementThreshold: 0.01, - restSpeedThreshold: 0.01, - stiffness: 200, - velocity: 32, - }); - }; - - useEffect(() => { - Keyboard.dismiss(); - fadeScreen(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const pan = Gesture.Pan() - .enabled(overlay === 'message') - .maxPointers(1) - .minDistance(10) - .onBegin(() => { - cancelAnimation(translateY); - offsetY.value = translateY.value; - }) - .onChange((event) => { - translateY.value = offsetY.value + event.translationY; - overlayOpacity.value = interpolate( - translateY.value, - [0, halfScreenHeight], - [1, 0.75], - Extrapolation.CLAMP, - ); - scale.value = interpolate( - translateY.value, - [0, halfScreenHeight], - [1, 0.85], - Extrapolation.CLAMP, - ); - }) - .onEnd((event) => { - const finalYPosition = event.translationY + event.velocityY * 0.1; - - if (finalYPosition > halfScreenHeight && translateY.value > 0) { - cancelAnimation(translateY); - overlayOpacity.value = withTiming( - 0, - { - duration: 200, - easing: Easing.out(Easing.ease), - }, - () => { - runOnJS(setOverlay)('none'); - }, - ); - translateY.value = - event.velocityY > 1000 - ? withDecay({ - velocity: event.velocityY, - }) - : withTiming(screenHeight, { - duration: 200, - easing: Easing.out(Easing.ease), - }); - } else { - translateY.value = withTiming(0); - scale.value = withTiming(1); - overlayOpacity.value = withTiming(1); - } - }); - - const tap = Gesture.Tap() - .maxDistance(32) - .onEnd(() => { - runOnJS(setOverlay)('none'); - }); - - const panStyle = useAnimatedStyle(() => ({ - transform: [ - { - translateY: translateY.value, - }, - { - scale: scale.value, - }, - ], - })); - - const showScreenStyle = useAnimatedStyle( - () => ({ - transform: [ - { - translateY: interpolate(showScreen.value, [0, 1], [messageHeight.value / 2, 0]), - }, - { - translateX: interpolate( - showScreen.value, - [0, 1], - [alignment === 'left' ? -messageWidth.value / 2 : messageWidth.value / 2, 0], - ), - }, - { - scale: showScreen.value, - }, - ], - }), - [alignment], - ); - - const groupStyle = `${alignment}_${(groupStyles?.[0] || 'bottom').toLowerCase()}`; - - const hasThreadReplies = !!message?.reply_count; - - const { Attachment, FileAttachmentGroup, Gallery, MessageAvatar, Reply } = messagesContext || {}; - - const renderContent = (messageTextNumberOfLines?: number) => ( - - - {message && ( - - {handleReaction && ownCapabilities?.sendReaction ? ( - reaction.type) || []} - setReactionListHeight={setReactionListHeight} - showScreen={showScreen} - /> - ) : null} - { - messageLayout.value = { - x: alignment === 'left' ? x + layoutWidth : x, - y, - }; - messageWidth.value = layoutWidth; - messageHeight.value = layoutHeight; - }} - style={[styles.alignEnd, styles.row, showScreenStyle]} - > - {alignment === 'left' && MessageAvatar && ( - - )} - - {messagesContext?.messageContentOrder?.map( - (messageContentType, messageContentOrderIndex) => { - switch (messageContentType) { - case 'quoted_reply': - return ( - message.quoted_message && - Reply && ( - - ['quotedMessage'] - } - styles={{ - messageContainer: { - maxWidth: vw(60), - }, - }} - /> - - ) - ); - case 'attachments': - return otherAttachments?.map( - (attachment, attachmentIndex) => - Attachment && ( - - ), - ); - case 'files': - return ( - FileAttachmentGroup && ( - - ) - ); - case 'gallery': - return ( - Gallery && ( - - ) - ); - case 'text': - default: - return otherAttachments?.length && otherAttachments[0].actions ? null : ( - - key={`message_text_container_${messageContentOrderIndex}`} - message={message} - messageOverlay - messageTextNumberOfLines={messageTextNumberOfLines} - onlyEmojis={onlyEmojis} - /> - ); - } - }, - )} - - - {messageActions && ( - - )} - {!!messageReactionTitle && ( - - )} - - )} - - - ); - - // Scroll will only be enabled for message overlay when we show actions. - // When we show the reactions, we don't want to enable scroll since OverlayReactions component - // in itself is scrollable (FlatList). FlatList inside a ScrollView is not a good idea and results in error from RN. - const isScrollEnabled = !!messageActions && overlay === 'message'; - - return ( - - - - - - - - - {isScrollEnabled ? ( - - {renderContent()} - - ) : ( - renderContent(messageTextNumberOfLines) - )} - - - - - - - - - ); -}; - -const areEqual = ( - prevProps: MessageOverlayPropsWithContext, - nextProps: MessageOverlayPropsWithContext, -) => { - const { - alignment: prevAlignment, - message: prevMessage, - messageReactionTitle: prevMessageReactionTitle, - messagesContext: prevMessagesContext, - } = prevProps; - const { - alignment: nextAlignment, - message: nextMessage, - messageReactionTitle: nextMessageReactionTitle, - messagesContext: nextMessagesContext, - } = nextProps; - - const alignmentEqual = prevAlignment === nextAlignment; - if (!alignmentEqual) return false; - - const messageReactionTitleEqual = prevMessageReactionTitle === nextMessageReactionTitle; - if (!messageReactionTitleEqual) return false; - - const prevMyMessageTheme = JSON.stringify(prevMessagesContext?.myMessageTheme); - const nextMyMessageTheme = JSON.stringify(nextMessagesContext?.myMessageTheme); - - const myMessageThemeEqual = prevMyMessageTheme === nextMyMessageTheme; - if (!myMessageThemeEqual) 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; - - return true; -}; - -const MemoizedMessageOverlay = React.memo( - MessageOverlayWithContext, - areEqual, -) as typeof MessageOverlayWithContext; - -export type MessageOverlayProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Partial, 'overlayOpacity'>> & - Pick, 'overlayOpacity'> & - Pick< - MessageOverlayPropsWithContext, - 'isMyMessage' | 'error' | 'isThreadMessage' | 'message' | 'messageReactions' - >; - -/** - * MessageOverlay - A high level component which implements all the logic required for a message overlay - */ -export const MessageOverlay = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: MessageOverlayProps, -) => { - const { - data, - MessageActionList, - MessageActionListItem, - OverlayReactionList, - OverlayReactions, - OverlayReactionsAvatar, - } = useMessageOverlayContext(); - const { overlay, setOverlay } = useOverlayContext(); - - const componentProps = { - MessageActionList: props.MessageActionList || MessageActionList, - MessageActionListItem: props.MessageActionListItem || MessageActionListItem, - OverlayReactionList: - props.OverlayReactionList || OverlayReactionList || data?.OverlayReactionList, - OverlayReactions: props.OverlayReactions || OverlayReactions, - OverlayReactionsAvatar: props.OverlayReactionsAvatar || OverlayReactionsAvatar, - }; - - return ( - - ); -}; diff --git a/package/src/components/MessageOverlay/OverlayBackdrop.tsx b/package/src/components/MessageOverlay/OverlayBackdrop.tsx deleted file mode 100644 index b574c2adb8..0000000000 --- a/package/src/components/MessageOverlay/OverlayBackdrop.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { StyleProp, View, ViewStyle } from 'react-native'; - -import { useTheme } from '../../contexts/themeContext/ThemeContext'; - -type OverlayBackdropProps = { - style?: StyleProp; -}; - -export const OverlayBackdrop = (props: OverlayBackdropProps): JSX.Element => { - const { style = {} } = props; - const { - theme: { - colors: { overlay }, - }, - } = useTheme(); - return ; -}; diff --git a/package/src/components/MessageOverlay/OverlayReactionList.tsx b/package/src/components/MessageOverlay/OverlayReactionList.tsx deleted file mode 100644 index c9aedc7400..0000000000 --- a/package/src/components/MessageOverlay/OverlayReactionList.tsx +++ /dev/null @@ -1,435 +0,0 @@ -import React from 'react'; -import { StyleSheet, useWindowDimensions, View, ViewStyle } from 'react-native'; -import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { - cancelAnimation, - interpolate, - runOnJS, - SharedValue, - useAnimatedReaction, - useAnimatedStyle, - useSharedValue, - withDelay, - withSequence, - withTiming, -} from 'react-native-reanimated'; -import { FillProps } from 'react-native-svg'; - -import { - MessageOverlayData, - useMessageOverlayContext, -} from '../../contexts/messageOverlayContext/MessageOverlayContext'; -import { - MessagesContextValue, - useMessagesContext, -} from '../../contexts/messagesContext/MessagesContext'; -import { - OverlayContextValue, - useOverlayContext, -} from '../../contexts/overlayContext/OverlayContext'; -import { useTheme } from '../../contexts/themeContext/ThemeContext'; -import { - IconProps, - LOLReaction, - LoveReaction, - ThumbsDownReaction, - ThumbsUpReaction, - WutReaction, -} from '../../icons'; - -import { triggerHaptic } from '../../native'; - -import type { DefaultStreamChatGenerics } from '../../types/types'; -import type { ReactionData } from '../../utils/utils'; - -const styles = StyleSheet.create({ - notLastReaction: { - marginRight: 16, - }, - reactionList: { - alignItems: 'center', - borderRadius: 24, - flexDirection: 'row', - justifyContent: 'center', - paddingHorizontal: 16, - paddingVertical: 12, - position: 'absolute', - }, - selectedIcon: { - position: 'absolute', - }, -}); - -const reactionData: ReactionData[] = [ - { - Icon: LoveReaction, - type: 'love', - }, - { - Icon: ThumbsUpReaction, - type: 'like', - }, - { - Icon: ThumbsDownReaction, - type: 'sad', - }, - { - Icon: LOLReaction, - type: 'haha', - }, - { - Icon: WutReaction, - type: 'wow', - }, -]; - -type ReactionButtonProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick< - OverlayReactionListPropsWithContext, - 'ownReactionTypes' | 'handleReaction' | 'setOverlay' -> & { - Icon: React.ComponentType; - index: number; - numberOfReactions: number; - showScreen: SharedValue; - type: string; -}; - -export const ReactionButton = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: ReactionButtonProps, -) => { - const { - handleReaction, - Icon, - index, - numberOfReactions, - ownReactionTypes, - setOverlay, - showScreen, - type, - } = props; - const { - theme: { - colors: { accent_blue, grey }, - overlay: { - reactionsList: { reaction, reactionSize }, - }, - }, - } = useTheme(); - const selected = ownReactionTypes.includes(type); - const animationScale = useSharedValue(0); - const hasShown = useSharedValue(0); - const scale = useSharedValue(1); - const selectedOpacity = useSharedValue(selected ? 1 : 0); - const tap = Gesture.Tap() - .hitSlop({ - bottom: - Number(reaction.paddingVertical || 0) || - Number(reaction.paddingBottom || 0) || - styles.reactionList.paddingVertical, - left: - (Number(reaction.paddingHorizontal || 0) || - Number(reaction.paddingLeft || 0) || - styles.notLastReaction.marginRight) / 2, - right: - (Number(reaction.paddingHorizontal || 0) || - Number(reaction.paddingRight || 0) || - styles.notLastReaction.marginRight) / 2, - top: - Number(reaction.paddingVertical || 0) || - Number(reaction.paddingTop || 0) || - styles.reactionList.paddingVertical, - }) - .maxDuration(3000) - .onStart(() => { - cancelAnimation(scale); - scale.value = withTiming(1.5, { duration: 100 }); - }) - .onEnd(() => { - runOnJS(triggerHaptic)('impactLight'); - selectedOpacity.value = withTiming(selected ? 0 : 1, { duration: 250 }, () => { - if (handleReaction) { - runOnJS(handleReaction)(type); - } - runOnJS(setOverlay)('none'); - }); - }) - .onFinalize(() => { - cancelAnimation(scale); - scale.value = withTiming(1, { duration: 100 }); - }); - - useAnimatedReaction( - () => { - if (showScreen.value > 0.8 && hasShown.value === 0) { - return 1; - } - return 0; - }, - (result) => { - if (hasShown.value === 0 && result !== 0) { - hasShown.value = 1; - animationScale.value = withSequence( - withDelay(60 * (numberOfReactions - (index + 1)), withTiming(0.1, { duration: 50 })), - withTiming(1.5, { duration: 250 }), - withTiming(1, { duration: 250 }), - ); - } - }, - [index, numberOfReactions], - ); - - const iconStyle = useAnimatedStyle( - () => ({ - transform: [ - { - scale: animationScale.value, - }, - { - scale: scale.value, - }, - ], - }), - [], - ); - - const selectedStyle = useAnimatedStyle(() => ({ - opacity: selectedOpacity.value, - })); - - return ( - - - - - - - - - ); -}; - -export type OverlayReactionListPropsWithContext< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick< - MessageOverlayData, - 'alignment' | 'handleReaction' | 'messagesContext' -> & - Pick, 'supportedReactions'> & - Pick & { - messageLayout: SharedValue<{ - x: number; - y: number; - }>; - ownReactionTypes: string[]; - setReactionListHeight: React.Dispatch>; - showScreen: SharedValue; - fill?: FillProps['fill']; - }; - -const OverlayReactionListWithContext = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: OverlayReactionListPropsWithContext, -) => { - const { - alignment, - fill, - handleReaction, - messageLayout, - ownReactionTypes, - setOverlay, - setReactionListHeight, - showScreen, - supportedReactions = reactionData, - } = props; - - const { - theme: { - colors: { white_snow }, - overlay: { - padding: screenPadding, - reactionsList: { radius, reactionList, reactionListBorderRadius }, - }, - }, - } = useTheme(); - - const reactionListHeight = useSharedValue(0); - const reactionBubbleWidth = useSharedValue(0); - const reactionListLayout = useSharedValue({ - height: 0, - width: 0, - }); - - const { width } = useWindowDimensions(); - - const animatedStyle = useAnimatedStyle(() => { - const borderRadius = reactionListBorderRadius || styles.reactionList.borderRadius; - const insideLeftBound = - messageLayout.value.x - reactionListLayout.value.width + borderRadius > screenPadding; - const insideRightBound = messageLayout.value.x + borderRadius < width - screenPadding; - const left = !insideLeftBound - ? screenPadding - : !insideRightBound - ? width - screenPadding - reactionListLayout.value.width - : messageLayout.value.x - reactionListLayout.value.width + borderRadius; - const top = messageLayout.value.y - reactionListLayout.value.height - radius * 2; - - return { - left, - top, - }; - }); - - const animatedBigCircleStyle = useAnimatedStyle(() => ({ - borderRadius: radius, - height: radius * 2, - left: messageLayout.value.x - radius * 3, - top: messageLayout.value.y - radius * 3, - width: radius * 2, - })); - - const animatedSmallCircleStyle = useAnimatedStyle(() => ({ - borderRadius: radius / 2, - height: radius, - left: messageLayout.value.x - radius, - top: messageLayout.value.y, - width: radius, - })); - - const showScreenStyle = useAnimatedStyle( - () => ({ - transform: [ - { - translateY: interpolate(showScreen.value, [0, 1], [-reactionListHeight.value / 2, 0]), - }, - { - translateX: interpolate( - showScreen.value, - [0, 1], - [ - alignment === 'left' ? -reactionBubbleWidth.value / 2 : reactionBubbleWidth.value / 2, - 0, - ], - ), - }, - { - scale: interpolate(showScreen.value, [0, 0.8, 1], [0, 0, 1]), - }, - ], - }), - [alignment], - ); - - const numberOfReactions = supportedReactions.length; - - return ( - - { - reactionBubbleWidth.value = layout.width; - }} - style={showScreenStyle} - > - - - - { - reactionListLayout.value = { height, width: layoutWidth }; - reactionListHeight.value = height; - setReactionListHeight(height); - }} - style={[ - styles.reactionList, - { backgroundColor: white_snow }, - animatedStyle, - reactionList, - ]} - > - {supportedReactions?.map(({ Icon, type }, index) => ( - - handleReaction={handleReaction} - Icon={Icon} - index={index} - key={`${type}_${index}`} - numberOfReactions={numberOfReactions} - ownReactionTypes={ownReactionTypes} - setOverlay={setOverlay} - showScreen={showScreen} - type={type} - /> - ))} - - - - ); -}; - -const areEqual = ( - prevProps: OverlayReactionListPropsWithContext, - nextProps: OverlayReactionListPropsWithContext, -) => { - const { alignment: prevAlignment, ownReactionTypes: prevOwnReactionTypes } = prevProps; - const { alignment: nextAlignment, ownReactionTypes: nextOwnReactionTypes } = nextProps; - - const alignmentEqual = prevAlignment === nextAlignment; - if (!alignmentEqual) return false; - - const ownReactionTypesEqual = prevOwnReactionTypes.length === nextOwnReactionTypes.length; - if (!ownReactionTypesEqual) return false; - - return true; -}; - -const MemoizedOverlayReactionList = React.memo( - OverlayReactionListWithContext, - areEqual, -) as typeof OverlayReactionListWithContext; - -export type OverlayReactionListProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Omit< - OverlayReactionListPropsWithContext, - 'setOverlay' | 'supportedReactions' -> & - Partial< - Pick< - OverlayReactionListPropsWithContext, - 'setOverlay' | 'supportedReactions' - > - >; - -/** - * OverlayReactionList - A high level component which implements all the logic required for a message overlay reaction list - */ -export const OverlayReactionList = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - props: OverlayReactionListProps, -) => { - const { data } = useMessageOverlayContext(); - const { supportedReactions } = useMessagesContext(); - const { setOverlay } = useOverlayContext(); - - return ( - - ); -}; - -OverlayReactionList.displayName = 'OverlayReactionList{overlay{reactionList}}'; diff --git a/package/src/components/MessageOverlay/OverlayReactions.tsx b/package/src/components/MessageOverlay/OverlayReactions.tsx deleted file mode 100644 index 982420154e..0000000000 --- a/package/src/components/MessageOverlay/OverlayReactions.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import React, { useMemo } from 'react'; -import { StyleSheet, Text, useWindowDimensions, View, ViewStyle } from 'react-native'; -import { FlatList } from 'react-native-gesture-handler'; -import Animated, { - interpolate, - SharedValue, - useAnimatedStyle, - useSharedValue, -} from 'react-native-reanimated'; - -import { ReactionSortBase } from 'stream-chat'; - -import { useFetchReactions } from './hooks/useFetchReactions'; - -import { OverlayReactionsItem } from './OverlayReactionsItem'; - -import type { Alignment } from '../../contexts/messageContext/MessageContext'; -import type { MessageOverlayContextValue } from '../../contexts/messageOverlayContext/MessageOverlayContext'; -import { useTheme } from '../../contexts/themeContext/ThemeContext'; -import { - LOLReaction, - LoveReaction, - ThumbsDownReaction, - ThumbsUpReaction, - WutReaction, -} from '../../icons'; - -import type { DefaultStreamChatGenerics } from '../../types/types'; -import type { ReactionData } from '../../utils/utils'; - -const styles = StyleSheet.create({ - avatarContainer: { - padding: 8, - }, - container: { - alignItems: 'center', - borderRadius: 16, - marginTop: 8, - width: '100%', - }, - flatListContainer: { - paddingHorizontal: 12, - paddingVertical: 8, - }, - flatListContentContainer: { - alignItems: 'center', - paddingBottom: 12, - }, - title: { - fontSize: 16, - fontWeight: '700', - paddingTop: 16, - }, - unseenItemContainer: { - opacity: 0, - position: 'absolute', - }, -}); - -const reactionData: ReactionData[] = [ - { - Icon: LoveReaction, - type: 'love', - }, - { - Icon: ThumbsUpReaction, - type: 'like', - }, - { - Icon: ThumbsDownReaction, - type: 'sad', - }, - { - Icon: LOLReaction, - type: 'haha', - }, - { - Icon: WutReaction, - type: 'wow', - }, -]; - -export type Reaction = { - alignment: Alignment; - id: string; - name: string; - type: string; - image?: string; -}; - -export type OverlayReactionsProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'OverlayReactionsAvatar'> & { - showScreen: SharedValue; - title: string; - alignment?: Alignment; - messageId?: string; - reactions?: Reaction[]; - supportedReactions?: ReactionData[]; -}; - -const sort: ReactionSortBase = { - created_at: -1, -}; - -/** - * OverlayReactions - A high level component which implements all the logic required for message overlay reactions - */ -export const OverlayReactions = (props: OverlayReactionsProps) => { - const [itemHeight, setItemHeight] = React.useState(0); - const { - alignment: overlayAlignment, - messageId, - OverlayReactionsAvatar, - reactions: propReactions, - showScreen, - supportedReactions = reactionData, - title, - } = props; - const layoutHeight = useSharedValue(0); - const layoutWidth = useSharedValue(0); - const { - loading, - loadNextPage, - reactions: fetchedReactions, - } = useFetchReactions({ - messageId, - sort, - }); - - const reactions = useMemo( - () => - propReactions || - (fetchedReactions.map((reaction) => ({ - alignment: 'left', - id: reaction.user?.id, - image: reaction.user?.image, - name: reaction.user?.name, - type: reaction.type, - })) as Reaction[]), - [propReactions, fetchedReactions], - ); - - const { - theme: { - colors: { black, white }, - overlay: { - padding: overlayPadding, - reactions: { avatarContainer, avatarSize, container, flatListContainer, title: titleStyle }, - }, - }, - } = useTheme(); - - const width = useWindowDimensions().width; - - const supportedReactionTypes = supportedReactions.map( - (supportedReaction) => supportedReaction.type, - ); - - const filteredReactions = reactions.filter((reaction) => - supportedReactionTypes.includes(reaction.type), - ); - - const numColumns = Math.floor( - (width - - overlayPadding * 2 - - ((Number(flatListContainer.paddingHorizontal || 0) || - styles.flatListContainer.paddingHorizontal) + - (Number(avatarContainer.padding || 0) || styles.avatarContainer.padding)) * - 2) / - (avatarSize + (Number(avatarContainer.padding || 0) || styles.avatarContainer.padding) * 2), - ); - - const renderItem = ({ item }: { item: Reaction }) => ( - - ); - - const showScreenStyle = useAnimatedStyle( - () => ({ - transform: [ - { - translateY: interpolate(showScreen.value, [0, 1], [-layoutHeight.value / 2, 0]), - }, - { - translateX: interpolate( - showScreen.value, - [0, 1], - [overlayAlignment === 'left' ? -layoutWidth.value / 2 : layoutWidth.value / 2, 0], - ), - }, - { - scale: showScreen.value, - }, - ], - }), - [overlayAlignment], - ); - - return ( - <> - { - layoutWidth.value = layout.width; - layoutHeight.value = layout.height; - }} - style={[ - styles.container, - { backgroundColor: white, opacity: itemHeight ? 1 : 0 }, - container, - showScreenStyle, - ]} - > - {title} - {!loading && ( - `${name}${id}_${index}`} - numColumns={numColumns} - onEndReached={loadNextPage} - renderItem={renderItem} - scrollEnabled={filteredReactions.length / numColumns > 1} - style={[ - styles.flatListContainer, - flatListContainer, - { - // we show the item height plus a little extra to tease for scrolling if there are more than one row - maxHeight: - itemHeight + (filteredReactions.length / numColumns > 1 ? itemHeight / 4 : 8), - }, - ]} - /> - )} - {/* The below view is unseen by the user, we use it to compute the height that the item must be */} - {!loading && ( - { - setItemHeight(layout.height); - }} - style={[styles.unseenItemContainer, styles.flatListContentContainer]} - > - {renderItem({ item: filteredReactions[0] })} - - )} - - - ); -}; - -OverlayReactions.displayName = 'OverlayReactions{overlay{reactions}}'; diff --git a/package/src/components/MessageOverlay/OverlayReactionsItem.tsx b/package/src/components/MessageOverlay/OverlayReactionsItem.tsx deleted file mode 100644 index cad971046f..0000000000 --- a/package/src/components/MessageOverlay/OverlayReactionsItem.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import React from 'react'; - -import { StyleSheet, Text, View } from 'react-native'; -import Svg, { Circle } from 'react-native-svg'; - -import { ReactionResponse } from 'stream-chat'; - -import { Reaction } from './OverlayReactions'; - -import { useChatContext } from '../../contexts/chatContext/ChatContext'; -import type { MessageOverlayContextValue } from '../../contexts/messageOverlayContext/MessageOverlayContext'; -import { useTheme } from '../../contexts/themeContext/ThemeContext'; -import { Unknown } from '../../icons'; - -import type { DefaultStreamChatGenerics } from '../../types/types'; -import { ReactionData } from '../../utils/utils'; - -export type OverlayReactionsItemProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'OverlayReactionsAvatar'> & { - reaction: Reaction; - supportedReactions: ReactionData[]; -}; - -type ReactionIconProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'type'> & { - pathFill: string; - size: number; - supportedReactions: ReactionData[]; -}; - -const ReactionIcon = ({ pathFill, size, supportedReactions, type }: ReactionIconProps) => { - const Icon = supportedReactions.find((reaction) => reaction.type === type)?.Icon || Unknown; - return ; -}; - -export const OverlayReactionsItem = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->({ - OverlayReactionsAvatar, - reaction, - supportedReactions, -}: OverlayReactionsItemProps) => { - const { id, name, type } = reaction; - const { - theme: { - colors: { accent_blue, black, grey_gainsboro, white }, - overlay: { - reactions: { - avatarContainer, - avatarName, - avatarSize, - radius, - reactionBubble, - reactionBubbleBackground, - reactionBubbleBorderRadius, - }, - }, - }, - } = useTheme(); - const { client } = useChatContext(); - const alignment = client.userID && client.userID === id ? 'right' : 'left'; - const x = avatarSize / 2 - (avatarSize / (radius * 4)) * (alignment === 'left' ? 1 : -1); - const y = avatarSize - radius; - - const left = - alignment === 'left' - ? x - - (Number(reactionBubbleBackground.width || 0) || styles.reactionBubbleBackground.width) + - radius - : x - radius; - const top = - y - - radius - - (Number(reactionBubbleBackground.height || 0) || styles.reactionBubbleBackground.height); - - return ( - - - - - - - - - - - - - - - - - - - - - - {name} - - - - ); -}; - -const styles = StyleSheet.create({ - avatarContainer: { - padding: 8, - }, - avatarInnerContainer: { - alignSelf: 'center', - }, - avatarName: { - flex: 1, - fontSize: 12, - fontWeight: '700', - paddingTop: 6, - textAlign: 'center', - }, - avatarNameContainer: { - alignItems: 'center', - flexDirection: 'row', - flexGrow: 1, - }, - reactionBubble: { - alignItems: 'center', - borderRadius: 24, - justifyContent: 'center', - position: 'absolute', - }, - reactionBubbleBackground: { - borderRadius: 24, - height: 24, - position: 'absolute', - width: 24, - }, -}); diff --git a/package/src/components/MessageOverlay/hooks/useMessageActionAnimation.tsx b/package/src/components/MessageOverlay/hooks/useMessageActionAnimation.tsx deleted file mode 100644 index 6125f88639..0000000000 --- a/package/src/components/MessageOverlay/hooks/useMessageActionAnimation.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import type { ViewStyle } from 'react-native'; -import { TapGestureHandlerStateChangeEvent } from 'react-native-gesture-handler'; -import { - runOnJS, - useAnimatedGestureHandler, - useAnimatedStyle, - useSharedValue, -} from 'react-native-reanimated'; - -import type { MessageActionListItemProps } from '../MessageActionListItem'; - -/** - * @deprecated - * The implementation is done in the component itself using new API of `react-native-gesture-handler`. - */ -export const useMessageActionAnimation = ({ - action, - activeOpacity = 0.2, -}: Pick & { - activeOpacity?: number; -}) => { - const opacity = useSharedValue(1); - - const onTap = useAnimatedGestureHandler( - { - onEnd: () => { - runOnJS(action)(); - }, - onFinish: () => { - opacity.value = 1; - }, - onStart: () => { - opacity.value = activeOpacity; - }, - }, - [action], - ); - - const animatedStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - })); - - return { - animatedStyle, - onTap, - opacity, - }; -}; 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 0adb115e5b..4a0914e857 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap @@ -245,289 +245,278 @@ exports[`Thread should match thread snapshot 1`] = ` - - - + + - - + testID="avatar-image" + /> - + + - - - Message6 - + Message6 - + - + + - - 2:50 PM - - - ⦁ - - - Edited - - + 2:50 PM + + + ⦁ + + + Edited + @@ -559,289 +548,278 @@ exports[`Thread should match thread snapshot 1`] = ` - - - + + - - + testID="avatar-image" + /> - + + - - - Message5 - + Message5 - + - + + - - 2:50 PM - - - ⦁ - - - Edited - - + 2:50 PM + + + ⦁ + + + Edited + @@ -873,289 +851,278 @@ exports[`Thread should match thread snapshot 1`] = ` - - - + + - - + testID="avatar-image" + /> - + + - - - Message4 - + } + > + Message4 - + - + + - - 2:50 PM - - - ⦁ - - - Edited - - + 2:50 PM + + + ⦁ + + + Edited + @@ -1233,283 +1200,275 @@ exports[`Thread should match thread snapshot 1`] = ` style={ [ undefined, + { + "backgroundColor": undefined, + }, ] } - testID="message-wrapper" > - - - + + - - + testID="avatar-image" + /> - + + - - - Message3 - + Message3 - + - + + - - 2:50 PM - - - ⦁ - - - Edited - - + 2:50 PM + + + ⦁ + + + Edited + @@ -1997,7 +1956,6 @@ exports[`Thread should match thread snapshot 1`] = ` "translateY": 0, }, ], - "zIndex": 2, }, {}, ] diff --git a/package/src/components/UIComponents/BottomSheetModal.tsx b/package/src/components/UIComponents/BottomSheetModal.tsx new file mode 100644 index 0000000000..fd8c955945 --- /dev/null +++ b/package/src/components/UIComponents/BottomSheetModal.tsx @@ -0,0 +1,167 @@ +import React, { PropsWithChildren, useEffect } from 'react'; +import { + Animated, + Keyboard, + KeyboardEvent, + Modal, + StyleSheet, + useWindowDimensions, + View, +} from 'react-native'; +import { + Gesture, + GestureDetector, + GestureHandlerRootView, + GestureUpdateEvent, + PanGestureHandlerEventPayload, +} from 'react-native-gesture-handler'; + +import { runOnJS } from 'react-native-reanimated'; + +import { useTheme } from '../../contexts/themeContext/ThemeContext'; + +export type BottomSheetModalProps = { + /** + * Function to call when the modal is closed. + * @returns void + */ + onClose: () => void; + /** + * Whether the modal is visible. + */ + visible: boolean; + /** + * The height of the modal. + */ + height?: number; +}; + +/** + * A modal that slides up from the bottom of the screen. + */ +export const BottomSheetModal = (props: PropsWithChildren) => { + const { height: windowHeight, width: windowWidth } = useWindowDimensions(); + const { children, height = windowHeight / 2, onClose, visible } = props; + const { + theme: { + colors: { grey, overlay, white_snow }, + }, + } = useTheme(); + + const translateY = new Animated.Value(height); + + const openAnimation = Animated.timing(translateY, { + duration: 200, + toValue: 0, + useNativeDriver: true, + }); + + const closeAnimation = Animated.timing(translateY, { + duration: 50, + toValue: height, + useNativeDriver: true, + }); + + const handleDismiss = () => { + closeAnimation.start(() => onClose()); + }; + + useEffect(() => { + if (visible) { + openAnimation.start(); + } + }, [visible, openAnimation]); + + useEffect(() => { + const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', keyboardDidShow); + const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', keyboardDidHide); + + return () => { + keyboardDidShowListener.remove(); + keyboardDidHideListener.remove(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const keyboardDidShow = (event: KeyboardEvent) => { + Animated.timing(translateY, { + duration: 250, + toValue: -event.endCoordinates.height, + useNativeDriver: true, + }).start(); + }; + + const keyboardDidHide = () => { + Animated.timing(translateY, { + duration: 250, + toValue: 0, + useNativeDriver: true, + }).start(); + }; + + const handleUpdate = (event: GestureUpdateEvent) => { + const translationY = Math.max(event.translationY, 0); + translateY.setValue(translationY); + }; + + const gesture = Gesture.Pan() + .onUpdate((event) => { + runOnJS(handleUpdate)(event); + }) + .onEnd((event) => { + if (event.velocityY > 500 || event.translationY > height / 2) { + runOnJS(handleDismiss)(); + } else { + runOnJS(openAnimation.start)(); + } + }); + + return ( + + + + + + + {children} + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + borderTopLeftRadius: 16, + borderTopRightRadius: 16, + }, + content: { + flex: 1, + padding: 16, + }, + contentContainer: { + flex: 1, + marginTop: 8, + }, + handle: { + alignSelf: 'center', + borderRadius: 4, + height: 4, + marginVertical: 8, + }, + overlay: { + flex: 1, + justifyContent: 'flex-end', + }, +}); diff --git a/package/src/components/ImageBackground.tsx b/package/src/components/UIComponents/ImageBackground.tsx similarity index 100% rename from package/src/components/ImageBackground.tsx rename to package/src/components/UIComponents/ImageBackground.tsx diff --git a/package/src/components/Spinner/Spinner.tsx b/package/src/components/UIComponents/Spinner.tsx similarity index 100% rename from package/src/components/Spinner/Spinner.tsx rename to package/src/components/UIComponents/Spinner.tsx diff --git a/package/src/components/UIComponents/index.ts b/package/src/components/UIComponents/index.ts new file mode 100644 index 0000000000..335a933cf4 --- /dev/null +++ b/package/src/components/UIComponents/index.ts @@ -0,0 +1,3 @@ +export * from './BottomSheetModal'; +export * from './ImageBackground'; +export * from './Spinner'; diff --git a/package/src/components/index.ts b/package/src/components/index.ts index abeb80568e..63b5372da8 100644 --- a/package/src/components/index.ts +++ b/package/src/components/index.ts @@ -152,20 +152,20 @@ export * from './MessageList/utils/getGroupStyles'; export * from './MessageList/utils/getLastReceivedMessage'; export * from './MessageList/utils/getReadStates'; -export * from './MessageOverlay/hooks/useMessageActionAnimation'; -export * from './MessageOverlay/MessageActionList'; -export * from './MessageOverlay/MessageActionListItem'; -export * from './MessageOverlay/MessageOverlay'; -export * from './MessageOverlay/OverlayBackdrop'; -export * from './MessageOverlay/OverlayReactions'; -export * from './MessageOverlay/OverlayReactionsAvatar'; -export * from './MessageOverlay/OverlayReactionList'; +export * from './MessageMenu/MessageActionList'; +export * from './MessageMenu/MessageActionListItem'; +export * from './MessageMenu/MessageMenu'; +export * from './MessageMenu/MessageUserReactions'; +export * from './MessageMenu/MessageUserReactionsAvatar'; +export * from './MessageMenu/MessageReactionPicker'; export * from './ProgressControl/ProgressControl'; export * from './Reply/Reply'; -export * from './Spinner/Spinner'; +export * from './UIComponents/BottomSheetModal'; +export * from './UIComponents/ImageBackground'; +export * from './UIComponents/Spinner'; export * from './Thread/Thread'; export * from './Thread/components/ThreadFooterComponent'; diff --git a/package/src/contexts/__tests__/index.test.tsx b/package/src/contexts/__tests__/index.test.tsx index 55768597da..e87199034f 100644 --- a/package/src/contexts/__tests__/index.test.tsx +++ b/package/src/contexts/__tests__/index.test.tsx @@ -9,7 +9,6 @@ import { useChannelsContext, useChatContext, useImageGalleryContext, - useMessageOverlayContext, useMessagesContext, useOverlayContext, useOwnCapabilitiesContext, @@ -74,10 +73,6 @@ describe('contexts hooks in a component throws an error with message when not wr useImageGalleryContext, `The useImageGalleryContext hook was called outside the ImageGalleryContext Provider. Make sure you have configured OverlayProvider component correctly - https://getstream.io/chat/docs/sdk/reactnative/basics/hello_stream_chat/#overlay-provider`, ], - [ - useMessageOverlayContext, - `The useMessageOverlayContext hook was called outside the MessageOverlayContext Provider. Make sure you have configured OverlayProvider component correctly - https://getstream.io/chat/docs/sdk/reactnative/basics/hello_stream_chat/#overlay-provider`, - ], [ useMessagesContext, `The useMessagesContext hook was called outside of the MessagesContext provider. Make sure you have configured MessageList component correctly - https://getstream.io/chat/docs/sdk/reactnative/basics/hello_stream_chat/#message-list`, diff --git a/package/src/contexts/index.ts b/package/src/contexts/index.ts index e5275fbc7d..24381db70b 100644 --- a/package/src/contexts/index.ts +++ b/package/src/contexts/index.ts @@ -9,7 +9,6 @@ export * from './messageContext/MessageContext'; export * from './messageInputContext/hooks/useCreateMessageInputContext'; export * from './messageInputContext/hooks/useMessageDetailsForState'; export * from './messageInputContext/MessageInputContext'; -export * from './messageOverlayContext/MessageOverlayContext'; export * from './messagesContext/MessagesContext'; export * from './paginatedMessageListContext/PaginatedMessageListContext'; export * from './overlayContext/OverlayContext'; diff --git a/package/src/contexts/messageContext/MessageContext.tsx b/package/src/contexts/messageContext/MessageContext.tsx index e482e19f5d..95ef5b21b6 100644 --- a/package/src/contexts/messageContext/MessageContext.tsx +++ b/package/src/contexts/messageContext/MessageContext.tsx @@ -27,6 +27,10 @@ export type MessageContextValue< actionsEnabled: boolean; /** Position of the message, either 'right' or 'left' */ alignment: Alignment; + /** + * Function to dismiss the overlay + */ + dismissOverlay: () => void; /** The files attached to a message */ files: Attachment[]; /** @@ -125,7 +129,12 @@ export type MessageContextValue< reactions: ReactionSummary[]; /** React set state function to set the state of `isEditedMessageOpen` */ setIsEditedMessageOpen: React.Dispatch>; - showMessageOverlay: (isMessageActionsVisible?: boolean, error?: boolean) => void; + /** + * Function to show the menu with all the message actions. + * @param showMessageReactions + * @returns void + */ + showMessageOverlay: (showMessageReactions?: boolean) => void; showMessageStatus: boolean; /** Whether or not the Message is part of a Thread */ threadList: boolean; diff --git a/package/src/contexts/messageInputContext/__tests__/isValidMessage.test.tsx b/package/src/contexts/messageInputContext/__tests__/isValidMessage.test.tsx index 73f38d52ec..2e9a04c1cc 100644 --- a/package/src/contexts/messageInputContext/__tests__/isValidMessage.test.tsx +++ b/package/src/contexts/messageInputContext/__tests__/isValidMessage.test.tsx @@ -13,13 +13,12 @@ import type { DefaultStreamChatGenerics } from '../../../types/types'; import { FileState } from '../../../utils/utils'; import { InputMessageInputContextValue, - MessageInputContextValue, MessageInputProvider, useMessageInputContext, } from '../MessageInputContext'; const user1 = generateUser(); -const message = generateMessage({ user: user1 }, 'isValidMessage'); +const message = generateMessage({ user: user1 }); type WrapperType = Partial>; @@ -32,7 +31,7 @@ const Wrapper = + } as InputMessageInputContextValue } > {children} diff --git a/package/src/contexts/messageInputContext/__tests__/uploadFile.test.tsx b/package/src/contexts/messageInputContext/__tests__/uploadFile.test.tsx index a0e71bb313..7d7b3c5049 100644 --- a/package/src/contexts/messageInputContext/__tests__/uploadFile.test.tsx +++ b/package/src/contexts/messageInputContext/__tests__/uploadFile.test.tsx @@ -9,7 +9,6 @@ import { generateUser } from '../../../mock-builders/generator/user'; import type { DefaultStreamChatGenerics } from '../../../types/types'; import { InputMessageInputContextValue, - MessageInputContextValue, MessageInputProvider, useMessageInputContext, } from '../MessageInputContext'; @@ -28,7 +27,7 @@ const Wrapper = + } as InputMessageInputContextValue } > {children} diff --git a/package/src/contexts/messageInputContext/__tests__/uploadImage.test.tsx b/package/src/contexts/messageInputContext/__tests__/uploadImage.test.tsx index c2d106a9e6..7bd2a571fc 100644 --- a/package/src/contexts/messageInputContext/__tests__/uploadImage.test.tsx +++ b/package/src/contexts/messageInputContext/__tests__/uploadImage.test.tsx @@ -8,7 +8,6 @@ import { generateUser } from '../../../mock-builders/generator/user'; import type { DefaultStreamChatGenerics } from '../../../types/types'; import { InputMessageInputContextValue, - MessageInputContextValue, MessageInputProvider, useMessageInputContext, } from '../MessageInputContext'; @@ -24,7 +23,7 @@ const Wrapper = + } as InputMessageInputContextValue } > {children} diff --git a/package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx b/package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx deleted file mode 100644 index b5d3d3aa07..0000000000 --- a/package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React, { PropsWithChildren, useContext } from 'react'; - -import type { ImageProps } from 'react-native'; - -import type { Attachment, TranslationLanguages } from 'stream-chat'; - -import { useResettableState } from './hooks/useResettableState'; - -import type { GroupType, MessageType } from '../../components/MessageList/hooks/useMessageList'; -import type { MessageActionListProps } from '../../components/MessageOverlay/MessageActionList'; -import type { - MessageActionListItemProps, - MessageActionType, -} from '../../components/MessageOverlay/MessageActionListItem'; -import type { OverlayReactionListProps } from '../../components/MessageOverlay/OverlayReactionList'; -import type { OverlayReactionsProps } from '../../components/MessageOverlay/OverlayReactions'; -import type { OverlayReactionsAvatarProps } from '../../components/MessageOverlay/OverlayReactionsAvatar'; -import type { DefaultStreamChatGenerics, UnknownType } from '../../types/types'; -import type { ReactionData } from '../../utils/utils'; -import type { ChatContextValue } from '../chatContext/ChatContext'; -import type { Alignment, MessageContextValue } from '../messageContext/MessageContext'; -import type { MessagesContextValue } from '../messagesContext/MessagesContext'; -import type { OwnCapabilitiesContextValue } from '../ownCapabilitiesContext/OwnCapabilitiesContext'; -import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue'; - -import { getDisplayName } from '../utils/getDisplayName'; -import { isTestEnvironment } from '../utils/isTestEnvironment'; - -export type MessageOverlayData< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = { - alignment?: Alignment; - chatContext?: ChatContextValue; - clientId?: string; - files?: Attachment[]; - groupStyles?: GroupType[]; - handleReaction?: (reactionType: string) => Promise; - ImageComponent?: React.ComponentType; - images?: Attachment[]; - message?: MessageType; - messageActions?: MessageActionType[]; - messageContext?: MessageContextValue; - messageReactionTitle?: string; - messagesContext?: MessagesContextValue; - onlyEmojis?: boolean; - otherAttachments?: Attachment[]; - OverlayReactionList?: React.ComponentType>; - ownCapabilities?: OwnCapabilitiesContextValue; - supportedReactions?: ReactionData[]; - threadList?: boolean; - userLanguage?: TranslationLanguages; - videos?: Attachment[]; -}; - -export type MessageOverlayContextValue< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = { - /** - * Custom UI component for rendering [message actions](https://github.com/GetStream/stream-chat-react-native/blob/main/screenshots/docs/2.png) in overlay. - * - * **Default** [MessageActionList](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageOverlay/MessageActions.tsx) - */ - MessageActionList: React.ComponentType>; - MessageActionListItem: React.ComponentType>; - /** - * Custom UI component for rendering [reaction selector](https://github.com/GetStream/stream-chat-react-native/blob/main/screenshots/docs/2.png) in overlay (which shows up on long press on message). - * - * **Default** [OverlayReactionList](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageOverlay/OverlayReactionList.tsx) - */ - OverlayReactionList: React.ComponentType>; - /** - * Custom UI component for rendering [reactions list](https://github.com/GetStream/stream-chat-react-native/blob/main/screenshots/docs/2.png), in overlay (which shows up on long press on message). - * - * **Default** [OverlayReactions](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageOverlay/OverlayReactions.tsx) - */ - OverlayReactions: React.ComponentType>; - OverlayReactionsAvatar: React.ComponentType; - reset: () => void; - setData: React.Dispatch>>; - data?: MessageOverlayData; -}; - -export const MessageOverlayContext = React.createContext( - DEFAULT_BASE_CONTEXT_VALUE as MessageOverlayContextValue, -); - -export const MessageOverlayProvider = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->({ - children, - value, -}: PropsWithChildren<{ - value?: MessageOverlayContextValue; -}>) => { - const messageOverlayContext = useResettableState(value); - return ( - - {children} - - ); -}; - -export const useMessageOverlayContext = < - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->() => { - const contextValue = useContext( - MessageOverlayContext, - ) as unknown as MessageOverlayContextValue; - - if (contextValue === DEFAULT_BASE_CONTEXT_VALUE && !isTestEnvironment()) { - throw new Error( - `The useMessageOverlayContext hook was called outside the MessageOverlayContext Provider. Make sure you have configured OverlayProvider component correctly - https://getstream.io/chat/docs/sdk/reactnative/basics/hello_stream_chat/#overlay-provider`, - ); - } - - return contextValue; -}; - -/** - * @deprecated - * - * This will be removed in the next major version. - * - * Typescript currently does not support partial inference so if ChatContext - * typing is desired while using the HOC withMessageOverlayContext the Props for the - * wrapped component must be provided as the first generic. - */ -export const withMessageOverlayContext = < - P extends UnknownType, - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, ->( - Component: React.ComponentType

, -): React.ComponentType>> => { - const WithMessageOverlayContextComponent = ( - props: Omit>, - ) => { - const messageContext = useMessageOverlayContext(); - - return ; - }; - WithMessageOverlayContextComponent.displayName = `WithMessageOverlayContext${getDisplayName( - Component, - )}`; - return WithMessageOverlayContextComponent; -}; diff --git a/package/src/contexts/messageOverlayContext/hooks/useResettableState.test.tsx b/package/src/contexts/messageOverlayContext/hooks/useResettableState.test.tsx deleted file mode 100644 index 4f7fb3a566..0000000000 --- a/package/src/contexts/messageOverlayContext/hooks/useResettableState.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; - -import { Button, Text } from 'react-native'; - -import { render, screen, userEvent, waitFor } from '@testing-library/react-native'; - -import { useResettableState } from './useResettableState'; - -const TestComponent = () => { - const { data, reset, setData } = useResettableState(0); - - return ( - <> - {`${data}`} -