UNPKG

maisonsport-common-ui

Version:

Suite of styled-components to be consumed by the React-Native App and by the Web (via React-Native for Web)

354 lines (317 loc) 10.1 kB
/* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable react/destructuring-assignment */ import React, { forwardRef, useEffect, useRef, useState, } from 'react'; import { Image, Keyboard, Platform, } from 'react-native'; import PropTypes from 'prop-types'; import styled, { ThemeProvider } from 'styled-components/native'; import { space, layout, flexbox, } from 'styled-system'; import composeRefs from '@seznam/compose-react-refs'; import Theme from '../../theme'; import Box from '../../atoms/Box'; import Text from '../../atoms/Text'; import Touchable from '../../atoms/Touchable'; import TextInput from '../../molecules/TextInput'; import MessageBubble from '../../molecules/MessageBubble'; import MessageSendButton from '../../molecules/MessageSendButton'; import { getFormattedTimeSent } from '../../util/dateTime'; const isIOS = Platform.OS === 'ios'; const StyledFlatList = styled.FlatList` ${layout} ${space} ${flexbox} `; StyledFlatList.defaultProps = { position: 'absolute', }; const MessageThread = forwardRef(({ messages = [], userID, recipientID, inputPlaceholder, inputValue, inputBusy, inputHeight, inputBottomOffset, unreadMessageIndicatorText, onInputSubmit, onInputChangeText, onMessagePress, onMessageLongPress, renderItem, t, ListHeaderComponent = null, ...props }, ref) => { const [inputHeightState, setInputHeightState] = useState(inputHeight); const [keyboardHeight, setKeyboardHeight] = useState(0); const [keyboardOpen, setKeyboardOpen] = useState(false); const [showUnreadMarker, setShowUnreadMarker] = useState(true); const threadRef = useRef(); const threadTrueHeight = useRef(0); const scrollPosition = useRef(); const previousMessagesLength = useRef(messages.length); const androidExtraInputPaddingBottom = ( isIOS ? 0 : Theme.space[3] ); const finalTextInputContainerHeight = ( inputHeightState + Theme.space[3] + androidExtraInputPaddingBottom + (showUnreadMarker ? (Theme.space[2] * 4) : 0) + inputBottomOffset ); const conversationThreadBottom = ( finalTextInputContainerHeight + keyboardHeight ); function keyboardDidShow(event) { const keyboardHeightAfterShow = ( isIOS ? event?.endCoordinates?.height || 0 : 0 ); if (isIOS) setKeyboardOpen(true); setKeyboardHeight(keyboardHeightAfterShow); } function keyboardDidHide() { setKeyboardOpen(false); setKeyboardHeight(0); } function handleSubmit() { if (!!inputValue !== false) { onInputSubmit(); } } function scrollToBottom() { // eslint-disable-next-line no-undef setTimeout(() => { if (threadRef?.current?.scrollToEnd) { threadRef.current.scrollToEnd(); setShowUnreadMarker(false); } }, 500); } function renderMessage(item) { const { content, createdAt, from } = item; const position = {}; if (from?.id === userID) position.right = true; if (from?.id === recipientID) position.left = true; const formattedCreatedAt = getFormattedTimeSent(createdAt, t, true); return ( <MessageBubble {...position} timeSent={formattedCreatedAt} onPress={() => onMessagePress({ ...item, userID, recipientID })} onLongPress={() => onMessageLongPress({ ...item, userID, recipientID })} > { from?.id ? content : `${content} \n ${formattedCreatedAt}` } </MessageBubble> ); } useEffect(() => { Keyboard.addListener('keyboardDidShow', keyboardDidShow); Keyboard.addListener('keyboardDidHide', keyboardDidHide); return () => { Keyboard.removeListener('keyboardDidShow', keyboardDidShow); Keyboard.removeListener('keyboardDidHide', keyboardDidHide); }; }, []); useEffect(() => { const { current: prevLength } = previousMessagesLength; const { current: lastKnownScrollPosition } = scrollPosition; const { current: threadContentHeight } = threadTrueHeight; if (prevLength < messages.length) { previousMessagesLength.current = messages.length; if (lastKnownScrollPosition < threadContentHeight - 800) { setShowUnreadMarker(true); } else { scrollToBottom(); } } }, [messages]); useEffect(() => { scrollToBottom(); }, []); return ( <ThemeProvider theme={Theme}> <Box height={1} position="relative" > <StyledFlatList ref={composeRefs(ref, threadRef)} top={0} bottom={conversationThreadBottom} left={0} right={0} backgroundColor={Theme.colors.background} data={messages} stickyHeaderIndices={ (!keyboardOpen && ListHeaderComponent) ? [0] : props.stickyHeaderIndices || [] } scrollEventThrottle={200} keyExtractor={(item) => item.id} ListHeaderComponent={keyboardOpen ? null : (ListHeaderComponent || null)} renderItem={({ item }) => { if (item.renderCustom) { return item.renderCustom({ item, userID, recipientID }); } if (renderItem) { return renderItem({ item }); } return renderMessage(item); }} onScroll={(event) => { threadTrueHeight.current = event.nativeEvent.contentSize.height; scrollPosition.current = event.nativeEvent.contentOffset.y; }} {...props} /> <Box height={finalTextInputContainerHeight} position="absolute" alignItems="flex-end" justifyContent="flex-end" bottom={keyboardHeight + androidExtraInputPaddingBottom} left={0} right={0} > {showUnreadMarker && ( <Touchable padding={2} borderRadius={20} borderWidth={0.5} borderColor="primary" marginBottom={2} flexDirection="row" alignSelf="center" alignItems="center" justifyContent="space-around" backgroundColor="background" onPress={scrollToBottom} > <Text color="primary" fontSize={0} fontWeight="bold" padding={0} marginRight={2}>{unreadMessageIndicatorText}</Text> <Image source={require('../../assets/images/chevron.png')} style={{ tintColor: Theme.colors.primary, width: 10, height: 10, transform: [{ rotate: '90deg' }], }} /> </Touchable> )} <Box flexDirection="row" justifyContent="space-between" alignItems="flex-end" paddingLeft={3} marginBottom={inputBottomOffset} > <Box flexGrow={1}> <TextInput placeholder={inputPlaceholder} returnKeyType="send" value={inputValue} onChangeText={onInputChangeText} onContentSizeChange={(event) => { const heightAfterChange = ( inputHeight + (event?.nativeEvent?.contentSize?.height || 0) ); if (heightAfterChange >= Theme.sizes.textInputHeight_raw * 5) return; setInputHeightState(heightAfterChange); }} fontSize={1} multiline numberOfLines={25} height={inputHeightState} maxHeight={Theme.sizes.textInputHeight_raw * 5} marginRight={3} width={1} iconRight={null} backgroundColor={Theme.colors.convoPink} style={{ backgroundColor: Theme.colors.convoPink, textAlignVertical: 'bottom', }} /> </Box> <Box height={inputHeight} alignItems="center" justifyContent="center"> <MessageSendButton alignSelf="center" onPress={handleSubmit} disabled={!!inputValue === false} busy={inputBusy} /> </Box> </Box> </Box> </Box> </ThemeProvider> ); }); MessageThread.defaultProps = { inputValue: '', inputBusy: false, inputHeight: Theme.sizes.textInputHeight_raw, inputBottomOffset: 0, refreshing: false, messages: [], unreadMessageIndicatorText: 'New messages', onMessagePress: () => {}, onMessageLongPress: () => {}, onRefresh: () => {}, t: (key) => key, }; MessageThread.propTypes = { inputPlaceholder: PropTypes.string.isRequired, inputValue: PropTypes.string, inputBusy: PropTypes.bool, inputHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), inputBottomOffset: PropTypes.number, refreshing: PropTypes.bool, onInputChangeText: PropTypes.func.isRequired, onInputSubmit: PropTypes.func.isRequired, onMessagePress: PropTypes.func, onMessageLongPress: PropTypes.func, onRefresh: PropTypes.func, messages: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, from: PropTypes.shape({ __typename: PropTypes.string, id: PropTypes.string.isRequired, displayName: PropTypes.string.isRequired, }), content: PropTypes.string.isRequired, createdAt: PropTypes.string.isRequired, readAt: PropTypes.string, renderCustom: PropTypes.func, }), ), unreadMessageIndicatorText: PropTypes.string, userID: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, recipientID: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, t: PropTypes.func, }; export default MessageThread;