-
Notifications
You must be signed in to change notification settings - Fork 324
Cookbook (1.x.x)
If you are using [email protected]
, then please refer to Cookbook (2.x.x)
-
What is KeyboardCompatibleView and how to customize collapsing/expanding animation
-
How to customize underlying
FlatList
inMessageList
orChannelList
? -
MessageInput customizations
MessageList
component accepts Message
prop, where you can mention or provide custom message (UI) component.
You can use built-in component as it is, but every product requires its own functionality/behaviour and styles.
For this you can either build your own component or you can also use in-built components with some modifications.
Here I am going to build some custom components, which use in-built components underneath with some modifications to its props. All the props accepted by MessageSimple component are mentioned here - https://getstream.github.io/stream-chat-react-native/#messagesimple
Then all you need to do is to pass this component to MessageList component:
e.g.,
<Chat client={chatClient}>
<Channel>
<MessageList Message={MessageSimpleModified} />
<MessageInput />
</Channel>
</Chat>
MessageSimple
accepts a prop function - handleDelete
. Default value (function) is provided by HOC Message component - https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/Message.js#L150
So in this example we will override handleDelete
prop:
import { Alert } from 'react-native';
import { MessageSimple } from 'stream-chat-react-native';
const MessageSimpleModified = (props) => {
const onDelete = () => {
// Custom behaviour
// If you face the issue of Alert disappearing instantly,
// then refer to this answer:
// https://stackoverflow.com/a/40041564/1460210
Alert.alert(
'Deleting message',
'Are you sure you want to delete the message?',
[
{
text: 'Cancel',
onPress: () => console.log(`Message won't be deleted`),
style: 'cancel',
},
{
text: 'OK',
onPress: () => {
// If user says ok, then go ahead with
// deleting the message.
props.handleDelete();
},
},
],
{ cancelable: false },
);
// Continue with original handler.
};
return <MessageSimple {...props} handleDelete={onDelete} />;
};
We use react-native-simple-markdown
library internally in MessageSimple
component, to render markdown
content of the text. Styling text in MessageSimple component needs a little different approach than styling
rest of the Stream chat components.
As you have already read in tutorial, to style any other component, you simply pass the theme object to Chat component, which forwards and applies styles to all the its children.
e.g.,
const theme = {
avatar: {
image: {
size: 32,
},
},
colors: {
primary: 'green',
},
// Following styles can also be provided as string directly to spinner:
// spinner: `
// width: 15px;
// height: 15px;
// `
spinner: {
css: `
width: 15px;
height: 15px;
`,
},
'messageInput.sendButton': 'padding: 20px',
};
<Chat client={chatClient} style={theme}>
...
</Chat>
To customize the styles of text, all you need to do is to add markdown styles to the key message.content.markdown
of this theme object. The provided markdown styles will be forwarded to internal Markdown component
e.g.
const theme = {
'message.content.markdown': {
// list of all available options are here: https://github.com/CharlesMangwa/react-native-simple-markdown/tree/next#styles-1
text: {
color: 'pink',
fontFamily: 'AppleSDGothicNeo-Bold'
},
url: {
color: 'red'
}
}
}
<Chat client={chatClient} style={theme}>
...
</Chat>
NOTE: Please read Message bubble with custom text styles/font before proceeding.
Global style will apply to both received and sent message. So in this case, we will provide styles to MessageSimple component separately, depending on whether the message belongs to current user or not.
Here I am aiming for following styles:
-
If message belongs to me
- White background
- Black colored text
-
If message doesn't belong to me
- Blue background
- white colored text
const MessageSimpleStyled = (props) => {
const { isMyMessage, message } = props;
const sentMessageStyles = {
'message.content.markdown': {
text: {
color: 'black',
},
},
'message.content.textContainer':
'background-color: white; border-color: black; border-width: 1',
};
const receivedMessageStyles = {
'message.content.markdown': {
text: {
color: 'white',
},
},
'message.content.textContainer': 'background-color: #9999FF;',
};
if (isMyMessage(message)) {
return <MessageSimple {...props} style={sentMessageStyles} />;
} else {
return <MessageSimple {...props} style={receivedMessageStyles} />;
}
};
MessageSimple
accepts a prop - supportedReactions
. You can pass your emoji data to this prop to set your own reactions.
In this example I will support only two reactions - Monkey face (🐵) and Lion (🦁)
const MessageSimpleWithCustomReactions = (props) => (
<MessageSimple
{...props}
supportedReactions={[
{
id: 'monkey',
icon: '🐵',
},
{
id: 'lion',
icon: '🦁',
},
]}
/>
);
This case has two aspects:
- Handle double tap and send
love
reaction
There is no built-in way of handling double-taps in react-native. So we will implement it on our own (thanks to this blog - https://medium.com/handlebar-labs/instagram-style-double-tap-with-react-native-49e757f68de)
-
Remove
Add Reaction
option from actionsheet, which is shown when message is long pressed.MessageSimple
accepts a array prop -messageActions
. You can use this prop to removeAdd Reaction
option from actionsheet. -
Limit the reactions to only
love
- Message with custom reactions
import { MessageSimple } from 'stream-chat-react-native';
const MessageSimpleIgReaction = (props) => {
let lastTap = null;
const handleDoubleTap = () => {
const now = Date.now();
if (lastTap && now - lastTap < 300) {
props.handleReaction('love');
} else {
lastTap = now;
}
};
return (
<MessageSimple
{...props}
onPress={handleDoubleTap}
supportedReactions={[
{
id: 'love',
icon: '❤️️',
},
]}
messageActions={['edit', 'delete', 'reply']} // not including `reactions` here.
/>
);
};
By default we show reactions on message on top of message. But in some designs, you may want to show it at bottom of message.
First you want to disable/hide the original reaction selector. MessageSimple
component accepts a custom
UI component as prop - ReactionList
. If you set this prop to null, then original reaction list and thus reaction selector
will be hidden/disabled
const MessageWithoutReactionPicker = props => {
return (
<MessageSimple
{...props}
ReactionList={null}
/>
);
};
Next, you want to introduce your own reaction selector or reaction picker. For this purpose, you can use
ReactionPickerWrapper
HOC (higher order component). ReactionPickerWrapper
component simply wraps its children
wtih TouchableOpacity
, which when pressed/touched - opens the reaction picker.
In following example, I am going to build my own reaction list component. And and add the wrapper ReactionPickerWrapper
around it, so that when user touches/presses on reaction list, it opens reaction picker.
import { renderReactions, MessageSimple } from 'stream-chat-react-native';
const reactionListStyles = StyleSheet.create({
container: {
backgroundColor: 'black',
flexDirection: 'row',
borderColor: 'gray',
borderWidth: 1,
padding: 5,
borderRadius: 10,
},
});
const CustomReactionList = props => (
<View style={reactionListStyles.container}>
{renderReactions(
props.message.latest_reactions,
props.supportedReactions,
)}
</View>
)
const MessageFooterWithReactionList = props => {
return (
<ReactionPickerWrapper {...props}>
{props.message.latest_reactions.length > 0 && <CustomReactionList {...props} />}
</ReactionPickerWrapper>
);
};
const MessageWithReactionsAtBottom = props => {
return (
<MessageSimple
{...props}
ReactionList={null}
MessageFooter={MessageFooterWithReactionList}
/>
);
};
And thats it, you have reactions at bottom of the message.
By default, received messages are shown on left side of MessageList and sent messages are shown on right side of the message list.
MessageSimple
component accepts the boolean prop - forceAlign
, which can be used to override the alignment of message bubble relative to MessageList component.
Its value could be either left
or right
.
const MessageSimpleLeftAligned = props => {
return <MessageSimple {...props} forceAlign="left" />;
};
In group messaging, its important to show the name of the sender with message bubble - similar to slack or whatsapp. In this example we are going to add name of the sender on top of message content.
I can foresee different types of designs for this:
In this case, we are going to override MessageContent
component via prop to add
the name of sender, right before MessageContent (which includes attachments and message text)
For the sake of simplicity, I am going to disable reaction selector (using ReactionList={null}
), since it will create conflict
in design. Check the example for moving reactions at bottom of the bubble.
class MessageContentWithName extends React.PureComponent {
render() {
return (
<View style={{flexDirection: 'column', padding: 5}}>
<Text style={{fontWeight: 'bold'}}>{this.props.message.user.name}</Text>
<MessageContent {...this.props} />
</View>
);
}
}
const MessageWithSenderName = props => {
return (
<MessageSimple
{...props}
ReactionList={null}
MessageContent={MessageContentWithName}
/>
);
};
In this case, you want to override the MessageText
component.
const MessageTextWithName = props => {
const markdownStyles = props.theme
? props.theme.message.content.markdown
: {};
return (
<View style={{flexDirection: 'column', padding: 5}}>
<Text style={{fontWeight: 'bold'}}>{props.message.user.name}</Text>
{props.renderText(props.message, markdownStyles)}
</View>
);
};
const MessageWithSenderNameInTextContainer = props => {
return <MessageSimple {...props} MessageText={MessageTextWithName} />;
};
TIP you can also mix above two cases. If there is an attachment to message, then show the name on top of everything, and if there is no attachment, then show the name inside text container/bubble. You can
conditionally render sender's name both in MessageText and MessageContent based on - message.attachments.length > 0
In this case, we can use MessageFooter
UI component prop to add name of the sender.
const MessageWithSenderNameAtBottom = props => {
return (
<MessageSimple
{...props}
MessageFooter={props => <Text>{props.message.user.name}</Text>}
/>
);
};
Some messaging apps usually show extra options such as delete or reply when you swipe the message.
In this example we will build a message which opens delete
option when swiped left, and will reply
to the message when swiped to left.
For swiping gesture, we are going to use external library called react-native-swipe-list-view
Also I am going to use following styles for this example:
const messageSwipeableStyles = StyleSheet.create({
row: {
flex: 1,
backgroundColor: 'blue',
},
messageActionsContainer: {
backgroundColor: '#F8F8F8',
},
deleteButton: {
alignSelf: 'flex-end',
width: 50,
height: '100%',
justifyContent: 'center',
alignContent: 'center',
borderLeftColor: '#A8A8A8',
borderLeftWidth: 1,
},
deleteIcon: {
width: 30,
height: 30,
alignSelf: 'center',
},
messageContainer: {
flex: 1,
backgroundColor: 'white',
},
messageContainerSelected: {
backgroundColor: '#F8F8F8',
},
});
First lets just build a simple swipeable message component, which when swiped left, opens the delete button.
We will use SwipeRow
as wrapper around our MessageSimple component.
import {SwipeRow} from 'react-native-swipe-list-view';
...
class MessageSwipeable extends React.Component {
render() {
return (
<SwipeRow
rightOpenValue={-55}
leftOpenValue={30}
recalculateHiddenLayout>
<View>
<TouchableOpacity style={messageSwipeableStyles.deleteButton}>
<Image source={deleteIcon} styles={messageSwipeableStyles.deleteIcon} />
</TouchableOpacity>
</View>
<View>
<MessageSimple {...this.props} />
</View>
</SwipeRow>
);
}
}
You will see some really distorted UI at this point. Don't worry, we just need to add some nice styles to fix it:
class MessageSwipeable extends React.Component {
render() {
return (
<SwipeRow
rightOpenValue={-55}
leftOpenValue={30}
+ style={messageSwipeableStyles.row}
recalculateHiddenLayout>
- <View>
+ <View style={messageSwipeableStyles.messageActionsContainer}>
- <TouchableOpacity>
+ <TouchableOpacity style={messageSwipeableStyles.deleteButton}>
<Image
source={deleteIcon}
+ style={messageSwipeableStyles.deleteIcon}
/>
</TouchableOpacity>
</View>
- <View>
+ <View style={messageSwipeableStyles.messageContainer}>
<MessageSimple {...this.props} />
</View>
</SwipeRow>
);
}
}
You will see a nice button appearing on right on the message, when message is swiped left.
Next, we want to open a thread or reply screen when message is swiped right. For this we are going to use onRowOpen
callback prop function on SwipeRow
. This callback gets the swipe offset as parameter. When this offset is positive,
that means its a right swipe, otherwise left.
So when user swipes right on message, we want to call the function onThreadSelect()
, which is available
in props of message component.
Also we are going to add some different styles when delete button/option is visible on message. For this we add a state variable
menuOpen
to keep track of whether delete button is visible or not.
class MessageSwipeable extends React.Component {
+ rowRef = null;
+ state = {menuOpen: false};
render() {
return (
<SwipeRow
rightOpenValue={-55}
leftOpenValue={30}
style={messageSwipeableStyles.row}
recalculateHiddenLayout
+ onRowOpen={value => {
+ if (value > 0) {
+ this.props.onThreadSelect(this.props.message);
+ this.rowRef.closeRow();
+ } else {
+ this.setState({menuOpen: true});
+ }
+ }}
+ ref={ref => {
+ this.rowRef = ref;
+ }}>
<View style={messageSwipeableStyles.messageActionsContainer}>
<TouchableOpacity style={messageSwipeableStyles.deleteButton}>
<Image
source={deleteIcon}
style={messageSwipeableStyles.deleteIcon}
/>
</TouchableOpacity>
</View>
- <View style={messageSwipeableStyles.messageContainer}>
+ <View
+ style={
+ this.state.menuOpen
+ ? {
+ ...messageSwipeableStyles.messageContainer,
+ ...messageSwipeableStyles.messageContainerSelected,
+ }
+ : {...messageSwipeableStyles.messageContainer}
+ }>
<MessageSimple {...this.props} />
</View>
</SwipeRow>
);
}
}
Next, when delete button gets pressed, we want to delete the message and close the row.
class MessageSwipeable extends React.Component {
rowRef = null;
state = {menuOpen: false};
render() {
return (
<SwipeRow
rightOpenValue={-55}
leftOpenValue={30}
recalculateHiddenLayout
onRowOpen={value => {
if (value > 0) {
this.props.onThreadSelect(this.props.message);
this.rowRef.closeRow();
} else {
this.setState({menuOpen: true});
}
}}
ref={ref => {
this.rowRef = ref;
}}>
<View style={messageSwipeableStyles.messageActionsContainer}>
- <TouchableOpacity style={messageSwipeableStyles.deleteButton}>
+ <TouchableOpacity
+ style={messageSwipeableStyles.deleteButton}
+ onPress={() => {
+ this.props.handleDelete();
+ this.rowRef.closeRow();
+ }}>
+ <Image
+ source={deleteIcon}
+ style={messageSwipeableStyles.deleteIcon}
+ />
</TouchableOpacity>
</View>
<View
style={
this.state.menuOpen
? {
...messageSwipeableStyles.messageContainer,
...messageSwipeableStyles.messageContainerSelected,
}
: {...messageSwipeableStyles.messageContainer}
}>
<MessageSimple {...this.props} />
</View>
</SwipeRow>
);
}
}
Next, we to restrict swipeable functionality only for non-deleted messages.
class MessageSwipeable extends React.Component {
rowRef = null;
state = {menuOpen: false};
render() {
return (
<SwipeRow
rightOpenValue={-55}
leftOpenValue={30}
recalculateHiddenLayout
+ disableLeftSwipe={!!this.props.message.deleted_at}
+ disableRightSwipe={!!this.props.message.deleted_at}
onRowOpen={value => {
if (value > 0) {
this.props.onThreadSelect(this.props.message);
this.rowRef.closeRow();
} else {
this.setState({menuOpen: true});
}
}}
ref={ref => {
this.rowRef = ref;
}}>
- <View style={messageSwipeableStyles.messageActionsContainer}>
+ <View
+ style={
+ !this.props.message.deleted_at
+ ? messageSwipeableStyles.messageActionsContainer
+ : null
+ }>
+ {!this.props.message.deleted_at ? (
+ <TouchableOpacity
+ style={messageSwipeableStyles.deleteButton}
+ onPress={() => {
+ this.props.handleDelete();
+ this.rowRef.closeRow();
+ }}>
+ <Image
+ source={deleteIcon}
+ style={messageSwipeableStyles.deleteIcon}
+ />
+ </TouchableOpacity>
+ ) : null}
</View>
<View
style={
this.state.menuOpen
? {
...messageSwipeableStyles.messageContainer,
...messageSwipeableStyles.messageContainerSelected,
}
: {...messageSwipeableStyles.messageContainer}
}>
<MessageSimple {...this.props} />
</View>
</SwipeRow>
);
}
}
And we are done. Soon we will publish the running example of this on our repo!
in progress ...
class MessageSwipeable extends React.Component {
rowRef = null;
state = {menuOpen: false};
render() {
return (
<SwipeRow
rightOpenValue={-55}
leftOpenValue={30}
disableLeftSwipe={!!this.props.message.deleted_at}
disableRightSwipe={!!this.props.message.deleted_at}
style={messageSwipeableStyles.row}
recalculateHiddenLayout
onRowClose={() => {
this.setState({menuOpen: false});
}}
onRowOpen={value => {
if (value > 0) {
this.props.onThreadSelect(this.props.message);
this.rowRef.closeRow();
} else {
this.setState({menuOpen: true});
}
}}
ref={ref => {
this.rowRef = ref;
}}>
<View
style={
!this.props.message.deleted_at
? messageSwipeableStyles.messageActionsContainer
: null
}>
{!this.props.message.deleted_at ? (
<TouchableOpacity
style={messageSwipeableStyles.deleteButton}
onPress={() => {
this.props.handleDelete();
this.rowRef.closeRow();
}}>
<Image
source={deleteIcon}
style={messageSwipeableStyles.deleteIcon}
/>
</TouchableOpacity>
) : null}
</View>
<View
style={
this.state.menuOpen
? {
...messageSwipeableStyles.messageContainer,
...messageSwipeableStyles.messageContainerSelected,
}
: {...messageSwipeableStyles.messageContainer}
}>
<MessageSimple {...this.props} />
</View>
</SwipeRow>
);
}
}
const messageSwipeableStyles = StyleSheet.create({
row: {
flex: 1,
backgroundColor: 'blue',
},
messageActionsContainer: {
backgroundColor: '#F8F8F8',
},
deleteButton: {
alignSelf: 'flex-end',
width: 50,
height: '100%',
justifyContent: 'center',
alignContent: 'center',
borderLeftColor: '#A8A8A8',
borderLeftWidth: 1,
},
deleteIcon: {
width: 30,
height: 30,
alignSelf: 'center',
},
messageContainer: {
flex: 1,
backgroundColor: 'white',
},
messageContainerSelected: {
backgroundColor: '#F8F8F8',
},
});
in progress ...
Internally we use react-native-actionsheet library. This library supports style customizations. But used our own components for header and actionsheet. So some basic styling could be done using theme object (provided to Chat component)
We use actionsheet at two places in our library.
(to display list of message options, when message is long pressed - in MessageContent
component inside MessageSimple
)
Basic styling can be achieved by providing styles for keys given in following example, in theme object.
const theme = {
'messageInput.actionSheet.titleContainer': `background-color: 'black', padding: 10px`,
'messageInput.actionSheet.titleText': `color: 'white'`
'messageInput.actionSheet.buttonContainer': `background-color: 'black', padding: 5px`,
'messageInput.actionSheet.buttonText': `color: 'white', margin-left: 20px`
}
<Chat client={chatClient} style={theme}></Chat>
If you want to customize further, e.g., container of the whole actionsheet or backdrop, then you need to add styles directly for
internal react-native-actionsheet component. You can do that by passing prop actionSheetStyles
to MessageInput component.
Full list of options: https://github.com/beefe/react-native-actionsheet/blob/master/lib/styles.js
import { Chat, Channel, MessageList, MessageInput, MessageSimple } from 'stream-chat-react-native';
const actionsheetStyles = {
overlay: {
backgroundColor: 'grey',
opacity: 0.6
},
wrapper: {
flex: 1,
flexDirection: 'row'
},
};
<Chat client={chatClient}>
<Channel>
<MessageList />
<MessageInput actionsheetStyles={actionsheetStyles} />
</Channel>
</Chat>
Note titleBox
, titleText
, buttonBox
and buttonText
won't work in above styles, since we have overridden those components with our own components.
(attachment options, when +
icon is pressed in MessageInput component.)
Basic styling can be achieved by providing styles for keys given in following example, in theme object.
const theme = {
'message.actionSheet.titleContainer': `background-color: 'black', padding: 10px`,
'message.actionSheet.titleText': `color: 'white'`,
'message.actionSheet.buttonContainer': `background-color: 'black', padding: 10px`,
'message.actionSheet.buttonText': `color: 'white'`,
'message.actionSheet.cancelButtonContainer': `background-color: 'red', padding: 10px`,
'message.actionSheet.cancelButtonText': `color: 'white'`,
};
<Chat client={chatClient} style={theme}></Chat>
If you want to customize further, e.g., container of the whole actionsheet or backdrop, then you need to add styles directly for
internal react-native-actionsheet component. You can do that by passing prop actionSheetStyles
to MessageSimple
component.
Full list of options: https://github.com/beefe/react-native-actionsheet/blob/master/lib/styles.js
import { Chat, Channel, MessageList, MessageInput, MessageSimple } from 'stream-chat-react-native';
const actionsheetStyles = {
overlay: {
backgroundColor: 'grey',
opacity: 0.6
},
wrapper: {
flex: 1,
flexDirection: 'row'
},
};
const MessageSimpleWithCustomActionsheet = props => (
<MessageSimple
{...props}
actionsheetStyles={actionsheetStyles} />
)}
);
// When you render chat components ...
<Chat client={chatClient}>
<Channel>
<MessageList Message={MessageSimpleWithCustomActionsheet} />
<MessageInput/>
</Channel>
</Chat>
React native provides an in built component called KeyboardAvoidingView
. This component works well for most of the cases where height of the component is 100% relative to screen. If you have some fixed height then it may create some issue (it depends on your case - how you use wrappers around chat components).
To avoid this issue we built our own component - KeyboardCompatibleView
. It contains simple logic - when keyboard is opened (which we can know from events of Keyboard
module), adjust the height of Channel component and when keyboard is dismissed, then again adjust the height of Channel component accordingly. While building this component, we realized that it has certain limitations. e.g., Keyboard module on emits the event keyboardDidHide, which means we can only adjust the height of Channel component after dismissal of keyboard has already started (which results in white gap between keyboard and Channel component during keyboard dismissal)
There are few customizations you can do regarding the keyboard behaviour
In this version of stream-chat-react-native, KeyboardCompatibleView is prety much the same as KeyboardAvoidingView from react-native, with some fixes for app state. You can provide following props to Channel component:
-
keyboardBehavior
- 'padding' | 'position' | 'height' -
keyboardVerticalOffset
- number -
disableKeyboardCompatibleView
- boolean
Or you can also customize the underlying KeyboardCompatibleView component as well:
import { KeyboardCompatibleView } from 'stream-chat-react-native';
// Define a custom keyboard view
const CustomKeyboardCompatibleView = ({children}) => (
<KeyboardCompatibleView
// You will need to fine-tune following keyboardVerticalOffset
// as per your app UI
// For iOS - its the height of your input box
// For android - its some negative number,
keyboardVerticalOffset={Platform.OS === 'ios' ? 86.5 : -300}
behavior={Platform.OS === 'ios' ? 'padding' : 'position'}>
{children}
</KeyboardCompatibleView>
);
// When you render the chat component
<Chat client={chatClient}>
<Channel
KeyboardCompatibleView={CustomizedKeyboardView}
...
/>
</Chat>
You can pass the custom component as prop - KeyboardCompatibleView
to channel component. Add the custom animation duration
to KeyboardCompatibleView
using props keyboardDismissAnimationDuration
and keyboardOpenAnimationDuration
, and pass it to Channel
component (as done in following example):
import { KeyboardCompatibleView } from 'stream-chat-react-native';
const CustomizedKeyboardView = props => (
<KeyboardCompatibleView keyboardDismissAnimationDuration={200} keyboardOpenAnimationDuration={200}>
{props.children}
</KeyboardCompatibleView>
)
// When you render the chat component
<Chat client={chatClient}>
<Channel
KeyboardCompatibleView={CustomizedKeyboardView}
...
/>
</Chat>
You can disable KeyboardCompatibleView
by using prop disableKeyboardCompatibleView
on Channel
component.
Following example shows how to use KeyboardAvoidingView
instead:
<SafeAreaView>
<Chat client={chatClient}>
// Note: Android and iOS both interact with `padding` prop differently.
// Android may behave better when given no behavior prop at all, whereas iOS is the opposite.
// reference - https://reactnative.dev/docs/keyboardavoidingview#behavior
<KeyboardAvoidingView behavior="padding">
<View style={{display: 'flex', height: '100%'}}>
<Channel channel={channel} disableKeyboardCompatibleView>
<MessageList />
<MessageInput />
</Channel>
</View>
</KeyboardAvoidingView>
</Chat>
</SafeAreaView>
You can pass additional any number of props to underlying FlatList using additionalFlatListProps
prop:
<ChannelList
filters={filters}
sort={sort}
additionalFlatListProps={{ bounces: true }}
/>
<MessageList additionalFlatListProps={{ bounces: true }} />
Please find list of all available FlatList props here - https://reactnative.dev/docs/flatlist#props
For image picker in our library, we use lightweight third party react-native-image-picker library. It works perfectly for basic image picking functionality. Although if the image is heavy in size (e.g., photo taken by iPhone XS camera can be between 8-12 MB), it takes long to upload a picture on stream server. Thats when you will see ever-lasting loader on image being uploaded (as shown in following screenshot).
Image compression is the solution here. But react-native-image-picker doesn't offer compression option. So we need to instead use react-native-image-crop-picker (or you can implement your own functionality as well ofcourse)
- Install
react-native-image-crop-picker
in your app/project. Please pay attention to the version that you are installing. If you are using RN 0.61+, then may use latest version of image-crop picker, otherwise use 0.25.3 - reference
yarn add react-native-image-crop-picker
-
Follow the steps mentioned here along with post installation steps - https://github.com/ivpusic/react-native-image-crop-picker#react-native--060-with-cocoapods
-
In your app, on chat screen - register the pickImage handler as follow:
import ImagePicker from 'react-native-image-crop-picker';
import {registerNativeHandlers} from 'stream-chat-react-native-core';
registerNativeHandlers({
pickImage: () =>
new Promise((resolve, reject) => {
ImagePicker.openPicker({
// Add your compression related config here.
height: 400,
width: 400,
cropping: false,
}).then(
image => {
resolve({
cancelled: false,
uri: `${image.path}`,
});
},
() => {
resolve({
cancelled: true,
});
},
);
}),
});
You can provide your compression config to ImagePicker.openPicker({ ... })
function. This library provides plenty of options for cropping or compression - https://github.com/ivpusic/react-native-image-crop-picker#request-object
And you are good to go :)
How can I override/intercept message actions such as edit, delete, reaction, reply? e.g. to track analytics
By default our library uses MessageSimple as UI component for message. It accepts following props:
- handleEdit
- handleDelete
- handleReaction
- handleAction
- handleRetry
Please find entire list of props here - https://getstream.github.io/stream-chat-react-native/#messagesimple
So lets take an example of tracking these function calls for analytics. We want to retain the original functionality, but just want to introduce a custom tracking call right before original call gets gets executed:
So in this case, create a UI component, which uses MessageSimple underneath and intercept functions such as handleDelete, handleEdit etc.
const CustomMessageComponent = (props) => {
const handleEdit = () => {
// This is call to your analytics related function.
handleEditAnalyticsCall(props.message);
// continue with original call
props.handleEdit();
}
const handleDelete = () => {
// This is call to your analytics related function.
handleDeleteAnalyticsCall(props.message);
// continue with original call
props.handleDelete();
}
const handleReaction = (type) => {
// This is call to your analytics related function.
handleReactionAnalyticsCall(props.message);
// continue with original call
props.handleReaction(type);
}
return (
<MessageSimple
handleDelete={handleDelete}
handleEdit={handleEdit}
handleReaction={handleReaction}
/>
)
}
// Use the custom message component in MessageList
<MessageList
Message={CustomMessageComponent}
/>
We provide MessageInput component OOTB which looks something like this:
But your design may require a bit different layout or positioning of inner components, such as send button, attachment button or inputbox inself. One use case could be Slack (workplace chat application) style - where all the buttons are bellow the inputbox.
Here I will show you how you can build above design with some small modifications to MessageInput
MessageInput component accepts Input as a UI component prop. Library also exports all the inner components of MessageInput. So all you need to do is build a UI component which arranges those inner child components in whatever layout you prefer and pass it to MessageInput.
import React from 'react';
import {TouchableOpacity, View, Text, StyleSheet} from 'react-native';
import {
AutoCompleteInput,
AttachButton,
SendButton,
} from 'stream-chat-react-native';
const InputBox = props => {
return (
<View style={inputBoxStyles.container}>
<AutoCompleteInput {...props} />
<View style={inputBoxStyles.actionsContainer}>
<View style={inputBoxStyles.row}>
<TouchableOpacity
onPress={() => {
props.appendText('@');
}}>
<Text style={inputBoxStyles.textActionLabel}>@</Text>
</TouchableOpacity>
{/* Text editor is not functional yet. We will cover it in some future tutorials */}
<TouchableOpacity style={inputBoxStyles.textEditorContainer}>
<Text style={inputBoxStyles.textActionLabel}>Aa</Text>
</TouchableOpacity>
</View>
<View style={inputBoxStyles.row}>
<AttachButton {...props} />
<SendButton {...props} />
</View>
</View>
</View>
);
};
const inputBoxStyles = StyleSheet.create({
container: {
flexDirection: 'column',
flex: 1,
height: 60,
},
actionsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignContent: 'center',
height: 30,
},
row: {flexDirection: 'row'},
textActionLabel: {
color: '#787878',
fontSize: 18,
},
textEditorContainer: {
marginLeft: 10,
},
});
And then pass this InputBox component to MessageInput
. I am adding some additional styles in theme to make it look more like Slack's input box.
const theme = {
'messageInput.container':
'border-top-color: #979A9A;border-top-width: 0.4; background-color: white; margin: 0; border-radius: 0;',
}
<Chat client={chatClient} style={theme}>
<Channel>
<MessageList />
<MessageInput Input={InputBox} />
</Channel>
</Chat>
Additionally if you want separate buttons for file picker and image picker or just want to open either one of them on some action, you can use _pickImage() and _pickFile() functions available on props of InputBox
component (in above example)
So basically you can add some button in InputBox
component following way
const InputBox = props => {
return (
<View style={inputBoxStyles.container}>
{/** Here you will put all the other components such as AutoCompleteInput */}
{/** Following button will only open filePicker */}
<Button onPress={props._pickFile()} />
{/** Following button will only open image picker */}
<Button onPress={props._pickImage()} />
</View>
);
};
// And then pass this InputBox to MessageInput
<MessageInput Input={InputBox} />
MessageInput
internally uses TextInput component from react-native. We attach some default props to it internally. But if you want to add some additional props, you can do it via additionalTextInputProps
<MessageInput
additionalTextInputProps={{
allowFontScaling: true,
clearTextOnFocus: true
}}
/>
For general idea, you may want to read this post first (thanks to @manojsinghnegi) - https://medium.com/@manojsinghnegi/react-native-auto-growing-text-input-8638ac0931c8
// Override the default max-height on inner TextInput
const theme = {
'messageInput.inputBox': 'max-height: 250px',
}
class ChannelScreen extends React.Component {
constructor(props) {
this.state = {
height: 40,
}
}
updateSize = (height) => {
this.setState({
height
});
}
render() {
const additionalTextInputProps = {
onContentSizeChange: (e) => this.updateSize(e.nativeEvent.contentSize.height),
style: {height: this.state.height},
};
return (
<Chat>
<Channel>
<MessageInput
additionalTextInputProps={additionalTextInputProps}
/>
</Channel>
</Chat>
)
}
}
For setup regarding push notifications, first of all make sure you have followed all the steps:
- https://getstream.io/chat/docs/push_ios/?language=java
- https://getstream.io/chat/docs/rn_push_initial/?language=java
- https://getstream.io/chat/docs/rn_push_ios/?language=java
- https://getstream.io/chat/docs/push_android/?language=java
- https://getstream.io/chat/docs/rn_push_initial/?language=java
- https://getstream.io/chat/docs/rn_push_android/?language=java
-
User must be a member of channel if they expect a push notification for a message on that channel.
-
We only send a push notification, when user is NOT connected to chat, or in other words, if user does NOT have any active WS (websocket) connection. WS connection is established when you do
await client.setUser({ id: 'user_id' }) await client.addDevice(token.token, token.os === 'ios' ? 'apn' : 'firebase')
Usually you want to receive push notification, when your app goes to background. When you put your app to background, WS connection stays active for approximately 15-20 seconds, after which system will break the connection automatically. But for those 15-20 seconds, you won't receive any push (since WS is still active).
-
To handle this case, you will have to manually break the WS connection, when your app goes to background:
await client.wsConnection.disconnect();
And when app comes to foreground, re-establish the connection:
await client._setupConnection();
You can use AppState module of react-native to detect weather app is on foreground or background.
-
If you don't like the idea of manually breaking websocket connection, but still want to receive push notification immediately when app goes to background, please use the following approach:
-
Add a listener for
message.new
(and/ornotification.message_new
) event. -
When you receive one of these events, check if your app is in foreground or background. If the app is backgrounded, then generate a local notification
const _handleMessageNewEvent = event => { // If the app is on foreground, then do nothing. if (appState === 'active') return; // If app is on background, then generate a local notification. PushNotification.localNotification({ bigText: event.message.text }); } client.on('message.new', _handleMessageNewEvent) client.on('notification.message_new', _handleMessageNewEvent)
App example will look something like following:
import React, { useEffect, useState } from "react"; import { AppState, StyleSheet, Text, View } from "react-native"; import PushNotifications from 'react-native-push-notification'; const ChatExample = () => { const [appState, setAppState] = useState(AppState.currentState); const [client, setClient] = useState(null); useEffect(() => { const setupClient = async () => { const client = new StreamChat("API_KEY"); await client.setUser({id: 'userId'}, 'token'); client.on('message.new', _handleMessageNewEvent) client.on('notification.message_new', _handleMessageNewEvent) setClient(client); } setupClient(); return () => { client.off('message.new'); client.off('notification.message_new'); } }, []) useEffect(() => { PushNotification.configure({ /** push notification config */ }) AppState.addEventListener("change", _handleAppStateChange); return () => { AppState.removeEventListener("change", _handleAppStateChange); }; }, []); const _handleAppStateChange = nextAppState => { if (appState.match(/inactive|background/) && nextAppState === "active") { console.log("App has come to the foreground!"); } setAppState(nextAppState); }; const _handleMessageNewEvent = event => { // If the app is on foreground, then do nothing. if (appState === 'active') return; // If app is on background, then generate a local notification. PushNotification.localNotification({ bigText: event.message.text }); } if (!client) return null; return ( <View style={styles.container}> <Chat client={client}> {/** All the chat components */} </Chat> </View> ); };
-