dappros/ethora

View on GitHub
client-reactnative/src/components/Chat/MessageBubble.tsx

Summary

Maintainability
F
4 days
Test Coverage
/*
Copyright 2019-2022 (c) Dappros Ltd, registered in England & Wales, registration number 11455432. All rights reserved.
You may not use this file except in compliance with the License.
You may obtain a copy of the License at https://github.com/dappros/ethora/blob/main/LICENSE.
Note: linked open-source libraries and components may be subject to their own licenses.
*/

import React, {useState} from 'react';
import {
  StyleSheet,
  Image,
  Animated,
  TouchableWithoutFeedback,
  TouchableOpacity,
} from 'react-native';
import {
  heightPercentageToDP as hp,
  widthPercentageToDP as wp,
} from 'react-native-responsive-screen';
import {colors} from '../../constants/messageColors';
import {MessageImage, Time} from 'react-native-gifted-chat';
import {coinImagePath, commonColors, textStyles} from '../../../docs/config';
import {QuickReplies} from './QuickReplies';
import {MessageText} from './MessageText';
import {Box, HStack, Text, View} from 'native-base';
import {observer} from 'mobx-react-lite';
import {TcontainerType} from './ChatContainer';
import {IMessage, roomListProps} from '../../stores/chatStore';
import {isSameDay, isSameUser} from '../../helpers/chat/chatUtils';
import {useStores} from '../../stores/context';
import {GestureResponderEvent} from 'react-native';

// const {isSameUser, isSameDay, StylePropType} = utils;

interface BubbleProps {
  onLongPress: any;
  currentMessage: IMessage;
  onTap: (message: IMessage) => void;
  containerStyle?: any;
  wrapperStyle?: any;
  messageTextStyle?: any;
  messageTextProps?: any;
  renderMessageText?: any;
  renderMessageImage?: any;
  renderTicks?: any;
  user: any;
  tickStyle?: any;
  renderUsername?: any;
  renderTime?: any;
  position: 'left' | 'right';
  renderCustomView?: any;
  nextMessage?: any;
  containerToNextStyle?: any;
  previousMessage?: any;
  containerToPreviousStyle?: any;
  isCustomViewBottom?: any;
  image?: any;
  bottomContainerStyle?: any;
  touchableProps?: any;
  timeProps?: any;
  usernameProps?: any;
  messageImageProps?: any;
  containerType: TcontainerType;
  scrollToParentMessage: any;
  handleReply: (message: any) => void;
}

const Bubble = observer((props: BubbleProps) => {
  const {chatStore} = useStores();
  const [width, setWidth] = useState(0);
  const initialAnimationValue = new Animated.Value(0);

  const {
    onLongPress,
    currentMessage,
    onTap,
    containerStyle,
    wrapperStyle,
    messageTextStyle,
    messageTextProps,
    renderMessageText,
    renderMessageImage,
    renderUsername,
    renderTime,
    position,
    renderCustomView,
    nextMessage,
    containerToNextStyle,
    previousMessage,
    containerToPreviousStyle,
    isCustomViewBottom,
    bottomContainerStyle,
    timeProps,
    usernameProps,
    messageImageProps,
    scrollToParentMessage,
    handleReply,
    containerType,
  } = props;

  const room: roomListProps = chatStore.roomList.find(
    item => item.jid === currentMessage.roomJid,
  ) || {
    avatar: '',
    counter: 0,
    createdAt: '',
    jid: currentMessage.roomJid,
    lastUserName: '',
    lastUserText: '',
    name: '',
    participants: 0,
    isFavourite: false,
    muted: false,
    priority: 0,
    roomBackground: '',
    roomBackgroundIndex: 0,
    roomThumbnail: '',
  };

  const onLongPressHandle = () => {
    if (onLongPress) {
      onLongPress(currentMessage);
    }
  };

  const onPressMessage = () => {
    onTap(currentMessage);
  };

  const renderMessageTextHandle = () => {
    if (currentMessage.text) {
      if (renderMessageText) {
        return renderMessageText(messageTextProps);
      }
      return (
        <MessageText
          {...props}
          textStyle={{
            left: [styles.content.userTextStyleLeft, messageTextStyle],
            right: [styles.content.userTextStyleLeft],
          }}
        />
      );
    }
    return null;
  };

  const renderMessageImageHandle = () => {
    if (currentMessage.image || currentMessage.realImageURL) {
      if (renderMessageImage) {
        return renderMessageImage(props);
      }
      return (
        <MessageImage
          {...messageImageProps}
          imageStyle={[messageImageProps.imageStyle]}
        />
      );
    }
    return null;
  };

  // const renderTicksHandle = () => {
  //   if (renderTicks) {
  //     return renderTicks(currentMessage);
  //   }
  //   if (currentMessage.user._id !== user._id) {
  //     return null;
  //   }
  //   if (currentMessage.sent || currentMessage.received) {
  //     return (
  //       <View style={[styles.headerItem, styles.tickView]}>
  //         {currentMessage.sent && (
  //           <Text style={[styles.standardFont, styles.tick, tickStyle]}>✓</Text>
  //         )}
  //         {currentMessage.received && (
  //           <Text style={[styles.standardFont, styles.tick, tickStyle]}>✓</Text>
  //         )}
  //       </View>
  //     );
  //   }
  //   return null;
  // };

  const renderUsernameHandle = () => {
    const username = currentMessage.user.name;
    if (username) {
      if (renderUsername) {
        return renderUsername(usernameProps);
      }
      return (
        <View style={styles.content.usernameView}>
          <Text
            color={'white'}
            fontSize={hp('2%')}
            fontFamily={textStyles.lightFont}>
            {username}
          </Text>
        </View>
      );
    }
    return null;
  };

  const renderTimeHandle = () => {
    if (currentMessage.createdAt) {
      if (renderTime) {
        return renderTime(timeProps);
      }
      return (
        //@ts-ignore
        <Time
          {...props}
          timeTextStyle={{
            left: {
              fontFamily: textStyles.lightFont,
            },
            right: {
              fontFamily: textStyles.lightFont,
            },
          }}
        />
      );
    }
    return null;
  };

  const renderTokenCount = () => {
    if (currentMessage.tokenAmount) {
      return (
        <View style={[styles[position].tokenContainerStyle]}>
          <Text style={[styles[position].tokenTextStyle]}>
            {currentMessage.tokenAmount}
          </Text>
          <Image
            source={coinImagePath}
            resizeMode={'contain'}
            style={styles[position].tokenIconStyle}
          />
        </View>
      );
    }
  };
  const renderReplyCount = () => {
    if (currentMessage.numberOfReplies) {
      const replyConst =
        currentMessage.numberOfReplies > 1 ? 'replies' : 'reply';
      return (
        <HStack style={styles[position].numberOfRepliesContainerStyle}>
          <TouchableOpacity onPress={() => handleReply(currentMessage)}>
            <Text
              fontFamily={textStyles.regularFont}
              color={commonColors.primaryColor}>
              {currentMessage.numberOfReplies} {replyConst} (tap to review)
            </Text>
          </TouchableOpacity>
        </HStack>
      );
    }
  };
  const renderQuickReplies = () => {
    if (currentMessage.quickReplies && width) {
      let quickReplies = [];
      try {
        quickReplies = JSON.parse(currentMessage.quickReplies);
      } catch (error) {
        console.log(error, 'cannot parse quick replies');
      }
      return (
        <QuickReplies
          quickReplies={quickReplies}
          roomJid={currentMessage.roomJid}
          roomName={room.name}
          width={width}
          messageAuthor={currentMessage.user._id.split('@')[0]}
        />
      );
    }
  };

  const renderCustomViewHandle = () => {
    if (renderCustomView) {
      return renderCustomView(props);
    }
    return null;
  };

  const styledBubbleToNext = () => {
    if (
      currentMessage &&
      nextMessage &&
      position &&
      isSameUser(currentMessage, nextMessage) &&
      isSameDay(currentMessage, nextMessage)
    ) {
      return [
        styles[position].containerToNext,
        containerToNextStyle && containerToNextStyle[position],
      ];
    }
    return null;
  };

  const styledBubbleToPrevious = () => {
    if (
      currentMessage &&
      previousMessage &&
      position &&
      isSameUser(currentMessage, previousMessage) &&
      isSameDay(currentMessage, previousMessage)
    ) {
      return [
        styles[position].containerToPrevious,
        containerToPreviousStyle && containerToPreviousStyle[position],
      ];
    }
    return null;
  };

  const renderBubbleContent = () => {
    return isCustomViewBottom ? (
      <View>
        {renderMessageImageHandle()}
        {!currentMessage.image && renderMessageTextHandle()}

        {renderCustomViewHandle()}
      </View>
    ) : (
      <View>
        {renderCustomViewHandle()}
        {renderMessageImageHandle()}
        {!currentMessage.image && renderMessageTextHandle()}
      </View>
    );
  };

  const setBubbleWidth = (bubbleWidth: any) => {
    setWidth(bubbleWidth);
  };

  const AnimatedStyle = {
    backgroundColor: initialAnimationValue.interpolate({
      inputRange: [0, 100],
      outputRange: [
        position === 'left' ? colors.leftBubbleBackground : colors.defaultBlue,
        '#F0B310',
      ],
    }),
  };

  const replyComponent = () => {
    return currentMessage.isReply ? (
      <TouchableOpacity onPress={() => scrollToParentMessage(currentMessage)}>
        <HStack
          style={styles[position].replyWrapper}
          alignItems={'center'}
          justifyContent={'flex-start'}
          minH={hp('6%')}
          w="100%"
          bg={'white'}>
          <View
            margin={2}
            borderRadius={5}
            height={hp('4%')}
            width={wp('2%')}
            bg={
              position === 'left'
                ? colors.leftBubbleBackground
                : colors.defaultBlue
            }
          />
          <View justifyContent={'center'}>
            <Text fontSize={hp('1.5%')} fontFamily={textStyles.boldFont}>
              {currentMessage.mainMessage?.userName || 'N/A'}
            </Text>
            {currentMessage.mainMessage?.imagePreview ? (
              <Image
                source={{uri: currentMessage.mainMessage.imagePreview}}
                style={{
                  height: hp('10%'),
                  width: hp('10%'),
                }}
              />
            ) : null}
            {!currentMessage.mainMessage?.imagePreview && (
              <Text fontSize={hp('1.5%')} fontFamily={textStyles.mediumFont}>
                {currentMessage?.mainMessage?.text}
              </Text>
            )}
            <Text color={'blue.100'}>{currentMessage.showInChannel}</Text>
          </View>
        </HStack>
      </TouchableOpacity>
    ) : null;
  };

  const isEditedComponent = () => {
    return (
      <Box
        style={styles[position ? position : 'left'].editWraper}
        alignItems={'flex-end'}>
        <Text
          fontFamily={textStyles.lightFont}
          fontSize={hp('1.2%')}
          color={'white'}>
          edited
        </Text>
      </Box>
    );
  };

  return (
    <View
      accessibilityLabel="Message Menu"
      onLayout={e => setBubbleWidth(e.nativeEvent.layout.width)}
      style={[
        styles[position].container,
        containerStyle && containerStyle[position],
        {position: 'relative'},
      ]}>
      <Animated.View
        style={[
          styles[position].wrapper,
          styledBubbleToNext(),
          styledBubbleToPrevious(),
          wrapperStyle && wrapperStyle[position],
          AnimatedStyle,
          // {maxWidth: 200}
        ]}>
        {containerType === 'main' ? replyComponent() : null}
        {!isSameUser(currentMessage, previousMessage)
          ? renderUsernameHandle()
          : null}
        <TouchableWithoutFeedback
          onPress={() => onPressMessage()}
          onLongPress={() => onLongPressHandle()}
          accessibilityTraits="text"
          {...props.touchableProps}>
          <View>
            {renderBubbleContent()}
            <View
              style={[
                styles[position].bottom,
                bottomContainerStyle && bottomContainerStyle[position],
              ]}>
              <View
                style={{
                  flexDirection: position === 'left' ? 'row-reverse' : 'row',
                  alignItems: 'center',
                }}>
                {!!currentMessage.isEdited && isEditedComponent()}
                {renderTokenCount()}
                {renderTimeHandle()}
                {/* {renderTicksHandle()} */}
              </View>
            </View>
          </View>
        </TouchableWithoutFeedback>
      </Animated.View>
      {renderQuickReplies()}
      {renderReplyCount()}
    </View>
  );
});

export default Bubble;

// Note: Everything is forced to be "left" positioned with this component.
// The "right" position is only used in the default Bubble.
const styles = {
  left: StyleSheet.create({
    container: {
      marginTop: 2,
    },
    replyWrapper: {
      borderRadius: 15,
      padding: 5,
      borderLeftWidth: 2,
      borderColor: colors.leftBubbleBackground,
      borderWidth: 2,
      borderBottomWidth: 0,
    },
    editWraper: {
      alignItems: 'flex-end',
      justifyContent: 'center',
      paddingBottom: 5,
    },
    wrapper: {
      borderRadius: 15,
      backgroundColor: colors.leftBubbleBackground,
      marginRight: 60,
      minHeight: 20,
      justifyContent: 'flex-end',
      minWidth: 100,
    },
    tokenContainerStyle: {
      flexDirection: 'row',
      marginRight: 10,
      marginBottom: 5,
      justifyContent: 'center',
      alignItems: 'center',
    },
    tokenIconStyle: {
      height: hp('2%'),
      width: hp('2%'),
    },
    tokenTextStyle: {
      color: colors.white,
      fontFamily: textStyles.regularFont,
      fontSize: 10,
      fontWeight: 'bold',
      backgroundColor: 'transparent',
      textAlign: 'right',
    },
    numberOfRepliesContainerStyle: {
      flexDirection: 'row',
      justifyContent: 'flex-start',
      alignItems: 'center',
    },
    containerToNext: {
      borderBottomLeftRadius: 3,
    },
    containerToPrevious: {
      borderTopLeftRadius: 3,
    },
    bottom: {
      flexDirection: 'row',
      justifyContent: 'flex-start',
    },
  }),
  right: StyleSheet.create({
    container: {
      marginTop: 2,
    },
    editWraper: {
      alignItems: 'flex-start',
      justifyContent: 'center',
      paddingBottom: 5,
    },
    replyWrapper: {
      borderTopRightRadius: 15,
      borderTopLeftRadius: 15,
      padding: 2,
      borderLeftWidth: 2,
      borderWidth: 2,
      borderColor: colors.defaultBlue,
      borderBottomWidth: 0,
    },
    wrapper: {
      borderRadius: 15,
      backgroundColor: colors.defaultBlue,
      marginLeft: 60,
      minHeight: 20,
      justifyContent: 'flex-end',
      minWidth: 100,
    },
    containerToNext: {
      borderBottomRightRadius: 3,
    },
    tokenContainerStyle: {
      flexDirection: 'row',
      marginLeft: 10,
      marginBottom: 5,
      justifyContent: 'flex-end',
      alignItems: 'center',
    },
    tokenIconStyle: {
      height: hp('2%'),
      width: hp('2%'),
    },
    tokenTextStyle: {
      color: colors.white,
      fontFamily: textStyles.regularFont,
      fontSize: 10,
      fontWeight: 'bold',
      backgroundColor: 'transparent',
      textAlign: 'right',
    },
    numberOfRepliesContainerStyle: {
      flexDirection: 'row',
      justifyContent: 'flex-end',
      alignItems: 'center',
    },
    containerToPrevious: {
      borderTopRightRadius: 3,
    },
    bottom: {
      flexDirection: 'row',
      justifyContent: 'flex-end',
    },
  }),
  content: StyleSheet.create({
    tick: {
      fontFamily: textStyles.regularFont,
      fontSize: 10,
      backgroundColor: colors.backgroundTransparent,
      color: colors.white,
    },
    tickView: {
      flexDirection: 'row',
      marginRight: 10,
    },
    username: {
      top: -3,
      left: 0,
      fontSize: 12,
      backgroundColor: 'transparent',
      color: '#aaa',
    },
    usernameView: {
      flexDirection: 'row',
      marginHorizontal: 10,
    },
    userTextStyleLeft: {
      fontFamily: textStyles.regularFont,
      color: '#FFFF',
    },
  }),
};