import PropTypes from 'prop-types' import React from 'react' import { Text, StyleSheet, TouchableWithoutFeedback, View, StyleProp, ViewStyle, TextStyle, } from 'react-native' import * as Clipboard from 'expo-clipboard' import { GiftedChatContext } from './GiftedChatContext' import { QuickReplies, QuickRepliesProps } from './QuickReplies' import { MessageText, MessageTextProps } from './MessageText' import { MessageImage, MessageImageProps } from './MessageImage' import { MessageVideo } from './MessageVideo' import { MessageAudio } from './MessageAudio' import { Time, TimeProps } from './Time' import Color from './Color' import { StylePropType, isSameUser, isSameDay } from './utils' import { User, IMessage, LeftRightStyle, Reply, Omit, MessageVideoProps, MessageAudioProps, } from './Models' const styles = { left: StyleSheet.create({ container: { flex: 1, alignItems: 'flex-start', }, wrapper: { borderRadius: 15, backgroundColor: Color.leftBubbleBackground, marginRight: 60, minHeight: 20, justifyContent: 'flex-end', }, containerToNext: { borderBottomLeftRadius: 3, }, containerToPrevious: { borderTopLeftRadius: 3, }, bottom: { flexDirection: 'row', justifyContent: 'flex-start', }, }), right: StyleSheet.create({ container: { flex: 1, alignItems: 'flex-end', }, wrapper: { borderRadius: 15, backgroundColor: Color.defaultBlue, marginLeft: 60, minHeight: 20, justifyContent: 'flex-end', }, containerToNext: { borderBottomRightRadius: 3, }, containerToPrevious: { borderTopRightRadius: 3, }, bottom: { flexDirection: 'row', justifyContent: 'flex-end', }, }), content: StyleSheet.create({ tick: { fontSize: 10, backgroundColor: Color.backgroundTransparent, color: Color.white, }, tickView: { flexDirection: 'row', marginRight: 10, }, username: { top: -3, left: 0, fontSize: 12, backgroundColor: 'transparent', color: '#aaa', }, usernameView: { flexDirection: 'row', marginHorizontal: 10, }, }), } const DEFAULT_OPTION_TITLES = ['Copy Text', 'Cancel'] export type RenderMessageImageProps = Omit< BubbleProps, 'containerStyle' | 'wrapperStyle' > & MessageImageProps export type RenderMessageVideoProps = Omit< BubbleProps, 'containerStyle' | 'wrapperStyle' > & MessageVideoProps export type RenderMessageAudioProps = Omit< BubbleProps, 'containerStyle' | 'wrapperStyle' > & MessageAudioProps export type RenderMessageTextProps = Omit< BubbleProps, 'containerStyle' | 'wrapperStyle' > & MessageTextProps export interface BubbleProps { user?: User touchableProps?: object renderUsernameOnMessage?: boolean isCustomViewBottom?: boolean inverted?: boolean position: 'left' | 'right' currentMessage?: TMessage nextMessage?: TMessage previousMessage?: TMessage optionTitles?: string[] containerStyle?: LeftRightStyle wrapperStyle?: LeftRightStyle textStyle?: LeftRightStyle bottomContainerStyle?: LeftRightStyle tickStyle?: StyleProp containerToNextStyle?: LeftRightStyle containerToPreviousStyle?: LeftRightStyle usernameStyle?: TextStyle quickReplyStyle?: StyleProp quickReplyTextStyle?: StyleProp onPress?(context?: any, message?: any): void onLongPress?(context?: any, message?: any): void onQuickReply?(replies: Reply[]): void renderMessageImage?(props: RenderMessageImageProps): React.ReactNode renderMessageVideo?(props: RenderMessageVideoProps): React.ReactNode renderMessageAudio?(props: RenderMessageAudioProps): React.ReactNode renderMessageText?(props: RenderMessageTextProps): React.ReactNode renderCustomView?(bubbleProps: BubbleProps): React.ReactNode renderTime?(timeProps: TimeProps): React.ReactNode renderTicks?(currentMessage: TMessage): React.ReactNode renderUsername?(user?: TMessage['user']): React.ReactNode renderQuickReplySend?(): React.ReactNode renderQuickReplies?( quickReplies: QuickRepliesProps, ): React.ReactNode } export default class Bubble< TMessage extends IMessage = IMessage, > extends React.Component> { static contextType = GiftedChatContext static defaultProps = { touchableProps: {}, onPress: null, onLongPress: null, renderMessageImage: null, renderMessageVideo: null, renderMessageAudio: null, renderMessageText: null, renderCustomView: null, renderUsername: null, renderTicks: null, renderTime: null, renderQuickReplies: null, onQuickReply: null, position: 'left', optionTitles: DEFAULT_OPTION_TITLES, currentMessage: { text: null, createdAt: null, image: null, }, nextMessage: {}, previousMessage: {}, containerStyle: {}, wrapperStyle: {}, bottomContainerStyle: {}, tickStyle: {}, usernameStyle: {}, containerToNextStyle: {}, containerToPreviousStyle: {}, } static propTypes = { user: PropTypes.object.isRequired, touchableProps: PropTypes.object, onLongPress: PropTypes.func, renderMessageImage: PropTypes.func, renderMessageVideo: PropTypes.func, renderMessageAudio: PropTypes.func, renderMessageText: PropTypes.func, renderCustomView: PropTypes.func, isCustomViewBottom: PropTypes.bool, renderUsernameOnMessage: PropTypes.bool, renderUsername: PropTypes.func, renderTime: PropTypes.func, renderTicks: PropTypes.func, renderQuickReplies: PropTypes.func, onQuickReply: PropTypes.func, position: PropTypes.oneOf(['left', 'right']), optionTitles: PropTypes.arrayOf(PropTypes.string), currentMessage: PropTypes.object, nextMessage: PropTypes.object, previousMessage: PropTypes.object, containerStyle: PropTypes.shape({ left: StylePropType, right: StylePropType, }), wrapperStyle: PropTypes.shape({ left: StylePropType, right: StylePropType, }), bottomContainerStyle: PropTypes.shape({ left: StylePropType, right: StylePropType, }), tickStyle: StylePropType, usernameStyle: StylePropType, containerToNextStyle: PropTypes.shape({ left: StylePropType, right: StylePropType, }), containerToPreviousStyle: PropTypes.shape({ left: StylePropType, right: StylePropType, }), } onPress = () => { if (this.props.onPress) this.props.onPress(this.context, this.props.currentMessage) } onLongPress = () => { const { currentMessage } = this.props if (this.props.onLongPress) { this.props.onLongPress(this.context, this.props.currentMessage) } else if (currentMessage && currentMessage.text) { const { optionTitles } = this.props const options = optionTitles && optionTitles.length > 0 ? optionTitles.slice(0, 2) : DEFAULT_OPTION_TITLES const cancelButtonIndex = options.length - 1 const context = this.context as any context.actionSheet().showActionSheetWithOptions( { options, cancelButtonIndex, }, (buttonIndex: number) => { switch (buttonIndex) { case 0: Clipboard.setStringAsync(currentMessage.text) break default: break } }, ) } } styledBubbleToNext() { const { currentMessage, nextMessage, position, containerToNextStyle } = this.props if ( currentMessage && nextMessage && position && isSameUser(currentMessage, nextMessage) && isSameDay(currentMessage, nextMessage) ) return [ styles[position].containerToNext, containerToNextStyle && containerToNextStyle[position], ] return null } styledBubbleToPrevious() { const { currentMessage, previousMessage, position, containerToPreviousStyle, } = this.props if ( currentMessage && previousMessage && position && isSameUser(currentMessage, previousMessage) && isSameDay(currentMessage, previousMessage) ) return [ styles[position].containerToPrevious, containerToPreviousStyle && containerToPreviousStyle[position], ] return null } renderQuickReplies() { const { currentMessage, onQuickReply, nextMessage, renderQuickReplySend, quickReplyStyle, quickReplyTextStyle, } = this.props if (currentMessage && currentMessage.quickReplies) { const { containerStyle, wrapperStyle, ...quickReplyProps } = this.props if (this.props.renderQuickReplies) return this.props.renderQuickReplies(quickReplyProps) return ( ) } return null } renderMessageText() { if (this.props.currentMessage && this.props.currentMessage.text) { const { containerStyle, wrapperStyle, optionTitles, ...messageTextProps } = this.props if (this.props.renderMessageText) return this.props.renderMessageText(messageTextProps) return } return null } renderMessageImage() { if (this.props.currentMessage && this.props.currentMessage.image) { const { containerStyle, wrapperStyle, ...messageImageProps } = this.props if (this.props.renderMessageImage) return this.props.renderMessageImage(messageImageProps) return } return null } renderMessageVideo() { if (this.props.currentMessage && this.props.currentMessage.video) { const { containerStyle, wrapperStyle, ...messageVideoProps } = this.props if (this.props.renderMessageVideo) return this.props.renderMessageVideo(messageVideoProps) return } return null } renderMessageAudio() { if (this.props.currentMessage && this.props.currentMessage.audio) { const { containerStyle, wrapperStyle, ...messageAudioProps } = this.props if (this.props.renderMessageAudio) return this.props.renderMessageAudio(messageAudioProps) return } return null } renderTicks() { const { currentMessage, renderTicks, user } = this.props if (renderTicks && currentMessage) return renderTicks(currentMessage) if ( currentMessage && user && currentMessage.user && currentMessage.user._id !== user._id ) return null if ( currentMessage && (currentMessage.sent || currentMessage.received || currentMessage.pending) ) return ( {!!currentMessage.sent && ( {'✓'} )} {!!currentMessage.received && ( {'✓'} )} {!!currentMessage.pending && ( {'🕓'} )} ) return null } renderTime() { if (this.props.currentMessage && this.props.currentMessage.createdAt) { const { containerStyle, wrapperStyle, textStyle, ...timeProps } = this.props if (this.props.renderTime) return this.props.renderTime(timeProps) return