From cd14d8ab5cda36a0d42b4a0381b8668109c16a53 Mon Sep 17 00:00:00 2001 From: Khushal Agarwal Date: Wed, 25 Sep 2024 12:18:06 +0530 Subject: [PATCH 01/19] feat: add new message action list and reaction selector UI --- package/src/components/Channel/Channel.tsx | 28 +- .../Channel/hooks/useCreateMessagesContext.ts | 14 +- package/src/components/Message/Message.tsx | 160 +++-- .../MessageSimple/MessageTextContainer.tsx | 7 +- .../Message/MessageSimple/ReactionList.tsx | 12 +- .../Message/hooks/useMessageActions.tsx | 95 +-- .../Message/hooks/useProcessReactions.ts | 6 +- .../Message/utils/messageActions.ts | 7 +- .../components/MessageInput/MessageInput.tsx | 1 - .../AudioRecordingLockIndicator.tsx | 1 - .../MessageOverlay/MessageActionList.tsx | 147 +--- .../MessageOverlay/MessageActionListItem.tsx | 112 +-- .../MessageOverlay/MessageOverlay.tsx | 680 +++--------------- .../MessageOverlay/OverlayBackdrop.tsx | 18 - .../MessageOverlay/OverlayReactionList.tsx | 438 ++--------- .../MessageOverlay/OverlayReactions.tsx | 311 +++----- .../MessageOverlay/OverlayReactionsAvatar.tsx | 3 +- .../MessageOverlay/OverlayReactionsItem.tsx | 105 +-- .../MessageOverlay/ReactionButton.tsx | 71 ++ .../MessageOverlay/hooks/useFetchReactions.ts | 2 + package/src/components/index.ts | 1 - package/src/contexts/index.ts | 1 - .../MessageOverlayContext.tsx | 147 ---- .../hooks/useResettableState.test.tsx | 48 -- .../hooks/useResettableState.ts | 22 - .../messagesContext/MessagesContext.tsx | 34 +- .../overlayContext/OverlayContext.tsx | 24 +- .../overlayContext/OverlayProvider.tsx | 87 +-- .../src/contexts/themeContext/utils/theme.ts | 41 +- .../__tests__/useTranslatedMessage.test.tsx | 12 +- package/src/hooks/useTranslatedMessage.ts | 5 +- .../store/apis/getReactionsforFilterSort.ts | 4 +- package/src/types/types.ts | 7 + 33 files changed, 673 insertions(+), 1978 deletions(-) delete mode 100644 package/src/components/MessageOverlay/OverlayBackdrop.tsx create mode 100644 package/src/components/MessageOverlay/ReactionButton.tsx delete mode 100644 package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx delete mode 100644 package/src/contexts/messageOverlayContext/hooks/useResettableState.test.tsx delete mode 100644 package/src/contexts/messageOverlayContext/hooks/useResettableState.ts diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 63a9ae0501..440796043b 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -7,6 +7,7 @@ import { View, } from 'react-native'; +import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import debounce from 'lodash/debounce'; import omit from 'lodash/omit'; import throttle from 'lodash/throttle'; @@ -171,7 +172,12 @@ 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 { MessageActionList as MessageActionListDefault } from '../MessageOverlay/MessageActionList'; +import { MessageActionListItem as MessageActionListItemDefault } from '../MessageOverlay/MessageActionListItem'; import { OverlayReactionList as OverlayReactionListDefault } from '../MessageOverlay/OverlayReactionList'; +import { OverlayReactions as OverlayReactionDefault } from '../MessageOverlay/OverlayReactions'; +import { OverlayReactionsAvatar as OverlayReactionsAvatarDefault } from '../MessageOverlay/OverlayReactionsAvatar'; +import { OverlayReactionsItem as OverlayReactionsItemDefault } from '../MessageOverlay/OverlayReactionsItem'; 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' @@ -319,12 +327,16 @@ export type ChannelPropsWithContext< | 'MessageStatus' | 'MessageSystem' | 'MessageText' + | 'messageTextNumberOfLines' | 'MessageTimestamp' | 'myMessageTheme' | 'onLongPressMessage' | 'onPressInMessage' | 'onPressMessage' + | 'OverlayReactions' | 'OverlayReactionList' + | 'OverlayReactionsAvatar' + | 'OverlayReactionsItem' | 'ReactionList' | 'Reply' | 'ScrollToBottomButton' @@ -540,6 +552,8 @@ const ChannelWithContext = < mentionAllAppUsersEnabled = false, mentionAllAppUsersQuery, Message = MessageDefault, + MessageActionList = MessageActionListDefault, + MessageActionListItem = MessageActionListItemDefault, messageActions, MessageAvatar = MessageAvatarDefault, MessageBounce = MessageBounceDefault, @@ -560,6 +574,7 @@ const ChannelWithContext = < MessageStatus = MessageStatusDefault, MessageSystem = MessageSystemDefault, MessageText, + messageTextNumberOfLines, MessageTimestamp = MessageTimestampDefault, MoreOptionsButton = MoreOptionsButtonDefault, myMessageTheme, @@ -571,6 +586,9 @@ const ChannelWithContext = < onPressInMessage, onPressMessage, OverlayReactionList = OverlayReactionListDefault, + OverlayReactions = OverlayReactionDefault, + OverlayReactionsAvatar = OverlayReactionsAvatarDefault, + OverlayReactionsItem = OverlayReactionsItemDefault, overrideOwnCapabilities, ReactionList = ReactionListDefault, read, @@ -2360,6 +2378,8 @@ const ChannelWithContext = < legacyImageViewerSwipeBehaviour, markdownRules, Message, + MessageActionList, + MessageActionListItem, messageActions, MessageAvatar, MessageBounce, @@ -2378,12 +2398,16 @@ const ChannelWithContext = < MessageStatus, MessageSystem, MessageText, + messageTextNumberOfLines, MessageTimestamp, myMessageTheme, onLongPressMessage, onPressInMessage, onPressMessage, OverlayReactionList, + OverlayReactions, + OverlayReactionsAvatar, + OverlayReactionsItem, ReactionList, removeMessage, Reply, @@ -2455,7 +2479,9 @@ const ChannelWithContext = < value={threadContext}> value={suggestionsContext}> value={inputMessageInputContext}> - {children} + + {children} + diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts index a0789400be..7f74cf8dca 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, @@ -70,12 +72,16 @@ export const useCreateMessagesContext = < MessageStatus, MessageSystem, MessageText, + messageTextNumberOfLines, MessageTimestamp, myMessageTheme, onLongPressMessage, onPressInMessage, onPressMessage, OverlayReactionList, + OverlayReactions, + OverlayReactionsAvatar, + OverlayReactionsItem, ReactionList, removeMessage, Reply, @@ -101,7 +107,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 +156,8 @@ export const useCreateMessagesContext = < legacyImageViewerSwipeBehaviour, markdownRules, Message, + MessageActionList, + MessageActionListItem, messageActions, MessageAvatar, MessageBounce, @@ -168,12 +176,16 @@ export const useCreateMessagesContext = < MessageStatus, MessageSystem, MessageText, + messageTextNumberOfLines, MessageTimestamp, myMessageTheme, onLongPressMessage, onPressInMessage, onPressMessage, OverlayReactionList, + OverlayReactions, + OverlayReactionsAvatar, + OverlayReactionsItem, ReactionList, removeMessage, Reply, diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index 1e962a96d3..0bd13696e3 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -1,6 +1,8 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; import { GestureResponderEvent, Keyboard, StyleProp, View, ViewStyle } from 'react-native'; +import type { BottomSheetModal } from '@gorhom/bottom-sheet'; + import type { Attachment, UserResponse } from 'stream-chat'; import { useCreateMessageContext } from './hooks/useCreateMessageContext'; @@ -19,18 +21,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 +47,7 @@ import { isMessageWithStylesReadByAndDateSeparator, MessageType, } from '../MessageList/hooks/useMessageList'; -import type { MessageActionListItemProps } from '../MessageOverlay/MessageActionListItem'; +import { MessageOverlay } from '../MessageOverlay/MessageOverlay'; export type TouchableEmitter = | 'fileAttachment' @@ -159,6 +153,8 @@ export type MessagePropsWithContext< | 'handleRetry' | 'handleThreadReply' | 'isAttachmentEqual' + | 'MessageActionList' + | 'MessageActionListItem' | 'messageActions' | 'messageContentOrder' | 'MessageBounce' @@ -167,6 +163,9 @@ export type MessagePropsWithContext< | 'onPressInMessage' | 'onPressMessage' | 'OverlayReactionList' + | 'OverlayReactions' + | 'OverlayReactionsAvatar' + | 'OverlayReactionsItem' | 'removeMessage' | 'deleteReaction' | 'retrySendMessage' @@ -176,8 +175,6 @@ export type MessagePropsWithContext< | 'supportedReactions' | 'updateMessage' > & - Pick, 'setData'> & - Pick & Pick, 'openThread'> & Pick & { chatContext: ChatContextValue; @@ -238,8 +235,10 @@ const MessageWithContext = < >( props: MessagePropsWithContext, ) => { + const [isErrorInMessage, setIsErrorInMessage] = useState(false); const [isBounceDialogOpen, setIsBounceDialogOpen] = useState(false); const [isEditedMessageOpen, setIsEditedMessageOpen] = useState(false); + const [isMessageActionsVisible, setIsMessageActionsVisible] = useState(true); const isMessageTypeDeleted = props.message.type === 'deleted'; const { @@ -270,6 +269,8 @@ const MessageWithContext = < lastReceivedId, members, message, + MessageActionList, + MessageActionListItem, messageActions: messageActionsProp = defaultMessageActions, MessageBounce, messageContentOrder: messageContentOrderProp, @@ -284,14 +285,15 @@ const MessageWithContext = < onThreadSelect, openThread, OverlayReactionList, + OverlayReactions, + OverlayReactionsAvatar, + OverlayReactionsItem, preventPress, removeMessage, retrySendMessage, selectReaction, sendReaction, - setData, setEditingState, - setOverlay, setQuotedMessageState, showAvatar, showMessageStatus, @@ -309,6 +311,21 @@ const MessageWithContext = < messageSimple: { targetedMessageContainer, targetedMessageUnderlay }, }, } = useTheme(); + const messageActionsBottomSheetRef = useRef(null); + + const openMessageActionsBottomSheet = () => { + if (messageActionsBottomSheetRef.current?.present) { + messageActionsBottomSheetRef.current.present(); + } else { + console.warn('bottom and top insets must be set for the image picker to work correctly'); + } + }; + + const closeMessageActionsBottomSheet = () => { + if (messageActionsBottomSheetRef.current?.dismiss) { + messageActionsBottomSheetRef.current.dismiss(); + } + }; const actionsEnabled = message.type === 'regular' && message.status === MessageStatusTypes.RECEIVED; @@ -346,6 +363,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 +377,7 @@ const MessageWithContext = < setIsBounceDialogOpen(true); return; } - showMessageOverlay(true, true); + showMessageOverlay(); } else if (quotedMessage) { onPressQuotedMessage(quotedMessage); } @@ -531,6 +549,7 @@ const MessageWithContext = < client, deleteMessage: deleteMessageFromContext, deleteReaction, + dismissOverlay: closeMessageActionsBottomSheet, enforceUniqueReaction, handleBan, handleBlock, @@ -552,72 +571,44 @@ const MessageWithContext = < selectReaction, sendReaction, setEditingState, - setOverlay, setQuotedMessageState, supportedReactions, t, updateMessage, }); - const { userLanguage } = useTranslationContext(); + // const { userLanguage } = useTranslationContext(); + const isThreadMessage = threadList || !!message.parent_id; + + const messageActions = + typeof messageActionsProp !== 'function' + ? messageActionsProp + : messageActionsProp({ + banUser, + blockUser, + copyMessage, + deleteMessage, + dismissOverlay: closeMessageActionsBottomSheet, + editMessage, + error: isErrorInMessage, + flagMessage, + isMessageActionsVisible, + isMyMessage, + isThreadMessage, + message, + muteUser, + ownCapabilities, + pinMessage, + quotedReply, + retry, + threadReply, + unpinMessage, + }); - const showMessageOverlay = async (isMessageActionsVisible = true, error = errorOrFailed) => { + const showMessageOverlay = async (isVisible = true) => { + setIsMessageActionsVisible(isVisible); 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'); + openMessageActionsBottomSheet(); }; const actionHandlers: MessageActionHandlers = { @@ -664,7 +655,7 @@ const MessageWithContext = < return; } triggerHaptic('impactMedium'); - showMessageOverlay(true); + showMessageOverlay(); } : () => null; @@ -776,6 +767,23 @@ const MessageWithContext = < {isBounceDialogOpen && } + @@ -942,9 +950,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 +965,6 @@ export const Message = < members, messagesContext, openThread, - setData, - setOverlay, t, }} {...props} 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) { diff --git a/package/src/components/Message/hooks/useMessageActions.tsx b/package/src/components/Message/hooks/useMessageActions.tsx index 3ee3ad8bf1..4525c15254 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'; @@ -30,38 +29,9 @@ import { MessageStatusTypes } from '../../../utils/utils'; import type { MessageType } from '../../MessageList/hooks/useMessageList'; import type { MessageActionType } from '../../MessageOverlay/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,45 @@ export const useMessageActions = < > & Pick, 'channel' | 'enforceUniqueReaction'> & Pick, 'client'> & - Pick & Pick, 'openThread'> & Pick, 'message'> & Pick & { + dismissOverlay: () => void; 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 +144,7 @@ export const useMessageActions = < const banUser: MessageActionType = { action: async () => { - setOverlay('none'); + dismissOverlay(); if (message.user?.id) { if (handleBan) { handleBan(message); @@ -160,7 +163,7 @@ export const useMessageActions = < */ const blockUser: MessageActionType = { action: async () => { - setOverlay('none'); + dismissOverlay(); if (message.user?.id) { if (handleBlock) { handleBlock(message); @@ -176,7 +179,7 @@ export const useMessageActions = < const copyMessage: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handleCopy) { handleCopy(message); } @@ -189,7 +192,7 @@ export const useMessageActions = < const deleteMessage: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handleDelete) { handleDelete(message); } @@ -203,7 +206,7 @@ export const useMessageActions = < const editMessage: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handleEdit) { handleEdit(message); } @@ -216,7 +219,7 @@ export const useMessageActions = < const pinMessage: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handlePinMessage) { handlePinMessage(message); } @@ -229,7 +232,7 @@ export const useMessageActions = < const unpinMessage: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handlePinMessage) { handlePinMessage(message); } @@ -242,7 +245,7 @@ export const useMessageActions = < const flagMessage: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handleFlag) { handleFlag(message); } @@ -268,7 +271,7 @@ export const useMessageActions = < const muteUser: MessageActionType = { action: async () => { - setOverlay('none'); + dismissOverlay(); if (message.user?.id) { if (handleMute) { handleMute(message); @@ -284,7 +287,7 @@ export const useMessageActions = < const quotedReply: MessageActionType = { action: () => { - setOverlay('none'); + dismissOverlay(); if (handleQuotedReply) { handleQuotedReply(message); } @@ -297,7 +300,7 @@ export const useMessageActions = < const retry: MessageActionType = { action: async () => { - setOverlay('none'); + dismissOverlay(); const messageWithoutReservedFields = removeReservedFields(message); if (handleRetry) { handleRetry(messageWithoutReservedFields as MessageType); @@ -312,7 +315,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..6d2f6cbccb 100644 --- a/package/src/components/Message/utils/messageActions.ts +++ b/package/src/components/Message/utils/messageActions.ts @@ -19,10 +19,6 @@ export type MessageActionsParams< */ isMessageActionsVisible: boolean; isThreadMessage: boolean; - /** - * @deprecated use `isMessageActionsVisible` instead. - */ - messageReactions: boolean; muteUser: MessageActionType; ownCapabilities: OwnCapabilitiesContextValue; pinMessage: MessageActionType; @@ -54,7 +50,6 @@ export const messageActions = < isMyMessage, isThreadMessage, message, - messageReactions, ownCapabilities, pinMessage, quotedReply, @@ -62,7 +57,7 @@ export const messageActions = < threadReply, unpinMessage, }: MessageActionsParams) => { - if (messageReactions || !isMessageActionsVisible) { + if (!isMessageActionsVisible) { 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/MessageOverlay/MessageActionList.tsx b/package/src/components/MessageOverlay/MessageActionList.tsx index 72e44c2b69..0f344c6f32 100644 --- a/package/src/components/MessageOverlay/MessageActionList.tsx +++ b/package/src/components/MessageOverlay/MessageActionList.tsx @@ -1,103 +1,45 @@ import React from 'react'; -import { StyleSheet, ViewStyle } from 'react-native'; -import Animated, { - interpolate, - SharedValue, - useAnimatedStyle, - useSharedValue, -} from 'react-native-reanimated'; +import { StyleSheet, View } from 'react-native'; -import { MessageActionListItem as DefaultMessageActionListItem } from './MessageActionListItem'; +import { MessageActionType } from './MessageActionListItem'; -import { - MessageOverlayData, - useMessageOverlayContext, -} from '../../contexts/messageOverlayContext/MessageOverlayContext'; +import { MessagesContextValue } from '../../contexts/messagesContext/MessagesContext'; 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< +export type MessageActionListProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, > = Pick< OverlayProviderProps, - | 'MessageActionListItem' - | 'error' - | 'isMyMessage' - | 'isThreadMessage' - | 'message' - | 'messageReactions' + 'error' | 'isMyMessage' | 'isThreadMessage' | 'message' > & - Pick, 'alignment' | 'messageActions'> & { - showScreen: SharedValue; + Pick & { + messageActions?: MessageActionType[]; }; -const MessageActionListWithContext = < +export const MessageActionList = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( - props: MessageActionListPropsWithContext, + props: MessageActionListProps, ) => { + const { error, isMyMessage, isThreadMessage, message, MessageActionListItem, messageActions } = + props; const { - alignment, - error, - isMyMessage, - isThreadMessage, - message, - MessageActionListItem = DefaultMessageActionListItem, - messageActions, - messageReactions, - showScreen, - } = props; + theme: { + messageActionList: { container }, + }, + } = useTheme(); 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, + flex: 1, + paddingHorizontal: 16, }, }); diff --git a/package/src/components/MessageOverlay/MessageActionListItem.tsx b/package/src/components/MessageOverlay/MessageActionListItem.tsx index 4ffe14ad74..b781e7c386 100644 --- a/package/src/components/MessageOverlay/MessageActionListItem.tsx +++ b/package/src/components/MessageOverlay/MessageActionListItem.tsx @@ -1,12 +1,7 @@ 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 { Pressable, StyleProp, StyleSheet, Text, TextStyle, View } from 'react-native'; 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' @@ -48,110 +43,41 @@ export type MessageActionType = { titleStyle?: StyleProp; }; -export type MessageActionListItemProps< - StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = MessageActionType & - Pick< - MessageOverlayPropsWithContext, - 'error' | 'isMyMessage' | 'isThreadMessage' | 'message' | 'messageReactions' - > & { - index: number; - length: number; - }; +/** + * MessageActionListItem - A high-level component that implements all the logic required for a `MessageAction` in a `MessageActionList` + */ +export type MessageActionListItemProps = MessageActionType; -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; +export const MessageActionListItem = (props: MessageActionListItemProps) => { + const { action, actionType, icon, title, titleStyle } = props; const { theme: { - colors: { black, border }, - overlay: { messageActions }, + colors: { black }, + overlay: { + messageActions: { actionContainer, icon: iconTheme, title: titleTheme }, + }, }, } = 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} - - - + + + {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, + paddingVertical: 8, }, titleStyle: { - paddingLeft: 20, + paddingLeft: 16, }, }); diff --git a/package/src/components/MessageOverlay/MessageOverlay.tsx b/package/src/components/MessageOverlay/MessageOverlay.tsx index 74e5f62e72..65bfab5d5e 100644 --- a/package/src/components/MessageOverlay/MessageOverlay.tsx +++ b/package/src/components/MessageOverlay/MessageOverlay.tsx @@ -1,615 +1,133 @@ -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 React, { useCallback, useMemo } from 'react'; +import { StyleSheet } from 'react-native'; -import { MessageActionList as DefaultMessageActionList } from './MessageActionList'; -import { OverlayReactionList as OverlayReactionListDefault } from './OverlayReactionList'; -import { OverlayReactionsAvatar as OverlayReactionsAvatarDefault } from './OverlayReactionsAvatar'; +import { BottomSheetModal, BottomSheetView } from '@gorhom/bottom-sheet'; +import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; -import { ChatProvider } from '../../contexts/chatContext/ChatContext'; -import { MessageProvider } from '../../contexts/messageContext/MessageContext'; -import { - MessageOverlayContextValue, - MessageOverlayData, - useMessageOverlayContext, -} from '../../contexts/messageOverlayContext/MessageOverlayContext'; +import { MessageActionType } from './MessageActionListItem'; -import { MessagesProvider } from '../../contexts/messagesContext/MessagesContext'; +import { useMessageContext } from '../../contexts/messageContext/MessageContext'; 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' }, -}); + MessagesContextValue, + useMessagesContext, +} from '../../contexts/messagesContext/MessagesContext'; +import { OverlayProviderProps } from '../../contexts/overlayContext/OverlayContext'; +import { DefaultStreamChatGenerics } from '../../types/types'; -const DefaultMessageTextNumberOfLines = 5; - -export type MessageOverlayPropsWithContext< +export type MessageOverlayProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick< - MessageOverlayContextValue, - | 'MessageActionList' - | 'MessageActionListItem' - | 'OverlayReactionList' - | 'OverlayReactions' - | 'OverlayReactionsAvatar' -> & - Omit, 'supportedReactions'> & - Pick & +> = Partial< Pick< - OverlayProviderProps, - | 'error' - | 'isMyMessage' - | 'isThreadMessage' - | 'message' - | 'messageReactions' - | 'messageTextNumberOfLines' + MessagesContextValue, + | 'MessageActionList' + | 'MessageActionListItem' + | 'OverlayReactionList' + | 'OverlayReactions' + | 'OverlayReactionsAvatar' + | 'OverlayReactionsItem' + > +> & + Partial< + Pick, 'isMyMessage' | 'isThreadMessage' | 'message'> > & { - overlayOpacity: SharedValue; - showScreen?: SharedValue; + closeMessageActionsBottomSheet: () => void; + isErrorInMessage: boolean; + isMessageActionsVisible: boolean; + messageActions: MessageActionType[]; + messageActionsBottomSheetRef: React.RefObject; + handleReaction?: (reactionType: string) => Promise; }; -const MessageOverlayWithContext = < +export const MessageOverlay = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( - props: MessageOverlayPropsWithContext, + props: MessageOverlayProps, ) => { const { - alignment, - chatContext, - clientId, - error, - files, - groupStyles, + closeMessageActionsBottomSheet, handleReaction, - images, - isMyMessage, + isErrorInMessage, + isMessageActionsVisible, + isMyMessage: propIsMyMessage, isThreadMessage, message, - MessageActionList = DefaultMessageActionList, - MessageActionListItem, + MessageActionList: propMessageActionList, + MessageActionListItem: propMessageActionListItem, messageActions, - messageContext, - messageReactions, - messageReactionTitle, - messagesContext, - messageTextNumberOfLines = DefaultMessageTextNumberOfLines, - onlyEmojis, - otherAttachments, - overlay, - overlayOpacity, - OverlayReactionList = OverlayReactionListDefault, - OverlayReactions = DefaultOverlayReactions, - OverlayReactionsAvatar = OverlayReactionsAvatarDefault, - ownCapabilities, - setOverlay, - threadList, - videos, + messageActionsBottomSheetRef, + OverlayReactionList: propOverlayReactionList, + OverlayReactions: propOverlayReactions, + OverlayReactionsAvatar: propOverlayReactionsAvatar, + OverlayReactionsItem: propOverlayReactionsItem, } = props; + const { + MessageActionList: contextMessageActionList, + MessageActionListItem: contextMessageActionListItem, + OverlayReactionList: contextOverlayReactionList, + OverlayReactions: contextOverlayReactions, + OverlayReactionsAvatar: contextOverlayReactionsAvatar, + OverlayReactionsItem: contextOverlayReactionsItem, + } = useMessagesContext(); + const { isMyMessage: contextIsMyMessage } = useMessageContext(); + const snapPoints = useMemo(() => ['50%', '50%'], []); + const isMyMessage = propIsMyMessage ?? contextIsMyMessage; + const MessageActionList = propMessageActionList ?? contextMessageActionList; + const MessageActionListItem = propMessageActionListItem ?? contextMessageActionListItem; + const OverlayReactionList = propOverlayReactionList ?? contextOverlayReactionList; + const OverlayReactions = propOverlayReactions ?? contextOverlayReactions; + const OverlayReactionsAvatar = propOverlayReactionsAvatar ?? contextOverlayReactionsAvatar; + const OverlayReactionsItem = propOverlayReactionsItem ?? contextOverlayReactionsItem; + + const handleSheetChanges = useCallback((index: number) => { + console.log('handleSheetChanges', index); + }, []); const messageActionProps = { - error, + error: isErrorInMessage, 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, - }); + messageActions, }; - 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 && ( + return ( + + + {isMessageActionsVisible ? ( + <> + reaction.type) || []} + /> + {messageActions?.length ? ( - )} - {!!messageReactionTitle && ( - - )} - + ) : null} + + ) : ( + )} - - - ); - - // 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 ( - - ); -}; +const styles = StyleSheet.create({ + contentContainer: { + flex: 1, + }, +}); 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 index c9aedc7400..3ac8e54038 100644 --- a/package/src/components/MessageOverlay/OverlayReactionList.tsx +++ b/package/src/components/MessageOverlay/OverlayReactionList.tsx @@ -1,435 +1,81 @@ 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 { StyleSheet, View } from 'react-native'; import { FillProps } from 'react-native-svg'; -import { - MessageOverlayData, - useMessageOverlayContext, -} from '../../contexts/messageOverlayContext/MessageOverlayContext'; +import { ReactionButton } from './ReactionButton'; + 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 { useTheme } from '../../contexts/themeContext/ThemeContext'; 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< +export type OverlayReactionListProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick< - OverlayReactionListPropsWithContext, - 'ownReactionTypes' | 'handleReaction' | 'setOverlay' -> & { - Icon: React.ComponentType; - index: number; - numberOfReactions: number; - showScreen: SharedValue; - type: string; +> = Pick, 'supportedReactions'> & { + dismissOverlay: () => void; + ownReactionTypes: string[]; + fill?: FillProps['fill']; + handleReaction?: (reactionType: string) => Promise; }; -export const ReactionButton = < +/** + * 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: ReactionButtonProps, + props: OverlayReactionListProps, ) => { const { + dismissOverlay, handleReaction, - Icon, - index, - numberOfReactions, ownReactionTypes, - setOverlay, - showScreen, - type, + supportedReactions: propSupportedReactions, } = props; + const { supportedReactions: contextSupportedReactions } = useMessagesContext(); const { theme: { - colors: { accent_blue, grey }, overlay: { - reactionsList: { reaction, reactionSize }, + reactionsList: { container }, }, }, } = 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 ( - - - - - - - - - ); -}; + const supportedReactions = propSupportedReactions || contextSupportedReactions; -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 onSelectReaction = (type: string) => { + triggerHaptic('impactLight'); + if (handleReaction) { + handleReaction(type); + } + dismissOverlay(); }; -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} - > - - + {supportedReactions?.map(({ Icon, type }, index) => ( + - - { - 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 ( - - ); -}; +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingHorizontal: 16, + }, +}); OverlayReactionList.displayName = 'OverlayReactionList{overlay{reactionList}}'; diff --git a/package/src/components/MessageOverlay/OverlayReactions.tsx b/package/src/components/MessageOverlay/OverlayReactions.tsx index 982420154e..6a6901a77e 100644 --- a/package/src/components/MessageOverlay/OverlayReactions.tsx +++ b/package/src/components/MessageOverlay/OverlayReactions.tsx @@ -1,138 +1,94 @@ 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 { FlatList, StyleSheet, Text, View } from 'react-native'; import { ReactionSortBase } from 'stream-chat'; import { useFetchReactions } from './hooks/useFetchReactions'; +import { ReactionButton } from './ReactionButton'; -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; -}; + 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 OverlayReactionsProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'OverlayReactionsAvatar'> & { - showScreen: SharedValue; - title: string; - alignment?: Alignment; - messageId?: string; +> = Pick< + MessagesContextValue, + 'OverlayReactionsAvatar' | 'OverlayReactionsItem' | 'supportedReactions' +> & { + message?: MessageType; 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, + message, + OverlayReactionsAvatar: propOverlayReactionsAvatar, + OverlayReactionsItem: propOverlayReactionsItem, reactions: propReactions, - showScreen, - supportedReactions = reactionData, - title, + supportedReactions: propSupportedReactions, } = props; - const layoutHeight = useSharedValue(0); - const layoutWidth = useSharedValue(0); + const reactionTypes = Object.keys(message?.reaction_groups ?? {}); + const [selectedReaction, setSelectedReaction] = React.useState( + reactionTypes[0], + ); + const { + OverlayReactionsAvatar: contextOverlayReactionsAvatar, + OverlayReactionsItem: contextOverlayReactionsItem, + supportedReactions: contextSupportedReactions, + } = useMessagesContext(); + const supportedReactions = propSupportedReactions ?? contextSupportedReactions; + const OverlayReactionsAvatar = propOverlayReactionsAvatar ?? contextOverlayReactionsAvatar; + const OverlayReactionsItem = propOverlayReactionsItem ?? contextOverlayReactionsItem; + const messageReactions = reactionTypes.reduce((acc, reaction) => { + const reactionData = supportedReactions?.find( + (supportedReaction) => supportedReaction.type === reaction, + ); + if (reactionData) { + acc.push(reactionData); + } + return acc; + }, []); + const { loading, loadNextPage, reactions: fetchedReactions, } = useFetchReactions({ - messageId, + messageId: message?.id, + reactionType: selectedReaction, sort, }); + const { + theme: { + overlay: { + reactions: { + container, + flatlistColumnContainer, + flatlistContainer, + reactionSelectorContainer, + reactionsText, + }, + }, + }, + } = useTheme(); + const { t } = useTranslationContext(); + const reactions = useMemo( () => propReactions || (fetchedReactions.map((reaction) => ({ - alignment: 'left', id: reaction.user?.id, image: reaction.user?.image, name: reaction.user?.name, @@ -141,115 +97,70 @@ export const OverlayReactions = (props: OverlayReactionsProps) => { [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], + const renderHeader = () => ( + {t('Message Reactions')} ); + const onSelectReaction = (reactionType: string) => { + setSelectedReaction(reactionType); + }; + 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), - }, - ]} + + + {messageReactions?.map(({ Icon, type }, index) => ( + - )} - {/* 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] })} - - )} - - + ))} + + + {!loading ? ( + item.id} + ListHeaderComponent={renderHeader} + numColumns={4} + onEndReached={loadNextPage} + renderItem={renderItem} + /> + ) : null} + ); }; -OverlayReactions.displayName = 'OverlayReactions{overlay{reactions}}'; +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/MessageOverlay/OverlayReactionsAvatar.tsx index 26e0a4ccc7..c1b92f6e91 100644 --- a/package/src/components/MessageOverlay/OverlayReactionsAvatar.tsx +++ b/package/src/components/MessageOverlay/OverlayReactionsAvatar.tsx @@ -1,8 +1,7 @@ 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 = { diff --git a/package/src/components/MessageOverlay/OverlayReactionsItem.tsx b/package/src/components/MessageOverlay/OverlayReactionsItem.tsx index cad971046f..21dc20f748 100644 --- a/package/src/components/MessageOverlay/OverlayReactionsItem.tsx +++ b/package/src/components/MessageOverlay/OverlayReactionsItem.tsx @@ -1,23 +1,20 @@ 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 { MessagesContextValue } from '../../contexts/messagesContext/MessagesContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { Unknown } from '../../icons'; -import type { DefaultStreamChatGenerics } from '../../types/types'; +import type { DefaultStreamChatGenerics, Reaction } from '../../types/types'; import { ReactionData } from '../../utils/utils'; export type OverlayReactionsItemProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, -> = Pick, 'OverlayReactionsAvatar'> & { +> = Pick, 'OverlayReactionsAvatar'> & { reaction: Reaction; supportedReactions: ReactionData[]; }; @@ -45,14 +42,13 @@ export const OverlayReactionsItem = < const { id, name, type } = reaction; const { theme: { - colors: { accent_blue, black, grey_gainsboro, white }, + colors: { accent_blue, black, grey, grey_gainsboro, white }, overlay: { reactions: { avatarContainer, avatarName, avatarSize, radius, - reactionBubble, reactionBubbleBackground, reactionBubbleBorderRadius, }, @@ -60,7 +56,7 @@ export const OverlayReactionsItem = < }, } = useTheme(); const { client } = useChatContext(); - const alignment = client.userID && client.userID === id ? 'right' : 'left'; + const alignment = client.userID && client.userID === id ? 'left' : 'right'; const x = avatarSize / 2 - (avatarSize / (radius * 4)) * (alignment === 'left' ? 1 : -1); const y = avatarSize - radius; @@ -79,70 +75,25 @@ export const OverlayReactionsItem = < - - - - - - + - - - - - - - - @@ -156,7 +107,7 @@ export const OverlayReactionsItem = < const styles = StyleSheet.create({ avatarContainer: { - padding: 8, + marginBottom: 8, }, avatarInnerContainer: { alignSelf: 'center', @@ -173,15 +124,11 @@ const styles = StyleSheet.create({ flexDirection: 'row', flexGrow: 1, }, - reactionBubble: { - alignItems: 'center', - borderRadius: 24, - justifyContent: 'center', - position: 'absolute', - }, reactionBubbleBackground: { + alignItems: 'center', borderRadius: 24, height: 24, + justifyContent: 'center', position: 'absolute', width: 24, }, diff --git a/package/src/components/MessageOverlay/ReactionButton.tsx b/package/src/components/MessageOverlay/ReactionButton.tsx new file mode 100644 index 0000000000..8e545d0f11 --- /dev/null +++ b/package/src/components/MessageOverlay/ReactionButton.tsx @@ -0,0 +1,71 @@ +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 }, + overlay: { + reactionButton: { filledColor, unfilledColor }, + reactionsList: { 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/MessageOverlay/hooks/useFetchReactions.ts b/package/src/components/MessageOverlay/hooks/useFetchReactions.ts index 76eb848e73..898bf90954 100644 --- a/package/src/components/MessageOverlay/hooks/useFetchReactions.ts +++ b/package/src/components/MessageOverlay/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/index.ts b/package/src/components/index.ts index abeb80568e..0492d71211 100644 --- a/package/src/components/index.ts +++ b/package/src/components/index.ts @@ -156,7 +156,6 @@ 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'; 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/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}`} -