JOE DESIGNS

A dev shop in Albuquerque New Mexico.

React Native Flyer Chat

Filed under "code" on 10/5/24
react native flyer chat

I recently built out an app chat solution that was built with React Native, Expo and uses a UI Library @flyerhq/react-native-chat-ui to render the chat room. The backend was built in Node Js, but that really won't be covered in this post. I will cover the UI Library and how it works.

I chose @flyerhq/react-native-chat-ui as it was simple and exactly what I needed. The Github Repo has some great examples and documentation. Several other libraries are used to support file uploads and sockets.

To handle the document picker, I am using expo-document-picker.. For photo picker, I am using react-native-image-picker. RNFS is used to read the file and upload it to your backend. react-native-file-viewer

Installation

To install the library in your React Native project, run the following command:

yarn add @flyerhq/react-native-chat-ui

Add the chat UI to the screen in your app, this is a very simple example. Note: The backend will need to be implemented to send and upload messages. Sockets will be need to be integrated on the backend as well.

Create a screen called ChatScreen.jsx and add the following code:

./screens/ChatScreen.jsx
import { useActionSheet } from '@expo/react-native-action-sheet';
import { launchImageLibrary } from 'react-native-image-picker';
import React, { useState, useEffect } from 'react';
import { useIsFocused } from '@react-navigation/native';
import { inject, observer } from 'mobx-react';
import { View, AppState } from 'react-native';
import rnfs from 'react-native-fs';
import FileViewer from 'react-native-file-viewer';
import * as DocumentPicker from 'expo-document-picker';
import { Chat, darkTheme } from '@flyerhq/react-native-chat-ui';
import { SafeAreaProvider } from 'react-native-safe-area-context';

import Socket from 'components/Socket';

function useIsAppForeground() {
  const [isForeground, setForeGround] = useState(true);

  useEffect(() => {
    const list = AppState.addEventListener('change', nextAppState => {
      setForeGround(nextAppState === 'active');
    });

    return () => {
      list.remove();
    };
  }, []);

  return isForeground;
}

function ChatGroup({ navigation, message, auth, route }) {
  const { showActionSheetWithOptions } = useActionSheet();
  const [uploading, setUploading] = useState(false);

  const isFocused = useIsFocused();
  const isAppForeground = useIsAppForeground();

  const [group, setGroup] = useState({});
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    if (isFocused) {
      getMessages();
    }
  }, [isFocused]);

  useEffect(() => {
    if (isAppForeground) {
      getMessages();
    }
  }, [isAppForeground]);

  const handleAttachmentPress = () => {
    showActionSheetWithOptions(
      {
        options: ['Photo', 'File', 'Cancel'],
        cancelButtonIndex: 2,
      },
      buttonIndex => {
        switch (buttonIndex) {
          case 0:
            handleImageSelection();
            break;
          case 1:
            handleFileSelection();
            break;
        }
      }
    );
  };

  const handleFileSelection = async () => {
    const { assets } = await DocumentPicker.getDocumentAsync({
      type: [
        'image/*',
        'application/pdf',
        'application/doc',
        'application/docx',
        'application/xls',
        'application/xlsx',
      ],
    });
    const response = assets?.[0];

    try {
      const data = await rnfs.readFile(response.uri, 'base64');
      await uploadFile(response.uri, response.mimeType, response.name, data);
    } catch (e) {
      console.log('Error reading document', e);
      return;
    }
  };

  const handleImageSelection = () => {
    launchImageLibrary(
      {
        includeBase64: true,
        maxWidth: 1440,
        mediaType: 'photo',
        quality: 0.7,
      },
      async ({ assets }) => {
        const response = assets?.[0];

        await uploadFile(
          response.uri,
          response.type,
          response.fileName,
          response.base64
        );
      }
    );
  };

  async function uploadFile(uri, type, name, data) {
    if (!uri || !type || !name || !data) {
      console.log(
        'Error uploading file, not enough data',
        uri,
        type,
        name,
        data
      );
      return;
    }

    setUploading(true);

    const formData = new FormData();
    formData.append('attachment', {
      uri,
      type,
      name,
      data,
    });

    try {
      // needs implenting to your backend
      await fetch('https://YOURBACKEND/api/upload?group=' + group.id, {
        method: 'POST',
        body: formData,
      });

      setUploading(false);
    } catch (e) {
      console.log('ERROR', e);
      setUploading(false);
    }
  }

  async function getMessages() {
    try {
      const res = await fetch('https://YOURBACKEND/api/messages');
      const data = await res.json();
      
      // take your data out and transform it into the format for the chat ui library
      const transformedMessages = data.messages.map(theMessage => {
          return {
            id: theMessage.id,
            createdAt: theMessage.created_at,
            type,
            uri,
            originalUri,
            mimeType,
            name,
            size,
            text: theMessage.content,
            author: {
              id: theMessage.user.id,
              firstName: theMessage.user.first_name,
              lastName: theMessage.user.last_name,
              imageUrl: theMessage.user.photo_thumb,
            },
          };
        });

        setMessages(transformedMessages);
      }
      if (res.group) {
        setGroup(res.group);
      }
    } catch (e) {
      console.log(e);
    }
  }

  const addMessage = newMessage => {
    setMessages([newMessage, ...messages]);
  };

  const handleSendPress = async newMessage => {

    // needs implenting to your backend

    const res = await fetch('https://YOURBACKEND/api/messages', {
      method: 'POST',
      body: JSON.stringify({
        content: newMessage.text,
      }),
    });

    const data = await res.json();
    
    // transform your data into the format for the chat ui library
    const textMessage = {
      author: {
        id: auth.user.id,
        firstName: auth.user.first_name,
        lastName: auth.user.last_name,
        imageUrl: auth.user.photo_url,
      },
      createdAt: res.message.created_at,
      id: res.message.id,
      text: res.message.content,
      type: 'text',
    };

    addMessage(textMessage);
  };

  const handleMessagePress = async message => {
    // console.log('handleMessagePress', message);
    if (message.type === 'file') {
      try {
        await FileViewer.open(message.uri, { showOpenWithDialog: true });
      } catch {}
    }
  };

  return (
    <SafeAreaProvider>
      <Socket
        event="new-message"
        channel={'messages'}
        onEvent={async () => await getMessages()}
      />
      <Socket
        event="delete-message"
        channel={'messages'}
        onEvent={async e => setMessages(messages.filter(m => m.id !== e.data.message))}
      />
      <Chat
        messages={messages}
        onSendPress={handleSendPress}
        onAttachmentPress={handleAttachmentPress}
        user={auth.user.id}
        showUserAvatars={true}
        showUserNames={true}
        timeFormat="h:mm A"
        onMessagePress={handleMessagePress}
        isAttachmentUploading={uploading}
        theme={{
          ...darkTheme,
          colors: {
            ...darkTheme.colors,
            background: theme.bg,
            inputBackground: 'rgba(255, 255, 255, 0.05)',
          },
          fonts: {
            ...darkTheme.fonts,
            userAvatarTextStyle: {
              ...darkTheme.fonts.userAvatarTextStyle,
              color: '#222',
            },
          },
        }}
      />
    </SafeAreaProvider>
  );
}

export default ChatGroup

Now you will want to have a socket component that will listen for new messages and update the chat UI. This is a simple example of a socket component.

import React, { useEffect, useCallback } from 'react';

import { useSocket } from 'context/socketContext';

function Socket({ channel, event, onEvent }) {
  const socket = useSocket();

  const handleMessage = useCallback(
    msg => {
      // console.log('msg', msg);
      if (msg?.event === event) onEvent(msg);
    },
    [event, onEvent]
  );

  useEffect(() => {
    socket.on(channel, handleMessage);

    return () => {
      socket.off(channel, handleMessage);
    };
  }, [channel, event, handleMessage]);

  return <></>;
}

export default Socket;

Wrap your app with the SocketContext.Provider and ActionSheetProvider. This will allow the socket to be used in your app.

import React, { useEffect } from 'react';
import { ActionSheetProvider } from '@expo/react-native-action-sheet';
import { View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { SocketContext, socket } from 'context/socketContext';

function App() {
  return (
    <View>
      <SocketContext.Provider value={socket}>
        <ActionSheetProvider>
          <NavigationContainer>
            ...
          </NavigationContainer>
        </ActionSheetProvider>
      </SocketContext.Provider>
    </View>
    );
}

export default App;

Now you should have the parts to build your chat app. I will be using the @flyerhq/react-native-chat-ui library to render the chat room. The backend will need to be implemented to send and upload messages. Sockets will be need to be integrated on the backend as well.

Happy coding!