diff --git a/examples/SampleApp/App.tsx b/examples/SampleApp/App.tsx index ac97c99b28..a2390ac435 100644 --- a/examples/SampleApp/App.tsx +++ b/examples/SampleApp/App.tsx @@ -1,11 +1,10 @@ import React from 'react'; -import { LogBox, Platform, useColorScheme } from 'react-native'; +import { Alert, LogBox, Platform, Text, useColorScheme, View } from 'react-native'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'; import { Chat, OverlayProvider, ThemeProvider, useOverlayContext } from 'stream-chat-react-native'; - import { AppContext } from './src/context/AppContext'; import { AppOverlayProvider } from './src/context/AppOverlayProvider'; import { UserSearchProvider } from './src/context/UserSearchContext'; diff --git a/examples/SampleApp/android/app/build.gradle b/examples/SampleApp/android/app/build.gradle index a338823b2c..7d35eeb12d 100644 --- a/examples/SampleApp/android/app/build.gradle +++ b/examples/SampleApp/android/app/build.gradle @@ -197,11 +197,11 @@ dependencies { implementation platform('com.google.firebase:firebase-bom:25.12.0') // For animated GIF support - implementation 'com.facebook.fresco:animated-gif:2.0.0' + implementation 'com.facebook.fresco:animated-gif:2.6.0' // For WebP support, including animated WebP - implementation 'com.facebook.fresco:animated-webp:2.1.0' - implementation 'com.facebook.fresco:webpsupport:2.0.0' + implementation 'com.facebook.fresco:animated-webp:2.6.0' + implementation 'com.facebook.fresco:webpsupport:2.6.0' implementation 'com.google.firebase:firebase-crashlytics' implementation 'com.google.firebase:firebase-analytics' diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx index d3364ce3ed..b5b2d44739 100644 --- a/examples/SampleApp/src/screens/ChannelScreen.tsx +++ b/examples/SampleApp/src/screens/ChannelScreen.tsx @@ -1,7 +1,6 @@ import React, { useContext, useEffect, useState } from 'react'; -import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native'; +import type { Channel as StreamChatChannel } from 'stream-chat'; import { RouteProp, useFocusEffect, useNavigation } from '@react-navigation/native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Channel, ChannelAvatar, @@ -14,14 +13,15 @@ import { useTheme, useTypingString, } from 'stream-chat-react-native'; +import { Platform, StyleSheet, View } from 'react-native'; +import type { StackNavigationProp } from '@react-navigation/stack'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { ScreenHeader } from '../components/ScreenHeader'; import { AppContext } from '../context/AppContext'; +import { ScreenHeader } from '../components/ScreenHeader'; +import { TouchableOpacity } from 'react-native-gesture-handler'; import { useChannelMembersStatus } from '../hooks/useChannelMembersStatus'; -import type { StackNavigationProp } from '@react-navigation/stack'; -import type { Channel as StreamChatChannel } from 'stream-chat'; - import type { LocalAttachmentType, LocalChannelType, diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 761c7569e8..0ac5da0106 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -227,22 +227,17 @@ export type ChannelPropsWithContext< | 'animatedLongPress' | 'Attachment' | 'AttachmentActions' - | 'blockUser' | 'Card' | 'CardCover' | 'CardFooter' | 'CardHeader' - | 'copyMessage' | 'DateHeader' | 'deletedMessagesVisibilityType' - | 'deleteMessage' | 'disableTypingIndicator' | 'dismissKeyboardOnMessageTouch' - | 'editMessage' | 'FileAttachment' | 'FileAttachmentIcon' | 'FileAttachmentGroup' - | 'flagMessage' | 'FlatList' | 'forceAlignMessages' | 'formatDate' @@ -279,22 +274,17 @@ export type ChannelPropsWithContext< | 'MessageStatus' | 'MessageSystem' | 'MessageText' - | 'muteUser' | 'myMessageTheme' | 'onDoubleTapMessage' | 'onLongPressMessage' | 'onPressInMessage' | 'onPressMessage' | 'OverlayReactionList' - | 'pinMessage' - | 'quotedReply' | 'ReactionList' | 'Reply' - | 'retry' | 'ScrollToBottomButton' | 'selectReaction' | 'supportedReactions' - | 'threadReply' | 'TypingIndicator' | 'TypingIndicatorContainer' | 'UrlPreview' @@ -419,7 +409,6 @@ const ChannelWithContext = < AttachmentActions = AttachmentActionsDefault, autoCompleteSuggestionsLimit, autoCompleteTriggerSettings, - blockUser, Card = CardDefault, CardCover, CardFooter, @@ -431,10 +420,8 @@ const ChannelWithContext = < CommandsButton = CommandsButtonDefault, compressImageQuality, CooldownTimer = CooldownTimerDefault, - copyMessage, DateHeader = DateHeaderDefault, deletedMessagesVisibilityType = 'always', - deleteMessage, disableIfFrozenChannel = true, disableKeyboardCompatibleView = false, disableTypingIndicator, @@ -444,7 +431,6 @@ const ChannelWithContext = < doMarkReadRequest, doSendMessageRequest, doUpdateMessageRequest, - editMessage: editMessageProp, EmptyStateIndicator = EmptyStateIndicatorDefault, enableMessageGroupingByUser = true, enforceUniqueReaction = false, @@ -452,7 +438,6 @@ const ChannelWithContext = < FileAttachmentGroup = FileAttachmentGroupDefault, FileAttachmentIcon = FileIconDefault, FileUploadPreview = FileUploadPreviewDefault, - flagMessage, FlatList = FlatListDefault, forceAlignMessages, formatDate, @@ -519,7 +504,6 @@ const ChannelWithContext = < MessageText, MoreOptionsButton = MoreOptionsButtonDefault, mutesEnabled: mutesEnabledProp, - muteUser, myMessageTheme, newMessageStateUpdateThrottleInterval = defaultThrottleInterval, NetworkDownIndicator = NetworkDownIndicatorDefault, @@ -531,16 +515,13 @@ const ChannelWithContext = < onPressMessage, openSuggestions, OverlayReactionList = OverlayReactionListDefault, - pinMessage, pinMessageEnabled: pinMessageEnabledProp, quotedRepliesEnabled: quotedRepliesEnabledProp, - quotedReply, ReactionList = ReactionListDefault, reactionsEnabled: reactionsEnabledProp, read, readEventsEnabled: readEventsEnabledProp, Reply = ReplyDefault, - retry, ScrollToBottomButton = ScrollToBottomButtonDefault, selectReaction, SendButton = SendButtonDefault, @@ -563,7 +544,6 @@ const ChannelWithContext = < threadList, threadMessages, threadRepliesEnabled: threadRepliesEnabledProp, - threadReply, typing, typingEventsEnabled: typingEventsEnabledProp, TypingIndicator = TypingIndicatorDefault, @@ -1749,24 +1729,19 @@ const ChannelWithContext = < animatedLongPress, Attachment, AttachmentActions, - blockUser, Card, CardCover, CardFooter, CardHeader, channelId, - copyMessage, DateHeader, deletedMessagesVisibilityType, - deleteMessage, disableTypingIndicator, dismissKeyboardOnMessageTouch, - editMessage: editMessageProp, enableMessageGroupingByUser, FileAttachment, FileAttachmentGroup, FileAttachmentIcon, - flagMessage, FlatList, forceAlignMessages, formatDate, @@ -1804,19 +1779,15 @@ const ChannelWithContext = < MessageStatus, MessageSystem, MessageText, - muteUser, myMessageTheme, onDoubleTapMessage, onLongPressMessage, onPressInMessage, onPressMessage, OverlayReactionList, - pinMessage, - quotedReply, ReactionList, removeMessage, Reply, - retry, retrySendMessage, ScrollToBottomButton, selectReaction, @@ -1824,7 +1795,6 @@ const ChannelWithContext = < setQuotedMessageState, supportedReactions, targetedMessage, - threadReply, TypingIndicator, TypingIndicatorContainer, updateMessage, diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts index e1670b9723..c331796971 100644 --- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts +++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts @@ -25,24 +25,19 @@ export const useCreateMessagesContext = < animatedLongPress, Attachment, AttachmentActions, - blockUser, Card, CardCover, CardFooter, CardHeader, channelId, - copyMessage, DateHeader, deletedMessagesVisibilityType, - deleteMessage, disableTypingIndicator, dismissKeyboardOnMessageTouch, - editMessage, enableMessageGroupingByUser, FileAttachment, FileAttachmentGroup, FileAttachmentIcon, - flagMessage, FlatList, forceAlignMessages, formatDate, @@ -81,22 +76,18 @@ export const useCreateMessagesContext = < MessageSystem, MessageText, mutesEnabled, - muteUser, myMessageTheme, onDoubleTapMessage, onLongPressMessage, onPressInMessage, onPressMessage, OverlayReactionList, - pinMessage, pinMessageEnabled, quotedRepliesEnabled, - quotedReply, ReactionList, reactionsEnabled, removeMessage, Reply, - retry, retrySendMessage, ScrollToBottomButton, selectReaction, @@ -105,7 +96,6 @@ export const useCreateMessagesContext = < supportedReactions, targetedMessage, threadRepliesEnabled, - threadReply, TypingIndicator, TypingIndicatorContainer, updateMessage, @@ -127,23 +117,18 @@ export const useCreateMessagesContext = < animatedLongPress, Attachment, AttachmentActions, - blockUser, Card, CardCover, CardFooter, CardHeader, - copyMessage, DateHeader, deletedMessagesVisibilityType, - deleteMessage, disableTypingIndicator, dismissKeyboardOnMessageTouch, - editMessage, enableMessageGroupingByUser, FileAttachment, FileAttachmentGroup, FileAttachmentIcon, - flagMessage, FlatList, forceAlignMessages, formatDate, @@ -182,22 +167,18 @@ export const useCreateMessagesContext = < MessageSystem, MessageText, mutesEnabled, - muteUser, myMessageTheme, onDoubleTapMessage, onLongPressMessage, onPressInMessage, onPressMessage, OverlayReactionList, - pinMessage, pinMessageEnabled, quotedRepliesEnabled, - quotedReply, ReactionList, reactionsEnabled, removeMessage, Reply, - retry, retrySendMessage, ScrollToBottomButton, selectReaction, @@ -206,7 +187,6 @@ export const useCreateMessagesContext = < supportedReactions, targetedMessage, threadRepliesEnabled, - threadReply, TypingIndicator, TypingIndicatorContainer, updateMessage, diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index aa293d3be2..961836283b 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -1,7 +1,5 @@ import React, { useEffect, useRef } from 'react'; import { - Alert, - Clipboard, GestureResponderEvent, Image, Keyboard, @@ -25,7 +23,6 @@ import Animated, { import { useCreateMessageContext } from './hooks/useCreateMessageContext'; import { messageActions as defaultMessageActions } from './utils/messageActions'; -import { removeReservedFields } from './utils/removeReservedFields'; import { isMessageWithStylesReadByAndDateSeparator, @@ -47,10 +44,10 @@ import { Reactions, } from '../../contexts/messageContext/MessageContext'; import { - MessageAction, MessageOverlayContextValue, useMessageOverlayContext, } from '../../contexts/messageOverlayContext/MessageOverlayContext'; +import type { MessageActionListItemProps } from '../MessageOverlay/MessageActionListItem'; import { MessagesContextValue, useMessagesContext, @@ -60,31 +57,16 @@ import { useOverlayContext, } from '../../contexts/overlayContext/OverlayContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; -import { useToastContext } from '../../contexts/toastContext/ToastContext'; import { ThreadContextValue, useThreadContext } from '../../contexts/threadContext/ThreadContext'; import { TranslationContextValue, useTranslationContext, } from '../../contexts/translationContext/TranslationContext'; -import { - Copy, - CurveLineLeftUp, - Delete, - Edit, - MessageFlag, - Mute, - Pin, - SendUp, - ThreadReply, - Unpin, - UserDelete, -} from '../../icons'; import { triggerHaptic } from '../../native'; -import { StreamCache } from '../../StreamCache'; import { emojiRegex } from '../../utils/utils'; -import type { Attachment, MessageResponse, Reaction } from 'stream-chat'; +import type { Attachment } from 'stream-chat'; import type { DefaultAttachmentType, @@ -96,6 +78,8 @@ import type { DefaultUserType, UnknownType, } from '../../types/types'; +import { useMessageActions } from './hooks/useMessageActions'; +import { useMessageActionHandlers } from './hooks/useMessageActionHandlers'; const prefetchImage = ({ height, url }: { height: number | string; url: string }) => { if (url.includes('&h=%2A')) { @@ -144,6 +128,7 @@ export type MessageTouchableHandlerPayload< export type MessageActionHandlers = { deleteMessage: () => Promise; editMessage: () => void; + pinMessage: () => Promise; quotedReply: () => void; resendMessage: () => Promise; showMessageOverlay: () => void; @@ -177,12 +162,7 @@ export type MessagePropsWithContext< Pick< MessagesContextValue, | 'animatedLongPress' - | 'blockUser' - | 'copyMessage' - | 'deleteMessage' | 'dismissKeyboardOnMessageTouch' - | 'editMessage' - | 'flagMessage' | 'forceAlignMessages' | 'handleBlock' | 'handleCopy' @@ -199,26 +179,21 @@ export type MessagePropsWithContext< | 'messageContentOrder' | 'MessageSimple' | 'mutesEnabled' - | 'muteUser' | 'onDoubleTapMessage' | 'onLongPressMessage' | 'onPressInMessage' | 'onPressMessage' | 'OverlayReactionList' - | 'pinMessage' | 'pinMessageEnabled' | 'quotedRepliesEnabled' - | 'quotedReply' | 'reactionsEnabled' | 'removeMessage' - | 'retry' | 'retrySendMessage' | 'selectReaction' | 'setEditingState' | 'setQuotedMessageState' | 'supportedReactions' | 'threadRepliesEnabled' - | 'threadReply' | 'updateMessage' > & Pick, 'setData'> & @@ -298,18 +273,13 @@ const MessageWithContext = < const { animatedLongPress = Platform.OS === 'ios' && !isMessageTypeDeleted, - blockUser: blockUserProp, channel, client, - copyMessage: copyMessageProp, - deleteMessage: deleteMessageProp, disabled, dismissKeyboard, dismissKeyboardOnMessageTouch, - editMessage: editMessageProp, enableLongPress = true, enforceUniqueReaction, - flagMessage: flagMessageProp, forceAlignMessages = false, goToMessage, groupStyles = ['bottom'], @@ -334,7 +304,6 @@ const MessageWithContext = < messagesContext, MessageSimple, mutesEnabled, - muteUser: muteUserProp, onDoubleTapMessage: onDoubleTapMessageProp, onLongPress: onLongPressProp, onLongPressMessage: onLongPressMessageProp, @@ -345,15 +314,12 @@ const MessageWithContext = < onThreadSelect, openThread, OverlayReactionList, - pinMessage: pinMessageProp, pinMessageEnabled, preventPress, quotedRepliesEnabled, reactionsEnabled, readEventsEnabled, removeMessage, - quotedReply: quotedReplyProp, - retry: retryProp, retrySendMessage, selectReaction, setData, @@ -369,15 +335,12 @@ const MessageWithContext = < isTargetedMessage, threadList = false, threadRepliesEnabled, - threadReply: threadReplyProp, updateMessage, } = props; - const toast = useToastContext(); - const { theme: { - colors: { accent_blue, accent_red, bg_gradient_start, grey, targetedMessageBackground }, + colors: { bg_gradient_start, targetedMessageBackground }, messageSimple: { gallery: { halfSize, size }, targetedMessageUnderlay, @@ -605,85 +568,69 @@ const MessageWithContext = < }, [] as Reactions) : []; - const handleToggleReaction = async (reactionType: string) => { - const messageId = message.id; - const ownReaction = !!reactions.find( - (reaction) => reaction.own && reaction.type === reactionType, - ); - - // Change reaction in local state, make API call in background, revert to old message if fails - try { - if (channel && messageId) { - if (ownReaction) { - await channel.deleteReaction(messageId, reactionType); - } else { - await channel.sendReaction( - messageId, - { - type: reactionType, - } as Reaction, - { enforce_unique: enforceUniqueReaction }, - ); - } - } - } catch (err) { - console.log(err); - } - }; - - const handleResendMessage = () => - retrySendMessage(message as MessageResponse); - - const handleQuotedReplyMessage = () => { - setQuotedMessageState(message); - }; - - const isMuted = (client.mutedUsers || []).some( - (mute) => mute.user.id === client.userID && mute.target.id === message.user?.id, - ); - - const handleToggleMuteUser = async () => { - if (!message.user?.id) { - return; - } - - if (isMuted) { - await client.unmuteUser(message.user.id); - } else { - await client.muteUser(message.user.id); - } - }; - - const handleDeleteMessage = async () => { - const data = await client.deleteMessage(message.id); - updateMessage(data.message); - }; - - const handleEditMessage = () => { - setEditingState(message); - }; - - const handleTogglePinMessage = async () => { - const MessagePinnedHeaderStatus = message.pinned; - if (!MessagePinnedHeaderStatus) { - await client.pinMessage(message, null); - } else { - await client.unpinMessage(message); - } - }; - - const handleToggleBanUser = async () => { - const messageUser = message.user; - if (!messageUser) { - return; - } + const { + handleDeleteMessage, + handleEditMessage, + handleQuotedReplyMessage, + handleResendMessage, + handleToggleBanUser, + handleToggleMuteUser, + handleTogglePinMessage, + handleToggleReaction, + } = useMessageActionHandlers({ + channel, + client, + enforceUniqueReaction, + message, + reactionsEnabled, + retrySendMessage, + setEditingState, + setQuotedMessageState, + supportedReactions, + updateMessage, + }); - if (messageUser.banned) { - await client.unbanUser(messageUser.id); - } else { - await client.banUser(messageUser.id); - } - }; + const { + blockUser, + copyMessage, + deleteMessage, + editMessage, + flagMessage, + handleReaction, + muteUser, + pinMessage, + quotedReply, + retry, + threadReply, + unpinMessage, + } = useMessageActions({ + channel, + client, + enforceUniqueReaction, + handleBlock, + handleCopy, + handleDelete, + handleEdit, + handleFlag, + handleMute, + handlePinMessage, + handleQuotedReply, + handleReaction: handleReactionProp, + handleRetry, + handleThreadReply, + message, + onThreadSelect, + openThread, + reactionsEnabled, + retrySendMessage, + selectReaction, + setEditingState, + setOverlay, + setQuotedMessageState, + supportedReactions, + t, + updateMessage, + }); const showMessageOverlay = async ( messageReactions = false, @@ -691,302 +638,6 @@ const MessageWithContext = < ) => { await dismissKeyboard(); - const blockUser = blockUserProp - ? blockUserProp(message) - : blockUserProp === null - ? null - : { - action: () => async () => { - if (!StreamCache.getInstance().currentNetworkState) { - toast.show(t('Something went wrong'), 2000); - } else { - setOverlay('none'); - if (message.user?.id) { - if (handleBlock) { - handleBlock(message); - } - - await handleToggleBanUser(); - } - } - }, - icon: , - title: message.user?.banned ? t('Unblock User') : t('Block User'), - }; - - const copyMessage = copyMessageProp - ? copyMessageProp(message) - : copyMessageProp === null - ? null - : { - // using depreciated Clipboard from react-native until expo supports the community version or their own - action: () => { - if (!StreamCache.getInstance().currentNetworkState) { - toast.show(t('Something went wrong'), 2000); - } else { - setOverlay('none'); - if (handleCopy) { - handleCopy(message); - } - Clipboard.setString(message.text || ''); - } - }, - icon: , - title: t('Copy Message'), - }; - - const deleteMessage = deleteMessageProp - ? deleteMessageProp(message) - : deleteMessageProp === null - ? null - : { - action: () => { - if (!StreamCache.getInstance().currentNetworkState) { - toast.show(t('Something went wrong'), 2000); - } else { - setOverlay('alert'); - if (message.id) { - Alert.alert( - t('Delete Message'), - t('Are you sure you want to permanently delete this message?'), - [ - { onPress: () => setOverlay('none'), text: t('Cancel') }, - { - onPress: async () => { - setOverlay('none'); - if (handleDelete) { - handleDelete(message); - } - - await handleDeleteMessage(); - }, - style: 'destructive', - text: t('Delete'), - }, - ], - { cancelable: false }, - ); - } - } - }, - icon: , - title: t('Delete Message'), - titleStyle: { color: accent_red }, - }; - - const editMessage = editMessageProp - ? editMessageProp(message) - : editMessageProp === null - ? null - : { - action: () => { - if (!StreamCache.getInstance().currentNetworkState) { - toast.show(t('Something went wrong'), 2000); - } else { - setOverlay('none'); - if (handleEdit) { - handleEdit(message); - } - handleEditMessage(); - } - }, - icon: , - title: t('Edit Message'), - }; - - const pinMessage = pinMessageProp - ? pinMessageProp(message) - : pinMessageProp === null - ? null - : { - action: () => { - setOverlay('none'); - if (handlePinMessage) { - handlePinMessage(message); - } - handleTogglePinMessage(); - }, - icon: , - title: t('Pin to Conversation'), - }; - - const unpinMessage = pinMessageProp - ? pinMessageProp(message) - : pinMessageProp === null - ? null - : { - action: () => { - setOverlay('none'); - if (handlePinMessage) { - handlePinMessage(message); - } - handleTogglePinMessage(); - }, - icon: , - title: t('Unpin from Conversation'), - }; - - const flagMessage = flagMessageProp - ? flagMessageProp(message) - : flagMessageProp === null - ? null - : { - action: () => { - if (!StreamCache.getInstance().currentNetworkState) { - toast.show(t('Something went wrong'), 2000); - } else { - setOverlay('alert'); - if (message.id) { - Alert.alert( - t('Flag Message'), - t( - 'Do you want to send a copy of this message to a moderator for further investigation?', - ), - [ - { onPress: () => setOverlay('none'), text: t('Cancel') }, - { - onPress: async () => { - try { - if (handleFlag) { - handleFlag(message); - } - await client.flagMessage(message.id); - Alert.alert( - t('Message flagged'), - t('The message has been reported to a moderator.'), - [ - { - onPress: () => setOverlay('none'), - text: t('Dismiss'), - }, - ], - ); - } catch (err) { - Alert.alert( - t('Something went wrong'), - t("The operation couldn't be completed."), - [ - { - onPress: () => setOverlay('none'), - text: t('Dismiss'), - }, - ], - ); - } - }, - text: t('Flag'), - }, - ], - { cancelable: false }, - ); - } - } - }, - icon: , - title: t('Flag Message'), - }; - - const handleReaction = !error - ? selectReaction - ? selectReaction(message) - : async (reactionType: string) => { - if (!StreamCache.getInstance().currentNetworkState) { - toast.show(t('Something went wrong'), 2000); - } else { - if (handleReactionProp) { - handleReactionProp(message, reactionType); - } - - await handleToggleReaction(reactionType); - } - } - : undefined; - - const muteUser = muteUserProp - ? muteUserProp(message) - : muteUserProp === null - ? null - : { - action: async () => { - if (!StreamCache.getInstance().currentNetworkState) { - toast.show(t('Something went wrong'), 2000); - } else { - setOverlay('none'); - if (message.user?.id) { - if (handleMute) { - handleMute(message); - } - - await handleToggleMuteUser(); - } - } - }, - icon: , - title: isMuted ? t('Unmute User') : t('Mute User'), - }; - - const quotedReply = quotedReplyProp - ? quotedReplyProp(message) - : quotedReplyProp === null - ? null - : { - action: () => { - if (!StreamCache.getInstance().currentNetworkState) { - toast.show(t('Something went wrong'), 2000); - } else { - setOverlay('none'); - if (handleQuotedReply) { - handleQuotedReply(message); - } - handleQuotedReplyMessage(); - } - }, - icon: , - title: t('Reply'), - }; - - const retry = retryProp - ? retryProp(message) - : retryProp === null - ? null - : { - action: async () => { - if (!StreamCache.getInstance().currentNetworkState) { - toast.show(t('Something went wrong'), 2000); - } else { - setOverlay('none'); - const messageWithoutReservedFields = removeReservedFields(message); - if (handleRetry) { - handleRetry(messageWithoutReservedFields); - } - - await handleResendMessage(); - } - }, - icon: , - title: t('Resend'), - }; - - const threadReply = threadReplyProp - ? threadReplyProp(message) - : threadReplyProp === null - ? null - : { - action: () => { - if (!StreamCache.getInstance().currentNetworkState) { - toast.show(t('Something went wrong'), 2000); - } else { - setOverlay('none'); - if (handleThreadReply) { - handleThreadReply(message); - } - onOpenThread(); - } - }, - icon: , - title: t('Thread Reply'), - }; - const isThreadMessage = threadList || !!message.parent_id; const dismissOverlay = () => setOverlay('none'); @@ -1027,7 +678,7 @@ const MessageWithContext = < handleReaction: reactionsEnabled ? handleReaction : undefined, images: attachments.images, message, - messageActions: messageActions?.filter(Boolean) as MessageAction[] | undefined, + messageActions: messageActions?.filter(Boolean) as MessageActionListItemProps[] | undefined, messageContext: { ...messageContext, disabled: true, preventPress: true }, messageReactionTitle: !error && messageReactions ? t('Message Reactions') : undefined, messagesContext: { ...messagesContext, messageContentOrder }, @@ -1044,6 +695,7 @@ const MessageWithContext = < const actionHandlers: MessageActionHandlers = { deleteMessage: handleDeleteMessage, editMessage: handleEditMessage, + pinMessage: handleTogglePinMessage, quotedReply: handleQuotedReplyMessage, resendMessage: handleResendMessage, showMessageOverlay, diff --git a/package/src/components/Message/hooks/useMessageActionHandlers.ts b/package/src/components/Message/hooks/useMessageActionHandlers.ts new file mode 100644 index 0000000000..4ff30c834f --- /dev/null +++ b/package/src/components/Message/hooks/useMessageActionHandlers.ts @@ -0,0 +1,168 @@ +import type { MessageResponse, Reaction } from 'stream-chat'; + +import type { ChannelContextValue } from '../../../contexts/channelContext/ChannelContext'; +import type { ChatContextValue } from '../../../contexts/chatContext/ChatContext'; +import type { MessageContextValue } from '../../../contexts/messageContext/MessageContext'; +import type { MessagesContextValue } from '../../../contexts/messagesContext/MessagesContext'; +import type { Reactions } from '../../../contexts/messageContext/MessageContext'; + +import type { + DefaultAttachmentType, + DefaultChannelType, + DefaultCommandType, + DefaultEventType, + DefaultMessageType, + DefaultReactionType, + DefaultUserType, + UnknownType, +} from '../../../types/types'; + +export const useMessageActionHandlers = < + At extends UnknownType = DefaultAttachmentType, + Ch extends UnknownType = DefaultChannelType, + Co extends string = DefaultCommandType, + Ev extends UnknownType = DefaultEventType, + Me extends UnknownType = DefaultMessageType, + Re extends UnknownType = DefaultReactionType, + Us extends UnknownType = DefaultUserType, +>({ + channel, + client, + enforceUniqueReaction, + message, + reactionsEnabled, + retrySendMessage, + setEditingState, + setQuotedMessageState, + supportedReactions, + updateMessage, +}: Pick< + MessagesContextValue, + | 'reactionsEnabled' + | 'retrySendMessage' + | 'setEditingState' + | 'setQuotedMessageState' + | 'supportedReactions' + | 'updateMessage' +> & + Pick, 'channel' | 'enforceUniqueReaction'> & + Pick, 'client'> & + Pick, 'message'>) => { + const handleResendMessage = () => + retrySendMessage(message as MessageResponse); + + const handleQuotedReplyMessage = () => { + setQuotedMessageState(message); + }; + + const isMuted = (client.mutedUsers || []).some( + (mute) => mute.user.id === client.userID && mute.target.id === message.user?.id, + ); + + const handleDeleteMessage = async () => { + const data = await client.deleteMessage(message.id); + updateMessage(data.message); + }; + + const handleToggleMuteUser = async () => { + if (!message.user?.id) { + return; + } + + if (isMuted) { + await client.unmuteUser(message.user.id); + } else { + await client.muteUser(message.user.id); + } + }; + + const handleToggleBanUser = async () => { + const messageUser = message.user; + if (!messageUser) { + return; + } + + if (messageUser.banned) { + await client.unbanUser(messageUser.id); + } else { + await client.banUser(messageUser.id); + } + }; + + const handleTogglePinMessage = async () => { + const MessagePinnedHeaderStatus = message.pinned; + if (!MessagePinnedHeaderStatus) { + await client.pinMessage(message, null); + } else { + await client.unpinMessage(message); + } + }; + + const handleEditMessage = () => { + setEditingState(message); + }; + + const clientId = client.userID; + const isMessageTypeDeleted = message.type === 'deleted'; + + const hasReactions = + !!reactionsEnabled && + !isMessageTypeDeleted && + !!message.latest_reactions && + message.latest_reactions.length > 0; + + const reactions = hasReactions + ? supportedReactions.reduce((acc, cur) => { + const reactionType = cur.type; + const reactionsOfReactionType = message.latest_reactions?.filter( + (reaction) => reaction.type === reactionType, + ); + + if (reactionsOfReactionType?.length) { + const hasOwnReaction = reactionsOfReactionType.some( + (reaction) => reaction.user_id === clientId, + ); + acc.push({ own: hasOwnReaction, type: reactionType }); + } + + return acc; + }, [] as Reactions) + : []; + + const handleToggleReaction = async (reactionType: string) => { + const messageId = message.id; + const ownReaction = !!reactions.find( + (reaction) => reaction.own && reaction.type === reactionType, + ); + + // Change reaction in local state, make API call in background, revert to old message if fails + try { + if (channel && messageId) { + if (ownReaction) { + await channel.deleteReaction(messageId, reactionType); + } else { + await channel.sendReaction( + messageId, + { + type: reactionType, + } as Reaction, + { enforce_unique: enforceUniqueReaction }, + ); + } + } + } catch (err) { + console.log(err); + } + }; + + return { + handleDeleteMessage, + handleEditMessage, + handleQuotedReplyMessage, + handleResendMessage, + handleToggleBanUser, + handleToggleMuteUser, + handleTogglePinMessage, + handleToggleReaction, + }; +}; diff --git a/package/src/components/Message/hooks/useMessageActions.tsx b/package/src/components/Message/hooks/useMessageActions.tsx new file mode 100644 index 0000000000..744e8d2650 --- /dev/null +++ b/package/src/components/Message/hooks/useMessageActions.tsx @@ -0,0 +1,427 @@ +import React from 'react'; +import { Alert, Clipboard } from 'react-native'; + +import { StreamCache } from '../../../StreamCache'; +import type { ChannelContextValue } from '../../../contexts/channelContext/ChannelContext'; +import type { ChatContextValue } from '../../../contexts/chatContext/ChatContext'; +import type { MessageActionType } from '../../MessageOverlay/MessageActionListItem'; +import type { MessageContextValue } from '../../../contexts/messageContext/MessageContext'; +import type { MessagesContextValue } from '../../../contexts/messagesContext/MessagesContext'; +import { removeReservedFields } from '../utils/removeReservedFields'; +import { useToastContext } from '../../../contexts/toastContext/ToastContext'; +import { useTheme } from '../../../contexts/themeContext/ThemeContext'; +import type { MessageType } from '../../MessageList/hooks/useMessageList'; +import { + Copy, + CurveLineLeftUp, + Delete, + Edit, + MessageFlag, + Mute, + Pin, + SendUp, + ThreadReply, + Unpin, + UserDelete, +} from '../../../icons'; +import type { OverlayContextValue } from '../../../contexts/overlayContext/OverlayContext'; +import type { ThreadContextValue } from '../../../contexts/threadContext/ThreadContext'; +import type { TranslationContextValue } from '../../../contexts/translationContext/TranslationContext'; + +import type { + DefaultAttachmentType, + DefaultChannelType, + DefaultCommandType, + DefaultEventType, + DefaultMessageType, + DefaultReactionType, + DefaultUserType, + UnknownType, +} from '../../../types/types'; +import { useMessageActionHandlers } from './useMessageActionHandlers'; + +export const useMessageActions = < + At extends UnknownType = DefaultAttachmentType, + Ch extends UnknownType = DefaultChannelType, + Co extends string = DefaultCommandType, + Ev extends UnknownType = DefaultEventType, + Me extends UnknownType = DefaultMessageType, + Re extends UnknownType = DefaultReactionType, + Us extends UnknownType = DefaultUserType, +>({ + channel, + client, + enforceUniqueReaction, + handleBlock, + handleCopy, + handleDelete, + handleEdit, + handleFlag, + handleMute, + handlePinMessage, + handleQuotedReply, + handleReaction: handleReactionProp, + handleRetry, + handleThreadReply, + message, + onThreadSelect, + openThread, + reactionsEnabled, + retrySendMessage, + selectReaction, + setEditingState, + setOverlay, + setQuotedMessageState, + supportedReactions, + t, + updateMessage, +}: Pick< + MessagesContextValue, + | 'handleBlock' + | 'handleCopy' + | 'handleDelete' + | 'handleEdit' + | 'handleFlag' + | 'handleQuotedReply' + | 'handleMute' + | 'handlePinMessage' + | 'handleRetry' + | 'handleReaction' + | 'handleThreadReply' + | 'reactionsEnabled' + | 'retrySendMessage' + | 'setEditingState' + | 'setQuotedMessageState' + | 'selectReaction' + | 'supportedReactions' + | 'updateMessage' +> & + Pick, 'channel' | 'enforceUniqueReaction'> & + Pick, 'client'> & + Pick & + Pick, 'openThread'> & + Pick, 'message'> & + Pick & { + onThreadSelect?: (message: MessageType) => void; + }) => { + const toast = useToastContext(); + const { + theme: { + colors: { accent_blue, accent_red, grey }, + }, + } = useTheme(); + const { + handleDeleteMessage, + handleEditMessage, + handleQuotedReplyMessage, + handleResendMessage, + handleToggleBanUser, + handleToggleMuteUser, + handleTogglePinMessage, + handleToggleReaction, + } = useMessageActionHandlers({ + channel, + client, + enforceUniqueReaction, + message, + reactionsEnabled, + retrySendMessage, + setEditingState, + setQuotedMessageState, + supportedReactions, + updateMessage, + }); + + const error = message.type === 'error' || message.status === 'failed'; + + const onOpenThread = () => { + if (onThreadSelect) { + onThreadSelect(message); + } + if (openThread) { + openThread(message); + } + }; + + const isMuted = (client.mutedUsers || []).some( + (mute) => mute.user.id === client.userID && mute.target.id === message.user?.id, + ); + + const blockUser: MessageActionType = { + action: () => async () => { + if (!StreamCache.getInstance().currentNetworkState) { + toast.show(t('Something went wrong'), 2000); + } else { + setOverlay('none'); + if (message.user?.id) { + if (handleBlock) { + handleBlock(message); + } + + await handleToggleBanUser(); + } + } + }, + actionType: 'blockUser', + icon: , + title: message.user?.banned ? t('Unblock User') : t('Block User'), + }; + + const copyMessage: MessageActionType = { + // using depreciated Clipboard from react-native until expo supports the community version or their own + action: () => { + if (!StreamCache.getInstance().currentNetworkState) { + toast.show(t('Something went wrong'), 2000); + } else { + setOverlay('none'); + if (handleCopy) { + handleCopy(message); + } + Clipboard.setString(message.text || ''); + } + }, + actionType: 'copyMessage', + icon: , + title: t('Copy Message'), + }; + + const deleteMessage: MessageActionType = { + action: () => { + if (!StreamCache.getInstance().currentNetworkState) { + toast.show(t('Something went wrong'), 2000); + } else { + setOverlay('alert'); + if (message.id) { + Alert.alert( + t('Delete Message'), + t('Are you sure you want to permanently delete this message?'), + [ + { onPress: () => setOverlay('none'), text: t('Cancel') }, + { + onPress: async () => { + setOverlay('none'); + if (handleDelete) { + handleDelete(message); + } + + await handleDeleteMessage(); + }, + style: 'destructive', + text: t('Delete'), + }, + ], + { cancelable: false }, + ); + } + } + }, + actionType: 'deleteMessage', + icon: , + title: t('Delete Message'), + titleStyle: { color: accent_red }, + }; + + const editMessage: MessageActionType = { + action: () => { + if (!StreamCache.getInstance().currentNetworkState) { + toast.show(t('Something went wrong'), 2000); + } else { + setOverlay('none'); + if (handleEdit) { + handleEdit(message); + } + handleEditMessage(); + } + }, + actionType: 'editMessage', + icon: , + title: t('Edit Message'), + }; + + const pinMessage: MessageActionType = { + action: () => { + setOverlay('none'); + if (handlePinMessage) { + handlePinMessage(message); + } + handleTogglePinMessage(); + }, + actionType: 'pinMessage', + icon: , + title: t('Pin to Conversation'), + }; + + const unpinMessage: MessageActionType = { + action: () => { + setOverlay('none'); + if (handlePinMessage) { + handlePinMessage(message); + } + handleTogglePinMessage(); + }, + actionType: 'unpinMessage', + icon: , + title: t('Unpin from Conversation'), + }; + + const flagMessage: MessageActionType = { + action: () => { + if (!StreamCache.getInstance().currentNetworkState) { + toast.show(t('Something went wrong'), 2000); + } else { + setOverlay('alert'); + if (message.id) { + Alert.alert( + t('Flag Message'), + t( + 'Do you want to send a copy of this message to a moderator for further investigation?', + ), + [ + { onPress: () => setOverlay('none'), text: t('Cancel') }, + { + onPress: async () => { + try { + if (handleFlag) { + handleFlag(message); + } + await client.flagMessage(message.id); + Alert.alert( + t('Message flagged'), + t('The message has been reported to a moderator.'), + [ + { + onPress: () => setOverlay('none'), + text: t('Dismiss'), + }, + ], + ); + } catch (err) { + Alert.alert( + t('Something went wrong'), + t("The operation couldn't be completed."), + [ + { + onPress: () => setOverlay('none'), + text: t('Dismiss'), + }, + ], + ); + } + }, + text: t('Flag'), + }, + ], + { cancelable: false }, + ); + } + } + }, + actionType: 'flagMessage', + icon: , + title: t('Flag Message'), + }; + + const handleReaction = !error + ? selectReaction + ? selectReaction(message) + : async (reactionType: string) => { + if (!StreamCache.getInstance().currentNetworkState) { + toast.show(t('Something went wrong'), 2000); + } else { + if (handleReactionProp) { + handleReactionProp(message, reactionType); + } + + await handleToggleReaction(reactionType); + } + } + : undefined; + + const muteUser: MessageActionType = { + action: async () => { + if (!StreamCache.getInstance().currentNetworkState) { + toast.show(t('Something went wrong'), 2000); + } else { + setOverlay('none'); + if (message.user?.id) { + if (handleMute) { + handleMute(message); + } + + await handleToggleMuteUser(); + } + } + }, + actionType: 'muteUser', + icon: , + title: isMuted ? t('Unmute User') : t('Mute User'), + }; + + const quotedReply: MessageActionType = { + action: () => { + if (!StreamCache.getInstance().currentNetworkState) { + toast.show(t('Something went wrong'), 2000); + } else { + setOverlay('none'); + if (handleQuotedReply) { + handleQuotedReply(message); + } + handleQuotedReplyMessage(); + } + }, + actionType: 'quotedReply', + icon: , + title: t('Reply'), + }; + + const retry: MessageActionType = { + action: async () => { + if (!StreamCache.getInstance().currentNetworkState) { + toast.show(t('Something went wrong'), 2000); + } else { + setOverlay('none'); + const messageWithoutReservedFields = removeReservedFields(message); + if (handleRetry) { + handleRetry(messageWithoutReservedFields); + } + + await handleResendMessage(); + } + }, + actionType: 'retry', + icon: , + title: t('Resend'), + }; + + const threadReply: MessageActionType = { + action: () => { + if (!StreamCache.getInstance().currentNetworkState) { + toast.show(t('Something went wrong'), 2000); + } else { + setOverlay('none'); + if (handleThreadReply) { + handleThreadReply(message); + } + onOpenThread(); + } + }, + actionType: 'threadReply', + icon: , + title: t('Thread Reply'), + }; + + return { + blockUser, + copyMessage, + deleteMessage, + editMessage, + flagMessage, + handleReaction, + muteUser, + pinMessage, + quotedReply, + retry, + threadReply, + unpinMessage, + }; +}; diff --git a/package/src/components/Message/utils/messageActions.ts b/package/src/components/Message/utils/messageActions.ts index 70013954f7..1b5e75d532 100644 --- a/package/src/components/Message/utils/messageActions.ts +++ b/package/src/components/Message/utils/messageActions.ts @@ -1,6 +1,4 @@ -import type { MessageType } from '../../MessageList/hooks/useMessageList'; - -import type { MessageAction } from '../../../contexts/messageOverlayContext/MessageOverlayContext'; +import type { MessageActionType } from '../../MessageOverlay/MessageActionListItem'; import type { DefaultAttachmentType, DefaultChannelType, @@ -11,6 +9,22 @@ import type { DefaultUserType, UnknownType, } from '../../../types/types'; +import type { MessageContextValue } from '../../../contexts/messageContext/MessageContext'; +import type { MessagesContextValue } from '../../../contexts/messagesContext/MessagesContext'; + +export type MessageActionsProps = { + blockUser: MessageActionType; + copyMessage: MessageActionType; + deleteMessage: MessageActionType; + editMessage: MessageActionType; + flagMessage: MessageActionType; + muteUser: MessageActionType; + pinMessage: MessageActionType; + quotedReply: MessageActionType; + retry: MessageActionType; + threadReply: MessageActionType; + unpinMessage: MessageActionType; +}; export const messageActions = < At extends UnknownType = DefaultAttachmentType, @@ -43,33 +57,21 @@ export const messageActions = < threadReply, unpinMessage, }: { - blockUser: MessageAction | null; canModifyMessage: boolean; - copyMessage: MessageAction | null; - deleteMessage: MessageAction | null; - editMessage: MessageAction | null; error: boolean | Error; - flagMessage: MessageAction | null; - isMyMessage: boolean; isThreadMessage: boolean; - message: MessageType; messageReactions: boolean; - muteUser: MessageAction | null; - pinMessage: MessageAction | null; - quotedReply: MessageAction | null; - retry: MessageAction | null; - threadReply: MessageAction | null; - unpinMessage: MessageAction | null; - mutesEnabled?: boolean; - pinMessageEnabled?: boolean; - quotedRepliesEnabled?: boolean; - threadRepliesEnabled?: boolean; -}): Array | undefined => { +} & MessageActionsProps & + Pick, 'message' | 'isMyMessage'> & + Pick< + MessagesContextValue, + 'mutesEnabled' | 'quotedRepliesEnabled' | 'pinMessageEnabled' | 'threadRepliesEnabled' + >): Array | undefined => { if (messageReactions) { return undefined; } - const actions: Array = []; + const actions: Array = []; if (error && isMyMessage) { actions.push(retry); diff --git a/package/src/components/MessageOverlay/MessageActions.tsx b/package/src/components/MessageOverlay/MessageActionList.tsx similarity index 59% rename from package/src/components/MessageOverlay/MessageActions.tsx rename to package/src/components/MessageOverlay/MessageActionList.tsx index c9b1fa824c..cb44652226 100644 --- a/package/src/components/MessageOverlay/MessageActions.tsx +++ b/package/src/components/MessageOverlay/MessageActionList.tsx @@ -1,20 +1,14 @@ import React from 'react'; -import { StyleSheet, Text, ViewStyle } from 'react-native'; -import { TapGestureHandler, TapGestureHandlerStateChangeEvent } from 'react-native-gesture-handler'; -import Animated, { - interpolate, - runOnJS, - useAnimatedGestureHandler, - useAnimatedStyle, - useSharedValue, -} from 'react-native-reanimated'; +import { StyleSheet, ViewStyle } from 'react-native'; +import Animated, { interpolate, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; import { - MessageAction as MessageActionType, MessageOverlayData, useMessageOverlayContext, } from '../../contexts/messageOverlayContext/MessageOverlayContext'; +import type { OverlayProviderProps } from '../../contexts/overlayContext/OverlayContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; +import { MessageActionListItem as DefaultMessageActionListItem } from './MessageActionListItem'; import { vw } from '../../utils/utils'; import type { @@ -50,66 +44,7 @@ const styles = StyleSheet.create({ }, }); -type MessageActionProps = MessageActionType & { - index: number; - length: number; -}; - -const MessageAction: React.FC = (props) => { - const { action, icon, index, length, title, titleStyle } = props; - - const { - theme: { - colors: { black, border }, - }, - } = useTheme(); - - const opacity = useSharedValue(1); - - const onTap = useAnimatedGestureHandler( - { - onEnd: () => { - runOnJS(action)(); - }, - onFinish: () => { - opacity.value = 1; - }, - onStart: () => { - opacity.value = 0.2; - }, - }, - [action], - ); - - const animatedStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - })); - - return ( - - - {icon} - {title} - - - ); -}; - -const messageActionIsEqual = (prevProps: MessageActionProps, nextProps: MessageActionProps) => - prevProps.length === nextProps.length; - -const MemoizedMessageAction = React.memo( - MessageAction, - messageActionIsEqual, -) as typeof MessageAction; - -export type MessageActionsPropsWithContext< +export type MessageActionListPropsWithContext< At extends UnknownType = DefaultAttachmentType, Ch extends UnknownType = DefaultChannelType, Co extends string = DefaultCommandType, @@ -117,10 +52,25 @@ export type MessageActionsPropsWithContext< Me extends UnknownType = DefaultMessageType, Re extends UnknownType = DefaultReactionType, Us extends UnknownType = DefaultUserType, -> = Pick, 'alignment' | 'messageActions'> & { - showScreen: Animated.SharedValue; -}; -const MessageActionsWithContext = < +> = Pick< + OverlayProviderProps, + | 'MessageActionListItem' + | 'canModifyMessage' + | 'error' + | 'isMyMessage' + | 'isThreadMessage' + | 'message' + | 'messageReactions' + | 'mutesEnabled' + | 'quotedRepliesEnabled' + | 'pinMessageEnabled' + | 'threadRepliesEnabled' +> & + Pick, 'alignment' | 'messageActions'> & { + showScreen: Animated.SharedValue; + }; + +const MessageActionListWithContext = < At extends UnknownType = DefaultAttachmentType, Ch extends UnknownType = DefaultChannelType, Co extends string = DefaultCommandType, @@ -129,9 +79,37 @@ const MessageActionsWithContext = < Re extends UnknownType = DefaultReactionType, Us extends UnknownType = DefaultUserType, >( - props: MessageActionsPropsWithContext, + props: MessageActionListPropsWithContext, ) => { - const { alignment, messageActions, showScreen } = props; + const { + alignment, + messageActions, + showScreen, + MessageActionListItem = DefaultMessageActionListItem, + isMyMessage, + message, + messageReactions, + mutesEnabled, + quotedRepliesEnabled, + pinMessageEnabled, + threadRepliesEnabled, + canModifyMessage, + error, + isThreadMessage, + } = props; + + const messageActionProps = { + canModifyMessage, + error, + isMyMessage, + isThreadMessage, + message, + messageReactions, + mutesEnabled, + pinMessageEnabled, + quotedRepliesEnabled, + threadRepliesEnabled, + }; const { theme: { @@ -172,8 +150,9 @@ const MessageActionsWithContext = < style={[styles.container, { backgroundColor: white_snow }, showScreenStyle]} > {messageActions?.map((messageAction, index) => ( - ))} @@ -190,8 +169,8 @@ const areEqual = < Re extends UnknownType = DefaultReactionType, Us extends UnknownType = DefaultUserType, >( - prevProps: MessageActionsPropsWithContext, - nextProps: MessageActionsPropsWithContext, + prevProps: MessageActionListPropsWithContext, + nextProps: MessageActionListPropsWithContext, ) => { const { alignment: prevAlignment, messageActions: prevMessageActions } = prevProps; const { alignment: nextAlignment, messageActions: nextMessageActions } = nextProps; @@ -205,12 +184,12 @@ const areEqual = < return true; }; -const MemoizedMessageActions = React.memo( - MessageActionsWithContext, +const MemoizedMessageActionList = React.memo( + MessageActionListWithContext, areEqual, -) as typeof MessageActionsWithContext; +) as typeof MessageActionListWithContext; -export type MessageActionsProps< +export type MessageActionListProps< At extends UnknownType = DefaultAttachmentType, Ch extends UnknownType = DefaultChannelType, Co extends string = DefaultCommandType, @@ -218,13 +197,26 @@ export type MessageActionsProps< Me extends UnknownType = DefaultMessageType, Re extends UnknownType = DefaultReactionType, Us extends UnknownType = DefaultUserType, -> = Partial, 'showScreen'>> & - Pick, 'showScreen'>; +> = Partial, 'showScreen'>> & + Pick< + MessageActionListPropsWithContext, + | 'showScreen' + | 'message' + | 'isMyMessage' + | 'canModifyMessage' + | 'quotedRepliesEnabled' + | 'mutesEnabled' + | 'pinMessageEnabled' + | 'threadRepliesEnabled' + | 'error' + | 'isThreadMessage' + | 'messageReactions' + >; /** - * MessageActions - A high level component which implements all the logic required for MessageActions + * MessageActionList - A high level component which implements all the logic required for MessageActions */ -export const MessageActions = < +export const MessageActionList = < At extends UnknownType = DefaultAttachmentType, Ch extends UnknownType = DefaultChannelType, Co extends string = DefaultCommandType, @@ -233,11 +225,11 @@ export const MessageActions = < Re extends UnknownType = DefaultReactionType, Us extends UnknownType = DefaultUserType, >( - props: MessageActionsProps, + props: MessageActionListProps, ) => { const { data } = useMessageOverlayContext(); const { alignment, messageActions } = data || {}; - return ; + return ; }; diff --git a/package/src/components/MessageOverlay/MessageActionListItem.tsx b/package/src/components/MessageOverlay/MessageActionListItem.tsx new file mode 100644 index 0000000000..04c8a8933e --- /dev/null +++ b/package/src/components/MessageOverlay/MessageActionListItem.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native'; +import { TapGestureHandler } from 'react-native-gesture-handler'; +import Animated from 'react-native-reanimated'; +import { useTheme } from '../../contexts/themeContext/ThemeContext'; +import { useMessageActionAnimation } from './hooks/useMessageActionAnimation'; +import { vw } from '../../utils/utils'; +import type { MessageOverlayPropsWithContext } from '../MessageOverlay/MessageOverlay'; + +const styles = StyleSheet.create({ + bottomBorder: { + borderBottomWidth: 1, + }, + container: { + borderRadius: 16, + marginTop: 8, + maxWidth: 275, + }, + row: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'flex-start', + minWidth: vw(65), + paddingHorizontal: 20, + paddingVertical: 10, + }, + titleStyle: { + paddingLeft: 20, + }, +}); + +export type ActionType = + | 'blockUser' + | 'copyMessage' + | 'deleteMessage' + | 'editMessage' + | 'flagMessage' + | 'muteUser' + | 'pinMessage' + | 'selectReaction' + | 'reply' + | 'retry' + | 'quotedReply' + | 'threadReply' + | 'unpinMessage'; + +export type MessageActionType = { + action: () => void; + actionType: ActionType; + title: string; + icon?: React.ReactElement; + titleStyle?: StyleProp; +}; + +export type MessageActionListItemProps = MessageActionType & + Pick< + MessageOverlayPropsWithContext, + | 'canModifyMessage' + | 'error' + | 'isMyMessage' + | 'isThreadMessage' + | 'message' + | 'messageReactions' + | 'mutesEnabled' + | 'quotedRepliesEnabled' + | 'pinMessageEnabled' + | 'threadRepliesEnabled' + > & { + index: number; + length: number; + }; + +const MessageActionListItemWithContext = (props: MessageActionListItemProps) => { + const { action, icon, index, length, title, titleStyle } = props; + + const { + theme: { + colors: { black, border }, + overlay: { messageActions }, + }, + } = useTheme(); + + const { animatedStyle, onTap } = useMessageActionAnimation({ action }); + + return ( + + + {icon} + + {title} + + + + ); +}; + +const messageActionIsEqual = ( + 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 = (props: MessageActionListItemProps) => ( + +); diff --git a/package/src/components/MessageOverlay/MessageOverlay.tsx b/package/src/components/MessageOverlay/MessageOverlay.tsx index 8882ed31b5..4cf9054bb2 100644 --- a/package/src/components/MessageOverlay/MessageOverlay.tsx +++ b/package/src/components/MessageOverlay/MessageOverlay.tsx @@ -24,7 +24,7 @@ import { import { OverlayReactionList as OverlayReactionListDefault } from './OverlayReactionList'; import { MessageTextContainer } from '../Message/MessageSimple/MessageTextContainer'; -import { MessageActions as DefaultMessageActions } from '../MessageOverlay/MessageActions'; +import { MessageActionList as DefaultMessageActionList } from './MessageActionList'; import { OverlayReactions as DefaultOverlayReactions } from '../MessageOverlay/OverlayReactions'; import { @@ -35,6 +35,7 @@ import { import { MessagesProvider } from '../../contexts/messagesContext/MessagesContext'; import { OverlayContextValue, + OverlayProviderProps, useOverlayContext, } from '../../contexts/overlayContext/OverlayContext'; import { mergeThemes, ThemeProvider, useTheme } from '../../contexts/themeContext/ThemeContext'; @@ -95,11 +96,29 @@ export type MessageOverlayPropsWithContext< Us extends DefaultUserType = DefaultUserType, > = Pick< MessageOverlayContextValue, - 'MessageActions' | 'OverlayReactionList' | 'OverlayReactions' | 'reset' + | 'MessageActionList' + | 'MessageActionListItem' + | 'OverlayReactionList' + | 'OverlayReactions' + | 'reset' > & Omit, 'supportedReactions'> & - Pick & { - overlayOpacity: Animated.SharedValue; + Pick & + Pick< + OverlayProviderProps, + | 'canModifyMessage' + | 'error' + | 'isMyMessage' + | 'isThreadMessage' + | 'message' + | 'messageReactions' + | 'mutesEnabled' + | 'overlayOpacity' + | 'quotedRepliesEnabled' + | 'pinMessageEnabled' + | 'threadRepliesEnabled' + > & { + showScreen?: Animated.SharedValue; visible?: boolean; }; @@ -123,7 +142,8 @@ const MessageOverlayWithContext = < images, message, messageActions, - MessageActions = DefaultMessageActions, + MessageActionList = DefaultMessageActionList, + MessageActionListItem, messageContext, messageReactionTitle, messagesContext, @@ -137,8 +157,30 @@ const MessageOverlayWithContext = < setOverlay, threadList, visible, + isMyMessage, + messageReactions, + mutesEnabled, + quotedRepliesEnabled, + pinMessageEnabled, + threadRepliesEnabled, + canModifyMessage, + error, + isThreadMessage, } = props; + const messageActionProps = { + canModifyMessage, + error, + isMyMessage, + isThreadMessage, + message, + messageReactions, + mutesEnabled, + pinMessageEnabled, + quotedRepliesEnabled, + threadRepliesEnabled, + }; + const { theme } = useTheme(); const myMessageTheme = messagesContext?.myMessageTheme; @@ -167,6 +209,7 @@ const MessageOverlayWithContext = < replyContainer, }, }, + overlay: { container: containerStyle, padding: overlayPadding }, } = wrapMessageInTheme ? modifiedTheme : theme; const messageHeight = useSharedValue(0); @@ -311,7 +354,7 @@ const MessageOverlayWithContext = < @@ -498,7 +542,13 @@ const MessageOverlayWithContext = < )} - {messageActions && } + {messageActions && ( + + )} {!!messageReactionTitle && message.latest_reactions && message.latest_reactions.length > 0 ? ( @@ -593,7 +643,19 @@ export type MessageOverlayProps< Re extends UnknownType = DefaultReactionType, Us extends DefaultUserType = DefaultUserType, > = Partial, 'overlayOpacity'>> & - Pick, 'overlayOpacity'>; + Pick, 'overlayOpacity'> & + Pick< + MessageOverlayPropsWithContext, + | 'isMyMessage' + | 'canModifyMessage' + | 'quotedRepliesEnabled' + | 'mutesEnabled' + | 'pinMessageEnabled' + | 'threadRepliesEnabled' + | 'error' + | 'isThreadMessage' + | 'messageReactions' + >; /** * MessageOverlay - A high level component which implements all the logic required for a message overlay @@ -609,12 +671,19 @@ export const MessageOverlay = < >( props: MessageOverlayProps, ) => { - const { data, MessageActions, OverlayReactionList, OverlayReactions, reset } = - useMessageOverlayContext(); + const { + data, + MessageActionList, + MessageActionListItem, + OverlayReactionList, + OverlayReactions, + reset, + } = useMessageOverlayContext(); const { overlay, setOverlay } = useOverlayContext(); const componentProps = { - MessageActions: props.MessageActions || MessageActions, + MessageActionList: props.MessageActionList || MessageActionList, + MessageActionListItem: props.MessageActionListItem || MessageActionListItem, OverlayReactionList: props.OverlayReactionList || OverlayReactionList || data?.OverlayReactionList, OverlayReactions: props.OverlayReactions || OverlayReactions, diff --git a/package/src/components/MessageOverlay/hooks/useMessageActionAnimation.tsx b/package/src/components/MessageOverlay/hooks/useMessageActionAnimation.tsx new file mode 100644 index 0000000000..249d8dedf7 --- /dev/null +++ b/package/src/components/MessageOverlay/hooks/useMessageActionAnimation.tsx @@ -0,0 +1,43 @@ +import type { ViewStyle } from 'react-native'; +import type { TapGestureHandlerStateChangeEvent } from 'react-native-gesture-handler'; +import { + runOnJS, + useAnimatedGestureHandler, + useAnimatedStyle, + useSharedValue, +} from 'react-native-reanimated'; +import type { MessageActionListItemProps } from '../MessageActionListItem'; + +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/index.ts b/package/src/components/index.ts index d12d581584..f59266fee9 100644 --- a/package/src/components/index.ts +++ b/package/src/components/index.ts @@ -98,6 +98,8 @@ export * from './Indicators/LoadingIndicator'; export * from './KeyboardCompatibleView/KeyboardCompatibleView'; export * from './Message/hooks/useCreateMessageContext'; +export * from './Message/hooks/useMessageActions'; +export * from './Message/hooks/useMessageActionHandlers'; export * from './Message/Message'; export * from './Message/MessageSimple/MessageAvatar'; export * from './Message/MessageSimple/MessageContent'; @@ -145,7 +147,9 @@ export * from './MessageList/utils/getGroupStyles'; export * from './MessageList/utils/getLastReceivedMessage'; export * from './MessageList/utils/getReadStates'; -export * from './MessageOverlay/MessageActions'; +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'; diff --git a/package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx b/package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx index cacd5c5176..54a46dfa76 100644 --- a/package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx +++ b/package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx @@ -2,14 +2,17 @@ import React, { PropsWithChildren, useContext, useState } from 'react'; import { getDisplayName } from '../utils/getDisplayName'; -import type { StyleProp, TextStyle } from 'react-native'; import type { Attachment } from 'stream-chat'; import type { Alignment, MessageContextValue } from '../messageContext/MessageContext'; import type { MessagesContextValue } from '../messagesContext/MessagesContext'; import type { GroupType, MessageType } from '../../components/MessageList/hooks/useMessageList'; -import type { MessageActionsProps } from '../../components/MessageOverlay/MessageActions'; +import type { + MessageActionListItemProps, + MessageActionType, +} from '../../components/MessageOverlay/MessageActionListItem'; +import type { MessageActionListProps } from '../../components/MessageOverlay/MessageActionList'; import type { OverlayReactionListProps } from '../../components/MessageOverlay/OverlayReactionList'; import type { OverlayReactionsProps } from '../../components/MessageOverlay/OverlayReactions'; import type { @@ -24,13 +27,6 @@ import type { } from '../../types/types'; import type { ReactionData } from '../../utils/utils'; -export type MessageAction = { - action: () => void; - title: string; - icon?: React.ReactElement; - titleStyle?: StyleProp; -}; - export type MessageOverlayData< At extends UnknownType = DefaultAttachmentType, Ch extends UnknownType = DefaultChannelType, @@ -47,7 +43,7 @@ export type MessageOverlayData< handleReaction?: (reactionType: string) => Promise; images?: Attachment[]; message?: MessageType; - messageActions?: MessageAction[]; + messageActions?: MessageActionType[]; messageContext?: MessageContextValue; messageReactionTitle?: string; messagesContext?: MessagesContextValue; @@ -70,9 +66,10 @@ export type MessageOverlayContextValue< /** * Custom UI component for rendering [message actions](https://github.com/GetStream/stream-chat-react-native/blob/master/screenshots/docs/2.png) in overlay. * - * **Default** [MessageActions](https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/MessageOverlay/MessageActions.tsx) + * **Default** [MessageActionList](https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/MessageOverlay/MessageActions.tsx) */ - MessageActions: React.ComponentType>; + MessageActionList: React.ComponentType>; + MessageActionListItem: React.ComponentType; /** * Custom UI component for rendering [reaction selector](https://github.com/GetStream/stream-chat-react-native/blob/master/screenshots/docs/2.png) in overlay (which shows up on long press on message). * diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx index 4507ce018a..d8b82e7a77 100644 --- a/package/src/contexts/messagesContext/MessagesContext.tsx +++ b/package/src/contexts/messagesContext/MessagesContext.tsx @@ -7,7 +7,7 @@ import type { TouchableOpacityProps } from 'react-native'; import type { ChannelState, MessageResponse } from 'stream-chat'; import type { Alignment } from '../messageContext/MessageContext'; -import type { MessageAction } from '../messageOverlayContext/MessageOverlayContext'; +import type { MessageActionType } from '../../components/MessageOverlay/MessageActionListItem'; import type { SuggestionCommand } from '../suggestionsContext/SuggestionsContext'; import type { DeepPartial } from '../themeContext/ThemeContext'; import type { Theme } from '../themeContext/utils/theme'; @@ -260,12 +260,6 @@ export type MessagesContextValue< * When false, pop-out animation will be disabled for message bubble, onLongPress */ animatedLongPress?: boolean; - /** - * Full override of the block user button in the Message Actions - * - * Please check [cookbook](https://github.com/GetStream/stream-chat-react-native/wiki/Cookbook-v3.0#override-or-intercept-message-actions-edit-delete-reaction-reply-etc) for details. - */ - blockUser?: ((message: MessageType) => MessageAction) | null; /** * Custom UI component to override default cover (between Header and Footer) of Card component. @@ -282,12 +276,7 @@ export type MessagesContextValue< * Accepts the same props as Card component. */ CardHeader?: React.ComponentType>; - /** - * Full override of the copy message button in the Message Actions - * - * Please check [cookbook](https://github.com/GetStream/stream-chat-react-native/wiki/Cookbook-v3.0#override-or-intercept-message-actions-edit-delete-reaction-reply-etc) for details. - */ - copyMessage?: ((message: MessageType) => MessageAction) | null; + /** * Full override of the delete message button in the Message Actions * @@ -297,21 +286,8 @@ export type MessagesContextValue< /** Control if the deleted message is visible to both the send and reciever, either of them or none */ deletedMessagesVisibilityType?: 'always' | 'never' | 'receiver' | 'sender'; - deleteMessage?: ((message: MessageType) => MessageAction) | null; disableTypingIndicator?: boolean; - /** - * Full override of the edit message button in the Message Actions - * - * Please check [cookbook](https://github.com/GetStream/stream-chat-react-native/wiki/Cookbook-v3.0#override-or-intercept-message-actions-edit-delete-reaction-reply-etc) for details. - */ - editMessage?: ((message: MessageType) => MessageAction) | null; - /** - * Full override of the flag message button in the Message Actions - * - * Please check [cookbook](https://github.com/GetStream/stream-chat-react-native/wiki/Cookbook-v3.0#override-or-intercept-message-actions-edit-delete-reaction-reply-etc) for details. - */ - flagMessage?: ((message: MessageType) => MessageAction) | null; /** * Whether messages should be aligned to right or left part of screen. * By default, messages will be received messages will be aligned to left and @@ -334,7 +310,9 @@ export type MessagesContextValue< /** Handler to access when a mute user action is invoked */ handleMute?: (message: MessageType) => Promise; /** Handler to access when a pin/unpin user action is invoked*/ - handlePinMessage?: ((message: MessageType) => MessageAction) | null; + handlePinMessage?: + | ((message: MessageType) => MessageActionType) + | null; /** Handler to access when a quoted reply action is invoked */ handleQuotedReply?: (message: MessageType) => Promise; /** Handler to process a reaction */ @@ -400,7 +378,7 @@ export type MessagesContextValue< * @overrideType Function | Array */ messageActions?: - | (MessageAction | null)[] + | (MessageActionType | null)[] | (({ blockUser, canModifyMessage, @@ -422,38 +400,33 @@ export type MessagesContextValue< threadRepliesEnabled, threadReply, }: { - blockUser: MessageAction | null; + blockUser: MessageActionType; canModifyMessage: boolean; - copyMessage: MessageAction | null; - deleteMessage: MessageAction | null; + copyMessage: MessageActionType; + deleteMessage: MessageActionType; dismissOverlay: () => void; - editMessage: MessageAction | null; + editMessage: MessageActionType; error: boolean | Error; - flagMessage: MessageAction | null; + flagMessage: MessageActionType; isMyMessage: boolean; isThreadMessage: boolean; message: MessageType; messageReactions: boolean; - muteUser: MessageAction | null; - quotedReply: MessageAction | null; - retry: MessageAction | null; - threadReply: MessageAction | null; + muteUser: MessageActionType; + quotedReply: MessageActionType; + retry: MessageActionType; + threadReply: MessageActionType; pinMessageEnabled?: boolean; quotedRepliesEnabled?: boolean; threadRepliesEnabled?: boolean; - }) => (MessageAction | null)[] | undefined); + }) => MessageActionType[] | undefined); /** * Custom message header component */ MessageHeader?: React.ComponentType>; /** Custom UI component for message text */ MessageText?: React.ComponentType>; - /** - * Full override of the mute user button in the Message Actions - * - * Please check [cookbook](https://github.com/GetStream/stream-chat-react-native/wiki/Cookbook-v3.0#override-or-intercept-message-actions-edit-delete-reaction-reply-etc) for details. - * */ - muteUser?: ((message: MessageType) => MessageAction) | null; + /** * Theme provided only to messages that are the current users */ @@ -567,24 +540,7 @@ export type MessagesContextValue< * ``` */ onPressMessage?: (payload: MessageTouchableHandlerPayload) => void; - /** - * Full override of the pin message button in the Message Actions - * - * Please check [cookbook](https://github.com/GetStream/stream-chat-react-native/wiki/Cookbook-v3.0#override-or-intercept-message-actions-edit-delete-reaction-reply-etc) for details. - */ - pinMessage?: ((message: MessageType) => MessageAction) | null; - /** - * Full override of the quoted reply button in the Message Actions - * - * Please check [cookbook](https://github.com/GetStream/stream-chat-react-native/wiki/Cookbook-v3.0#override-or-intercept-message-actions-edit-delete-reaction-reply-etc) for details. - */ - quotedReply?: ((message: MessageType) => MessageAction) | null; - /** - * Full override of the resend button in the Message Actions - * - * Please check [cookbook](https://github.com/GetStream/stream-chat-react-native/wiki/Cookbook-v3.0#override-or-intercept-message-actions-edit-delete-reaction-reply-etc) for details. - * */ - retry?: ((message: MessageType) => MessageAction) | null; + /** * Full override of the reaction function on Message and Message Overlay * @@ -595,12 +551,6 @@ export type MessagesContextValue< ) => (reactionType: string) => Promise; targetedMessage?: string; - /** - * Full override of the thread reply button in the Message Actions - * - * Please check [cookbook](https://github.com/GetStream/stream-chat-react-native/wiki/Cookbook-v3.0#override-or-intercept-message-actions-edit-delete-reaction-reply-etc) for details. - * */ - threadReply?: (message: MessageType) => MessageAction; }; export const MessagesContext = React.createContext({} as MessagesContextValue); diff --git a/package/src/contexts/overlayContext/OverlayContext.tsx b/package/src/contexts/overlayContext/OverlayContext.tsx index 94b0fa8e13..a707305510 100644 --- a/package/src/contexts/overlayContext/OverlayContext.tsx +++ b/package/src/contexts/overlayContext/OverlayContext.tsx @@ -1,4 +1,5 @@ import React, { useContext } from 'react'; +import type Animated from 'react-native-reanimated'; import { getDisplayName } from '../utils/getDisplayName'; @@ -6,6 +7,7 @@ import type { BottomSheetMethods } from '@gorhom/bottom-sheet/lib/typescript/typ import type { AttachmentPickerContextValue } from '../attachmentPickerContext/AttachmentPickerContext'; import type { MessageOverlayContextValue } from '../messageOverlayContext/MessageOverlayContext'; +import type { MessagesContextValue } from '../../contexts/messagesContext/MessagesContext'; import type { DeepPartial } from '../themeContext/ThemeContext'; import type { Theme } from '../themeContext/utils/theme'; @@ -22,6 +24,7 @@ import type { UnknownType, } from '../../types/types'; import type { Streami18n } from '../../utils/Streami18n'; +import type { MessageType } from '../../components/MessageList/hooks/useMessageList'; export type Overlay = 'alert' | 'gallery' | 'message' | 'none'; @@ -59,15 +62,26 @@ export type OverlayProviderProps< Partial< Pick< MessageOverlayContextValue, - 'MessageActions' | 'OverlayReactionList' | 'OverlayReactions' + 'MessageActionList' | 'MessageActionListItem' | 'OverlayReactionList' | 'OverlayReactions' > > & - Pick & { + Pick & + Pick< + MessagesContextValue, + 'mutesEnabled' | 'quotedRepliesEnabled' | 'pinMessageEnabled' | 'threadRepliesEnabled' + > & { + overlayOpacity: Animated.SharedValue; + canModifyMessage?: boolean; closePicker?: (ref: React.RefObject) => void; + error?: boolean | Error; /** https://github.com/GetStream/stream-chat-react-native/wiki/Internationalization-(i18n) */ i18nInstance?: Streami18n; imageGalleryGridHandleHeight?: number; imageGalleryGridSnapPoints?: [string | number, string | number]; + isMyMessage?: boolean; + isThreadMessage?: boolean; + message?: MessageType; + messageReactions?: boolean; numberOfImageGalleryGridColumns?: number; openPicker?: (ref: React.RefObject) => void; value?: Partial; diff --git a/package/src/contexts/overlayContext/OverlayProvider.tsx b/package/src/contexts/overlayContext/OverlayProvider.tsx index 1afa9ae8de..861b79a7b0 100644 --- a/package/src/contexts/overlayContext/OverlayProvider.tsx +++ b/package/src/contexts/overlayContext/OverlayProvider.tsx @@ -102,7 +102,8 @@ export const OverlayProvider = < imageGalleryGridSnapPoints, ImageOverlaySelectedComponent = DefaultImageOverlaySelectedComponent, ImageSelectorIcon = DefaultImageSelectorIcon, - MessageActions, + MessageActionList, + MessageActionListItem, numberOfAttachmentImagesToLoadPerCall, numberOfAttachmentPickerImageColumns, numberOfImageGalleryGridColumns, @@ -225,7 +226,8 @@ export const OverlayProvider = < - MessageActions={MessageActions} + MessageActionList={MessageActionList} + MessageActionListItem={MessageActionListItem} overlayOpacity={overlayOpacity} OverlayReactionList={OverlayReactionList} OverlayReactions={OverlayReactions} diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts index d1e983c4c2..dfc19e1503 100644 --- a/package/src/contexts/themeContext/utils/theme.ts +++ b/package/src/contexts/themeContext/utils/theme.ts @@ -451,6 +451,12 @@ export type Theme = { targetedMessageUnderlay: ViewStyle; }; overlay: { + container: ViewStyle; + messageActions: { + actionContainer: ViewStyle; + icon: ViewStyle; + title: TextStyle; + }; padding: number; reactions: { avatarContainer: ViewStyle; @@ -929,6 +935,12 @@ export const defaultTheme: Theme = { targetedMessageUnderlay: {}, }, overlay: { + container: {}, + messageActions: { + actionContainer: {}, + icon: {}, + title: {}, + }, padding: 8, reactions: { avatarContainer: {},