React Native Chat with image and audio

Jordan Daniels
6 min readAug 19, 2018

--

The title says it all!

Here is a video demo:

Here’s a link to my github demo repo as well: https://github.com/liplylie/ReactNativeChatImageAudio. Please give it a star if you enjoyed this article!

The tech stack will be: React Native (duh), React-Native Gifted Chat, React-Native-Audio, React-Native-Sound, React-Native-Image-Picker, React-Native-aws3, and Firebase.

I wanted to share my code and explain it to help others who may go through the struggle of adding image and audio recordings features to their chat. The code cuts off some sensitive data, so you’ll have to fill it in with your own.

Also, you’ll need to have a bit of an understanding of AWS S3 and Firebase real time database for this.

Please take a look at my github page, and in the src directory, create a config directory and addAWSconfig.js and FirebaseConfig.js. You will also need a sensitive.json file in the root directory with this structure:

{ “BUCKET”: “”, “ACCESS_KEY”: “”, “SECRET_KEY”: “”, “APIKEY”: “”, “keyPrefix”: “”, “region”: “” “authDomain”: “”, “databaseURL”: “”, “projectId”: “”, “storageBucket”: “”, “messagingSenderId”: “”}

Let’s start by taking a look at the state and componentWillMount:

state = {  messages: [],  startAudio: false,  hasPermission: false,  audioPath: `${    AudioUtils.DocumentDirectoryPath    }/${this.messageIdGenerator()}test.aac`,  playAudio: false,  fetchChats: false,  audioSettings: {    SampleRate: 22050,    Channels: 1,    AudioQuality: "Low",    AudioEncoding: "aac",    MeteringEnabled: true,    IncludeBase64: true,    AudioEncodingBitRate: 32000  }};componentWillMount() {  this.chatsFromFB = firebaseDB.ref(    `enter path to your group/one-one chat data`  );  this.chatsFromFB.on("value", snapshot => {    if (!snapshot.val()) {      this.setState({        fetchChats: true      });     return;    }    let { messages } = snapshot.val();     messages = messages.map(node => {       const message = {};        message._id = node._id;       message.text = node.messageType === "message" ? node.text : "";       message.createdAt = node.createdAt;       message.user = {         _id: node.user._id,         name: node.user.name,         avatar: node.user.avatar       };       message.image = node.messageType === "image" ? node.image : "";       message.audio = node.messageType === "audio" ? node.audio : "";       message.messageType = node.messageType;       return message;    });      this.setState({        messages: [...messages]      });    });}

state: Messages is an array containing all of the chat messages. StartAudio is a boolean that changes the color of the microphone when audio is being recorded. HasPermission checks the user’s device for permission to record audio. AudioSetting is the config needed for React-Native-Audio. PlayAudio is a boolean that changes the color of the play symbol when an audio recording is played. AudioPath is the location in the device where the audio recording is stored. FetchChat is a boolean showing when the messages have been retrieved. When false, a loading circle renders on the screen.

ComponentWillMount: Here, you will need to retrieve the messages from your firebase real time database, and keep a reference to this db so you can store new messages.

My firebase data structure looks like this:

- roomId
- messages
- 0
- _id : "",
- createdAt: "",
- messageType: "",
- audio: "",
- image: "",
- text: "",
- recipientId: "",
- jobId: ""
- user
- _id: "",
- avatar: "",
- name: ""

Message type is either message, image, or audio. If the type is message, the chat will be a simple string. If the message type is image, the chat will render an image. The url of the image will be stored in image. If the message type is audio, the audio url will be stored in audio.

The creator of the chat’s relevant information is stored in user.

RecipientId and jobId correspond to room Ids. Recipient Id is used for one to one chats in this case, and job Id is used for group chats.

Use this.setState() to set the messages to the state of the component.

If you need to format your data so it will work with GiftedChat’s structure, you can create a chat object like this:

const message = {};message._id = "";message.text = "";message.createdAt = "";message.user = {_id: "",name: "",avatar: ""};message.image = "";message.audio = "";message.messageType = "";

Next look at the render function:

render() {  const { user } = data; // wherever you user info is  return (    <View style={{ flex: 1 }}>      <secretNavBar
title = "your app name"
onLeftButtonPress={() => `some function to go back to previous screen`} leftButtonTitle={"Back"} rightButtonIcon={"camera"} onRightButtonPress={() => this.handleAddPicture()} /> {this.renderLoading()} {this.renderAndroidMicrophone()} <GiftedChat messages={this.state.messages} onSend={messages => this.onSend(messages)} alwaysShowSend showUserAvatar isAnimated showAvatarForEveryMessage renderBubble={this.renderBubble} messageIdGenerator={this.messageIdGenerator} onPressAvatar={this.handleAvatarPress} renderActions={() => { if (Platform.OS === "ios") { return ( <Ionicons name="ios-mic" size={35} hitSlop={{ top: 20, bottom: 20, left: 50, right: 50 }} color={this.state.startAudio ? "red" : "black"} style={{ bottom: 50, right: Dimensions.get("window").width / 2, position: "absolute", shadowColor: "#000", shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.5, zIndex: 2, backgroundColor: "transparent" }} onPress={this.handleAudio} />); } }} user={{ _id: user.userId, name: `${user.firstName} ${user.lastName}`, avatar: `Your photo` }} /> <KeyboardAvoidingView /> </View> );}

SecretNavBar: this is just a component for the nav bar. I purposefully didn’t show the code for my navbar, so you’ll have to make your own. In my case, mine looks like this:

I don’t want to spend to much time on this component, but the important part of this is the camera icon on the right. Pressing on it will trigger this.handleAddPicture().

handleAddPicture: Uses RN-Image-Picker to choose or take a picture, and send that picture to firebase and the db.

Here is the code for that:

handleAddPicture = () => {  const { user } = data; // wherever you user data is stored;  const options = {    title: "Select Profile Pic",    mediaType: "photo",    takePhotoButtonTitle: "Take a Photo",    maxWidth: 256,    maxHeight: 256,    allowsEditing: true,    noData: true  };  ImagePicker.showImagePicker(options, response => {    console.log("Response = ", response);    if (response.didCancel) {      // do nothing    } else if (response.error) {      // alert error    } else {      const { uri } = response;      const extensionIndex = uri.lastIndexOf(".");      const extension = uri.slice(extensionIndex + 1);      const allowedExtensions = ["jpg", "jpeg", "png"];      const correspondingMime = ["image/jpeg", "image/jpeg", "image/png"];      const options = {        keyPrefix: "****",        bucket: "****",        region: "****",        accessKey: "****",        secretKey: "****"      };      const file = {        uri,        name: `${this.messageIdGenerator()}.${extension}`,        type: correspondingMime[allowedExtensions.indexOf(extension)]      };      RNS3.put(file, options)     .progress(event => {       console.log(`percent: ${event.percent}`);     })     .then(response => {       console.log(response, "response from rns3");       if (response.status !== 201) {         alert(         "Something went wrong, and the profile pic was not uploaded."         );         console.error(response.body);         return;       }       const message = {};       message._id = this.messageIdGenerator();       message.createdAt = Date.now();       message.user = {         _id: user.userId,         name: `${user.firstName} ${user.lastName}`,         avatar: "user avatar here"       };       message.image = response.headers.Location;       message.messageType = "image";       this.chatsFromFB.update({         messages: [message, ...this.state.messages]       });     });     if (!allowedExtensions.includes(extension)) {       return alert("That file type is not allowed.");     }   }});};

renderLoading: loading circle renders while the chats are loading.

renderAndroidMicrophone: This was a tricky one. I needed an Ionicon microphone to press on to start the audio recording. However, placing the microphone in RN-Gifted-Chat’s renderActions caused a weird issue where the microphone would not show properly. Having the microphone render above the GiftedChat component worked.

renderBubble: This method is needed for RN-Gifted-Chat in order to render the play symbol for audio recordings and render the names of the chat creator.

renderBubble = props => {  return (    <View>      {this.renderName(props)}      {this.renderAudio(props)}      <Bubble {...props} />    </View>  );};

renderName: Shows the chat creator’s name by the chats they wrote. Is called in the renderBubble method.

renderAudio: renders the play icon for recorded audio and plays the recording on press.

OnAvatarPress: When the avatar is clicked, navigate to that user’s profile.

render: just wanted to note the keyboardAvoidingView placed after the GiftedChat component. This is needed for the keyboard input to show appropriately on android devices.

renderActions: renders the microphone for iOS devices.

ComponentDidMount: Message status is set to “read” for all messages. Message receipts go beyond the scope of this article, so don’t worry too much about this. Checks for permissions to enable audio recording. Audio recording progress logs are written here as well.

OnSend: this function fires whenever a chat message (a string, not image or audio) sends. Sends the message to firebase and the db (if needed).

renderAudio: renders the play icon for recorded audio and plays the recording on press.

handleAudio: records the user’s audio, sends the audio to AWS S3, retrieves the S3 url, sends the audio recording to firebase and the db.

messageIdGenerator: Generates uuid for the chat and audio recording path. Can use any uuid generator for this.

--

--