Created
March 25, 2021 14:43
-
-
Save myckhel/d88af0d6a328f61b8023b48fe25639fe to your computer and use it in GitHub Desktop.
react native gifted chat swipe to reply
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {useRef, useMemo, useState, useCallback} from 'react'; | |
import {Vibration, View, StyleSheet} from 'react-native'; | |
import {Button, Text, Row} from '../../theme'; | |
import {Send as SendIcon} from '../../Icons'; | |
import {useMessage, useConversationEventType} from '../../../redux/msg/hooks'; | |
import Animated, { | |
useAnimatedStyle, | |
useSharedValue, | |
} from 'react-native-reanimated'; | |
import {SwipeRow} from 'react-native-swipe-list-view'; | |
import { | |
Reply, | |
PlayCircle as PlayIcon, | |
DisabledAlt, | |
Sent, | |
Clock, | |
DoubleCheck, | |
} from '../../Icons'; | |
import FastImage from '../../theme/FastImage'; | |
import LinearGradient from 'react-native-linear-gradient'; | |
import {fastMemo} from '../../../func'; | |
import DateTime from '../../DateTime'; | |
import {ImageOrNot} from '../../../func/helpers'; | |
export const Send = ({onPress}) => ( | |
<Button onPress={onPress} iconProp={{fill: '#fff'}} Icon={SendIcon} /> | |
); | |
/* Message Views */ | |
export default fastMemo( | |
({ | |
conversationId, | |
setChatState, | |
id, | |
nextSenderId, | |
showMenu, | |
userId, | |
selecting, | |
}) => { | |
const ref = useRef(); | |
const { | |
message: { | |
system, | |
image, | |
downloaded, | |
videos, | |
created_at, | |
sending, | |
reply_type, | |
message, | |
isSender, | |
reply, | |
conversation_id, | |
user_id, | |
trashed, | |
} = {}, | |
} = useMessage( | |
{conversationId, messageId: id}, | |
({ | |
system, | |
image, | |
downloaded, | |
videos, | |
created_at, | |
sending, | |
reply_type, | |
message, | |
isSender, | |
reply, | |
conversation_id, | |
user_id, | |
id, | |
trashed, | |
} = {}) => | |
system | |
? {message, system} | |
: trashed | |
? {trashed, created_at, isSender} | |
: videos?.length | |
? { | |
videos, | |
conversation_id, | |
id, | |
isSender, | |
sending, | |
created_at, | |
} | |
: image | |
? { | |
image, | |
sending, | |
downloaded, | |
created_at, | |
isSender, | |
conversation_id, | |
id, | |
} | |
: { | |
message, | |
user_id, | |
isSender, | |
conversation_id, | |
id, | |
reply, | |
reply_type, | |
sending, | |
created_at, | |
}, | |
); | |
const onLongPress = useCallback(() => { | |
const pressed = {id, message: message}; | |
if (user_id === userId) { | |
pressed.user = {_id: userId}; | |
} | |
showMenu(ref, pressed); | |
}, [ref, user_id, id]); | |
const onPress = useCallback(() => { | |
if (videos || image) { | |
navigate('MediaView', {media: {videos, image, id}}); | |
} else if (reply_type === 'App\\Models\\Story') { | |
navigate('StoriesGroup', { | |
story: {...reply, user: {id: reply.user_id, name: reply?.user_name}}, | |
single: true, | |
}); | |
} | |
}, [reply_type, videos, image, id]); | |
const onLeftAction = useCallback( | |
({isActivated}) => { | |
if (isActivated) { | |
setChatState({replyId: id}, (s, p) => ({...s, ...p})); | |
Vibration.vibrate(50); | |
} | |
}, | |
[id], | |
); | |
const x = useSharedValue(-26.11 + -30); | |
const transformStyle = useAnimatedStyle(() => { | |
return { | |
transform: [ | |
{ | |
translateX: x.value, | |
}, | |
], | |
}; | |
}); | |
const onSwipeValueChange = useCallback( | |
({value}) => { | |
const valueX = value + (-26.11 + -30); | |
if (valueX < 50) { | |
x.value = valueX; | |
} | |
}, | |
[x], | |
); | |
return ( | |
<SwipeRow | |
onSwipeValueChange={onSwipeValueChange} | |
useNativeDriver | |
onLeftActionStatusChange={onLeftAction} | |
disableLeftSwipe | |
disableRightSwipe={!!(system || trashed || selecting)} | |
leftActivationValue={90} | |
leftActionValue={0} | |
style={styles.swipe} | |
swipeKey={id + ''}> | |
{system || trashed || selecting ? ( | |
<></> | |
) : ( | |
<Animated.View style={[styles.reply, transformStyle]}> | |
<Reply width={wp(26.11)} height={hp(21.76)} fill="#000" /> | |
</Animated.View> | |
)} | |
<Button | |
renderToHardwareTextureAndroid={true} | |
collapsable={false} | |
disabled={system || trashed || selecting} | |
color={ | |
system | |
? '#EBEBEB' | |
: message | |
? isSender | |
? '#0C67F0' | |
: '#fff' | |
: undefined | |
} | |
left={isSender ? 'auto' : undefined} // crashable if edit | |
ref={ref} | |
onPress={onPress} | |
onLongPress={onLongPress} | |
style={[ | |
system ? styles.sysMsg : styles.message, | |
user_id === nextSenderId && styles.nextSender, | |
]}> | |
{trashed ? ( | |
<DeletedMessage {...{created_at, isSender}} /> | |
) : ( | |
<RenderContent | |
{...{ | |
system, | |
image, | |
downloaded, | |
videos, | |
created_at, | |
sending, | |
reply_type, | |
message, | |
user_id, | |
isSender, | |
reply, | |
conversation_id, | |
}} | |
/> | |
)} | |
</Button> | |
</SwipeRow> | |
); | |
}, | |
); | |
const SystemMessage = ({message}) => { | |
return ( | |
<Text color="#5F5F5F" lineHeight={15} center fontSize={13}> | |
{message} | |
</Text> | |
); | |
}; | |
export const MessageStatus = fastMemo( | |
({sending, conversationId, created_at}) => { | |
const eventType = useConversationEventType(conversationId, created_at); | |
return ( | |
<View left={wp(5)}> | |
{eventType ? ( | |
eventType === 'read' ? ( | |
<DoubleCheck fill="transparent" stroke="#4CC5F7" /> | |
) : ( | |
<DoubleCheck fill="transparent" stroke="#ACACAC" /> | |
) | |
) : sending ? ( | |
<Clock fill="grey" /> | |
) : ( | |
<Sent /> | |
)} | |
</View> | |
); | |
}, | |
); | |
export const DeletedText = ({color, style}) => ( | |
<Row style={style}> | |
<DisabledAlt fill="#C9C9C9" top={2} width={wp(15)} /> | |
<Text | |
langId="msg_deleted" | |
left={10} | |
fontSize={20} | |
italic | |
color={'#C9C9C9' || '#C9C9C9'} | |
style={styles.deltext} | |
/> | |
</Row> | |
); | |
const RenderContent = fastMemo( | |
({ | |
system, | |
image, | |
downloaded, | |
videos, | |
created_at, | |
sending, | |
reply_type, | |
message, | |
isSender, | |
reply, | |
conversation_id, | |
user_id, | |
}) => { | |
const type = useMemo( | |
() => | |
system ? 'system' : videos?.length ? 'video' : image ? 'image' : 'text', | |
[!!videos?.length, image, system], | |
); | |
const MView = useMemo(() => { | |
switch (type) { | |
case 'system': | |
return SystemMessage; | |
case 'video': | |
return MessageVideos; | |
case 'image': | |
return MessageImage; | |
default: | |
return MessageText; | |
} | |
}, [type]); | |
const MViewProps = useMemo(() => { | |
switch (type) { | |
case 'system': | |
return {message}; | |
case 'video': | |
return { | |
videos, | |
}; | |
case 'image': | |
return { | |
image, | |
sending, | |
downloaded, | |
}; | |
default: | |
return { | |
message, | |
isSender, | |
}; | |
} | |
}, [ | |
type, | |
videos, | |
image, | |
system, | |
conversation_id, | |
isSender, | |
sending, | |
created_at, | |
downloaded, | |
reply, | |
reply_type, | |
user_id, | |
message, | |
]); | |
const MFooterProps = useMemo(() => { | |
switch (type) { | |
case 'video': | |
case 'image': | |
case 'text': | |
return { | |
isSender, | |
sending, | |
created_at, | |
conversationId: conversation_id, | |
}; | |
default: | |
return {created_at}; | |
} | |
}, [type, isSender, sending, created_at, conversation_id]); | |
const _Footer = useCallback( | |
({style}) => <Footer style={style} {...MFooterProps} />, | |
[MFooterProps], | |
); | |
const replyProps = useMemo(() => { | |
if (reply) { | |
const {message, user_name, text, image, videos} = reply; | |
return { | |
style: {marginHorizontal: wp(5)}, | |
message, | |
user_name: user_id !== reply.user_id && user_name, | |
text, | |
image, | |
videos, | |
type: reply_type?.split('\\').pop(), | |
user: user_id === reply.user_id, | |
}; | |
} else { | |
return {style: {marginHorizontal: wp(5)}}; | |
} | |
}, [reply, reply_type, user_id]); | |
return ( | |
<> | |
{reply && <ReplyRapper {...replyProps} />} | |
<MView {...MViewProps} Footer={_Footer} /> | |
</> | |
); | |
}, | |
); | |
export const ReplyRapper = fastMemo( | |
({ | |
message, | |
text, | |
user_name = 'User', | |
image, | |
videos, | |
type, | |
style, | |
user, | |
replyUserStyle, | |
imageStyle, | |
numberOfLines, | |
}) => { | |
const replyText = | |
message || text || (image ? 'Photo' : videos ? 'Video' : ''); | |
return ( | |
<View style={[styles.replyWrapper, style]}> | |
{image ? ( | |
<FastImage | |
source={ImageOrNot(image, 'thumb')} | |
style={[styles.replyImage, imageStyle]} | |
/> | |
) : null} | |
<View style={[styles.replyText, image && styles.replyImgText]}> | |
<Text | |
langId={ | |
type === 'Story' | |
? user | |
? 'self_story' | |
: 'user_story' | |
: user | |
? 'txt_you' | |
: '_value' | |
} | |
values={{_name: user_name, _value: user_name}} | |
fontSize={20} | |
bold | |
color={'#0C67F0'} | |
style={[styles.replyUser, replyUserStyle]} | |
/> | |
<Text | |
numberOfLines={numberOfLines || 2} | |
fontSize={18} | |
color="#000" | |
style={styles.replyText}> | |
{replyText} | |
</Text> | |
</View> | |
</View> | |
); | |
}, | |
); | |
export const MessageText = fastMemo(({message, isSender, Footer}) => { | |
return ( | |
<Row | |
flexWrap={message?.length > 20 ? 'wrap' : undefined} //Wrapsettings | |
style={styles.msg}> | |
<Text bottom={6} color={isSender ? '#fff' : '#000000'} fontSize={19}> | |
{message} | |
</Text> | |
<Footer style={styles.textFoot} /> | |
</Row> | |
); | |
}); | |
export const MessageVideos = fastMemo(({videos, Footer}) => { | |
const video = useMemo(() => videos[0], [videos]); | |
return ( | |
<FastImage | |
retry | |
id={video?.id} | |
source={{uri: video?.thumb || video?.uri}} | |
style={styles.image}> | |
<LinearGradient style={styles.linear} colors={linears} /> | |
<PlayIcon width={40} height={40} style={styles.playIcon} /> | |
<Footer style={styles.imageRight} /> | |
</FastImage> | |
); | |
}); | |
const Footer = ({style, isSender, sending, created_at, conversationId}) => { | |
return ( | |
<Row | |
align="center" | |
left="auto" | |
style={[styles.right, style, !isSender && styles.rightSender]}> | |
<DateTime | |
style={styles.time} | |
data={created_at} | |
render={({datetime}) => ( | |
<Text fontSize={15} color={isSender ? '#C3C3C3' : '#C3C3C3'}> | |
{datetime} | |
</Text> | |
)} | |
/> | |
{isSender && ( | |
<MessageStatus | |
{...{ | |
sending, | |
conversationId, | |
created_at, | |
}} | |
/> | |
)} | |
</Row> | |
); | |
}; | |
const linears = [ | |
'rgba(0, 0, 0, 0)', | |
'rgba(0, 0, 0, 0.2)', | |
'rgba(0, 0, 0, 0.3)', | |
'rgba(0, 0, 0, 0.8)', | |
]; | |
export const MessageImage = fastMemo(({image, sending, downloaded, Footer}) => { | |
// const dispatch = useDispatch(); | |
const [loadFull, setLoadFull] = useState(downloaded); | |
const thumb = sending && image ? null : image?.thumb || ''; | |
const url = sending && image ? image : image?.url || ''; | |
const flagImageLoaded = () => { | |
// dispatch(updateMsg({msg: {...msg, id, downloaded: true}})); | |
}; | |
return ( | |
<FastImage | |
onLoadEnd={flagImageLoaded} | |
loadFull={loadFull} | |
onDownload={setLoadFull} | |
style={styles.image} | |
thumbSrc={{uri: thumb}} | |
source={{uri: url}}> | |
<LinearGradient style={styles.linear} colors={linears} /> | |
<Footer style={styles.imageRight} /> | |
</FastImage> | |
); | |
}); | |
const DeletedMessage = fastMemo(({style, color, created_at, isSender}) => ( | |
<Row style={[styles.msg, style]}> | |
<DeletedText color={color} /> | |
<Footer | |
style={styles.textFoot} | |
{...{ | |
created_at, | |
}} | |
/> | |
</Row> | |
)); | |
const SENDER_MAX_WIDTH = '85%'; | |
export const IMG_HEIGHT = hp(254); | |
const styles = StyleSheet.create({ | |
deltext: {fontStyle: 'italic'}, | |
msg: {flex: 1, padding: 10, paddingVertical: hp(8)}, | |
replyUser: {marginBottom: 10}, | |
message: { | |
flex: 1, | |
alignSelf: 'flex-start', | |
marginVertical: 6, | |
backgroundColor: '#F0F0F0', | |
maxWidth: '80%', | |
borderRadius: 5, | |
marginHorizontal: 10, | |
}, | |
linear: { | |
position: 'absolute', | |
left: 0, | |
right: 0, | |
bottom: 0, | |
height: hp(30), | |
}, | |
swipe: {flex: 1}, | |
replyWrapper: { | |
flex: 1, | |
flexDirection: 'row', | |
backgroundColor: '#C8D9F4', | |
borderRadius: 5, | |
marginBottom: 12, | |
marginTop: 5, | |
overflow: 'hidden', | |
}, | |
reply: { | |
marginTop: 'auto', | |
marginBottom: 'auto', | |
marginLeft: wp(10), | |
backgroundColor: '#C9C9C9', | |
height: hp(43), | |
width: wp(43), | |
alignItems: 'center', | |
justifyContent: 'center', | |
borderRadius: 30, | |
}, | |
replyImgText: {width: '90%'}, | |
replyImage: {width: hp(53), height: '100%'}, | |
replyText: { | |
padding: 5, | |
}, | |
rightSender: {marginLeft: 0}, | |
right: { | |
alignSelf: 'flex-end', | |
}, | |
imageRight: { | |
flexDirection: 'row', | |
alignItems: 'center', | |
marginLeft: 'auto', | |
marginTop: 'auto', | |
marginRight: 20, | |
marginBottom: 5, | |
}, | |
sysMsg: { | |
alignItems: 'center', | |
justifyContent: 'center', | |
marginVertical: 20, | |
maxWidth: SENDER_MAX_WIDTH, | |
alignSelf: 'center', | |
padding: wp(10), | |
paddingHorizontal: wp(10), | |
borderRadius: 5, | |
}, | |
image: { | |
flex: 1, | |
width: wp(267), | |
height: IMG_HEIGHT, | |
borderRadius: 5, | |
backgroundColor: '#F0F0F0', | |
justifyContent: 'center', | |
alignItems: 'center', | |
}, | |
textFoot: { | |
marginLeft: 'auto', | |
}, | |
time: { | |
flexDirection: 'row', | |
bottom: 2, | |
paddingLeft: 25, | |
}, | |
playIcon: { | |
position: 'absolute', | |
left: 'auto', | |
top: 'auto', | |
}, | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { | |
useCallback, | |
useMemo, | |
useRef, | |
useEffect, | |
useLayoutEffect, | |
} from 'react'; | |
// redux | |
import {useDispatch} from 'react-redux'; | |
import { | |
useReduxState, | |
getState as getReduxState, | |
useMemoSelector, | |
useSetState, | |
} from 'use-redux-state-hook'; | |
// actions | |
import {addMsg, updateMsgs} from '../../../redux/actions'; | |
import {useNavigation, useRoute} from '@react-navigation/native'; | |
import queue from 'react-native-job-queue'; | |
import {TextInput, StyleSheet, Clipboard, View, Platform} from 'react-native'; | |
// icon | |
import { | |
GalleryIcon, | |
Camera, | |
Pointer as SendIcon, | |
Reply, | |
Copy, | |
X as CancelIcon, | |
DisabledAlt, | |
DropArrow as ScrolldownIcon, | |
Check, | |
} from '../../Icons'; | |
// async | |
import {multiDeleteMessage} from '../../../func/async/message'; | |
// selectors | |
import {selectMessage} from '../../../redux/selectors'; | |
import {useConversation} from '../../../redux/msg/hooks'; | |
import { | |
useSelected, | |
useMergeState, | |
usePicker, | |
useCameraEvent, | |
} from '../../../redux/app/hooks'; | |
// func | |
import {Toastify, toVideo, mediaType} from '../../../func/helpers'; | |
import {fastMemo} from '../../../func'; | |
// components | |
import {Text, Button, Row, Menu} from '../../theme'; | |
import Popover from 'react-native-popover-view'; | |
import Day from '../../Day'; | |
import Message, {ReplyRapper} from './Message'; | |
import {GiftedChat} from 'react-native-gifted-chat'; | |
// package | |
import {keys, values} from 'lodash'; | |
export default fastMemo(({style, userId, conversationId, otherUserId}) => { | |
const dispatch = useDispatch(); | |
const {params} = useRoute(); | |
const lastConversationId = useRef(conversationId); | |
const previousState = useRef(null); | |
useLayoutEffect(() => { | |
if (lastConversationId.current !== conversationId) { | |
// dispatch state move | |
previousState.current = getReduxState( | |
store, | |
`conversations.${lastConversationId.current}`, | |
); | |
lastConversationId.current = conversationId; | |
} | |
}, [conversationId]); | |
const {messages, nextable, loading, next} = useConversation({ | |
otherUserId, | |
conversationId, | |
}); | |
const { | |
setState: setChatState, | |
cleanup: cleanupConversationState, | |
} = useReduxState({ | |
name: `conversations.${conversationId}`, | |
state: { | |
text: '', | |
image: null, | |
replyId: params?.replyId, | |
}, | |
reducer: (s, p) => { | |
if (previousState.current) { | |
const prevState = previousState.current; | |
previousState.current = null; | |
return prevState; | |
} else { | |
return {...p, ...s, replyId: p.replyId}; | |
} | |
}, | |
}); | |
const [state, setState] = useMergeState({ | |
isVisible: false, | |
fromView: null, | |
pressed: {}, | |
showDeleteMenu: false, | |
}); | |
const {isVisible, fromView, pressed, showDeleteMenu} = state; | |
const {select, cleanup, getState: getSelectState, reset} = useSelected({ | |
name: 'chat-screen-selects', | |
unmount: true, | |
}); | |
useEffect(() => { | |
return () => { | |
if (!conversationId) { | |
cleanupConversationState(); | |
} | |
cleanup(); | |
}; | |
}, [conversationId]); | |
const onRequestClose = useCallback(() => setState({isVisible: false}), []); | |
const showMenu = useCallback( | |
(ref, pressed) => | |
setState(({isVisible}) => ({ | |
pressed, | |
isVisible: !isVisible, | |
fromView: ref, | |
})), | |
[], | |
); | |
const toggleDeleteMenu = useCallback( | |
bool => setState({showDeleteMenu: bool}), | |
[], | |
); | |
const onCopy = useCallback(() => { | |
if (pressed?.message) { | |
Clipboard.setString(`${pressed.message}`); | |
Toastify('Message Copied to Clipboard'); | |
} | |
onRequestClose(); | |
}, [onRequestClose, pressed]); | |
const onReply = useCallback(() => { | |
setChatState({replyId: pressed.id}); | |
onRequestClose(); | |
}, [onRequestClose]); | |
const onDelete = useCallback(() => { | |
onRequestClose(); | |
select({key: pressed.id, value: !!pressed.user}); | |
}, [select, pressed]); | |
const onDeleteMultiMessage = useCallback( | |
async everyone => { | |
try { | |
setState({showDeleteMenu: false}); | |
const events = await multiDeleteMessage({ | |
everyone, | |
messages: getSelectState(state => keys(state || {})), | |
}); | |
reset(); | |
const msgs = events?.map(({made_id, type, created_at}) => ({ | |
conversation_id: conversationId, | |
id: made_id, | |
trashed: { | |
made_id, | |
type, | |
created_at, | |
}, | |
})); | |
dispatch(updateMsgs({msgs})); | |
} catch (e) { | |
reset(); | |
console.log({e}); | |
} | |
}, | |
[conversationId], | |
); | |
const renderMessage = useCallback( | |
d => { | |
const {currentMessage, nextMessage, previousMessage} = d; | |
const {_id, system, trashed, user_id} = currentMessage; | |
const {_id: id} = d.user; | |
return ( | |
<> | |
<SelectableMessage | |
conversationId={conversationId} | |
setChatState={setChatState} | |
showMenu={showMenu} | |
_id={_id} | |
userId={id} | |
msgUserId={user_id} | |
trashed={!!trashed} | |
system={system} | |
nextSenderId={nextMessage?.user?._id} | |
/> | |
{!system && ( | |
<Day | |
containerStyle={styles.dayTime} | |
textStyle={styles.dayTimeText} | |
previousMessageAt={previousMessage?.created_at} | |
currentMessageAt={currentMessage?.created_at} | |
/> | |
)} | |
</> | |
); | |
}, | |
[conversationId], | |
); | |
const renderComposer = useCallback( | |
() => ( | |
<ChatBottom | |
conversationId={conversationId} | |
otherUserId={otherUserId} | |
userId={userId} | |
toggleDeleteMenu={toggleDeleteMenu} | |
/> | |
), | |
[otherUserId, conversationId, userId], | |
); | |
const scrollToBottomComponent = useCallback( | |
() => <ScrolldownIcon width={wp(18)} />, | |
[], | |
); | |
const renderChatFooter = useCallback( | |
() => <ChatFooter conversationId={conversationId} />, | |
[conversationId], | |
); | |
return ( | |
<View accessible accessibilityLabel="main" style={[styles.messages, style]}> | |
<GiftedChat | |
scrollToBottomStyle={styles.scrollToBottomStyle} | |
scrollToBottomComponent={scrollToBottomComponent} | |
renderChatFooter={renderChatFooter} | |
scrollToBottom | |
bottomOffset={-20} | |
infiniteScroll | |
loadEarlier={nextable} | |
onLoadEarlier={next} | |
isLoadingEarlier={loading} | |
// renderLoading | |
// renderChatEmpty | |
renderMessage={renderMessage} | |
renderComposer={renderComposer} | |
minComposerHeight={hp(40)} | |
maxComposerHeight={hp(100)} | |
messages={messages} | |
user={{_id: userId}} | |
// isTyping | |
/> | |
<MessageMenu | |
onDelete={onDelete} | |
onCopy={onCopy} | |
onReply={onReply} | |
canCopy={!!pressed?.message} | |
isVisible={isVisible} | |
fromView={fromView} | |
onRequestClose={onRequestClose} | |
/> | |
<DeleteMenu | |
getSelectState={getSelectState} | |
deleteMessage={onDeleteMultiMessage} | |
toggleMenu={toggleDeleteMenu} | |
isVisible={showDeleteMenu} | |
/> | |
</View> | |
); | |
}); | |
const SelectableMessage = fastMemo( | |
({ | |
conversationId, | |
setChatState, | |
showMenu, | |
_id, | |
userId, | |
nextSenderId, | |
trashed, | |
system, | |
msgUserId, | |
}) => { | |
const {select, useIsSelected, useIsSelecting} = useSelected({ | |
name: 'chat-screen-selects', | |
id: _id, | |
cleanup: true, | |
unmount: true, | |
}); | |
const selected = useIsSelected(); | |
const selecting = useIsSelecting(); | |
return ( | |
<Button | |
disabled={!selecting} | |
flexDirection="row" | |
onPress={() => select({value: userId === msgUserId})}> | |
{!trashed && !system && selecting && <Select selected={selected} />} | |
<Message | |
conversationId={conversationId} | |
setChatState={setChatState} | |
showMenu={showMenu} | |
id={_id} | |
selecting={selecting} | |
userId={userId} | |
nextSenderId={nextSenderId} | |
/> | |
</Button> | |
); | |
}, | |
); | |
const Select = ({selected}) => ( | |
<View style={[styles.select, selected && styles.selected]}> | |
{selected && <Check fill="#fff" />} | |
</View> | |
); | |
export const useMediaType = ({latest_media, image, videos}) => { | |
return useMemo(() => { | |
return ( | |
latest_media?.name || (image && 'image') || (videos?.length && 'videos') | |
); | |
}, [latest_media, image, videos]); | |
}; | |
export const MessageMenu = fastMemo( | |
({ | |
isVisible, | |
fromView, | |
onRequestClose, | |
onReply, | |
onDelete, | |
onCopy, | |
canCopy, | |
}) => { | |
return ( | |
<Popover | |
useNativeDriver | |
isVisible={!!isVisible} | |
popoverStyle={styles.messageMenuPop} | |
arrowStyle={{width: wp(40)}} | |
from={fromView} | |
onRequestClose={onRequestClose}> | |
<MessageMenuItem | |
opacity="0.35" | |
line | |
onPress={onReply} | |
text="Reply" | |
Icon={Reply} | |
/> | |
{canCopy && ( | |
<MessageMenuItem line onPress={onCopy} text="Copy" Icon={Copy} /> | |
)} | |
<MessageMenuItem | |
opacity="0.35" | |
onPress={onDelete} | |
text="Delete" | |
Icon={DisabledAlt} | |
/> | |
</Popover> | |
); | |
}, | |
); | |
const ChatFooter = ({conversationId}) => { | |
const replyId = useMemoSelector( | |
`conversations.${conversationId}`, | |
s => s?.replyId, | |
); | |
return <View style={[styles.footer, replyId && styles.replyFooter]} />; | |
}; | |
const ChatBottom = fastMemo( | |
({toggleDeleteMenu, conversationId, otherUserId, userId}) => { | |
const {useIsSelecting, reset, useMemoSelector, selector} = useSelected({ | |
name: 'chat-screen-selects', | |
unmount: true, | |
}); | |
const selecting = useIsSelecting(); | |
const _count = useMemoSelector( | |
selector, | |
state => Object.keys(state).length, | |
); | |
if (selecting) { | |
return ( | |
<Row flex={1} p={wp(20)} justify="space-between"> | |
<Button | |
textProp={{ | |
langId: 'txt_delete', | |
color: '#FF1010', | |
fontSize: 21, | |
bold: true, | |
}} | |
onPress={() => toggleDeleteMenu(true)} | |
/> | |
<Text fontSize={21} langId="count_selecetd" values={{_count}} /> | |
<Button | |
textProp={{langId: 'reg.cancel', color: '#106FFF', fontSize: 21}} | |
onPress={() => reset()} | |
/> | |
</Row> | |
); | |
} else { | |
return ( | |
<MessageInput | |
conversationId={conversationId} | |
otherUserId={otherUserId} | |
userId={userId} | |
/> | |
); | |
} | |
}, | |
); | |
export const MessageInput = fastMemo( | |
({userId, conversationId, otherUserId}) => { | |
const ref = useRef(); | |
const dispatch = useDispatch(); | |
const setState = useSetState(`conversations.${conversationId}`); | |
const state = useMemoSelector(`conversations.${conversationId}`) || {}; | |
const {text, replyId} = state; | |
const setText = text => setState({text}); | |
const onSend = useCallback( | |
msg => { | |
setState({ | |
replyId: null, | |
text: undefined, | |
image: null, | |
}); | |
dispatch(addMsg({msg, otherUserId, conversationId})); | |
queue.addJob('message.upload', {message: msg}); | |
}, | |
[otherUserId, conversationId], | |
); | |
const sendMessage = ({image, message, videos, ...params} = {}) => { | |
const {text} = state; | |
if (empty([videos, message, text, image], 'and')) { | |
return; | |
} | |
ref.current?.clear(); | |
const rand = `pending-${Math.random()}`; | |
onSend({ | |
...params, | |
videos, | |
image, | |
reply_id: replyId && replyId, | |
reply: replyId && { | |
...store?.getState().msg.msgs[replyId], | |
}, | |
reply_type: replyId ? 'App\\Models\\Message' : undefined, | |
id: rand, | |
_id: rand, | |
message: message || text, | |
isSender: true, | |
created_at: new Date(), | |
conversation_id: conversationId || `${userId}-${otherUserId}`, | |
notSent: true, | |
sending: true, | |
user_id: userId, | |
other_user_id: otherUserId, | |
user: {_id: userId}, | |
}); | |
}; | |
const onCancel = useCallback(() => setState({replyId: null}), [setState]); | |
return ( | |
<View style={styles.chatInput}> | |
{replyId && ( | |
<InputReply | |
conversationId={conversationId} | |
reply={replyId} | |
userId={userId} | |
onCancel={onCancel} | |
style={styles.InputReply} | |
rapperStyle={styles.rapperStyle} | |
/> | |
)} | |
<Row style={styles.MessageInput}> | |
<TextInput | |
ref={ref} | |
onChangeText={setText} | |
value={text} | |
multiline | |
placeholderTextColor="#9C9696" | |
style={styles.input} | |
placeholder="Type a message..." | |
/> | |
{text?.length > 0 ? ( | |
<Button | |
color="#106FFF" | |
onPress={() => sendMessage()} | |
style={styles.sendBtn}> | |
<SendIcon width={22.38} height={22.38} fill="#fff" /> | |
</Button> | |
) : ( | |
<> | |
<CameraBtn sendMessage={sendMessage} /> | |
<GalleryBtn sendMessage={sendMessage} /> | |
</> | |
)} | |
</Row> | |
</View> | |
); | |
}, | |
); | |
const CameraBtn = ({sendMessage}) => { | |
const {pop} = useNavigation(); | |
const onBackEventName = useCameraEvent(({media}) => { | |
pop(2); | |
sendMessage(splitMedia([media])); | |
}); | |
const openCamera = async () => navigate('Camera', {onBackEventName}); | |
return ( | |
<Button | |
iconProp={{stroke: '#A8A8A8', fill: '#fff'}} | |
Icon={Camera} | |
onPress={openCamera} | |
width={wp(41)} | |
height={wp(41)} | |
curve={20} | |
color="#fff" | |
align="center" | |
justify="center" | |
/> | |
); | |
}; | |
const GalleryBtn = ({sendMessage}) => { | |
const {pick} = usePicker({ | |
mediaType: 'photo', | |
}); | |
const openMedia = useCallback(async () => { | |
try { | |
const medias = await pick(); | |
sendMessage(splitMedia(medias)); | |
} catch (e) {} | |
}, []); | |
return ( | |
<Button | |
iconProp={{stroke: '#A8A8A8', fill: '#fff'}} | |
Icon={GalleryIcon} | |
width={wp(41)} | |
height={wp(41)} | |
curve={20} | |
color="#fff" | |
align="center" | |
justify="center" | |
onPress={() => openMedia()} | |
/> | |
); | |
}; | |
const splitMedia = medias => { | |
const mediaCont = { | |
videos: [], | |
}; | |
medias.map(media => { | |
switch (mediaType(media)) { | |
case 'video': | |
mediaCont.videos.push(toVideo(media)); | |
break; | |
case 'image': | |
mediaCont.image = `data:${media.mime};base64,${media.data}`; | |
break; | |
} | |
}); | |
return mediaCont; | |
}; | |
const InputReply = ({ | |
style, | |
userId, | |
rapperStyle, | |
onCancel, | |
conversationId, | |
reply, | |
}) => { | |
const {message, sender, user_id, text, image, videos, type} = useMemoSelector( | |
state => selectMessage(state, conversationId, reply), | |
({ | |
message, | |
text, | |
image, | |
videos, | |
type, | |
user_id, | |
sender: {name} = {}, | |
} = {}) => ({ | |
message, | |
text, | |
image, | |
videos, | |
type, | |
user_id, | |
sender: {name}, | |
}), | |
); | |
return ( | |
<View style={[style]}> | |
<ReplyRapper | |
numberOfLines={1} | |
type={message ? 'Message' : 'Story'} | |
user={userId === user_id} | |
user_name={userId !== user_id && sender?.name} | |
imageStyle={styles.replyImg} | |
replyUserStyle={styles.replyUser} | |
style={rapperStyle} | |
{...{message, text, image, videos, type}} | |
/> | |
<Button | |
p={wp(5)} | |
Icon={CancelIcon} | |
onPress={onCancel} | |
style={styles.CancelIcon} | |
/> | |
</View> | |
); | |
}; | |
/* Menus */ | |
const getDeleteMenus = (func, mine = true) => { | |
let menu = [ | |
{text: 'Delete for everyone', color: '#FF0000', onPress: () => func(true)}, | |
{text: 'Delete for me', color: '#FF0000', onPress: () => func()}, | |
]; | |
if (!mine) { | |
menu = [{text: 'Delete for me', color: '#FF0000', onPress: () => func()}]; | |
} | |
return menu; | |
}; | |
export const DeleteMenu = ({ | |
isVisible, | |
getSelectState, | |
toggleMenu, | |
deleteMessage, | |
}) => { | |
const menus = useMemo( | |
() => | |
// select if messages doesnt contain other user message | |
getDeleteMenus( | |
deleteMessage, | |
getSelectState(s => values(s)?.find(sender => !sender)), | |
), | |
[isVisible], | |
); | |
return ( | |
<Menu | |
isVisible={isVisible} | |
cancelColor="#51A4F7" | |
menus={menus} | |
toggleMenu={toggleMenu} | |
/> | |
); | |
}; | |
const MessageMenuItem = ({onPress, opacity, Icon, text, line}) => ( | |
<Button | |
p={wp(15)} | |
flexDirection="row" | |
onPress={onPress} | |
align="center" | |
style={[line && styles.line]}> | |
<Icon width={wp(25)} height={hp(25)} fill="#000" opacity={opacity} /> | |
<Text left={5} fontSize={17}> | |
{text} | |
</Text> | |
</Button> | |
); | |
const styles = StyleSheet.create({ | |
selected: { | |
backgroundColor: '#0C67F0', | |
borderWidth: 0, | |
}, | |
select: { | |
width: wp(25), | |
height: wp(25), | |
borderRadius: 25, | |
marginLeft: wp(5), | |
borderWidth: 1, | |
borderColor: '#707070', | |
alignItems: 'center', | |
justifyContent: 'center', | |
}, | |
replyUser: {marginBottom: 10}, | |
replyImg: {marginRight: 10}, | |
messages: { | |
flex: 1, | |
}, | |
messageMenuPop: {borderRadius: 10, minWidth: wp(138)}, | |
chatInput: {flex: 1}, | |
InputReply: { | |
flex: 1, | |
flexDirection: 'row', | |
borderLeftWidth: 10, | |
borderLeftColor: colors.primary, | |
backgroundColor: '#f0f0f0', | |
minHeight: hp(64), | |
// fontSize: 14, | |
}, | |
rapperStyle: { | |
backgroundColor: '#f0f0f0', | |
}, | |
CancelIcon: { | |
marginLeft: 'auto', | |
margin: 10, | |
}, | |
MessageInput: { | |
flex: 1, | |
alignItems: 'center', | |
backgroundColor: '#F7F7F7', | |
padding: 10, | |
}, | |
sendBtn: { | |
width: 38, | |
height: 38, | |
borderRadius: 20, | |
justifyContent: 'center', | |
alignItems: 'center', | |
}, | |
line: { | |
borderBottomWidth: 1, | |
borderBottomColor: 'rgba(112, 112, 112, 0.16)', | |
}, | |
input: { | |
flex: 1, | |
fontSize: 15, | |
color: '#382C2C', | |
paddingVertical: hp(10), | |
paddingHorizontal: wp(10), | |
// borderWidth: 0.1, | |
// borderColor: '#707070', | |
borderRadius: wp(29), | |
backgroundColor: '#fff', | |
minHeight: Platform.select({ios: hp(40)}), | |
height: 'auto', | |
maxHeight: hp(100), | |
}, | |
dayTimeText: { | |
color: '#000', | |
backgroundColor: '#E6F3F6', | |
textAlign: 'center', | |
fontSize: 12, | |
padding: 5, | |
fontFamily: 'HelveticaNeue', | |
borderRadius: 5, | |
}, | |
dayTime: {}, | |
footer: {marginTop: hp(20)}, | |
replyFooter: {marginTop: Platform.select({ios: hp(120), android: hp(130)})}, | |
scrollToBottomStyle: {opacity: 1, backgroundColor: '#F0F0F0'}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Oh i can't really.
The missing files are just ones you can create urself and import