Adding user mentions (tagging) for React Native Gifted Chat
October 25, 2019
Taking advantage of pre-built components is a great way to get your project off the ground fast, but it can mean losing the ability to customise every last detail. I’ve been using React Native Gifted Chat recently which is an absolutely fantastic Chat UI package for React Native. All was going well until I wanted to introduce user mentions - where you type the ”@” symbol to bring up a list of group users, and then tag the user in the post. This short post shows the solution I reached to tag users in a React Native Gifted Chat message.
This post assumes you have React Native Gifted Chat installed and are looking to add the tagging a user feature from an already working setup.
Showing the user select box
The first aim is to show the user select box when the user types the @
symbol. This requires you use the renderComposer
prop of the GiftedChat
component:
import { GiftedChat, Composer } from 'react-native-gifted-chat';
...
_renderComposer = props => {
}
return (
<View style={styles.composer}>
<Composer
{...props}
onTextChanged={(text) => {
// get last character so the tag user popup can be
// displayed if last char is an @
let lastChar = text.substr(text.length - 1);
this.setState({
tagUser: {
...this.state.tagUser,
tagDisplayActive: lastChar === '@' ? true : false,
}
});
props.onTextChanged(text);
}}
/>
</View>
);
};
...
...
<GiftedChat
messages={this.state.messages}
onSend={messages => this._sendMessage(messages)}
user={{
_id: userId,
name: name,
avatar: profileImg
}}
renderComposer={this._renderComposer}
/>
...
Note: this is not the answer to the problem - keep reading!
There’s a bit going on there so I’ll cover in a little detail. The renderComposer
prop of GiftedChat
allows us to extend the functionality of the composer part of the chat system - the input field basically. We can import the actual Composer
component from the library too, and use this as a base to extend the features as this saves us having to write our own text input component from scratch (which would then need to be linked in to the library).
The Composer
component has a useful prop called onTextChanged
, which is a function that’s called each time the user enters a character in to the composer. We want to use this prop to determine if the user has just entered an @
symbol (from above):
onTextChanged={(text) => {
// get last character so the tag user popup can be
// displayed if last char is an @
let lastChar = text.substr(text.length - 1);
this.setState({
tagUser: {
...this.state.tagUser,
tagDisplayActive: lastChar === '@' ? true : false,
}
});
props.onTextChanged(text);
}}
This gets the last character, and if it’s an @
, sets the state so tagDisplayActive
is true. Now, anywhere in your component you can check reference this state update and show a list of users. Here’s what that might look like:
import { GiftedChat, Composer } from 'react-native-gifted-chat';
...
_renderComposer = props => {
}
return (
<React.Fragment>
{tagUser.tagDisplayActive && (
<TagUserChatSelect
userSelected={({ userId, userName }) => {
// update state so we remove popop, and add selected user
// to an array
this.setState(prevState => ({
tagUser: {
...this.state.tagUser,
tagDisplayActive: false,
taggedUsers: [...prevState.tagUser.taggedUsers, userId]
}
}));
// add the selected user to the input field
props.onTextChanged(`${props.text}${userName} `);
}}
/>
)}
<View style={styles.composer}>
<Composer
{...props}
onTextChanged={(text) => {
let lastChar = text.substr(text.length - 1);
this.setState({
tagUser: {
...this.state.tagUser,
tagDisplayActive: lastChar === '@' ? true : false,
}
});
props.onTextChanged(text);
}}
/>
</View>
</React.Fragment>
);
};
...
...
<GiftedChat
messages={this.state.messages}
onSend={messages => this._sendMessage(messages)}
user={{
_id: userId,
name: name,
avatar: profileImg
}}
renderComposer={this._renderComposer}
/>
...
The TagUserChatSelect
component can be created to list the users, and once a user is selected we can add their details to an array in state, and don’t forget to hide the component on each selection by setting state.tagDisplayActive
to false!
The problem with the above approach
The above approach works well at first, but an issue arises when the user goes back to somewhere in the middle of an already typed string and enters an @
symbol. The solution above looks for the @
at the very end of the string, but will not open the user list if it’s anywhere else. The only way I could think to solve this is to update the React Native Gifted Chat component to pass back the current text position along with the whole text string. This will then allow us to determine the character that was just typed, allowing us to open the user list wherever an @
symbol is typed.
I have recently submitted a PR for this update to be merged in to the main repo, and the update request can be found here. In the meantime if you want to experiment, feel free to fork this until if/when it’s merged.
Keeping tack of who has been tagged in a message
We’re able to tag a user and their name appears in the input field when selected, and we now have an array of user ID’s for the user’s we’re selecting. The next issue is how we keep track of these user’s we select. We can keep an array of all users tagged in the message, but what if we want to assign a user ID to each specific tagged instance so we can tap their name and open their profile page for example? There’s currently no way to relate each tagged user to a specific array element.
My solution to this is to keep the array updated in the order that the users are tagged in the message. Let’s say we tag 2 people, and then move the cursor back so it’s in between the 2 tagged users, we want to insert the 3rd tagged user in the middle of the other 2 user ID’s in the tagged user array. This keeps the order of the message tags, and array elements in sync.
Here’s the component in use which lists the users in the chat group, and the logic which keeps track of the array positions to achieve the desired result:
<TagUserChatSelect
groupUsers={thisRoom.chatUsers}
closeList={() => {
this.setState({
tagUser: {
...this.state.tagUser,
tagDisplayActive: false,
tagListActive: false,
},
})
}}
userSelected={({ userId, userName }) => {
// get cursor position from my PR in gifted chat - this is send back
// up the component tree in the Composer component's "onTextChanged"
// prop.
const cursorPosition = this.state.tagUser.cursorPosition
// split the current text string where the cursor position is,
// and add the tag in between the split, and add back in to view
const beforeTag = tagUser.props.text.substring(0, cursorPosition)
const afterTag =
Platform.OS === "ios"
? tagUser.props.text.substring(cursorPosition)
: tagUser.props.text.substring(cursorPosition).substring(1)
tagUser.props.onTextChanged(`${beforeTag}${userName}${afterTag}`)
// get the count of occurrences of @ symbol in the text behind
// the cursor, which is used to add the new userId in the state below
// at the same index which they are tagged in the post
const tagsBefore = (beforeTag.match(/@/g) || []).length - 1
let taggedUsers = [...this.state.tagUser.taggedUsers]
taggedUsers.splice(tagsBefore, 0, userId)
this.setState((prevState) => ({
tagUser: {
...this.state.tagUser,
tagDisplayActive: false,
tagListActive: false,
taggedUsers: taggedUsers,
},
}))
}}
/>
The change to Gifted Chat has not yet been merged in which makes this work correctly, so if you want more info on how I achieved this, just drop me and email!
Thanks for reading.
Senior Engineer at Haven