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
JavaScript
/* 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;