-
-
Save myckhel/d88af0d6a328f61b8023b48fe25639fe to your computer and use it in GitHub Desktop.
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', | |
}, | |
}); |
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'}, | |
}); |
I hope you understand what i was doing.
shared Messages.js file because it contains the input which displays the reply.
hey, why do you have two message.js
? And can show the implementation cause other files are missing in the above message.js
, also I saw your snack
example but the right side swipe is not working
hey, why do you have two message.js ?
They are different
one is Messages.js
and other is Message.js
other files are missing in the above message.js
Missing files are not necessarily needed.
snack example but the right side swipe is not working
right swipe should be working.
swiping right will reply to the message swiped on.
it will be better to go for the snack example.
you can make right swipe to work only if you enable it.
But you didn't make any right swipe action, I think I have to make it myself
sorry i meant left action.
i will update my comment
sorry i meant left action.
i will update my comment
If you can, can you explain how it works and why you have to use redux?
hi bro can u provide more class? Thanks so much
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';
import {Button, Text, Row} from '../../theme';
import {Send as SendIcon} from '../../Icons';
import {useMessage, useConversationEventType} from '../../../redux/msg/hooks';
Oh i can't really.
The missing files are just ones you can create urself and import
I hope you understand what i was doing.
shared Messages.js file because it contains the input which displays the reply.