From b0e53f49fd0c3f09df0c1923c8a8e4cf256a2770 Mon Sep 17 00:00:00 2001 From: Bishwajeet Parhi Date: Tue, 7 Jan 2025 16:21:45 +0100 Subject: [PATCH 1/4] refactor components into different files --- .../mobile/src/components/Chat/ChatDetail.tsx | 830 ++++-------------- .../Chat/ui/RenderBottomContainer.tsx | 99 +++ .../src/components/Chat/ui/RenderBubble.tsx | 229 +++++ .../components/Chat/ui/RenderMessageText.tsx | 120 +++ .../Chat/ui/RenderReplyMessageView.tsx | 113 +++ 5 files changed, 709 insertions(+), 682 deletions(-) create mode 100644 packages/mobile/src/components/Chat/ui/RenderBottomContainer.tsx create mode 100644 packages/mobile/src/components/Chat/ui/RenderBubble.tsx create mode 100644 packages/mobile/src/components/Chat/ui/RenderMessageText.tsx create mode 100644 packages/mobile/src/components/Chat/ui/RenderReplyMessageView.tsx diff --git a/packages/mobile/src/components/Chat/ChatDetail.tsx b/packages/mobile/src/components/Chat/ChatDetail.tsx index f9365e26..b379c816 100644 --- a/packages/mobile/src/components/Chat/ChatDetail.tsx +++ b/packages/mobile/src/components/Chat/ChatDetail.tsx @@ -2,7 +2,6 @@ import { Actions, Avatar, AvatarProps, - Bubble, BubbleProps, Composer, ComposerProps, @@ -13,19 +12,14 @@ import { LoadEarlier, MessageImageProps, MessageProps, - MessageText, - MessageTextProps, Send, SendProps, - Time, - TimeProps, User, } from 'react-native-gifted-chat'; import React, { useCallback, memo, useMemo, useRef, useEffect, useState } from 'react'; import { Keyboard, Platform, - Pressable, StatusBar, StyleProp, StyleSheet, @@ -37,9 +31,7 @@ import { import { ArrowDown, Camera, - ImageLibrary, Microphone, - PaperClip, Plus, SendChat, Times, @@ -47,53 +39,33 @@ import { import MediaMessage from './MediaMessage'; import { launchCamera, launchImageLibrary } from 'react-native-image-picker'; import { useAuth } from '../../hooks/auth/useAuth'; -import { useChatMessage } from '../../hooks/chat/useChatMessage'; -import { ChatDrive } from '../../provider/chat/ConversationProvider'; import { Colors, getOdinIdColor } from '../../app/Colors'; import ReplyMessageBar from '../../components/Chat/Reply-Message-bar'; import ChatMessageBox from '../../components/Chat/Chat-Message-box'; -import { OdinImage } from '../../components/ui/OdinImage/OdinImage'; import { useDarkMode } from '../../hooks/useDarkMode'; -import { ChatDeliveryIndicator } from '../../components/Chat/Chat-Delivery-Indicator'; import { Avatar as AppAvatar, OwnerAvatar } from '../../components/ui/Avatars/Avatar'; -import { AuthorName, ConnectionName } from '../../components/ui/Name'; -import { DEFAULT_PAYLOAD_KEY, HomebaseFile } from '@homebase-id/js-lib/core'; -import { ChatDeletedArchivalStaus, ChatMessage } from '../../provider/chat/ChatProvider'; +import { ConnectionName } from '../../components/ui/Name'; +import { HomebaseFile } from '@homebase-id/js-lib/core'; +import { ChatMessage } from '../../provider/chat/ChatProvider'; import { useAudioRecorder } from '../../hooks/audio/useAudioRecorderPlayer'; import { Text } from '../ui/Text/Text'; -import { - assetsToImageSource, - fixDocumentURI, - isEmojiOnly, - millisToMinutesAndSeconds, - openURL, - URL_PATTERN, -} from '../../utils/utils'; +import { assetsToImageSource, fixDocumentURI, millisToMinutesAndSeconds } from '../../utils/utils'; import { SafeAreaView } from '../ui/SafeAreaView/SafeAreaView'; import Document from 'react-native-document-picker'; -import { getLocales, uses24HourClock } from 'react-native-localize'; +import { getLocales } from 'react-native-localize'; import { type PastedFile } from '@mattermost/react-native-paste-input'; import { useDraftMessage } from '../../hooks/chat/useDraftMessage'; -import { useBubbleContext } from '../BubbleContext/useBubbleContext'; -import { ChatMessageContent } from './Chat-Message-Content'; -import Animated, { - Easing, - useAnimatedStyle, - useSharedValue, - withTiming, -} from 'react-native-reanimated'; -import { FlatList, TouchableHighlight } from 'react-native-gesture-handler'; -import { ParseShape } from 'react-native-gifted-chat/src/MessageText'; +import { FlatList } from 'react-native-gesture-handler'; import { MentionDropDown } from './Mention-Dropdown'; import { LinkPreviewBar } from './Link-Preview-Bar'; import { LinkPreview } from '@homebase-id/js-lib/media'; -import { tryJsonParse } from '@homebase-id/js-lib/helpers'; import { EmptyChatContainer } from './EmptyChatContainer'; -import { getPlainTextFromRichText } from 'homebase-id-app-common'; import { ImageSource } from '../../provider/image/RNImageProvider'; -import { useChatMessagePayload } from '../../hooks/chat/useChatMessagePayload'; -import Clipboard from '@react-native-clipboard/clipboard'; -import Toast from 'react-native-toast-message'; +import { RenderBottomContainer } from './ui/RenderBottomContainer'; +import Animated from 'react-native-reanimated'; +import { RenderMessageText } from './ui/RenderMessageText'; +import { RenderBubble } from './ui/RenderBubble'; +import { RenderReplyMessageView } from './ui/RenderReplyMessageView'; export type ChatMessageIMessage = IMessage & HomebaseFile; @@ -142,14 +114,31 @@ export const ChatDetail = memo( onAssetsAdded: (assets: ImageSource[]) => void; }) => { + //Hooks const { isDarkMode } = useDarkMode(); const identity = useAuth().getIdentity(); + const { record, stop, duration, isRecording } = useAudioRecorder(); + // We will fetch the draft message from the cache only once + const { + getDraftMessage, + set: { mutate: onInputTextChanged }, + } = useDraftMessage(conversationId); const textRef = useRef(null); + const messageContainerRef = useRef>(null); const [draftMessage, setdraftMessage] = useState(initialMessage); - const messageContainerRef = useRef>(null); + const [bottomContainerVisible, setBottomContainerVisible] = useState(false); + // Icons Callback + const microphoneIcon = useCallback(() => , []); + const cameraIcon = useCallback(() => , []); + const crossIcon = useCallback(() => , []); + const scrollToBottomComponent = useCallback(() => , []); + + const locale = getLocales()[0].languageTag; + + /* Functions Callback */ const _doSend = useCallback( (message: ChatMessageIMessage[]) => { doSend(message); @@ -159,22 +148,6 @@ export const ChatDetail = memo( [doSend] ); - // We will fetch the draft message from the cache only once - const { - getDraftMessage, - set: { mutate: onInputTextChanged }, - } = useDraftMessage(conversationId); - - // Fetch draftmessage only once when the component mounts - useEffect(() => { - (async () => { - const draft = await getDraftMessage(); - if (!draft) return; - setdraftMessage(draft); - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const onTextInputChanged = useCallback( (text: string) => { if (text === '' && draftMessage) { @@ -208,74 +181,6 @@ export const ChatDetail = memo( [doOpenMessageInfo] ); - const renderMessageBox = useCallback( - ({ key, ...props }: MessageProps) => { - return ( - - ); - }, - [onLeftSwipe, setReplyMessage] - ); - - const renderChatFooter = useCallback( - ( - text: string | undefined, - updateText: React.Dispatch> - ) => { - const onMention = (mention: string) => { - if (!text) return; - const words = text.split(' '); - words[words.length - 1] = `@${mention}`; - updateText(words.join(' ') + ' '); - }; - - return ( - - {isGroup && ( - - )} - - {replyMessage ? ( - setReplyMessage(null)} /> - ) : null} - - ); - }, - [ - isDarkMode, - isGroup, - conversationId, - draftMessage, - onDismissLinkPreview, - onLinkData, - replyMessage, - setReplyMessage, - ] - ); - - const { record, stop, duration, isRecording } = useAudioRecorder(); - - const microphoneIcon = useCallback(() => , []); - const cameraIcon = useCallback(() => , []); - const crossIcon = useCallback(() => , []); - const onStopRecording = useCallback(() => { requestAnimationFrame(async () => { const { path, duration } = await stop(); @@ -347,8 +252,6 @@ export const ChatDetail = memo( }); }, [onAssetsAdded]); - const [bottomContainerVisible, setBottomContainerVisible] = useState(false); - const handlePlusIconPress = useCallback(async () => { if (Keyboard.isVisible()) Keyboard.dismiss(); setBottomContainerVisible(!bottomContainerVisible); @@ -368,6 +271,7 @@ export const ChatDetail = memo( setBottomContainerVisible(false); }, [onAssetsAdded]); + // Style Memoization const inputStyle = useMemo( () => ({ @@ -393,6 +297,99 @@ export const ChatDetail = memo( [isDarkMode] ); + const inputContainerStyle: StyleProp = useMemo(() => { + return { + position: 'relative', + flexDirection: 'column-reverse', + backgroundColor: isDarkMode ? Colors.gray[900] : Colors.slate[50], + justifyContent: 'center', + alignContent: 'center', + alignItems: 'center', + borderTopWidth: 0, + borderRadius: 10, + marginTop: Platform.OS === 'android' ? 'auto' : undefined, + paddingHorizontal: 7, + paddingBottom: 7, + }; + }, [isDarkMode]); + + const scrollToBottomStyle = useMemo(() => { + return { + backgroundColor: isDarkMode ? Colors.indigo[900] : Colors.indigo[200], + opacity: 1, + }; + }, [isDarkMode]); + + const wrapperStyle: StyleProp = useMemo(() => { + return { + backgroundColor: isDarkMode ? Colors.indigo[900] : Colors.slate[50], + opacity: 1, + }; + }, [isDarkMode]); + + /* Component Function Callbacks */ + const renderMessageBox = useCallback( + ({ key, ...props }: MessageProps) => { + return ( + + ); + }, + [onLeftSwipe, setReplyMessage] + ); + + const renderChatFooter = useCallback( + ( + text: string | undefined, + updateText: React.Dispatch> + ) => { + const onMention = (mention: string) => { + if (!text) return; + const words = text.split(' '); + words[words.length - 1] = `@${mention}`; + updateText(words.join(' ') + ' '); + }; + + return ( + + {isGroup && ( + + )} + + {replyMessage ? ( + setReplyMessage(null)} /> + ) : null} + + ); + }, + [ + isDarkMode, + isGroup, + conversationId, + draftMessage, + onDismissLinkPreview, + onLinkData, + replyMessage, + setReplyMessage, + ] + ); + const renderComposer = useCallback( (props: ComposerProps) => { if (isRecording) { @@ -482,12 +479,6 @@ export const ChatDetail = memo( ] ); - useEffect(() => { - if (replyMessage !== null && textRef.current) { - textRef.current?.focus(); - } - }, [textRef, replyMessage]); - const renderSend = useCallback( (props: SendProps) => { const hasText = props.text || draftMessage; @@ -587,22 +578,6 @@ export const ChatDetail = memo( [identity, isDarkMode] ); - const inputContainerStyle: StyleProp = useMemo(() => { - return { - position: 'relative', - flexDirection: 'column-reverse', - backgroundColor: isDarkMode ? Colors.gray[900] : Colors.slate[50], - justifyContent: 'center', - alignContent: 'center', - alignItems: 'center', - borderTopWidth: 0, - borderRadius: 10, - marginTop: Platform.OS === 'android' ? 'auto' : undefined, - paddingHorizontal: 7, - paddingBottom: 7, - }; - }, [isDarkMode]); - const renderInputToolbar = useCallback( (props: InputToolbarProps) => { return ( @@ -667,33 +642,6 @@ export const ChatDetail = memo( [identity] ); - const scrollToBottomStyle = useMemo(() => { - return { - backgroundColor: isDarkMode ? Colors.indigo[900] : Colors.indigo[200], - opacity: 1, - }; - }, [isDarkMode]); - - const wrapperStyle: StyleProp = useMemo(() => { - return { - backgroundColor: isDarkMode ? Colors.indigo[900] : Colors.slate[50], - opacity: 1, - }; - }, [isDarkMode]); - - const scrollToBottomComponent = useCallback(() => { - return ; - }, []); - - const locale = getLocales()[0].languageTag; - - useEffect(() => { - const listener = Keyboard.addListener('keyboardDidShow', () => { - if (bottomContainerVisible) setBottomContainerVisible(false); - }); - return () => listener.remove(); - }, [bottomContainerVisible]); - const renderBottomContainer = useMemo( () => ( { + (async () => { + const draft = await getDraftMessage(); + if (!draft) return; + setdraftMessage(draft); + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const listener = Keyboard.addListener('keyboardDidShow', () => { + if (bottomContainerVisible) setBottomContainerVisible(false); + }); + return () => listener.remove(); + }, [bottomContainerVisible]); + + useEffect(() => { + if (replyMessage !== null && textRef.current) { + textRef.current?.focus(); + } + }, [textRef, replyMessage]); + return ( @@ -791,513 +764,6 @@ export const ChatDetail = memo( } ); -const RenderBottomContainer = memo( - ({ - isVisible, - onGalleryPressed, - onAttachmentPressed, - }: { - isVisible?: boolean; - onGalleryPressed: () => void; - onAttachmentPressed: () => void; - }) => { - const { isDarkMode } = useDarkMode(); - const height = useSharedValue(0); - useEffect(() => { - if (isVisible) { - height.value = 250; - } else { - height.value = 0; - } - }, [height, isVisible]); - - const animatedStyle = useAnimatedStyle(() => { - return { - height: withTiming(height.value, { duration: 150, easing: Easing.inOut(Easing.ease) }), - opacity: withTiming(height.value > 0 ? 1 : 0, { duration: 300 }), - }; - }); - - return ( - - } onPress={onGalleryPressed} title="Gallery" /> - } - onPress={onAttachmentPressed} - title="Attachment" - /> - - ); - } -); - -const MediaPickerComponent = ({ - icon, - onPress, - title, -}: { - icon: React.ReactNode; - onPress: () => void; - title: string; -}) => { - const { isDarkMode } = useDarkMode(); - return ( - - - {icon} - - {title} - - ); -}; - -const RenderMessageText = memo((props: MessageTextProps) => { - const { isDarkMode } = useDarkMode(); - const [message, setMessage] = useState(props.currentMessage as ChatMessageIMessage); - const deleted = message?.fileMetadata.appData.archivalStatus === ChatDeletedArchivalStaus; - const hasMoreTextContent = message?.fileMetadata?.payloads?.some( - (e) => e.key === DEFAULT_PAYLOAD_KEY - ); - const { data: completeMessage } = useChatMessagePayload({ - fileId: message.fileId, - payloadKey: hasMoreTextContent ? DEFAULT_PAYLOAD_KEY : undefined, - }).getExpanded; - - const allowExpand = hasMoreTextContent && !!completeMessage; - const content = message?.fileMetadata.appData.content; - const plainMessage = getPlainTextFromRichText(content.message); - const onlyEmojis = isEmojiOnly(plainMessage); - /** - * An array of parse patterns used for parsing text in the chat detail component. - * Each pattern consists of a regular expression pattern, a style to apply to the matched text, - * an onPress function to handle the press event, and a renderText function to customize the rendered text. - * @param linkStyle The style to apply to the matched text. - * @returns An array of parse patterns. - */ - const parsePatterns = useCallback((linkStyle: StyleProp): ParseShape[] => { - const pattern = /(^|\s)@[a-zA-Z0-9._-]+(?!@)/; - return [ - { - pattern: pattern, - style: [ - linkStyle, - { - textDecorationLine: 'none', - }, - ], - onPress: (text: string) => openURL(`https://${text}`), - renderText: (text: string) => { - return () as unknown as string; - }, - }, - { - pattern: URL_PATTERN, - style: linkStyle, - onPress: (text: string) => openURL(text), - onLongPress: (text: string) => { - Clipboard.setString(text); - Toast.show({ - type: 'info', - text1: 'Copied to clipboard', - position: 'bottom', - }); - }, - }, - ]; - }, []); - - useEffect(() => { - if (!completeMessage) { - setMessage(props.currentMessage as ChatMessageIMessage); - } else { - const message = props.currentMessage as ChatMessageIMessage; - message.text = completeMessage.message; - setMessage(message); - } - }, [completeMessage, props.currentMessage]); - - const onExpand = useCallback(() => { - if (!hasMoreTextContent || !completeMessage) return; - const message = props.currentMessage as ChatMessageIMessage; - message.text = completeMessage.message; - setMessage(message); - }, [hasMoreTextContent, completeMessage, props]); - - return ( - - ); -}); - -const RenderBubble = memo( - ( - props: { - onReactionClick: (message: ChatMessageIMessage) => void; - onRetryClick: (message: ChatMessageIMessage) => void; - } & Readonly> - ) => { - const { bubbleColor } = useBubbleContext(); - const { isDarkMode } = useDarkMode(); - - const message = props.currentMessage as ChatMessageIMessage; - const content = message?.fileMetadata.appData.content; - - const plainMessage = getPlainTextFromRichText(content.message); - const onlyEmojis = isEmojiOnly(plainMessage); - const isReply = !!content?.replyId; - const showBackground = !onlyEmojis || isReply; - - const onRetryOpen = useCallback(() => { - props.onRetryClick(message); - }, [message, props]); - - const reactions = - (message.fileMetadata.reactionPreview?.reactions && - Object.values(message.fileMetadata.reactionPreview?.reactions).map((reaction) => { - return tryJsonParse<{ emoji: string }>(reaction.reactionContent).emoji; - })) || - []; - const filteredEmojis = Array.from(new Set(reactions)); - const hasReactions = (reactions && reactions?.length > 0) || false; - - // has pauload and no text but no audio payload - const hasPayloadandNoText = - message?.fileMetadata.payloads && - message?.fileMetadata.payloads?.length > 0 && - !content?.message && - !message?.fileMetadata?.payloads?.some( - (val) => val.contentType.startsWith('audio') || val.contentType.startsWith('application') - ); - - const deleted = message?.fileMetadata.appData.archivalStatus === ChatDeletedArchivalStaus; - - const renderTime = useCallback( - (timeProp: TimeProps) => { - const is24Hour = uses24HourClock(); - return ( -